笔记:JavaScript闭包

本文摘录及参考自:
1. 学习Javascript闭包(Closure)
2. 闭包的秘密
3. JavaScript 闭包
4. JavaScript深入之闭包
5. JavaScript深入之执行上下文
5. 闭包-JavasScript
6. 闭包
7. 闭包,懂不懂由你,反正我是懂了
8. 什么是闭包,我的理解

闭包的由来

1.JS变量的作用域

要理解闭包,首先要理解JavaScript的变量作用域。与大多数语言相同,JavaScript变量的作用域分为全局变量和局部变量。函数内部可以访问全局变量,但是函数外部无法访问函数内部的局部变量

例:

function f1(){
  let  n =100;
  var m=99;
  console.log(n); 
  console.log(m);  
}
f1();   //输出:100 , 99
console.log(n); //输出:undefined
console.log(m);  //输出:undefined

注意,在函数内部声明变量的时候一定要用let或者var。否则,实际上声明了一个全局变量

2. 函数外部如何读取局部变量

要在函数外部读取局部变量,可以通过在函数内部再定义一个函数的方法来实现

function f1(){
  let  n =100;
  var m=99;
  function f2(){
    console.log(n);
    console.log(m);
  }
  return f2;
}

let result =f1();
result();  //输出100,99

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的(子对象会一级一级向上寻找父对象的变量)。 所以,父对象的所有变量,对于子对象都是可见的,反之则不成立。因此,只要返回f2,我们就可以在f1的外部读取到局部变量了(通过f2访问)。

3. 闭包的概念

上一节代码中的f2函数,就是闭包。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部链接起来的一座桥梁。

4. 闭包的用途

闭包有两个非常重要的用途:

  • 可以读取函数内部的变量
  • 让这些变量的值使用保持在内存中(不被垃圾回收)

例:

function f1(){
    var n = 999;
    nAdd = function(){n=n+1};
    function f2(){
        alert(n);
    }
    return f2;
}

var result = f1();
result(); //输出:999
nAdd(); 
result(); //输出:1000
   

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次值是999,第二次值是1000。 这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。原因在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收。此外,这段代码中,nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。

例:

var funArr = []
for(var i=0;i<6;i++){
    funArr[i] = function(){
        console.log(i);
    }
}
funArr.forEach( (f) => f() )   //输出:6,6,6,6,6,6

查看控制台输出结果为 6,6,6,6,6,6。原因是f()函数保存的变量i,在其执行时,已经被赋值为6。可以使用闭包保存变量

var funArr = []
for(var i=0;i<6;i++){
    funArr[i] = (function(j){
            return function(){
                console.log(j);
            }
        }
    )(i)
}

funArr.forEach( (f) => f() )   // 0,1,2,3,4,5

注意,funArr[i]实际上获得的赋值是一个函数 function(){ console.log(j) } ,所以之后调用funArr[i]实际上就是调用console.log(j)。 闭包会持有父方法的局部变量并且不会随父方法销毁而销毁,因此j被一直保留在内存中。

避免使用过多的闭包,可以用let关键词:

var funArr = []
for(var i=0;i<6;i++){
    let temp = i;
    funArr[i] = function(){
        console.log(temp);
    }
}
funArr.forEach( (f) => f() )   // 0,1,2,3,4,5

这个例子使用let而不是var,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。

5. 闭包原理解析

要理解JavaScript闭包的原理,首先要先理解它的执行上下文,以下面这段代码为例进行分析
例:

let scope = "global scope";
function checkscope(){
    let scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

let foo = checkscope();
console.log(foo()) ; //local scope

简要分析上述代码的执行过程:

  1. 进行全局代码,创建全局执行上下文,全局执行上文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行checkscope函数,创建checkscope函数执行上下文,checkscope执行上下文被压入执行上下文栈
  4. checkscope执行上下文初始化,创建变量对象、作用域链、this等
  5. checkscope函数执行完毕,checkscop执行上下文从执行上下文栈中弹出
  6. 执行f函数,创建f函数执行上下文,f执行上下文被压入执行上下文栈
  7. f执行上下文初始化,创建变量对象、作用域链、this等
  8. f函数执行完毕,f函数上下文从执行上下文栈中弹出

问题1: 当f函数执行的时候,checkscope函数上下文已经被销毁,f函数为什么还可以读取到checkscope作用域下的scope值?

在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 checkscope() 执行完毕,我们会认为 scope 变量将不能被访问。

然后JavaScript却是可以的
当我们了解了具体的执行过程后,我们知道f执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。总结一下函数的作用域链的规则,函数会把自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以及类推直至全局对象为止。

6. 自检

  1. 一道非常有意思的考题,如果能清楚的知道为什么输出99和100,对于闭包算是理解清楚了
let nAdd;
let t = () => {
    let n = 99;
    nAdd = () => {
        n++;
    };
    let t2 = () => {
        console.log(n);
    };
    return t2;
};

let a1 = t();
let a2 = t();

nAdd();
a1();    //99
a2();    //100

当执行let a1= t() 的时候,变量nAdd被赋值为一个函数,该函数为fn1。接着执行let a2=t()的时候,变量nAdd又被重写了,这个函数跟以前的函数长得一摸一样,是fn2。所以当执行nAdd函数的时候,我们执行的其实是fn2,而不是fn1,我们更改的是a2形成的闭包里的n的值,并没有更改a1形成的闭包里的n的值。所以a1()的结果是99, a2()的结果是100.

7. 闭包可能引起的问题

内存泄漏

当闭包造成JavaScript对象之间的循环引用,导致对象无法被垃圾回收机制回收时,就导致了内存泄漏。

例:

function example(){  
    var element =document.getElementByID("div"); //①  
    element.onclick = function() {  
        alert("This is a leak!"); //②  
    }  
}  

由于JavaScript对象element引用了一个DOM对象,该DOM对象的onclick属性引用了匿名函数闭包,而闭包可以引用外部函数example()的整个活动对象,包括element,因此形成了dom对象引用js对象,js对象又引用dom对象的闭环。

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

推荐阅读更多精彩内容