mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JavaScript深入之闭包

mqyqingfeng opened this issue · comments

定义

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

举个例子:

var a = 1;

function foo() {
    console.log(a);
}

foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。

那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……

还真是这样的!

所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。

咦,这怎么跟我们平时看到的讲到的闭包不一样呢!?

别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量

接下来就来讲讲实践上的闭包。

分析

让我们先写个例子,例子依然是来自《JavaScript权威指南》,稍微做点改动:

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

var foo = checkscope();
foo();

首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。

另一个与这段代码相似的例子,在《JavaScript深入之执行上下文》中有着非常详细的分析。如果看不懂以下的执行过程,建议先阅读这篇文章。

这里直接给出简要的执行过程:

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

了解到这个过程,我们应该思考一个问题,那就是:

当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

以上的代码,要是转换成 PHP,就会报错,因为在 PHP 中,f 函数只能读取到自己作用域和全局作用域里的值,所以读不到 checkscope 下的 scope 值。(这段我问的PHP同事……)

然而 JavaScript 却是可以的!

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

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

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

所以,让我们再看一遍实践角度上闭包的定义:

  1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  2. 在代码中引用了自由变量

在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:

This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.

闭包在计算机科学中也只是一个普通的概念,大家不要去想得太复杂。

必刷题

接下来,看这道刷题必刷,面试必考的闭包题:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

答案是都是 3,让我们分析一下原因:

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

data[1] 和 data[2] 是一样的道理。

所以让我们改成闭包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

跟没改之前一模一样。

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

匿名函数执行上下文的AO为:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。

data[1] 和 data[2] 是一样的道理。

下一篇文章

JavaScript深入之参数按值传递

相关链接

如果想了解执行上下文的具体变化,不妨循序渐进,阅读这六篇:

《JavaScript深入之词法作用域和动态作用域》

《JavaScript深入之执行上下文栈》

《JavaScript深入之变量对象》

《JavaScript深入之作用域链》

《JavaScript深入之从ECMAScript规范解读this》

《JavaScript深入之执行上下文》

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

支持下一啊,虽然对闭包已经看了很多了,每次看一遍都会有一番不同的感受,学习就是一个重复的过程。

请问下学长为什么

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

这里是3??怎么来的 为什么不是0,1,2

当执行到data[0]函数的时候,for循环已经执行完了,i是全局变量,此时的值为3,举个例子:

for (var i = 0; i < 3; i++) {}
console.log(i) // 3

循环结束后

data[0] = function(){console.log(i)}
data[1] = function(){console.log(i)}
data[2] = function(){console.log(i)}

执行data[0]()data[1]()data[2]()时,i=3,所以都打印3
这个例子看完N遍后终于知道原理了

commented
匿名函数Context = {
    AO: {
        arguments: {
            0: 1,
            length: 1
        },
        i: 0
    }
}

里面的arguments0:1为什么是1呢,按我的理解应该和i的值相同,所以不是0: 0

@fi3ework 嗯,这里是笔误,感谢指出,o( ̄▽ ̄)d

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(c); 
        console.log(a);
    }
    fn = innnerFoo; 
}

function bar() {
    var c = 100;
    fn(); 
}

foo();
bar();

大神,帮我把这个例子分析下?自己解释感觉说服不了自己,c 为什么会报错,我怎么感觉会读取到bar 执行上下文中变量对象c

@xdwxls 词法作用域的问题,具体可以看第二篇《JavaScript深入之词法作用域和动态作用域》,关于这道题,你可以简单理解为函数能够读取到的值跟函数定义的位置有关,跟执行的位置无关

大神,那是不是执行上下文中的作用域scope仅是父级的一个VO记录,不会像跟ECStack那样跟函数执行的次序有关呢?

@xdwxls 我觉得可能是虽然fn(),即innerFoo()是在bar里面执行的,但是innerFoo函数的时候他的作用域scope里面分别是[AO,fooContext.AO,globalContext.AO],并没有包括barContext.AO在里面,所以根本就没有声明c这个变量,所以会显示is not define,我就猜猜而已......

@tangshuimei 是的,你可以这样理解,如果要更严谨的话,可以说,执行上下文中的作用域 scope 是由函数的 [[scope]]属性初始化,而函数的[[scope]] 属性保存了函数创建时词法层面上的父级们的 VO 引用,跟函数的执行顺序无关。

