我是怎麼搞懂閉包的?

先從一個簡單的閉包開始

想從函數外部訪問到函數內部的變量,固然是不能夠訪問的,可是你若是在這個函數裏面返回一個方法用來對這個函數裏面的變量進行操做的話,就能夠像是在外面訪問量函數內的變量(其實我暫時是認爲沒有訪問函數裏面變量的,實質上只是調用了對相應參數進行操做的方法),舉個栗子把java

function test(){
    var i = 0;
}
console.log(++i);
這固然會報錯
而後若是想執行這樣的操做呢
閉包是這麼解決的
我在方法裏面返回一個這樣的操做就行了
function test(){
    var i = 0;
    return function(){
        console.log(++i);
    }
}
var doAdd = test();
doAdd();
doAdd();

因此我認爲就是返回一個函數來供調用自加es6

問題

可是吧,我說這是一個閉包,你確定會認爲是的,若是要你本身寫一個閉包出來,那就寫不出了,確定會遇到不少問題,因此接下來我就講講我學習閉包的過程吧。閉包

先從閉包開始

先是在犀牛書上看閉包,而後他就要我先去了解,做用域和做用域鏈。函數

做用域

在沒有es6的let以前js的做用域只是函數做用域,每一個函數有本身的做用域,而後其餘的就是全局做用域了。學習

做用域鏈

在全局中每定義一個變量,這些變量都會存在於window下,也能夠經過最外層的this調用到優化

當即執行函數表達式

在我手寫閉包的時候發現這樣編寫能夠實現累加this

var test2 = (function () {
    var a2 = 0;
    return function () {return ++a2;}
})()
console.log(test2())
console.log(test2())

這麼寫的話每次都是一個新的累加.net

var tests = function () {
    var a = 0;
    return function () {return ++a;}
}
console.log((tests())())
console.log((tests())())
因而我就開始思考是否是當即執行函數的緣由
因而立刻學習了一波當即執行函數(IIFE)
其實當即執行函數就是在定義函數的時刻,立刻去把這個函數執行了

一個簡單的栗子code

(function () { dosomething; })();

若是這個轉換成正常的函數是對象

var a = function () { dosomething; }
a();

因此咱們上面的閉包的代碼轉換成正常的函數是

var test2 = function () {
    var a2 = 0;
    return function () {return ++a2;}
}
var test = test2()
console.log(test())
console.log(test())
若是這樣的話就能夠實現累加
和咱們上面的代碼一比較就發現
console.log((tests())())
他在(tests())的時候定義了一個空間而後執行了(tests())()方法
可是第二次的時候又分配了一個新的空間
而後在新的空間裏面累加,固然不會記錄在一塊兒
然而咱們當即執行函數的寫法是建立了一個test的空間
而後其餘的操做都是在這個閉包的空間裏面執行
的(也就是在test2返回的那個函數的空間裏面)
function test () {
    var i = 0;
    return function () {
        document.write(++i)
        document.write('<br>')
    }
}
var test1 = test();
test1();
test1();
var test2 = test();
test2();
test2();

test1和test2是兩個不一樣的閉包空間

再次對閉包的理解

看了廖雪峯大佬的博客忽然對閉包有了另一種理解

閉包就像java裏面類的私有變量同樣
你在外面經過實例化了也不能操做那些私有變量
你只能經過get方法和set方法等相似的方法來操做這個值
並且你實際上並非操做了這個值
你知道調用了這個對象裏面的方法
而後這個對象裏面的方法來操做這個值

上代碼

function student () {
    var name = 'xxx';
    var getName = function () {
        return name
    }
    var setName = function (newName) {
        name = newName;
    }
    return {
        getName: getName,
        setName: setName,
    }
}
var studentA = student();
console.log(studentA.getName())//xxx
studentA.setName("aaa");
console.log(studentA.getName())//aaa

而後出現新問題

問題1

我跟覺得大佬分享後她跟我提出

若是在return那裏加個name:name 而後studenta.name結果是啥

我發現竟然仍是以前的xxx我改爲aaa了並無變化
後來發如今get和set裏面加了個this後再調用name的時候就能夠看到變化了

function student () {
    var name = 'xxx';
    var getName = function () {
        return this.name
    }
    var setName = function (newName) {
        this.name = newName;
    }
    return {
        getName: getName,
        setName: setName,
        name: name
    }
}
var studentA = student();
console.log(studentA.getName())//xxx
studentA.setName("aaa");
console.log(studentA.getName())//aaa
console.log(studentA.name);//aaa

