JS闭包

一些基础概念

变量的生命周期

  • 全局作用域下的变量,定义的时候开始存在,页面关闭或者刷新就消失。
  • 函数内部的变量,在你写的时候是不存在的,一般来说,只有调用这个函数的时候,这个变量才会被声明。那么这个函数内部的a什么时候消失呢,当a所在的作用域不见的时候a就消失。
function fn () {
    var a = 1;
    // return undefined; 所有函数不写return时,默认有这一句
}
// 浏览器执行到这一行,此时a不存在

fn();//这个时候a就诞生了
//fn()执行完了之后,a就消失了
fn();//这时候会产生一个新的a,并不是原来的a
//fn() 执行完成后,新的a又消失了
  • 如果这个变量被引用着,就不能回收
function fn () {
    var a = 1;
    var b = 2;
    window.xxx = a;
    // return undefined; 所有函数不写return时,默认有这一句
}
// 浏览器执行到这一行,此时a不存在

fn();//这个时候a就诞生了
//此时fn()执行完,b消失,a还存在,因为a被引用了。
console.log(window.xxx); //1
//等window消失a也就消失了
//其实可以认为函数执行完成后,a这个变量名是消失了的,只是这段内存没消失,还在继续被引用,等window消失的时候再释放这段内存

var作用域

  • 就近原则

    var a;
    function fn1 () {
        var a;
        function fn2 () {
            var a;
            a = 1; //给f2的a赋值
        }
    }
    
    function f2() {};
    function f1 () {
        function f2 () {}
        f2(); //调用的是f1内部的f2
    };
  • 词法作用域
    var a;
    function fn1 () {
        var a;
        function fn2 () {
            var a;
            a = 1; //给f2的a赋值
        }
    }

不需要执行代码,分析语句的词法就能判断作用域叫词法作用域

  • 同名的不同变量

立即执行函数

  • 如果想得到一个独立的作用域,必须要声明一个函数
  • 如果想运行函数内部的代码,必须要执行函数
    立即执行函数即创建一个匿名函数作用域并且立即执行。前面可以是! + - 等符号。
  • 关于有没有var声明的区别:

var a = 100;

!function() {
    var a = 1;
    !function() {
        a = 2; // 没有用var声明,所以不是当前作用域的a,而是上一级的作用域的a,所以改变了上一级a的值
        console.log(a);//2
    }();
    console.log(a);//2   a的值被改变 输出2
}();
console.log(a);//100
  • 立即执行函数的传参:
    var a = 1;
    !function(a) { //这里的a是个形参
        console.log(a); //外面传入一个a,里面的作用域输出2
    }(2);
    console.log(a); //全局作用域下的a还是1


    //上面的形参a其实相当于下面这么写
    !function(){
        var a = arguments[0];
        console.log(a);//外面传入一个a,里面的作用域输出2
    }(2);

传参例子2


var a = 100;

!function(a){
    console.log(a);
}(a);
//这里的两个a,传入的a是外部传入的值,全局变量a,而匿名函数的a仅仅是个形参  等于不写参数,里面一个var a = arguments[0];
//只是在执行函数的时候恰好把传入参数的a赋值给了内部的a

变量(声明)提升

  • 浏览器在执行代码之前,会把所有声明提升到作用域的顶部
var a = 100;
var a = function () {};
function a() {};
console.log(a); //function
//等价于
var a;
var a;
function a () {};
a = 100;
a = function() {};
console.log(a); // 所以是function
  • 函数内部的变量提升
//函数内部的声明提升,不会提升到全局或者外部的作用域的,就只会提升到当前作用域顶部
function f1() {
    // var a = 100; 变量提升等价于
    var a;
    a = 100;
}
f1();
//另外,上述代码和下面的是一样的
function f1() {
    a = 100;
    var a;
}
//只有在当前作用域中有var,就不会跑出当前作用域
//如果是
var a = 1;
function f1 () {
    a = 100;
//    后面语句里没有用var声明a,那么这个a就不是当前f1下的a。会在f1的上级作用域中寻找a,并且改变a的值
}
f1();
console.log(a); //100
  • 带有迷惑性的变量提升
    先进行提升,在考虑代码
var a = 100;  // 第一个a
function f1 () {
    var b =2;
    if (b === 1) {
        var a; //第二个a
    }
    a = 99;
}
//上述例子中,f1内部写的a = 99指的是谁? 
// 进行变量提升,首先明确JS没有块级作用域,所以什么if for都不是新的作用域,变量提升的时候也不用考虑if的判断条件
//总而言之,先进行提升,在考虑代码,即f1内部等价于
var a = 100;
function f1 () {
    var b;
    var a;
    b = 2;
    if(b === 1){
        
    }
    a = 99;
}
//提升之后自然发现指的是f1内部的a

闭包

var items = document.querySelectorAll('li');
for(var i = 0; i < items.length; i ++) {
    items[i].onclick = function () {
        console.log(i);
    }
}

上述例子分析:

var items = document.querySelectorAll('li');
for(var i = 0; i < items.length; i ++) {
    items[i].onclick = function () {
        console.log(i);
    }
}
console.log(i);

//进行变量提升
var items;
var i;
//这里提升了i,整个作用域中只有这一个i,所以循环到6之后,所有的i的值都为6
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    items[i].onclick = function () {
        console.log(i); //C
    }
}
console.log(i); // D
// 变量提升完成后 D是一定比C先执行的
// 当D执行时 i已经循环为6,所以后面C输出的都是6


//所以如果要想使得每次输出的值不一样,就得使得每次的i不是同一个i
//要取得不同的i,就要创建新的作用域,那么我们来创建新的作用域:
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    function temp (i) {//创建一个新的函数作用域,在这个作用域中传入每次循环得到的i的值,就能得到不一样的i了
    //    还要注意前面提到的问题,temp(i);中的i和函数的形参i不是同一个东西,具体见立即执行函数的传参
        items[i].onclick = function () {
            console.log(i); //接收到每次的i值然后赋给i,这样每次循环的i的值就不一样了
        }
    }
    temp(i); // 传入每次循环的i值

}
//那么前面提到的立即执行函数可以简化代码,不用写temp(),直接写个立即执行函数并且传入i就好了
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    !function(i) {  //写成立即执行函数简化代码
        items[i].onclick = function () {
            console.log(i); //接收到每次的i值然后赋给i,这样每次循环的i的值就不一样了
        }
    }(i);

}

//上述代码就等价于下面的,temp函数时一个返回函数的函数,创造一个函数用于绑定事件
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    function temp(i) {  // 这里temp(i)其实也是要提升的,但是只要知道内部的i跟外部i不一样就好了
        return function () {
            console.log(i);
        }
    }

    var fn = temp(i); //传入每次的i的值  fn必须为一个函数才能赋给onclick事件,所以temp的返回值必须也是一个函数

    items[i].onclick = fn;
}


// 有了返回值为一个函数的思想,我们可以直接给onclick赋给一个返回函数的函数,这样创建了一个新的作用域保存每次循环的i值,如下:
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    items[i].onclick = function(i) {
        //函数内部作用域的i和外部的i不同
        return function (){
            console.log(i);
        }
    }(i); //传入每次循环的i值

}

总结一下思路:

  • 要得到每次循环的i值,就必须得创建新的作用域。因为全局作用域当中只有一个i,是不会变的,需要一个新的作用域来获取每次循环的i的值。
  • 我们给onclick绑定的一定是一个函数,所以赋给onclick一个立即执行函数之后,这个立即执行的值,即return的东西一定是一个函数。这就是为什么一定要return一个函数。

通过作用域链理解闭包

第一个例子:

    //通过作用域链理解闭包


    //下例中定义一个数组,遍历数组给每一项赋给一个函数,最后输出的 fnArr[1]的执行结果()   发现结果都是2,不管是fnArr[1]还是fnArr[0]
    // var fnArr = [];
    // for (var i = 0; i < 2; i ++) {
    //     fnArr[i] = function() {
    //         return i;
    //     }
    // }
    // console.log( fnArr[0]() ); //2
    // console.log( fnArr[1]() ); //2
    //分析: 当我们写一段函数体或者函数名的时候,就是一段代码,一个指针或者一个地址,只有后面加了括号(),它才会真正的去执行
    //没有执行的话,就相当于没有任何的作用,就是一段代码。
    //所以在上面的for循环中,赋给fnArr[i]的时候没有执行,当循环结束时,i的值为2,此时调用fnArr[1]()或者fnArr[0]并且输出,
    // 此时函数才会真正的执行,但是这个是函数里面没有i,只会向外部的全局作用域中寻找i,即for循环中的i,为2,所以不管fnArr[0]还是fnArr[1]都输出2


    //针对上面的例子,我们提出了对代码的改装要求,要求fnArr[0]就是输出0,fnArr[1]就是输出1。要做到这个效果,就要用到闭包
    // 第一种改装方法:
    // var fnArr = [];
    // for (var i = 0;i < 2; i ++) {
    //     (function(i){
    //         fnArr[i] = function () {
    //             return i;
    //         }
    //     })(i);
    // }
    // console.log( fnArr[0]() ); //0
    // console.log( fnArr[1]() ); //1
