New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript深入之闭包 #9

Open
mqyqingfeng opened this Issue Apr 27, 2017 · 51 comments

Comments

Projects
None yet
@mqyqingfeng
Owner

mqyqingfeng commented Apr 27, 2017

定义

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,对作者也是一种鼓励。

@jawil

This comment has been minimized.

Show comment
Hide comment
@jawil

jawil Apr 27, 2017

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

jawil commented Apr 27, 2017

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

@rivens-77

This comment has been minimized.

Show comment
Hide comment
@rivens-77

rivens-77 May 10, 2017

请问下学长为什么globalContext = {
VO: {
data: [...],
i: 3
}
}
这里是3??怎么来的 为什么不是0,1,2

rivens-77 commented May 10, 2017

请问下学长为什么globalContext = {
VO: {
data: [...],
i: 3
}
}
这里是3??怎么来的 为什么不是0,1,2

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng May 10, 2017

Owner

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

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

mqyqingfeng commented May 10, 2017

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

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

This comment has been minimized.

Show comment
Hide comment
@spicychocolate

spicychocolate May 12, 2017

循环结束后

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遍后终于知道原理了

spicychocolate commented May 12, 2017

循环结束后

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遍后终于知道原理了

@fi3ework

This comment has been minimized.

Show comment
Hide comment
@fi3ework

fi3ework May 22, 2017

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

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

fi3ework commented May 22, 2017

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

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng May 22, 2017

Owner

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

Owner

mqyqingfeng commented May 22, 2017

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

@xdwxls

This comment has been minimized.

Show comment
Hide comment
@xdwxls

xdwxls Jun 1, 2017

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 commented Jun 1, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Jun 1, 2017

Owner

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

Owner

mqyqingfeng commented Jun 1, 2017

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

@tangshuimei

This comment has been minimized.

Show comment
Hide comment
@tangshuimei

tangshuimei Jun 2, 2017

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

tangshuimei commented Jun 2, 2017

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

@tangshuimei

This comment has been minimized.

Show comment
Hide comment
@tangshuimei

tangshuimei Jun 2, 2017

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

tangshuimei commented Jun 2, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Jun 2, 2017

Owner

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

Owner

mqyqingfeng commented Jun 2, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Jun 2, 2017

Owner

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

Owner

mqyqingfeng commented Jun 2, 2017

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

@xdwxls

This comment has been minimized.

Show comment
Hide comment
@xdwxls

xdwxls Jun 4, 2017

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

xdwxls commented Jun 4, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Jun 4, 2017

Owner

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

Owner

mqyqingfeng commented Jun 4, 2017

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

@frankchou1

This comment has been minimized.

Show comment
Hide comment
@frankchou1

frankchou1 Jul 18, 2017

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

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

这里返回的是1,2,3

frankchou1 commented Jul 18, 2017

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

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

这里返回的是1,2,3

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Jul 19, 2017

Owner

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

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

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

Owner

mqyqingfeng commented Jul 19, 2017

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

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

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Jul 19, 2017

Owner

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

Owner

mqyqingfeng commented Jul 19, 2017

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

@Muscliy

This comment has been minimized.

Show comment
Hide comment
@Muscliy

Muscliy Jul 20, 2017

var data = [];

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

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

Muscliy commented Jul 20, 2017

var data = [];

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

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

@bighuang624

This comment has been minimized.

Show comment
Hide comment
@bighuang624

bighuang624 Jul 20, 2017

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

bighuang624 commented Jul 20, 2017

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

@Muscliy

This comment has been minimized.

Show comment
Hide comment
@Muscliy

Muscliy Jul 20, 2017

@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);
}

data0;
data1;
data2;

Muscliy commented Jul 20, 2017

@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);
}

data0;
data1;
data2;

@keyiran

This comment has been minimized.

Show comment
Hide comment
@keyiran

keyiran Aug 20, 2017

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

keyiran commented Aug 20, 2017

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

@pororoJ

This comment has been minimized.

Show comment
Hide comment
@pororoJ

pororoJ Sep 19, 2017

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

pororoJ commented Sep 19, 2017

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

@dengnan123

This comment has been minimized.

Show comment
Hide comment
@dengnan123

dengnan123 Oct 30, 2017

学习了啊

dengnan123 commented Oct 30, 2017

学习了啊

@jasonzhangdong

This comment has been minimized.

Show comment
Hide comment
@jasonzhangdong

