一个资深的同事在我出发去面试前告诫我,问JS知识点的时候千万别主动提闭包,它就是一个坑啊!坑啊!啊!html
闭包确实是js的难点和重点,其实也没那么可怕,关键是机制的理解,能够和函数一块儿单独拿出来讲说,其实关于闭包的解释不少文章都写得比较详细了,这篇文章就做为本身学习过程的记录吧。react
首先明确一下闭包的概念:面试
MDN (Mozilla Develop Network) 上的对闭包的定义:编程
闭包是指可以访问自由变量的函数 (变量在本地使用,但在闭包中定义)。换句话说,定义在闭包中的函数能够“记忆”它被建立时候的环境。segmentfault
分析:数组
闭包由函数和与其相关的引用环境(词法环境)的组合而成闭包
闭包容许函数访问其引用环境(词法环境)中的变量(又称自由变量)编程语言
广义上来讲,全部JS的函数均可以称为闭包,由于JS函数在建立时保存了当前的词法环境函数
仍是很拗口有木有,一脸懵逼的时候就应该从基础的概念开始找,因此咱们来谈谈词法环境。学习
定义(摘自wiki百科)。
词法环境是一个用于定义特定变量和函数标识符在ECMAScript代码的词法嵌套结构上关联关系的规范类型。一个词法环境由一个环境记录项和可能为空的外部词法环境引用构成。
通常来讲,在编程语言中都有变量做用域的概念,每一个变量都有本身的生命周期和做用范围。
做用域有两种解析方式:
静态做用域
又称为词法做用域,在编译阶就能够决定变量的引用,由程序定义的位置决定,和代码执行顺序无关,用嵌套的方式解析。
动态做用域
在程序运行时候,和代码的执行顺序决定。用动态栈动态管理。
var x = 10; function getX() { alert(x); } function foo() { var x = 20; getX(); } foo();
在静态做用域下:
全局做用域下有x
, getX
, foo
三个变量,getX
和foo
都有本身的做用域。执行foo函数的时候,getX()被执行,可是getX的定义位置在全局做用域下的,取到的x是10
,而不是20
。
在动态做用域下:
运行这段代码时,先把x=10
、getX
、foo
按顺序压栈,而后执行foo
函数,在函数中把x=20
压栈,而后执行getX()
,此时距离栈顶最近的x
值为20
,所以alert的值也是20
。
JavaScript使用的变量做用域是静态做用域。JS中做用域简单分为两部分:全局做用域和函数做用域。ES5中使用词法环境管理静态做用域。
词法环境包含两部分
环境记录
形参
函数声明
变量
其它...
对外部词法环境的引用(outer)
一段JS代码执行以前,会对环境记录进行初始化(声明提早),即将函数的形参、函数声明和变量先放入函数的环境记录中,特别须要注意的是:
形参会在初始化的时候定义值,可是函数内部定义的变量只声明不定义(不赋值),这个须要用JS中的Hoisting机制来解释,具体能够看这一篇文章:《理解 JavaScript(二):Scoping & Hoisting》。
如下面这段代码为例,解析环境记录初始化和代码执行的过程:
var x = 10; function foo(y) { var z = 30; function bar(q) { return x + y + z + q; } return bar; } var bar = foo(20); bar(40);
step1:初始化全局环境
全局环境 | |
---|---|
环境记录(record) | foo: <function> |
x: undefined(声明变量而非定义变量) | |
bar: undefined(声明变量而非定义变量) | |
外部环境(outer) | null |
step2: 执行x=10
全局环境 | |
---|---|
环境记录(record) | foo: <function> |
x: 10() | |
bar: undefined(声明变量而非定义变量) | |
外部环境(outer) | null |
step3:执行var bar = foo(20)语句以前,将foo函数的环境记录初始化
foo 环境 | |
---|---|
环境记录(record) | y: 20(定义形参) |
bar: <function> | |
z: undefined(声明变量而非定义变量) | |
外部环境(outer) | 全局环境 |
step4:执行var bar = foo(20)语句,变量bar接收foo函数中返回的bar函数
foo 环境 | |
---|---|
环境记录(record) | y: 20 |
bar: <function> | |
z: 30(定义z) | |
外部环境(outer) | 全局环境 |
step5:执行bar函数以前,初始化bar的词法环境
bar环境 | |
---|---|
环境记录(record) | q: 40(定义形参q) |
外部环境(outer) | foo环境 |
step6:在foo函数内执行bar函数
x + y + z + q = 10 + 20 + 30 + 40 = 100
其实说了那么多,也是想强调一点:形参的值在环境初始化的时候就赋值了!所以形参的做用之一就是保存外部变量的值!
查了一下关于闭包的面试题,用具体的例子说明闭包的应用场景。
最多见的答案来自于《JavaScript高级程序设计(第3版)》p181:
例子:
function creacteFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function () { return i; } } return result; }
这个函数返回了长度为10的函数数组,假设咱们调用函数数组的第3个函数,在控制台中输入creacteFunctions()[2]()
,即执行函数数组里面的第三个函数,creacteFunctions()
返回函数数组,[2]是取第三个函数的引用,最后一个()是执行第三个函数,返回结果却并非预期的2
,而是10
.
所以,为了可以让闭包的行为符合预期,须要建立一个匿名函数:
function creacteFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function (num) { return function() { return num; } }(i); } return result; }
此时在控制台中输入creacteFunctions()[2]()
,即执行函数数组里面的第三个函数,返回的就是预期中的2
。
有了词法环境的初始化过程,这里也就很是容易理解了。匿名函数的形参num保存了每次执行的i的值。在function(num){...}(i)
这个结构中,i做为形参num的实际值执行这个匿名函数,所以每次循环中的num直接初始化为i的值。
为了更清楚的提取这部分结构,咱们将匿名函数命名为helper:
var helper = function (num) { return function() { return num; } }
用helper函数重写第二段代码:
var helper = function (num) { return function() { return num; } } function creacteFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = helper(i); } return result; }
在控制台中输入creacteFunctions()[2]()
,输出的也是预期中的2
。
未完待续哦,闭包能够讲的东西太多啦!
真正理解了做用域也就理解了闭包.