深刻理解JavaScript閉包之閉包的使用場景

本篇文章是上一篇 深刻理解JavaScript閉包之什麼是閉包文章的下篇,閉包的使用場景。javascript

基礎概念

1.函數做用域

定義在函數中的參數和變量在函數外部是不可見的。html

2.塊級做用域(私有做用域)

任何一對花括號中的語句都屬於一個快,在這之中的全部變量在代碼塊外都是不可見的,咱們稱之爲塊級做用域。大多數類C語言都擁有塊級做用域,JS卻沒有,好比在for循環中定義的i,出了for循環仍是有這個i變量。前端

3.私有變量

私有變量包括函數的參數,局部變量和函數內部定義的其餘函數。java

4.靜態私有變量

私有變量是每一個實例都是獨立的,而靜態私有變量是共用的。react

5.特權方法

有權訪問私有變量的方法稱爲特權方法。面試

6.單例模式

確保一個類只有一個實例,即屢次實例化該類,也只返回第一次實例化後的實例對象。該模式不只能減小沒必要要的內存開銷,而且能夠減小全局的函數和變量衝突。
能夠來看一個簡單的例子:segmentfault

let userInfo = {
    getName() {},
    getAge() {},
}

上面代碼中,使用對象字面量建立的一個獲取用戶信息的對象。全局只暴露了一個 userInfo 對象,好比獲取用戶名,直接調用 userInfo.getName()。userInfo對象就是單例模式的體現。若是把 getName 和 getAge 定義在全局,很容易污染全局變量。命名空間也是單例模式的體現。平時開發網站中的登陸彈窗也是一個很典型的單例模式的應用,由於全局只有一個登陸彈窗。更多的能夠看從ES6從新認識JavaScript設計模式(一): 單例模式這邊文章。設計模式

7.構造函數模式

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function() {
        console.log(obj.name);
    }
}

const person1 = new Person('litterstar', 18);
console.log(person1);

特色:緩存

  1. 可使用 constructor 或 instanceof識別對象實例的類型
  2. 使用 new 來建立實例

缺點:微信

  1. 每次建立實例時,每一個方法都要被建立一次

8.原型模式

function Person() {}

Person.prototype.name = 'litterstar';
Person.prototype.age = 18;
Person.prototype.sayName = function () {
    console.log(this.name);
}
const person1 = new Person();

特色:
方法不會被重複建立

缺點:

  1. 不能初始化實例參數
  2. 全部的屬性和方法都被實例共享
構造函數模式 和 原型模式

閉包的應用場景

1. 模仿塊級做用域

好比咱們可使用閉包能使下面的代碼按照咱們預期的進行執行(每隔1s打印 0,1,2,3,4)。

for(var i = 0; i < 5; i++) {
    (function(j){
        setTimeout(() => {
            console.log(j);
        }, j * 1000);
    })(i)
}
咱們應該儘可能避免往全局做用域中添加變量和函數。經過閉包模擬的塊級做用域

2. 私有變量

JavaScript中沒有私有成員的概念,全部屬性都是公有的。可是有私有變量的概念,任何在函數中定義的變量,均可以認爲是私有變量,由於在函數的外部不能訪問這些變量。私有變量包括函數的參數,局部變量和函數內部定義的其餘函數。

來看下面這個例子

function add(a, b) {
    var sum = a + b;
    return sum;
}

add 函數內部,有3個私有變量,a, b, sum。只能在函數內部訪問,函數外面是訪問不到它們的。可是若是在函數內部建立一個閉包,閉包能夠經過本身的做用域鏈就能夠訪問這些變量。因此利用閉包,咱們就能夠建立用於訪問私有變量的公有方法(也稱爲特權方法)

有兩種在對象上建立特權的方法。
第一種,在構造函數中定義特權方法

function MyObject() {
    // 私有變量和私有函數
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    // 特權方法
    this.publicMethod = function() {
        privateVariable++;
        return privateFunction;
    }
}

這個模式在構造函數內部定義了私有變量和函數,同時建立了可以訪問這些私有成員的特權方法。可以在構造函數中定義特權方法,是由於特權方法做爲閉包有權訪問在構造函數中定義的全部變量和函數。
上面代碼中,變量 privateVariable 和函數 privateFunction() 只能經過特權方法 publicMethod()來訪問。在建立 MyObject 實例後,只能使用 publicMethod來訪問 變量 privateVariable 和函數 privateFunction()

第二種,利用私有和特權成員,能夠隱藏哪些不該該被直接修改的數據。

