【转】学习JavaScript闭包
什么是闭包?

1 function numberGenerator() {
2 // 闭包里的局部“自由”变量
3 var num = 1;
4 function checkNumber() {
5 console.log(num);
6 }
7 num++;
8 return checkNumber;
9 }
10
11 var number = numberGenerator();
12 number(); // 2


1 function sayHello() {
2 var say = function() { console.log(hello); }
3 // Local variable that ends up within the closure
4 var hello = 'Hello, world!';
5 return say;
6 }
7 var sayHelloClosure = sayHello();
8 sayHelloClosure(); // ‘Hello, world!’

注意变量hello是如何在匿名函数后定义的,但这个匿名函数依然可以访问hello变量,这是因为hello变量被创建时已经定义在函数“作用域”里了,这使得当匿名函数最终执行时,hello变量依然可用。(别急,我将随后在这篇文章中解释什么是“作用域”,现在,就让我们来看看)
高层次的理解
执行上下文


function foo(a) {
var b = 20;
function bar(c) {
var d = 30;
return boop(x + a + b + c + d);
}
function boop(e) {
return e * -1;
}
return bar;
}
var moar = foo(5); // Closure
/*
The function below executes the function bar which was returned
when we executed the function foo in the line above. The function bar
invokes boop, at which point bar gets suspended and boop gets push
onto the top of the call stack (see the screenshot below)
*/
moar(15);


- 代码评估状态:执行、暂停和恢复与此执行上下文相关的代码的任何状态。
- 函数:该执行上下文正在评估的函数对象。(如果被评估的上下文是脚本或模块,则为null)
- 领域:一组内部对象,ECMAScript全局环境,在该全局环境范围内加载的所有ECMAScript代码,以及其他关联的状态和资源。
- 词法环境: 用来解析该执行上下文中的代码所作的标识符引用。
- 变量环境:词法环境,环境记录保存由该执行上下文中的变量状态创建的绑定。
词法环境
- “用于定义标识符的关联”:词法环境的目的是用来管理代码里的数据(如标识符),换句话说,它使得标识符有意义。例如,如果我们有一行代码“console.log(x / 10)”,如果变量(或“标识符”)x没有任何含义,那么这行代码就没有任何意义了。词法环境就是通过它的环境记录来提供意义(或“关联”)。
- “词法环境由环境记录组成”:环境记录是用一种奇特的方式来描述它是保存了所有标识符和它们在词法环境里的绑定的记录。每个词法环境都有各自的环境记录。
- “词法嵌套结构”:这是最有意思的部分,这个基本上说是它的内部环境引用它的外部环境,而它的外部环境也一样可以有它的外部环境,所以,一个环境可以是多个内部环境的外部环境。全局环境是唯一一个没有外部环境的词法环境,这就是JS的棘手之处,我们可以用洋葱的皮层来表示词法环境:全局环境就是洋葱最外层的皮层,每一个子层都嵌套在它里面。


1 LexicalEnvironment = {
2 EnvironmentRecord: {
3 // Identifier bindings go here
4 },
5
6 // Reference to the outer environment
7 outer: < >
8 };

- “每次执行这样的代码就会创建一个新的词法环境”:每次一个封闭的外部函数被调用时,就会创建一个新的词法环境,这一点很重要——我们在文章最后将会再说到这点。(边注:函数不是唯一可以创建词法环境的方式,块语句和catch子句也可以创建词法环境,为了简单起见,在这篇文章中我们将只说函数创建的环境。)
作用域链

1 var x = 10;
2
3 function foo() {
4 var y = 20; // free variable
5 function bar() {
6 var z = 15; // free variable
7 return x + y + z;
8 }
9 return bar;
10 }


动态作用域 VS 静态作用域

1 var x = 10;
2
3 function foo() {
4 var y = x + 5;
5 return y;
6 }
7
8 function bar() {
9 var x = 2;
10 return foo();
11 }
12
13 function main() {
14 foo(); // Static scope: 15; Dynamic scope: 15
15 bar(); // Static scope: 15; Dynamic scope: 7
16 return 0;
17 }

当bar函数被调用时,我们可以看到上面的动态作用域和静态作用域返回了不同的值。

var myVar = 100; function foo() {
console.log(myVar);
} foo(); // Static scope: 100; Dynamic scope: 100 (function () {
var myVar = 50;
foo(); // Static scope: 100; Dynamic scope: 50
})(); // Higher-order function
(function (arg) {
var myVar = 1500;
arg(); // Static scope: 100; Dynamic scope: 1500
})(foo);

同样,在动态作用域的例子,上面的myVar变量在使用了myVar变量的函数被调用的地方解析。另一方面,在静态作用域里,将myVar解析为在创建两个IIFE函数的范围内保存的变量 。
闭包

var x = 10; function foo() {
var y = 20; // free variable
function bar() {
var z = 15; // free variable
return x + y + z;
}
return bar;
} var test = foo(); test(); // 45

基于我们对环境是如何工作的认识,我们可以说,上面例子中定义的环境看起来是这样的(注意,这个完全是伪代码):

GlobalEnvironment = {
EnvironmentRecord: {
// built-in identifiers
Array: '<func>',
Object: '<func>',
// etc.. // custom identifiers
x: 10
},
outer: null
}; fooEnvironment = {
EnvironmentRecord: {
y: 20,
bar: '<func>'
}
outer: GlobalEnvironment
}; barEnvironment = {
EnvironmentRecord: {
z: 15
}
outer: fooEnvironment
};


var result = []; for (var i = 0; i < 5; i++) {
result[i] = function () {
console.log(i);
};
} result[0](); // 5, expected 0
result[1](); // 5, expected 1
result[2](); // 5, expected 2
result[3](); // 5, expected 3
result[4](); // 5, expected 4

回到我们刚才所学的,我们就可以轻而易举就发现其中的错误所在!绝对,当for循环结束后,它这里的环境就像下面的一样:

environment: {
EnvironmentRecord: {
result: [...],
i: 5
},
outer: null,
}

这里错误的假想在作用域,以为结果数组中五个函数的作用域是不一样的,然而,事实上结果数组中五个函数的环境(或者/上下文/作用域)是一样的,因此,变量i每增加一次,它就更新了作用域里的值——这个作用域里的值是被所有函数共享的。这就是为什么五个函数中的任意一个去访问i时都返回5的原因(当for循环结束时,i等于5)。

var result = []; for (var i = 0; i < 5; i++) {
result[i] = (function inner(x) {
// additional enclosing context
return function() {
console.log(x);
}
})(i);
} result[0](); // 0, expected 0
result[1](); // 1, expected 1
result[2](); // 2, expected 2
result[3](); // 3, expected 3
result[4](); // 4, expected 4

对!这样就可以了:)

var result = []; for (let i = 0; i < 5; i++) {
result[i] = function () {
console.log(i);
};
} result[0](); // 0, expected 0
result[1](); // 1, expected 1
result[2](); // 2, expected 2
result[3](); // 3, expected 3
result[4](); // 4, expected 4

例2:

function iCantThinkOfAName(num, obj) {
// This array variable, along with the 2 parameters passed in,
// are 'captured' by the nested function 'doSomething'
var array = [1, 2, 3];
function doSomething(i) {
num += i;
array.push(num);
console.log('num: ' + num);
console.log('array: ' + array);
console.log('obj.value: ' + obj.value);
} return doSomething;
} var referenceObject = { value: 10 };
var foo = iCantThinkOfAName(2, referenceObject); // closure #1
var bar = iCantThinkOfAName(6, referenceObject); // closure #2 foo(2);
/*
num: 4
array: 1,2,3,4
obj.value: 10
*/ bar(2);
/*
num: 8
array: 1,2,3,8
obj.value: 10
*/ referenceObject.value++; foo(4);
/*
num: 8
array: 1,2,3,4,8
obj.value: 11
*/ bar(4);
/*
num: 12
array: 1,2,3,8,12
obj.value: 11
*/

在这个例子里,我们可以看到每次调用iCantThinkOfAName函数时都会创建一个新的闭包,也就是foo和bar。后续调用每个闭包函数都会更新闭包内的变量,这展示了iCantThinkOfAName函数返回后,每个闭包里的变量继续被iCantThinkOfAName函数里的doSomething函数所使用。

function mysteriousCalculator(a, b) {
var mysteriousVariable = 3;
return {
add: function() {
var result = a + b + mysteriousVariable;
return toFixedTwoPlaces(result);
}, subtract: function() {
var result = a - b - mysteriousVariable;
return toFixedTwoPlaces(result);
}
}
} function toFixedTwoPlaces(value) {
return value.toFixed(2);
} var myCalculator = mysteriousCalculator(10.01, 2.01);
myCalculator.add() // 15.02
myCalculator.subtract() // 5.00

我们能够看到的是mysteriousCalculator是在全局作用域里,而且它返回了两个函数。抽象来看,上面例子中的环境就像是这样的:

GlobalEnvironment = {
EnvironmentRecord: {
// built-in identifiers
Array: '<func>',
Object: '<func>',
// etc... // custom identifiers
mysteriousCalculator: '<func>',
toFixedTwoPlaces: '<func>',
},
outer: null,
}; mysteriousCalculatorEnvironment = {
EnvironmentRecord: {
a: 10.01,
b: 2.01,
mysteriousVariable: 3,
}
outer: GlobalEnvironment,
}; addEnvironment = {
EnvironmentRecord: {
result: 15.02
}
outer: mysteriousCalculatorEnvironment,
}; subtractEnvironment = {
EnvironmentRecord: {
result: 5.00
}
outer: mysteriousCalculatorEnvironment,
};

因为我们的add和subtract函数都有一个指向mysteriousCalculator函数环境的引用,它们可以使用那个环境里的变量来计算结果。

function secretPassword() {
var password = 'xh38sk';
return {
guessPassword: function(guess) {
if (guess === password) {
return true;
} else {
return false;
}
}
}
} var passwordGame = secretPassword();
passwordGame.guessPassword('heyisthisit?'); // false
passwordGame.guessPassword('xh38sk'); // true

这是一个很强大的技巧——它使得闭包函数guessPassword可以独占访问password变量,同时让password变量不能从外部访问。
摘要
- 执行上下文是ECMAScript规范用来根据运行时代码执行的一个抽象概念。在任何时候,在代码执行时都只有一个执行上下文。
- 每个执行上下文都有一个词法环境,这个词法环境保留着标识符绑定(如变量及其相关的值),同时还有一个指向它外部环境的引用。
- 每个环境都可以访问的标识符集称为“作用域”。我们可以嵌套这些作用域到层次环境链中,这就是“作用域链”。
- 每个函数都有一个执行上下文,它由一个给予函数里的变量意义的词法环境,和指向父环境的引用组成,这看起来就像是函数“记住”这个环境(或者作用域),因为函数事实上有一个指向这个环境的引用,这就是闭包。
- 每次一个封闭外部函数被调用时就会创建一个闭包,换句话说,内部函数不需要返回要创建的闭包。
- JavaScript里的闭包作用域就是词法,这意味着它是在源代码里的位置静态定义的。
- 闭包用许多实际的用处,最重要的一个用处是维护一个私有指向外部环境变量的引用。
结束语
延伸阅读
- 执行环境里的变量环境是什么?Axel Rauschmayer博士对这个问题做了解释,所以我把它的博客文章链接放在这里: http://www.2ality.com/2011/04/ecmascript-5-spec-lexicalenvironment.html
- 各种环境记录有什么不同?http://www.ecma-international.org/ecma-262/6.0/#sec-environment-records
- MDN上关于闭包的一片优秀的文章: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
【转】学习JavaScript闭包的更多相关文章
- [转载]学习Javascript闭包(Closure)
学习Javascript闭包(Closure) 源地址: http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures ...
- 通过示例学习JavaScript闭包
译者按: 在上一篇博客,我们通过实现一个计数器,了解了如何使用闭包(Closure),这篇博客将提供一些代码示例,帮助大家理解闭包. 原文: JavaScript Closures for Dummi ...
- 学习JavaScript闭包
作者: 阮一峰 日期: 2009年8月30日 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 下面就是我的学习笔记,对于Javascript初 ...
- [JS]学习Javascript闭包(Closure)
转自:阮一峰 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 下面就是我的学习笔记,对于Javascript初学者应该是很有用的. 一.变量的 ...
- 学习Javascript闭包(Closure)及几个经典面试题理解
今天遇到一个面试题,结果让我百思不得其解.后来在查阅了各种文档后,理清了来龙去脉.让我们先来看看这道题: function Foo( ){ var i = 0; return function( ){ ...
- 学习Javascript闭包(Closure)
闭包作用 1.让变量驻留在内存中 2.函数外部可以读取函数内部的私有变量 <!DOCTYPE html> <html lang="en"> <head ...
- 学习Javascript闭包(Closure) by 阮一峰
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...
- (译)学习JavaScript闭包
原文地址:https://medium.freecodecamp.org/lets-learn-javascript-closures-66feb44f6a44 闭包是JavaScript中一个基 ...
- 深入学习javaScript闭包(闭包的原理,闭包的作用,闭包与内存管理)
前言 虽然JavaScript是一门完整的面向对象的编程语言,但这门语言同时也拥有许多函数式语言的特性. 函数式语言的鼻祖是LISP,JavaScript在设计之初参考了LISP两大方言之一的Sche ...
随机推荐
- win7 快捷键 收集
1. 轻松访问键盘快捷方式 按住右 Shift 八秒钟:启用和关闭筛选键 按左 Alt + 左 Shift + PrtScn (或 PrtScn):启用或关闭高对比度 按左 Alt + 左 Shift ...
- nutz配置druid监控
druid 提供了一个web端的监控页面, 搭建起来不算麻烦, 建议添加. 打开web.xml, 在nutz的filter之前, 加入Web监控的配置 <filter> <filte ...
- python多个装饰器的执行顺序
def decorator_a(func): print 'Get in decorator_a' def inner_a(*args, **kwargs): print 'Get in inner_ ...
- c++ 模板template
1.函数模板的声明 声明形式 template<typename 数据类型参数标识符> <返回类型><函数名>(参数表) { 函数体 } 注: templa ...
- ios远程推送和python版push server相关笔记
今天研究了下ios的远程推送,网上的相关教程很多,做了一遍下来记录一下遇到的问题和注意事项(转载请注明) 1.证书及乱七八糟的配置 公钥:app id管理那儿的“Development Push SS ...
- idea文件全部变红, 文件全部红色
idea如果当前project用了版本控制器,其下面新建的所有的项目默认都是加入到版本控制里面,所以项目名称和文件都是红色的,如图: 看起来非常不爽, 那么如何解决呢? File–>Settin ...
- vue开发 - 根据vue-router的meta动态设置html里标签的内容
路由文件 :router/index.js import Vue from 'vue'import Router from 'vue-router'import index '@/view/index ...
- [实现] 利用 Seq2Seq 预测句子后续字词 (Pytorch)2
最近有个任务:利用 RNN 进行句子补全,即给定一个不完整的句子,预测其后续的字词.本文使用了 Seq2Seq 模型,输入为 5 个中文字词,输出为 1 个中文字词.目录 关于RNN 语料预处理 搭建 ...
- linux ping-测试主机之间网络的连通性
博主推荐:更多网络测试相关命令关注 网络测试 收藏linux命令大全 ping命令用来测试主机之间网络的连通性.执行ping指令会使用ICMP传输协议,发出要求回应的信息,若远端主机的网络功能没有问 ...
- Python之面向对象方法
Python之面向对象方法 property的用法: property属于类的封装的范畴 property是一种特殊的属性,访问它时会执行一段功能(函数),然后返回值. 用property的方法,就可 ...