-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Comments
支持下一啊,虽然对闭包已经看了很多了,每次看一遍都会有一番不同的感受,学习就是一个重复的过程。 |
请问下学长为什么 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 |
循环结束后
执行 |
里面的 |
@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 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。 |
@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](); |
非常感谢博主!以前对闭包总是雾里看花,终隔一层,提到闭包,有人说函数就是闭包,有人说必须是嵌套,又是引用怎么怎么样,其实现在看来,两者都是,只不过是一种狭义和广义上概念的区别。 |
看了这么多写闭包的,这个是我看完之后唯一恍然大悟的,之前都是一知半解的。感谢,比心💟 |
学习了啊 |
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 |
大神,图挂了,可以更新一下吗 |
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
// 8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
// 想问一下:
// 1. 这里虽然f执行上下文弹出了,但是外部foo变量仍然引用着函数f,是不是函数f的作用域链依然存在内存中?必须foo = null才能彻底销毁?
// 2. 如果不用var foo,直接checkscope()(); 这样函数执行完毕就会完全销毁,不会留在内存中,是么? |
为什么在执行上下文中的例子里,是checkscope 和 f 都压入栈中,然后 f 先弹出执行
而在闭包的例子里,是 checkscope 执行完弹出后,才初始化 f 的
|
因为for循环中是用var定义的变量i,没有全局作用域,相当于把i当做全局变量。 |
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2](); 将var改成let后,能不能这样理解: |
博主写的两个例子是不一样的, 上面的是弹出的f(), 执行完了弹出的, 在弹出checkscope, 而这次return的是f, 没有执行只是一个引用, 所以先弹出checkscope, 再执行foo之后弹出f |
被 React 函数式组件 + Hook,按在地上摩擦了一顿。很有必要温馨加固一下:执行上下文栈,执行上下文,作用域链,闭包。 |
@xsfxtsxxr ECStack = [
globalContext,
]
globalContext = {
VO: {
scope: 'global scope';
foo: reference to function f(){}
},
scope: [globalContext.VO],
this: window
} 回答第二个问题,checkscope()返回的函数没有被其他东西占用,但是还是需要再执行一次,所以暂时不销毁,当返回的值执行完成后,浏览器会在空闲的时间把它销毁。 |
好文,之前每次面试都看到这篇文章,理解得很模糊,现在再看清晰了很多! |
《你不知道的JavaScript》 中一句话:
|
文章的第一句话
感觉很妙、言简意赅,但找了半天(包括 archive.org ),都没找到 MDN 上有这句话;直到看了下时间 2017 年 4 月,继续在 archive 里往前找,终于找到了:https://web.archive.org/web/20170116063418/https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures 。分享出来,以飨各位同好 🥳 |
MDN: 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域 |
可以请问下, 下面这段代码 for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
} 相当于下面哪种代码 data[0] = function () {
console.log(i);
};
data[1] = function () {
console.log(i);
};
data[2] = function () {
console.log(i);
}; 第二种 : data[0] = function () {
console.log(0);
};
data[1] = function () {
console.log(1);
};
data[2] = function () {
console.log(2);
}; |
如果是第一种的话, 那么下面这段代码 for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
} 就会变成 data[0] = (function (i) {
return function () {
console.log(i);
};
})(i);
data[1] = (function (i) {
return function () {
console.log(i);
};
})(i);
data[2] = (function (i) {
return function () {
console.log(i);
};
})(i); 那么当执行data0的时候, 这里面的i值是哪来的 data[0] = (function (i) {
return function () {
console.log(i);
};
})(i); |
@Sakura-echos 在这个立即执行函数中,首先它会马上执行,并且i作为实参被传进来了,你可以理解为立刻把i"变现"成了对应的0,1,2。 |
感觉应该立即成,虽然执行上下文被销毁,但作用域链里仍然保存着相关信息 |
你要仔细观察上下文链中各个父级上下文中变量的情况 |
第一句说,即使上下文已经销毁,它仍然存在
|
我在《你不知道的JavaScript》上卷47页中看到:“在定时器、事件监听器中... 只要使用了回调函数,实际上就是在使用闭包”,那请问这段代码是闭包吗?(虽然使用了回调函数,但是timeHandler这个函数并没有被一个外层函数包围,它能记住的只有全局作用域的变量) setTimeout(function timeHandler(){ |
fContext在初始化时得到的作用域链会保存他的所有父变量对象,那么此时checkscope()函数的执行上下文对象已经出栈了,那么在foo()函数创建时的作用域链应该是两个全局变量对象啊?怎么还会引用到checkscope()的变量对象呢? 我自己一个理解是在var foo = checkscope(); 时,checkscope()虽然出栈了,但是foo 引用的还是 f 的地址,只要有存在地址引用,尽管无法直接在外层调用f,但f依然存在于内存中,则 f 所引用到的变量地址也都存在,也可访问。 但这又如何用作用域链来解释呢?还望大大指点 |
fn2(); 不需要 return么? |
这是来自QQ邮箱的假期自动回复邮件。你好,你的邮件我收到,谢谢。
|
定义
MDN 对闭包的定义为:
那什么是自由变量呢?
由此,我们可以看出闭包共有两部分组成:
举个例子:
foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。
那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……
还真是这样的!
所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。
咦,这怎么跟我们平时看到的讲到的闭包不一样呢!?
别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:
ECMAScript中,闭包指的是:
接下来就来讲讲实践上的闭包。
分析
让我们先写个例子,例子依然是来自《JavaScript权威指南》,稍微做点改动:
首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。
另一个与这段代码相似的例子,在《JavaScript深入之执行上下文》中有着非常详细的分析。如果看不懂以下的执行过程,建议先阅读这篇文章。
这里直接给出简要的执行过程:
了解到这个过程,我们应该思考一个问题,那就是:
当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
以上的代码,要是转换成 PHP,就会报错,因为在 PHP 中,f 函数只能读取到自己作用域和全局作用域里的值,所以读不到 checkscope 下的 scope 值。(这段我问的PHP同事……)
然而 JavaScript 却是可以的!
当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:
对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
所以,让我们再看一遍实践角度上闭包的定义:
在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:
闭包在计算机科学中也只是一个普通的概念,大家不要去想得太复杂。
必刷题
接下来,看这道刷题必刷,面试必考的闭包题:
答案是都是 3,让我们分析一下原因:
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
当执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。
data[1] 和 data[2] 是一样的道理。
所以让我们改成闭包看看:
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
跟没改之前一模一样。
当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
匿名函数执行上下文的AO为:
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,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: