Repository navigation
JavaScript深入之闭包 #9
Description
Activity
支持下一啊,虽然对闭包已经看了很多了,每次看一遍都会有一番不同的感受,学习就是一个重复的过程。
Reacted by 冴羽, 微醺岁月, ArthasDragon, zaofeng, 拾木 Ash, penouc, Zoey, 烂人文, btea, Dany and 215 moreReacted by 赵俊杰, NiuFang, Rocky Wong, Ron-LaoLuo, Rise_xie, Satoru, GSZS, guangpengliu-vungle, Charlie Li and mgt88888Reacted by 赵俊杰, Rocky Wong, Rise_xie, wanwenyun and guangpengliu-vungleReacted by prince12138 and guangpengliu-vungleReacted by NiuFang, Satoru, Rocky Wong, MarioSong, Ron-LaoLuo, Rise_xie, 大涛子 and guangpengliu-vungle请问下学长为什么
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
Reacted by PeterYuan, aafnnp, Prilo, yangsuguan, Rocky Wong, Hershel Hale, Eddie, Deer, 小兰 , kuangjiajia and 92 moreReacted by ♂京 - 花生, camo, Stavin Li, CodeWhat_ and mobsReacted by prince12138 and CodeWhat_Reacted by Jackcaos, 埃克斯占, YiBo, 李, Stavin Li and CodeWhat_Reacted by Jackcaos, riven, 埃克斯占, ZXYxc and CodeWhat_循环结束后
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遍后终于知道原理了Reacted by 赵俊杰, gentlemanZhang, Beats0, ssssyoki, ytt, Eddie, Peter Chen, Charlie, Tz, 九旬 and 37 moreReacted by ♂京 - 花生, kevin, Jin, harden, Alex and SaturdayReacted by Fynn, ♂京 - 花生, suxiao and HJ WangReacted by prince12138匿名函数Context = { AO: { arguments: { 0: 1, length: 1 }, i: 0 } }里面的
arguments的0:1为什么是1呢,按我的理解应该和i的值相同,所以不是0: 0吗Reacted by Eway, Kerrett, xuanzai, sifon, wzd1997, keyiwu, JFDTTTT, PaulaSham, MrLeihe, xieyezi and 3 moreReacted by ♂京 - 花生 and AlexReacted by 饭团@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深入之词法作用域和动态作用域》,关于这道题,你可以简单理解为函数能够读取到的值跟函数定义的位置有关,跟执行的位置无关
Reacted by wuxiaobin, okzhang, weiyang.su, sdp, wangyiman, bao_tao1997, hardestHu, andyyyf, michaelChe, flyflydogdog and 19 more大神,那是不是执行上下文中的作用域scope仅是父级的一个VO记录,不会像跟ECStack那样跟函数执行的次序有关呢?
@xdwxls 我觉得可能是虽然fn(),即innerFoo()是在bar里面执行的,但是innerFoo函数的时候他的作用域scope里面分别是[AO,fooContext.AO,globalContext.AO],并没有包括barContext.AO在里面,所以根本就没有声明c这个变量,所以会显示is not define,我就猜猜而已......
Reacted by 冴羽, wuxiaobin, 山水子农, wd2010, Handler, michaelChe, Rudy, weeqe, rongchenglin, Ricardo and 12 moreReacted by sdp@tangshuimei 是的,你可以这样理解,如果要更严谨的话,可以说,执行上下文中的作用域 scope 是由函数的 [[scope]]属性初始化,而函数的[[scope]] 属性保存了函数创建时词法层面上的父级们的 VO 引用,跟函数的执行顺序无关。
Reacted by 安静, beexpectedone, buwenxu, KaKa, ChenJiujiu and liu-yunnan@tangshuimei 哈哈,关于这道题的分析,我赞同你的观点~
@tangshuimei 你说 innerFoo函数的时候他的作用域scope里面分别是[AO,fooContext.AO,globalContext.AO], 那这个时候AO 具体表示什么呢???有点费解
@xdwxls AO 表示活动对象,储存了函数的参数、函数内声明的变量等,在 innnerFoo 中,查找变量 c,就要在 innerFoo 函数的作用域链,也就是 [AO,fooContext.AO,globalContext.AO] 中找到变量 c 的声明,因为没有,所以最终会报错~
Reacted by XionVV, weeqe and beexpectedone106 remaining items
《你不知道的JavaScript》 中一句话:
对创建时所在作用域的引用。
文章的第一句话
MDN 对闭包的定义为:
闭包是指那些能够访问自由变量的函数。
感觉很妙、言简意赅,但找了半天(包括 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,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures可以请问下, 下面这段代码
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。
“从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量”老兄,感觉这两句话可以再推敲一下呢?
第一句,“即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)”
你的意思我应该明白了,但是给人一种创建它的上下文销毁可能销毁、可能没销毁的感觉。
第二句,在代码中引用了自由变量,那如果下面两种算不算闭包呢?
情况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); }
就会变成
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);
你要仔细观察上下文链中各个父级上下文中变量的情况
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量第一句说,即使上下文已经销毁,它仍然存在
这个表述似乎有点歧义,下面这种也属于闭包,但是上下文销毁的时候,闭包也销毁了
个人觉得这个不是必要条件,只是一种情况而已?
请问是这样吗?function fn1() { var num = 10; function fn2() { console.log(num); } fn2(); } fn1(); //输出结果10我在《你不知道的JavaScript》上卷47页中看到:“在定时器、事件监听器中... 只要使用了回调函数,实际上就是在使用闭包”,那请问这段代码是闭包吗?(虽然使用了回调函数,但是timeHandler这个函数并没有被一个外层函数包围,它能记住的只有全局作用域的变量)
var name = 'jack';setTimeout(function timeHandler(){
console.log(name);
}, 100);fContext在初始化时得到的作用域链会保存他的所有父变量对象,那么此时checkscope()函数的执行上下文对象已经出栈了,那么在foo()函数创建时的作用域链应该是两个全局变量对象啊?怎么还会引用到checkscope()的变量对象呢?
我自己一个理解是在var foo = checkscope(); 时,checkscope()虽然出栈了,但是foo 引用的还是 f 的地址,只要有存在地址引用,尽管无法直接在外层调用f,但f依然存在于内存中,则 f 所引用到的变量地址也都存在,也可访问。
但这又如何用作用域链来解释呢?还望大大指点
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量第一句说,即使上下文已经销毁,它仍然存在 这个表述似乎有点歧义,下面这种也属于闭包,但是上下文销毁的时候,闭包也销毁了 个人觉得这个不是必要条件,只是一种情况而已? 请问是这样吗?
function fn1() { var num = 10; function fn2() { console.log(num); } fn2(); } fn1(); //输出结果10fn2(); 不需要 return么?
- 这是来自QQ邮箱的假期自动回复邮件。你好,你的邮件我收到,谢谢。
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最终在执行fn() 的时候,作用域链是 [fn.AO,foo.AO,global.VO], c始终保存在 bar函数的上下文中,在执行fn时已经被销毁了。
定义
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,对作者也是一种鼓励。