我是怎么搞懂闭包的?

先从一个简单的闭包开始

想从函数外部访问到函数内部的变量,固然是不能够访问的,可是你若是在这个函数里面返回一个方法用来对这个函数里面的变量进行操做的话,就能够像是在外面访问量函数内的变量(其实我暂时是认为没有访问函数里面变量的,实质上只是调用了对相应参数进行操做的方法),举个栗子把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 只有「建立」和「初始化」,没有「赋值」过程。都被提高了。
相关文章
相关标签/搜索