開始對閉包有一個很是大的誤解,一直覺得我修改這個閉包的變量的值就會直接修改前面的方法裏面的變量(直接修改student這個方法裏面的name),其實不是的,當你建立一個閉包後(var studentA = student())而後內存中就會對應於你建立的閉包分配一個空間(而後直接修改是修改studentA閉包裏面的name)(不信的話你能夠修改了name後再新建一個studentB發現仍是最開始的值)。
而後我又有一段時間把閉包分配的空間和我return的這個對象視爲一個空間了,其實否則,返回的對象是用studentA接受的一個空間,而閉包的空間是對應studentA生成的空間

對應上面爲何用this的話就能夠name的值,用以前的setName改變的值只能經過getName查看。由於不用this和用this修改的地方不一樣,你能夠在setName中試着輸出this,由於this在studentA中,因此這裏的this指向的是return的對象。因此加了this是修改return對象,沒加this是修改閉包的原始值。

function student () {
    let name = 'xxx';
    let getName = function () {
        return name;
    };
    let getName2 = function () {
        return this.name;
    };
    let setName = function (newName) {
        name = newName;
    };
    let setName2 = function (newName) {
        this.name = newName;
    };

    return {
        getName: getName,
        getName2: getName2,
        setName: setName,
        setName2: setName2,
        name2: name                //這裏我寫成Rname爲了後面setName2會看到新建了一個屬性
    };
}

let studentA = student();          //在這裏你分配了兩個空間,一個是return的空間還有一個是studentA對應的閉包的空間

console.log(studentA.name2);       //xxx        (在return中的name2的值是xxx)
console.log(studentA.getName());   //xxx        (在閉包空間中name的值是xxx)
console.log(studentA.getName2());  //undefined  (由於你在return的對象裏面沒有name這個屬性,因此就根本找不到)

studentA.setName('aaa');           //修改了閉包原始值
console.log(studentA.name2);       //xxx        (在return中的name2的值是xxx)
console.log(studentA.getName());   //aaa        (在閉包空間中name的值是aaa)
console.log(studentA.getName2());  //undefined  (由於你在return的對象裏面沒有name這個屬性,因此就根本找不到)

studentA.setName2('bbb');           //修改了return對象,可是發現沒有name這個屬性因而建立了一個name賦值爲bbb
console.log(studentA.name2);       //xxx        (在return中的name2的值是xxx)
console.log(studentA.getName());   //aaa        (在閉包空間中name的值是aaa)
console.log(studentA.getName2());  //undefined  (在return中的name的值是bbb)

let studentB = student();          //兩個新的空間一個studentB的對象,一個studentB對應的閉包

console.log(studentB.name2);       //xxx        (在return中的name2的值是xxx)
console.log(studentB.getName());   //xxx        (在閉包空間中name的值是xxx)
console.log(studentB.getName2());  //undefined  (在return中沒有name)

問題2

還有一個問題是一個很經典的問題

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push(function () {
            return i;
        });
    }
    return arr;
}
var results = count();
console.log(results[0]());//4
console.log(results[1]());//4
console.log(results[2]());//4

按道理是輸出1,2,3呀
而後大佬的解釋是

返回閉包時牢記的一點就是:返回函數不要引用任何循環變量,或者後續會發生變化的變量。

若是必定要引用循環變量怎麼辦?方法是再建立一個函數,用該函數的參數綁定循環變量當前的值,不管該循環變量後續如何更改,已綁定到函數參數的值不變:

這是優化後的代碼,加了一個當即執行函數後讓i等於1的時候當即把push執行。

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push((function (n) {
            return function () {
                return n;
            }
        })(i));
    }
    return arr;
}
var results = count();
console.log(results[0]());//1
console.log(results[1]());//2
console.log(results[2]());//3

可是爲何是這樣的呢

是由於在一個方法或者是變量的定義到執行會經歷
1.進入編譯環境
2.建立變量i
3.把i先初始化爲undefined
4.執行代碼給i賦值
因此在push裏面的方法,在循環階段的時候只進過了建立、初始化、賦值的操做,並無執行,因此裏面的i仍是i沒有變成真正的值。只有在後面執行的時候纔會去找這個i。
因此只須要改爲當即執行函數就能夠在定義的時候同時執行一波,而後這個i就已經賦值了,後面就不會改變了。

而後最後附上一個總結(來自這裏

  1. let 的「建立」過程被提高了,可是初始化沒有提高。
  2. var 的「建立」和「初始化」都被提高了。
  3. function 的「建立」「初始化」和「賦值」都被提高了。
  4. const 和 let 只有一個區別,那就是 const 只有「建立」和「初始化」,沒有「賦值」過程。都被提高了。
相關文章
相關標籤/搜索