JS详细图解作用域链与闭包
JS详细图解作用域链与闭包

初学JavaScript的时候,我在学习闭包上,走了很多弯路。而这次重新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个非常大的挑战。
闭包有多重要?如果你是初入前端的朋友,我没有办法直观的告诉你闭包在实际开发中的无处不在,但是我可以告诉你,前端面试,必问闭包。面试官们常常用对闭包的了解程度来判定面试者的基础水平,保守估计,10个前端面试者,至少5个都死在闭包上。
可是为什么,闭包如此重要,还是有那么多人没有搞清楚呢?是因为大家不愿意学习吗?还真不是,而是我们通过搜索找到的大部分讲解闭包的中文文章,都没有清晰明了的把闭包讲解清楚。要么浅尝辄止,要么高深莫测,要么干脆就直接乱说一通。包括我自己曾经也写过一篇关于闭包的总结,回头一看,不忍直视[捂脸]。
因此本文的目的就在于,能够清晰明了得把闭包说清楚,让读者老爷们看了之后,就把闭包给彻底学会了,而不是似懂非懂。
一、作用域与作用域链
在详细讲解作用域链之前,我默认你已经大概明白了JavaScript中的下面这些重要概念。这些概念将会非常有帮助。
- 基础数据类型与引用数据类型
- 内存空间
- 垃圾回收机制
- 执行上下文
- 变量对象与活动对象
作用域
在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
这里的标识符,指的是变量名或者函数名
JavaScript中只有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。
作用域与执行上下文是完全不同的两个概念。我知道很多人会混淆他们,但是一定要仔细区分。
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

作用域链
回顾一下上一篇文章我们分析的执行上下文的生命周期,如下图。

我们发现,作用域链是在执行上下文的创建阶段生成的。这个就奇怪了。上面我们刚刚说作用域在编译阶段确定规则,可是为什么作用域链却在执行阶段确定呢?
之所有有这个疑问,是因为大家对作用域和作用域链有一个误解。我们上面说了,作用域是一套规则,那么作用域链是什么呢?是这套规则的具体实现。所以这就是作用域与作用域链的关系,相信大家都应该明白了吧。
我们知道函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。之前一篇文章我们详细说明了变量对象,而这里,我们将详细说明作用域链。
作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
为了帮助大家理解作用域链,我我们先结合一个例子,以及相应的图示来说明。
var a = 20;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest();
}
test();
在上面的例子中,全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。
innerTestEC = {
VO: {...}, // 变量对象
scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
this: {}
}
是的,你没有看错,我们可以直接用一个数组来表示作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象。
很多人会误解为当前作用域与上层作用域为包含关系,但其实并不是。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图。

注意,因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,因此图中使用了AO来表示。Active Object
是的,作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。
二、闭包
对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,突破闭包的瓶颈可以使你功力大增。
- 闭包与作用域链息息相关;
- 闭包是在函数执行过程中被确认。
先直截了当的抛出闭包的定义:当函数可以记住并访问所在的作用域(全局作用域除外)时,就产生了闭包,即使函数是在当前作用域之外执行。
简单来说,假设函数A在函数B的内部进行定义了,并且当函数A在执行时,访问了函数B内部的变量对象,那么B就是一个闭包。
非常抱歉之前对于闭包定义的描述有一些不准确,现在已经改过,希望收藏文章的同学再看到的时候能看到吧,对不起大家了。
关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。
而我们知道,函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是闭包的存在,会阻止这一过程。
先来一个简单的例子。
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
fn(); // 此处的保留的innerFoo的引用
}
foo();
bar(); // 2
在上面的例子中,foo()
执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo
,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。
这样,我们就可以称foo为闭包。
下图展示了闭包fn的作用域链。

我们可以在chrome浏览器的开发者工具中查看这段代码运行时产生的函数调用栈与作用域链的生成情况。如下图。

在上面的图中,红色箭头所指的正是闭包。其中Call Stack为当前的函数调用栈,Scope为当前正在被执行的函数的作用域链,Local为当前的局部变量。
所以,通过闭包,我们可以在其他的执行上下文中,访问到函数的内部变量。比如在上面的例子中,我们在函数bar的执行环境中访问到了函数foo的a变量。个人认为,从应用层面,这是闭包最重要的特性。利用这个特性,我们可以实现很多有意思的东西。
不过读者老爷们需要注意的是,虽然例子中的闭包被保存在了全局变量中,但是闭包的作用域链并不会发生任何改变。在闭包中,能访问到的变量,仍然是作用域链上能够查询到的变量。
对上面的例子稍作修改,如果我们在函数bar中声明一个变量c,并在闭包fn中试图访问该变量,运行结果会抛出错误。
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
var c = 100;
fn(); // 此处的保留的innerFoo的引用
}
foo();
bar();
闭包的应用场景
接下来,我们来总结下,闭包的常用场景。
- 延迟函数setTimeout
我们知道setTimeout的第一个参数是一个函数,第二个参数则是延迟的时间。在下面例子中,
function fn() {
console.log('this is test.')
}
var timer = setTimeout(fn, 1000);
console.log(timer);
执行上面的代码,变量timer的值,会立即输出出来,表示setTimeout这个函数本身已经执行完毕了。但是一秒钟之后,fn才会被执行。这是为什么?
按道理来说,既然fn被作为参数传入了setTimeout中,那么fn将会被保存在setTimeout变量对象中,setTimeout执行完毕之后,它的变量对象也就不存在了。可是事实上并不是这样。至少在这一秒钟的事件里,它仍然是存在的。这正是因为闭包。
很显然,这是在函数的内部实现中,setTimeout通过特殊的方式,保留了fn的引用,让setTimeout的变量对象,并没有在其执行完毕后被垃圾收集器回收。因此setTimeout执行结束后一秒,我们任然能够执行fn函数。
- 柯里化
在函数式编程中,利用闭包能够实现很多炫酷的功能,柯里化算是其中一种。关于柯里化,我会在以后详解函数式编程的时候仔细总结。
- 模块
在我看来,模块是闭包最强大的一个应用场景。如果你是初学者,对于模块的了解可以暂时不用放在心上,因为理解模块需要更多的基础知识。但是如果你已经有了很多JavaScript的使用经验,在彻底了解了闭包之后,不妨借助本文介绍的作用域链与闭包的思路,重新理一理关于模块的知识。这对于我们理解各种各样的设计模式具有莫大的帮助。
(function () {
var a = 10;
var b = 20;
function add(num1, num2) {
var num1 = !!num1 ? num1 : a;
var num2 = !!num2 ? num2 : b;
return num1 + num2;
}
window.add = add;
})();
add(10, 20);
在上面的例子中,我使用函数自执行的方式,创建了一个模块。方法add被作为一个闭包,对外暴露了一个公共方法。而变量a,b被作为私有变量。在面向对象的开发中,我们常常需要考虑是将变量作为私有变量,还是放在构造函数中的this中,因此理解闭包,以及原型链是一个非常重要的事情。模块十分重要,因此我会在以后的文章专门介绍,这里就暂时不多说啦。