function Foo(name){
    this.getName = function(){
        return name;
    };
};
var foo = new Foo('luckyStar');
console.log(foo.name); //  => undefined
console.log(foo.getName()); //  => 'luckyStar'

上面代碼的構造函數中定義了一個特權方法 getName(),這個方法能夠在構造函數外面使用,能夠經過它訪問內部的私有變量name。由於該方法是在構造函數內部定義的,做爲閉包能夠經過做用域鏈訪問name。私有變量 nameFoo的每一個實例中都不同,所以每次調用構造函數都會從新建立該方法。

在構造函數中定義特權方法的缺點就是你必須使用構造函數模式。以前一篇文章 JavaScript的幾種建立對象的方式 中提到構造函數模式會針對每一個實例建立一樣一組新方法,使用靜態私有變量實現特權能夠避免這個問題。

3. 靜態私有變量

建立特權方法也經過在私有做用域中定義私有變量或函數來實現。

(function() {
    var name = '';
    //
    Person = function(value) {
        name = value;
    }
    Person.prototype.getName = function() {
        return name;
    }
    Person.prototype.setName = function(value) {
        name = value;
    }
})()

var person1 = new Person('xiaoming');
console.log(person1.getName()); // xiaoming
person1.setName('xiaohong');
console.log(person1.getName()); // xiaohong

var person2 = new Person('luckyStar');
console.log(person1.getName()); // luckyStar
console.log(person2.getName()); // luckyStar

上面代碼經過一個匿名函數實現塊級做用域,在塊級做用域中 變量 name 只能在該做用域中訪問,一樣的經過閉包(做用域鏈)的方式實現 getNamesetName 來訪問 name, 而 getNamesetName 又是原型對象的方法,因此它們成了 Person 實例的共享方法。
這種模式下,name 就變成了一個靜態的、由全部實例共享的屬性。在一個實例上調用 setName() 會影響全部的實例。

4. 模塊模式

模塊模式是爲單例建立私有變量和特權方法。單例(singleton),指的是隻有一個實例的對象。

var singleton = {
    name: value,
    method: function() {},
}

上面使用對象字面量的方式來建立單例對象,這種適用於簡單的應用場景。複雜點的,好比改對象須要一些私有變量和私有方法

模塊模式經過單例添加私有變量和特權方法可以使其加強。

var singleton = function() {
    var privateVarible = 10;
    function privateFunction() {
        return false;
    }

    return {
        publicProperty: true,
        publicMethod: function() {
            privateVarible++;
            return privateFunction();
        }
    }
}

模塊模式使用了一個返回對象的匿名函數。在這個匿名函數內部,首先定義了私有變量和函數.

加強模塊模式

var singleton = function() {
    var privateVarible = 10;
    function privateFunction() {
        return false;
    }

    var object = new CustomType();
    object.publicProperty = true;
    object.publicMethod = function() {
        privateVarible++;
        return privateFunction();
    }
    // 返回這個對象
    return object;
}

在返回對象以前加入對其加強的代碼。這種加強的模塊模式適合單例必須是某種類型的實例。

Vue源碼中的閉包

1. 數據響應式Observer中使用閉包(省略閉包以外的相關邏輯)

function defineReactive(obj, key, value) {
    return Object.defineProperty(obj, key, {
        get() {
            return value;
        },
        set(newVal) {
            value = newVal;
        }
    })
}

value 還函數中的一個形參,屬於私有變量,可是爲何在外部使用的時候給value賦值,仍是能達到修改變量的目的呢。

這樣就造成了一個閉包的結構了。根據閉包的特性,內層函數能夠引用外層函數的變量,而且當內層保持引用關係時外層函數的這個變量,不會被垃圾回收機制回收。那麼,咱們在設置值的時候,把newVal保存在value變量當中,而後get的時候再經過value去獲取,這樣,咱們再訪問 obj.name時,不管是設置值仍是獲取值,實際上都是對value這個形參進行操做的。

2. 結果緩存

Vue源碼中常常能看到下面這個cached函數(接收一個函數,返回一個函數)。

/**
* Create a cached version of a pure function.
*/
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
})
}

這個函數能夠讀取緩存,若是緩存中沒有就存一下放到緩存中再讀。閉包正是能夠作到這一點,由於它不會釋放外部的引用,從而函數內部的值能夠得以保留。

如今再看源碼或者如今再看本身寫的代碼的時候,就會發現,不經意間其實咱們已經寫過和見過不少閉包了,只是以前可能不太認識而已。好比這篇文章 記憶化技術介紹——使用閉包提高你的 React 性能也提到了閉包。

React Hooks中閉包的坑

咱們先來看一下使用 setState 的更新機制:

ReactsetState函數實現中,會根據一個變量isBatchingUpdates 判斷是直接更新this.state仍是放到 隊列中回頭再說。而isBatchingUpdates 默認是false,也就表示setState會同步更新this.state。可是,有一個函數 batchedUpdates, 這個函數會把isBatchingUpdates修改成true,而當React在調用事件處理函數以前就會調用這個batchedUpdates,形成的後果,就是由React控制的事件處理程序過程setState不會同步更新this.state

知道這些,咱們下面來看兩個例子。

下面的代碼輸出什麼?

class Example extends React.Component {
   constructor() {
     super();
     this.state = {
       val: 0
     };
   }
   
   componentDidMount() {
     this.setState({val: this.state.val + 1});
     console.log(this.state.val);    // 第 1 次 log
 
     this.setState({val: this.state.val + 1});
     console.log(this.state.val);    // 第 2 次 log
 
     setTimeout(() => {
       this.setState({val: this.state.val + 1});
       console.log(this.state.val);  // 第 3 次 log 1 
 
       this.setState({val: this.state.val + 1});
       console.log(this.state.val);  // 第 4 次 log 2
     }, 0);
   }
 
   render() {
     return null;
   }
 };

打印結果是: 0, 0, 2, 3。

  1. 第一次和第二次都是在react自身生命週期內,觸發 isBatchingUpdates 爲true, 因此並不會直接執行更新state, 而是加入了 dirtyComponents,因此打印時獲取的都是更新前的狀態 0
  2. 兩次setState時,獲取到 this.state.val 都是 0,因此執行時都是將0設置爲1,在react內部會被合併掉,只執行一次。設置完成後 state.val值爲1。
  3. setTimeout中的代碼,觸發時 isBatchingUpdates爲false,因此可以直接進行更新,因此連着輸出 2, 3

上面代碼改用react hooks的話

import React, { useEffect, useState } from 'react';

const MyComponent = () => {
    const [val, setVal] = useState(0);

    useEffect(() => {
        setVal(val+1);
        console.log(val);

        setVal(val+1);
        console.log(val);

        setTimeout(() => {
            setVal(val+1);
            console.log(val);

            setVal(val+1);
            console.log(val);
        }, 0)
    }, []);
    return null
};

export default MyComponent;

打印輸出: 0, 0, 0, 0。

更新的方式沒有改變。首先是由於 useEffect 函數只運行一次,其次setTimeout是個閉包,內部獲取到值val一直都是 初始化聲明的那個值,因此訪問到的值一直是0。以例子來看的話,並無執行更新的操做。

在這種狀況下,須要使用一個容器,你能夠將更新後的狀態值寫入其中,並在之後的 setTimeout中訪問它,這是useRef的一種用例。能夠將狀態值與refcurrent屬性同步,並在setTimeout中讀取當前值。

關於這部分詳細內容能夠查看 React useEffect的陷阱。React Hooks 的實現也用到了閉包,具體的能夠看 超性感的React Hooks(二)再談閉包

總結

當在函數內部定義了其餘函數,就建立了閉包。閉包有權訪問包含函數內部的全部變量,原理以下:

  • 在後臺執行環境中,閉包的做用域鏈包含它本身的做用域鏈、包含函數的做用域和全局做用域
  • 一般,函數的做用域及其全部變量都會在函數執行結束後銷燬
  • 可是,當函數返回來了一個閉包,這個函數的做用域將一直在內存中保存在閉包不存在爲止。

使用閉包能夠在JavaScript中模仿塊級做用域(JavaScript自己沒有塊級做用域的概念),要點以下:

  • 建立並當即調用一個函數,這樣既能夠執行其中的代碼,又不會在內存中留下對該函數的引用
  • 結果就是函數內部的全部變量都會被銷燬 -- 除非將某些變量賦值給了包含做用域(即外部做用域)中的變量

閉包還能夠用於在對象中建立私有變量,相關概念和要點以下。

  • 即便JavaScript中沒有正式的私有對象屬性的概念,但可使用閉包來實現公有方法,而經過公有方法能夠訪問在包含做用域中定義的變量
  • 可使用構造函數模式,原型模式來實現自定義類型的特權方法也可使用模塊模式、加強的模塊模式來實現單例的特權方法

參考

其餘

最近發起了一個100天前端進階計劃,主要是深挖每一個知識點背後的原理,歡迎關注 微信公衆號「牧碼的星星」,咱們一塊兒學習,打卡100天。同時也會分享一些本身學習的一些心得和想法,歡迎你們一塊兒交流。

相關文章
相關標籤/搜索