//    针对第一种改装方法,就等价于下面这么写,
//    第一种改装方法的改写1,去掉for循环:
//     var fnArr = [];
//     (function(i){
//         fnArr[i] = function () {
//             return i;
//         }
//     })(0);
//     (function(i){
//         fnArr[i] = function () {
//             return i;
//         }
//     })(1);
//     console.log( fnArr[0]() );
//     console.log( fnArr[1]() );
//    这种写法还有立即执行函数,不好理解,接着改写2,去掉立即执行函数,就等价于:
//    第一种改装方法的改写2:
    var fnArr = [];
    function fn1 (i) {
        fnArr[i] = function fn11() {
            return i;
        }
    }
    function fn2 (i) {
        fnArr[i] = function fn22 () {
            return i;
        }
    }
    fn1(0);
    fn2(1);

    console.log( fnArr[0]() );
    console.log( fnArr[1]() );

//    那么通过作用域链来分析改写2:
    globalContext = {
        AO : {
        //    活动对象,变量提升
            fnArr: undefined,
            fn1: function,
            fn2: function,
        }
    }
    fn1.[[scope]] = globalContext.AO;
    fn2.[[scope]] = globalContext.AO;

    fn1Context = {
        AO: {
            i: 0,
            fn11: function,
        },
        scope: fn1.[[scope]]
    }
    fn11.[[scope]] = fn1Context.AO;
    //运行到fn1的时候,没有自身的AO里面没有fnArr,所以就要到scope里面去找,fn1.[[scope]]是globalContext的AO,找到了fnArr
    //此时globalContext的AO的fnArr的值就由undefined变为[fn11],然后什么都没发生,退出来进入fn2
    fn2Context = {
        AO : {
            i: 1,
            fn22: function,
        },
        scope: fn2.[[scope]],
    }
    fn22.[[scope]] = fn2Context.AO;
    //此时进入fn2,寻找fnArr和上面一样,过程结束后,globalContext的AO里面的fnArr的值为[fn11,fn22];
    //现在    console.log( fnArr[0]() ); 要输出fnArr[0]() ,即在globalContext中找到AO里面的fnArr[0]并且执行,此时进入fn11的context
    fn11Context = {
        AO: {
        //    AO是空的
        },
        scope: fn11.[[scope]],
    }
    // fn11没有AO,所以只能通过scope来寻找i,fn11.[[scope]]就是fn1Context.AO, 所以找到了i,i为0,
    // 一样的,当你输出fnArr[i]的时候,结果是调用fn22,然后找到fn2中的AO的i,为1
    fn22Context = {
        AO: {
            //    AO是空的
        },
        scope: fn22.[[scope]],
    }
    //那么对比我们一开始没有达成效果的例子,那个例子是因为在当前函数中找不到i,所以直接去全局中找到了i
    //我们现在改写了之后,在函数和全局之间又加了一层包装,在scope的中途就能获取到i,所以能够达成效果,这就是闭包

第二个例子:

    //
    function fn () {
        var s  = 1;
        function sum () {
            ++s;
            console.log(s);
        }
        return sum;
    }
    var mySum = fn();
    mySum(); //2
    mySum(); //3
    mySum(); //4
    var mySum2 = fn();
    mySum2(); //2
    mySum2(); //3
    //上述写法等价于
    // function fn () {
    //     var s = 1;
    //     return function () {
    //         ++s;
    //         console.log(s);
    //     }
    // }
    //同样的,使用作用域链的伪代码来分析:
    globalContext = {
        AO : {
            fn: function,
            mySum: undefined,
            mySum2: undefined,
        }
    }
    fn.[[scope]] = globalContext.AO;
    //mySum = fn()时,进入fn的执行上下文
    fnContext = {
        AO: {
            s: 1,
            sum: function,
        }
        scope: fn.[[scope]]
    }
    sum.[[scope]] = fnContext.AO;
    //执行完成之后退出fn,此时globalContext的AO里面的mySum的值由undefined变为fn(),即sum,调用mySum(),即执行sum,此时进入sum的context
    sumContext = {
        AO : {
        //    为空
        },
        scope: sum.[[scope]],
    }
//    sum没有AO,找不到s,所以到scope里面,即fn的AO里面找到s,此时s的值由1变为2.
//    再次执行mySum(),此时sum还是没有s,继续到scope中去找,即找到了fnContext的AO的s,此时s为2,所以由2变为3

//    然后执行到了mySum2 = fn(),此时,会初始化一个执行上下文,此时的s为1
    fnContext = {
        AO: {
            s: 1,
            sum: function,
        }
        scope: fn.[[scope]],
    }
    sum.[[scope]] = fnContext.AO;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容