@tangshuimei 哈哈,关于这道题的分析,我赞同你的观点~

@tangshuimei 你说 innerFoo函数的时候他的作用域scope里面分别是[AO,fooContext.AO,globalContext.AO], 那这个时候AO 具体表示什么呢???有点费解

@xdwxls AO 表示活动对象,储存了函数的参数、函数内声明的变量等,在 innnerFoo 中,查找变量 c,就要在 innerFoo 函数的作用域链,也就是 [AO,fooContext.AO,globalContext.AO] 中找到变量 c 的声明,因为没有,所以最终会报错~

我在想,我们在想这个作用域链的时候是不是把for循环的AO给漏了?比如说下面这个例子:

var data = [ ];
for( var i=0; i<3 ; i++ ){
        data [ i ] = function ( ) {
            console.log ( i );
        };
       data [ i ]( i );
}

这里返回的是1,2,3

@frankchou1 for 循环不会创建一个执行上下文,所有不会有 AO, i 的值是在全局对象的 AO 中,代码初始的时候为:

globalContext = {
    VO: {
        data: [...],
        i: 0
    }
}

代码执行的时候,不断修改 i 的值

@frankchou1 看你修改了几次格式,Github 的评论支持 markdown 格式,使用代码块可以用 ```js 和 ``` 包裹

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

求教下,要是把var i 改成let 这个原理有事怎么样子的呢?

@Muscliy let 关键字将 for 循环的块隐式地声明为块作用域。而 for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
这是《你不知道的 JavaScript》中的解释。

@bighuang624 谢谢,我刚刚用babel 转了下发现其实多了_loop的函数,这个就解释的通了,看来《你不知道的 JavaScript》 这个书很好

"use strict";

var data = [];

var _loop = function _loop(i) {
  data[i] = function () {
    console.log(i);
  };
};

for (var i = 0; i < 3; i++) {
  _loop(i);
}

data[0]();
data[1]();
data[2]();

非常感谢博主!以前对闭包总是雾里看花,终隔一层,提到闭包,有人说函数就是闭包,有人说必须是嵌套,又是引用怎么怎么样,其实现在看来,两者都是,只不过是一种狭义和广义上概念的区别。
另外,通过楼主的分析,渐渐发现,只要理清了变量的查找规则,AO对象词法分析期和执行期的变化,闭包这东西,正是基于这些规则下产生的一种自然而然的现象。

看了这么多写闭包的,这个是我看完之后唯一恍然大悟的,之前都是一知半解的。感谢,比心💟

commented

学习了啊

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

答案是都是 3,让我们分析一下原因:

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

data[0]的时候i不是0吗?为什么是3,整个循环走完了吗?

@jasonzhangdong 正是如此,data[0] 是一个函数名,data0 表示执行这个函数,当执行函数的时候,循环已经走完了,i 的值为 3:

for (var i = 0; i < 3; i++) {

}
console.log(i) // 3

文中的:“ 即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中 ” 。"活在内存中" 这句话有点描述不完整的感觉。是所有的闭包函数都销毁之后,都会存活在内存中吗?哪什么时候才会从内存中删除呢?

commented

第一个例子,换成let,就变成012了

@jxZhangLi 非常好的问题,要解决这个问题,还要了解一些垃圾回收机制,不过我还没有怎么研究过……关于这个问题,我个人的看法是,并不会被删除,这些变量放在闭包和放在全局作用域,对内存而言是一样的。

@imnaifu 是的,这是 ES6 的特性,可是为什么换成 let 就会正常打印 012 呢?

@mqyqingfeng @jxZhangLi 第一点是内层函数f引用了自由变量scope;第二点是函数f返回到全局作用域中去了,这点使得f从根出发可到达;所以标记清除法并不会去清除这个scope,个人的一点理解。

@sinkinlife 我对标记清楚和引用计数这两种方法还没有怎么了解过,感谢补充哈~

在闭包这一节还有个很经典的loop

for (var i = 1; i <= 5; i++) {
    setTimeout((function(i) {
        return function() {
            console.log(i)
        }
    })(i), i * 1000)
}

输出值为1,2,3,4,5通过看全文我能够理解了。

关于打印i的行为为什么是每隔1s触发一次?

原因是因为setTimeout行为是表示在特定时间后将代码插入到队列中,所以在本段代码中,假设从0开始计时,则一次在第1秒,第2秒...第5秒的时候,将打印任务插入在队列中。若队列空闲,则会执行,因此就会在第1秒,第2秒...第5秒的时候分别打印i的值(说明队列基本空闲,一旦有任务插入,直接就执行了)

感谢大神 @frankchou1

你可以尝试用声明事件来理解,就是你们平时用的click事件这些(都是异步),你一次性声明了这么多个click,但执行的时候也只有你触发它的时候。对照settimeout来看,它其实也相当于一个声明,真正执行的时候是你设的那个时间。

@wnbupt 很抱歉回复晚了 @frankchou1 感谢回答哈~

@wnbupt 这里可以补充一篇事件循环原理 JavaScript 运行机制详解:再谈Event Loop,执行 for 循环的时候,相当于执行多个 setTimeout 函数,相当于往任务队列中添加进了多个任务,这些任务的起点时间基本是一致的,js 会轮询检查是否有任务完成,最终的效果会是相隔 1 s

必包只是词法作用域的一个规则而已。。。大佬们你们觉得的呢😄

首先感谢你的分享,但是呢看了你写的 js 深入系列这些文章,我是越看越懵比,就拿你这篇文章中例子:

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

var foo = checkscope();
foo();

你的解释我是一脸懵比,我直接翻开 javascript权威指南这本书的 183页面,解释非常清楚, 还有我看 阮老师的js标准教程 也是行云流水很流畅,看了你这个系列的 “JavaScript深入之从ECMAScript规范解读this” 也是很懵比,很有读者看了也有跟我一样的感觉,我直接看阮老师的 http://javascript.ruanyifeng.com/oop/this.html **很清晰啊,我不知道是不是作者表达的太乱了

@youbooks 好吧~~~

你讲的其他的我都还可以理解,但是这个闭包一直没理解,之后看了 http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
阮叔的博客,感觉比较清晰,建议结合他的方式改进,说不定效果会更好 😄

对于下面这个例子,我们是不是可以理解为,即便 fooContext 被从执行上下文中弹出来销毁,但是 fooContext.AO 被保存到被返回的匿名函数的 [[scope]] 里面,所以还能被访问得到。

function foo(){
  var a = 1;
  function increase(){
    a++
  }
  return function(){
    increase()
    console.log(a)
  }
}

let bar = foo();
bar() //2
bar() //3

“自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量”。下面这个闭包的j参数,应该不是自由变量(是函数参数),但是闭包是可以访问j的。能解答一下吗?

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

为什么我的代码没有语法高亮...

@Rainpia 感谢建议哈~ 其实本篇我想表达的是闭包的一种实现原理,即通过维护作用链中涉及变量的存活,从而导致能在执行上下文栈销毁后访问到变量的值,这也可以解释我以前一直以来的疑问,就是为什么闭包会导致内存上升的问题

@273539918 这个 j 是自由变量,因为

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

其实想表达的意思是:

自由变量是指在函数中使用的,但既不是(这个)函数参数也不是(这个)函数的局部变量的变量

@hp0912 现在看你的代码是不是高亮了,你编辑一下你的评论应该就知道了,是没有声明语法的缘故,JavaScript 语法被 ```js 和 ``` 包裹就会高亮

您好,想问下那个为什么会突然出现一个匿名函数??这是从哪里冒出来的呀?

commented

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

default

commented
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

bibao2

终于有点豁然开朗的感觉,匿名函数执行会产生一个上下文环境,跟全局变量的环境隔开了,所以就是1,2,3了。

谢谢大神的分享,但我有个地方有点困惑
文中说到:

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

fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

关于checkscopeContext.AO为什么会活在内存中未被销毁的问题。
我是这样认为的,首先有两个可能:
1、因为f中引用到了checkscope的变量,所以该变量未被回收,以至于checkscopeContext.AO 活在内存中
2、作用域链上的所有作用域的活动对象都不会被销毁
如果是1的话,那么就会有个问题,当checkscopeContext 被销毁时,f还未创建上下文,它是如何知道有函数依赖它的
如果是2的话,好像又和js的垃圾回收机制冲突了。

那么出现checkscopeContext.AO未被回收的原因是因为什么呢?

@sansui-orz 我试着回答一下,

1、因为f中引用到了checkscope的变量,所以该变量未被回收,以至于checkscopeContext.AO 活在内存中
...
如果是1的话,那么就会有个问题,当checkscopeContext 被销毁时,f还未创建上下文,它是如何知道有函数依赖它的

在checkscope上下文中,f 通过词法作用域通过作用域链[ checkscopeContext.AO, globalContext.AO]在checkscopeContext.AO中找到了变量scope,之后即使当checkscopeContext 被销毁时checkscopeContext.AO还是存在于fContext的作用域链中的, 至于保存的checkscopeContext.AO中只有scope这一个值还是全部值都包含的话,这个我也不懂了,想请大神指点 @mqyqingfeng

一句话, 闭包就是一个function.

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
``` 感觉这个scope 数组里面的顺序是不是有点问题呢,谁能解答下?
感觉是:```js [匿名函数Context.AO,AO, globalContext.VO]``

谢谢大神的分享,但我有个地方有点困惑
文中说到:

当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

关于checkscopeContext.AO为什么会活在内存中未被销毁的问题。
我是这样认为的,首先有两个可能:
1、因为f中引用到了checkscope的变量,所以该变量未被回收,以至于checkscopeContext.AO 活在内存中
2、作用域链上的所有作用域的活动对象都不会被销毁
如果是1的话,那么就会有个问题,当checkscopeContext 被销毁时,f还未创建上下文,它是如何知道有函数依赖它的
如果是2的话,好像又和js的垃圾回收机制冲突了。

那么出现checkscopeContext.AO未被回收的原因是因为什么呢?

1是对的。
确实是当checkscopeContext 被销毁时,f还未创建上下文。
但注意f创建执行上下文(fContext)并不依赖于checkscope的执行上下文(checkscopeContext ),因为他只引用了checkscope的执行上下文(checkscopeContext )中的活动对象(checkscopeContext.AO),所以checkscopeContext.AO没销毁就可以了

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

感觉是:```js [匿名函数Context.AO,AO, globalContext.VO]``![](chrome-extension://mdcffelghikdiafnfodjlgllenhlnejl/icons/copy.png)复制到剪切板

没问题。
函数自己的AO在顶级
《JavaScript高级程序设计》P178最下面

作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境
全局执行环境就是全局执行上下文(global execution context)翻译问题

`function checkscope(){
var scope = "local scope";
function f(){
console.log(scope);
scope = 'leeee' //此处的scope是放在了checkscope的AO中,还是放在了全局上下文的VO呢
}
return f;
}

var foo = checkscope();
console.log(foo());
console.log(scope)// scope is not defined`

为什么输出 scope 报错了

`function checkscope(){
var scope = "local scope";
function f(){
console.log(scope);
scope = 'leeee' //此处的scope是放在了checkscope的AO中,还是放在了全局上下文的VO呢
}
return f;
}

var foo = checkscope();
console.log(foo());
console.log(scope)// scope is not defined`


为什么输出 scope 报错了

此处的scope是放在了checkscope的AO中,还是放在了全局上下文的VO呢

scope是checkscope.AO中的。

为什么输出 scope 报错了

根据词法作用域规则scope已经绑定了,不会再声明一个全局变量,见
[(https://github.com//issues/3)]

`function checkscope(){
var scope = "local scope";
function f(){
console.log(scope);
scope = 'leeee' //此处的scope是放在了checkscope的AO中,还是放在了全局上下文的VO呢
}
return f;
}

var foo = checkscope();
console.log(foo());
console.log(scope)// scope is not defined`

为什么输出 scope 报错了

此处的scope是放在了checkscope的AO中,还是放在了全局上下文的VO呢

scope是checkscope.AO中的。

为什么输出 scope 报错了

根据词法作用域规则scope已经绑定了,不会再声明一个全局变量,见
[(https://github.com//issues/3)]

谢谢 老哥 你有时间能帮我看看我在 变量对象 那篇最下面提出的两个疑问吗?

这部分的必刷题部分,把var换为let时,输出会有不同,大佬能解释下吗

@wangxiaoxaio
这是经过转换的代码

var data = [];
var _loop_1 = function (i) {
    data[i] = function () {
        console.log(i);
    };
};
for (var i = 0; i < 3; i++) {
    _loop_1(i);
}
data[0]();
data[1]();
data[2]();

很自然可以看出闭包现象,这都是因为let加上{}会产生块级作用域,避免变量污染

@SageWu
你说的这些我也知道,我想问的是使用let声明局部变量后,即使不使用闭包,也可以得到理想的输出,这是为什么?
var data=[]; for(let i=0;i<3;i++) { data[i] = function(){ console.log(i) } } data[0](); //0 data[1](); //1 data[2](); //2

@wangxiaoxaio
每次循环都会产生新的块级作用域,就像初始化形参一样,都有各自的i属性,值分别为012
又由于data这个数组的元素引用着上述作用域里面的函数,故上述作用域在循环结束后不会被销毁。
然后接下来通过函数地址引用去调用这些函数,自然就获取的都是各自作用域中的i属性。

请问下,函数执行上下文是在函数执行的时候压入到执行上下文栈后进行初始化的,前提是函数需要执行才能进行上下文的初始化,那么checkscope中的f函数是先返回,但是如果不执行,[[scope]]属性中为什么会有checkscope和全局环境的变量对象呢?

请问下,函数执行上下文是在函数执行的时候压入到执行上下文栈后进行初始化的,前提是函数需要执行才能进行上下文的初始化,那么checkscope中的f函数是先返回,但是如果不执行,[[scope]]属性中为什么会有checkscope和全局环境的变量对象呢?

我想解释下我上面提出的疑问,不知道是不是正确:f函数在定义的时候就已经引用了checkscope的词法作用域,在checkscope()执行后生成了checkscopeVO,执行结束返回了f。由于f函数已经引用了checkscope的词法作用域,在checkscope执行后虽然对应的执行上下文对象被释放,但是由于f函数所在的词法作用域导致checkscopeVO不会被回收,在初始化fContext的后会将checkscopeVO设置到f函数的[[scope]]属性中 @mqyqingfeng

请问下,函数执行上下文是在函数执行的时候压入到执行上下文栈后进行初始化的,前提是函数需要执行才能进行上下文的初始化,那么checkscope中的f函数是先返回,但是如果不执行,[[scope]]属性中为什么会有checkscope和全局环境的变量对象呢?

我想解释下我上面提出的疑问,不知道是不是正确:f函数在定义的时候就已经引用了checkscope的词法作用域,在checkscope()执行后生成了checkscopeVO,执行结束返回了f。由于f函数已经引用了checkscope的词法作用域,在checkscope执行后虽然对应的执行上下文对象被释放,但是由于f函数所在的词法作用域导致checkscopeVO不会被回收,在初始化fContext的后会将checkscopeVO设置到f函数的[[scope]]属性中 @mqyqingfeng

博主,没事了,我回顾了你发布的执行上下文那篇博客,发现你有解释说,"同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]",应该在这个步骤的时候f函数的[[scope]]属性中就已经保存了checkscopeVO和globalVO,所以在checkscope执行上下文对象释放后,才会阻止checkscopeVO的释放,翻看了红宝书的P179,也讲述了类似的过程

@SageWu
ok,谢谢,这样能理解通

commented

之前看了很多关于闭包的文章,都是在死记硬背,直到看见这篇,才明白原理,学习到了

commented

let f;
let o = 10;
function a(o) {
if (!f) {
f = () => {
console.log('console f', o);
}
} else {
console.log('f true', o);
}
o+= 1;
f();
}
a(1); // 2
a(5); // 5,2
这个最后a(5)输出的是还是2却不是6,从原理方面应该怎么解释

支持下一啊,虽然对闭包已经看了很多了,每次看一遍都会有一番不同的感受,学习就是一个重复的过程。

for循环执行完成后,直接弹出执行栈,所以后面执行到data0时,访问作用域链,i=3,所以打印都是3

@mqyqingfeng 冴羽大大,
我想问一下函数的[[scope]]属性和[[scopes]]属性有什么区别?
scope的作用域链包含所有AO,
scopes的作用域链只包含函数中使用的自由变量,
这二者是完全不同的两个属性,还是有什么关联?

commented

支持下;顺便推荐一篇 Closure - TomXu

commented

1、即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)2
2、在代码中引用了自由变量
按照上面这个说法 下面的函数就不属于闭包了啊

function a() {
   var c = 1;
  function b(){
    console.log(c)
  }
  b()
}
a()

commented

以前对闭包知其然,不知其所以然,看了博主的解答,豁然开朗,膜拜!围观!

楼主知道有没有根据语言规范自己用C/C++写一个简单的JS引擎的例子?如果有这样的例子,那么这些概念都能用具体的C/C++代码实现了,概念毕竟看完过段时间就会淡忘。

看了前面的再来看闭包真是水到渠成的感觉啊!

@jxZhangLi
文中的:“ 即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中 ” 。"活在内存中" 这句话有点描述不完整的感觉。是所有的闭包函数都销毁之后,都会存活在内存中吗?哪什么时候才会从内存中删除呢?

回答:从《js高程》中的描述来说(p180),在一个函数内部定义的函数将包含函数(外部函数)的活动对象到它(内部定义的函数)的作用域链中。函数f作为返回值赋值给新的变量,checkscopeContext.AO存在的位置就是返回的函数f的[[scope]]中。所以闭包函数执行完成被销毁之后,checkscopeContext.AO自然也就跟着销毁了

闭包:是外部函数的整个作用域还是只有被引用的变量才保存在内存中呢?

牛牛!学到了

作者还会回复吗
有个问题个人感觉是
之所以checkscope上下文已经从调用栈弹出(销毁)了f函数还能访问到其作用域下的scope变量是因为创建checkscope上下文时遇到f函数定义所以f.[[scope]]=[checkscopeContext.AO,globalContext.VO]保存了上级的作用域链,后面又调用f函数初始化f函数上下文的Scope时把前面定义f函数的f.[[scope]] 做了contact啊所以fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
所以举的这个例子
var data = [];

for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}

data0;
data1;
data2;
data0;开始调用的时候去作用域链里找变量i找的 Scope: [AO, checkscopeContext.AO, globalContext.VO]里的checkscopeContext.AO实际就应该是f函数定义时候的f.[[scope]]=[checkscopeContext.AO,globalContext.VO]里的checkscopeContext.AO,这时候能说for循环走完了么 不能吧 所以为什么是3呢

匿名函数Context = {
    AO: {
        arguments: {
            0: 1,
            length: 1
        },
        i: 0
    }
}

里面的arguments0:1为什么是1呢,按我的理解应该和i的值相同,所以不是0: 0

为什么匿名函数Context.AO里的 i 值为0,大哥还在吗?实在看不懂了

看了这篇文章,才发现,通过学习函数执行上下文,突然发现闭包很好理解了,谢谢冴羽大神了。

commented

结合作用域链来理解闭包真的和以前对闭包的理解发生了质的改变!

问一下大佬,如果没有引用自由变量,这个自由变量在内存中还存在吗?只是我们访问不到?

data[1] 和 data[2] 是一样的道理。
所以让我们改成闭包看看:
...
这里是改成了立即执行函数

commented

即时 checkscopeContext被销毁了

但是javascript仍然会让 checkscopeContext.AO 活在内存中

f函数依然可以通过f函数的作用域链找到它....

读完这段话 怎么莫名有点小感动...莫名有点浪漫的感觉...大晚上的我在想啥呢...

继续学习 加油呀自己...

个人理解:所谓的必包是一个函数,创建这个函数的作用域已经销毁,但是这个函数还存在,而且引用了它上级作用域的变量,类似上级作用域的私有变量的getter或者setter

结合前面的文章反反复复看了很多遍,终于懂了,大爱作者😘

例子

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

赋值

因为data[i] = 一个匿名自执行函数。
所以在给data[i]赋值之前就已经执行了函数
因此:

data[0] = function(){console.log(0)}
data[1] = function(){console.log(1)}
data[2] = function(){console.log(2)}

执行

data[0](); // 0
data[1](); // 1
data[2](); // 2

不知道这样理解的对不对

commented

es2015 的规范使用 Lexical Environment 来解释,大佬可以更新一波

“从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量”

老兄,感觉这两句话可以再推敲一下呢?
第一句,“即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)”
你的意思我应该明白了,但是给人一种创建它的上下文销毁可能销毁、可能没销毁的感觉。

第二句,在代码中引用了自由变量,那如果下面两种算不算闭包呢?
情况1:

var x = 1;
function f() {
   return function () {  console.log(x) }
}
var theFn = f()

情况2:
function f() {
   var x = 1;
   return function () {
     return function() { console.log(x) }
  }
}
var theFn = f()()

感觉情况1不属于我们通常理解的闭包,情况2又属于。

所以,能不能改成:

从实践角度:闭包是:
能够访问已经被销毁的执行期上下文中的活动对象的函数。

感觉子自执行函数理解的话比较抽象,拆开的话,博主的意思更容易理解-_-!

// 自执行函数
for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}``

// 等于上面的自执行函数
for(var i = 0; i<3; i++){
    function test(j){
        return function(){
            console.log(j);
        }
    }

    data[i] = test(i);
}

Hello, 这样的理解不知道对不对啊?
例子和闭包没有任何关系

var data = [];

for(var i = 0; i < 5; i++) {
    data[i] = function() {
        console.log(i);
    }
}

data[0]();
data[1]();
data[2]();
data[3]();
data[4]();


// VS

var data = [];

for(var i = 0; i < 5; i++) {
    data[i] = (function(i) {
        return function() {
            console.log(i)
        }
    })(i)
}

data[0]();
data[1]();
data[2]();
data[3]();
data[4]();

只是通过LIFE人为制造一个函数作用域来弥补es5没有块作用域的缺陷而已。

Hello, 楼主!
不好意思!我新提了一个issue#182,也是关于这个问题点的,请楼主帮忙关闭下吧。

@mqyqingfeng 楼主上午好,请教一个问题:下面这些没看懂。。。。请问应该先执行哪个?

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

JavaScript深入之执行上下文,checkscope和f函数的执行顺序如下:

5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈

    ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];

8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

ECStack = [
        checkscopeContext,
        globalContext
    ];

9.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

ECStack = [
        globalContext
    ];

而在JavaScript深入之闭包文章中执行顺序颠倒了:
7. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
8. checkscope 执行上下文初始化,创建变量对象、作用域链、this等
9. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
10. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
11. f 执行上下文初始化,创建变量对象、作用域链、this等
12. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

看了那么多闭包,终于才知道原理,:)

结合作用域链来理解闭包真的和以前对闭包的理解发生了质的改变!

起飞

首先感谢你的分享,但是呢看了你写的 js 深入系列这些文章,我是越看越懵比,就拿你这篇文章中例子:

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

var foo = checkscope();
foo();

你的解释我是一脸懵比,我直接翻开 javascript权威指南这本书的 183页面,解释非常清楚, 还有我看 阮老师的js标准教程 也是行云流水很流畅,看了你这个系列的 “JavaScript深入之从ECMAScript规范解读this” 也是很懵比,很有读者看了也有跟我一样的感觉,我直接看阮老师的 http://javascript.ruanyifeng.com/oop/this.html **很清晰啊,我不知道是不是作者表达的太乱了

从第一篇看到现在,觉得作者讲的非常好,也很透彻

“自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量”。下面这个闭包的j参数,应该不是自由变量(是函数参数),但是闭包是可以访问j的。能解答一下吗?

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

这个 j 是最内层函数 `return function() {

        console.log(j);
    }`  中return 出来的匿名函数的自由变量

let f;
let o = 10;
function a(o) {
if (!f) {
f = () => {
console.log('console f', o);
}
} else {
console.log('f true', o);
}
o+= 1;
f();
}
a(1); // 2
a(5); // 5,2
这个最后a(5)输出的是还是2却不是6,从原理方面应该怎么解释

当第一次执行a时, f被赋值为箭头函数,此时函数创建 , 会复制所有父对象的变量对象到[[scope]]属性,即[ a.AO, global.VO],然后打印2, 当再次执行 函数a,时形参为5, 会重新创建上下文,并且初始化,但后面f执行为什么打印不是6,就是因为 f [[scope]] 属性引用的上下文是第一次执行函数a时候的上下文, 里面的 o为2 ,不是第二次执行时候的

1、即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)2
2、在代码中引用了自由变量
按照上面这个说法 下面的函数就不属于闭包了啊

function a() {
   var c = 1;
  function b(){
    console.log(c)
  }
  b()
}
a()

从理论角度看是, 从实践角度看不是