为了验证自己有没有搞懂作用域链与闭包,这里留下一个经典的思考题,常常也会在面试中被问到。
利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
- for (var i = 1; i <= 5; i++) {
- (function (i) {
- setTimeout(function timer() {
- console.log(i);
- }, i * 1000);
- })(i);
- }
- for (var i = 1; i <= 5; i++) {
- setTimeout(function (i) {
- return function () {
- console.log(i);
- }
- }(i), i * 1000);
- }
JS详细图解作用域链与闭包的更多相关文章
- 前端高质量知识(四)-JS详细图解作用域链与闭包
攻克闭包难题 初学JavaScript的时候,我在学习闭包上,走了很多弯路.而这次重新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个非常大的挑战. 闭包有多重要?如果你是初入前端的朋友,我没有办法 ...
- javascript深入浅出图解作用域链和闭包
一.概要 对于闭包的定义(红宝书P178):闭包就是指有权访问另外一个函数的作用域中的变量的函数. 关键点: 1.闭包是一个函数 2.能够访问另外一个函数作用域中的变量 文章首发地址于sau交流学习社 ...
- 【进阶2-2期】JavaScript深入之从作用域链理解闭包(转)
这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://github.com/yygmind/blog/issues/18 红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一 ...
- js深入(三)作用域链与闭包
在之前我们根绝对象的原型说过了js的原型链,那么同样的js 万物皆对象,函数也同样存在这么一个链式的关系,就是函数的作用域链 作用域链 首先先来回顾一下之前讲到的原型链的寻找机制,就是实例会先从本身开 ...
- 1--面试总结-js深入理解,对象,原型链,构造函数,执行上下文堆栈,执行上下文,变量对象,活动对象,作用域链,闭包,This
参考一手资料:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/中文翻译版本:https://zhuanlan.zhihu.com/p ...
- js之作用域链到闭包
一.作用域 全局作用域和函数作用域(局部作用域). 一个变量的作用域就是源代码中定义这个变量的区域. 二.作用域链和闭包 全局变量只有一个(window,globel),全局环境下每一个函数都会形成一 ...
- JS详细图解全方位解读this
JS详细图解全方位解读this 对于this指向的理解中,有这样一种说法:谁调用它,this就指向谁.在我刚开始学习this的时候,我是非常相信这句话的.因为在一些情况下,这样理解也还算说得通.可是我 ...
- 在chrome开发者工具中观察函数调用栈、作用域链与闭包
在chrome开发者工具中观察函数调用栈.作用域链与闭包 在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量 ...
- 深入理解JavaScript中的作用域、作用域链和闭包
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/qappleh/article/detai ...
随机推荐
- 【转载】COM编程入门不得不看的文章 :第一部分 什么是COM,如何使用COM
原文:COM编程入门不得不看的文章 :第一部分 什么是COM,如何使用COM 原文:http://www.codeproject.com/Articles/633/Introduction-to-CO ...
- c++ 重载运算与类型转换
1. 基础概念 重载的运算符是具有特殊名字的函数:(重载运算符函数,运算符函数.重载运算符) 依次包含返回类型,函数名(operator=),参数列表,函数体. 只有重载的函数调用运算符operato ...
- 180710-MySql插入唯一键冲突的三种可选方式
MySql插入时唯一键冲突的几种处理方式 MySql插入一条记录,结果提示主键冲突,怎么办? 批量插入数据时,发现插入的这批数据中,有某些记录存在唯一键冲突,一个一个跳出来就比较麻烦了,有什么好的办法 ...
- Appium Inspector定位元素与录制简单脚本
本次以微信为例, 使用Appium自带的Inspector定位工具定位元素, 以及进行最最最简单脚本的录制: capabilities = { "platformName": &q ...
- Python安装教程最新版
Python安装教程最新版 目前Python官网已经更新到了最新版Python 3.7.1, 相比Python 2系列,它的兼容性不是太好, 不过应该会在不久的将来会全面解决.它的安装比较容易,具体步 ...
- Zookeeper与Eureka的区别
Zookeeper与Eureka的区别 想要了解Zk与eureka的区别首先要知道CAP定理 CAP定理 Mysql强一致性(数据唯一出处),设计数据库设计的三范式 (表必须有主键:表不能有重复的列: ...
- 【RandomString】- 随机字符串
RandomString 随机字符串的用法
- 利用nohup后台运行jar文件包程序
Linux 运行jar包命令如下: 方式一: java -jar XXX.jar特点:当前ssh窗口被锁定,可按CTRL + C打断程序运行,或直接关闭窗口,程序退出 那如何让窗口不锁定? 方式二 j ...
- [堆+贪心]牛客练习赛40-B
传送门:牛客练习赛40 题面: 小A手头有 n 份任务,他可以以任意顺序完成这些任务,只有完成当前的任务后,他才能做下一个任务 第 i 个任务需要花费 x_i 的时间,同时完成第 i 个任务的时间不 ...
- 幸运的袋子(深度优先遍历(Depth First Search,DFS))
题目描述 一个袋子里面有n个球,每个球上面都有一个号码(拥有相同号码的球是无区别的).如果一个袋子是幸运的当且仅当所有球的号码的和大于所有球的号码的积. 例如:如果袋子里面的球的号码是{1, 1, 2 ...