jasonzhangdong Nov 19, 2017

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 commented Nov 19, 2017

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,整个循环走完了吗?

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Nov 20, 2017

Owner

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

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

}
console.log(i) // 3
Owner

mqyqingfeng commented Nov 20, 2017

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

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

}
console.log(i) // 3
@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Dec 5, 2017

Owner

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

Owner

mqyqingfeng commented Dec 5, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Dec 5, 2017

Owner

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

Owner

mqyqingfeng commented Dec 5, 2017

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

@sinkinlife

This comment has been minimized.

Show comment
Hide comment
@sinkinlife

sinkinlife Dec 5, 2017

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

sinkinlife commented Dec 5, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Dec 5, 2017

Owner

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

Owner

mqyqingfeng commented Dec 5, 2017

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

@wnbupt

This comment has been minimized.

Show comment
Hide comment
@wnbupt

wnbupt Jan 9, 2018

在闭包这一节还有个很经典的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

wnbupt commented Jan 9, 2018

在闭包这一节还有个很经典的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

@frankchou1

This comment has been minimized.

Show comment
Hide comment
@frankchou1

frankchou1 Jan 10, 2018

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

frankchou1 commented Jan 10, 2018

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Jan 10, 2018

Owner

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

Owner

mqyqingfeng commented Jan 10, 2018

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Jan 10, 2018

Owner

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

Owner

mqyqingfeng commented Jan 10, 2018

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

@webjscss

This comment has been minimized.

Show comment
Hide comment
@webjscss

webjscss Feb 11, 2018

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

webjscss commented Feb 11, 2018

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

@youbooks

This comment has been minimized.

Show comment
Hide comment
@youbooks

youbooks Apr 18, 2018

首先感谢你的分享,但是呢看了你写的 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 commented Apr 18, 2018

首先感谢你的分享,但是呢看了你写的 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 思想很清晰啊,我不知道是不是作者表达的太乱了

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng
Owner

mqyqingfeng commented Apr 24, 2018

@youbooks 好吧~~~

@Rainpia

This comment has been minimized.

Show comment
Hide comment
@Rainpia

Rainpia May 11, 2018

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

Rainpia commented May 11, 2018

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

@morelearn1990

This comment has been minimized.

Show comment
Hide comment
@morelearn1990

morelearn1990 May 14, 2018

对于下面这个例子,我们是不是可以理解为,即便 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

morelearn1990 commented May 14, 2018

对于下面这个例子,我们是不是可以理解为,即便 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
@273539918

This comment has been minimized.

Show comment
Hide comment
@273539918

273539918 May 24, 2018

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

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

273539918 commented May 24, 2018

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

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

This comment has been minimized.

Show comment
Hide comment
@hp0912

hp0912 May 31, 2018

function test(){
  console.log(1);
}

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

hp0912 commented May 31, 2018

function test(){
  console.log(1);
}

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng May 31, 2018

Owner

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

Owner

mqyqingfeng commented May 31, 2018

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng
Owner

mqyqingfeng commented May 31, 2018

@morelearn1990 是的~

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng May 31, 2018

Owner

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

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

其实想表达的意思是:

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

Owner

mqyqingfeng commented May 31, 2018

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

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

其实想表达的意思是:

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng May 31, 2018

Owner

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

Owner

mqyqingfeng commented May 31, 2018

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

@YBFJ

This comment has been minimized.

Show comment
Hide comment
@YBFJ

YBFJ Jul 4, 2018

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

YBFJ commented Jul 4, 2018

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

@zhangzhongjiang

This comment has been minimized.

Show comment
Hide comment
@zhangzhongjiang

zhangzhongjiang Aug 1, 2018


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

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

default

zhangzhongjiang commented Aug 1, 2018


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

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

default

@zhangzhongjiang

This comment has been minimized.

Show comment
Hide comment
@zhangzhongjiang

zhangzhongjiang Aug 1, 2018

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

zhangzhongjiang commented Aug 1, 2018

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

@hankanon

This comment has been minimized.

Show comment
Hide comment
@hankanon

hankanon Aug 14, 2018

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

hankanon commented Aug 14, 2018

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

@sansui-orz

This comment has been minimized.

Show comment
Hide comment
@sansui-orz

sansui-orz Oct 12, 2018

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

当我们了解了具体的执行过程后,我们知道 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 commented Oct 12, 2018

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

当我们了解了具体的执行过程后,我们知道 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未被回收的原因是因为什么呢?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment