(译)学习JavaScript闭包
什么是闭包?
function numberGenerator() {
// 闭包里的局部“自由”变量
var num = 1;
function checkNumber() {
console.log(num);
}
num++;
return checkNumber;
} var number = numberGenerator();
number(); //
function sayHello() {
var say = function() { console.log(hello); }
// Local variable that ends up within the closure
var hello = 'Hello, world!';
return say;
}
var sayHelloClosure = sayHello();
sayHelloClosure(); // ‘Hello, world!’
注意变量hello是如何在匿名函数后定义的,但这个匿名函数依然可以访问hello变量,这是因为hello变量被创建时已经定义在函数“作用域”里了,这使得当匿名函数最终执行时,hello变量依然可用。(别急,我将随后在这篇文章中解释什么是“作用域”,现在,就让我们来看看)
高层次的理解
执行上下文
var x = 10;
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的棘手之处,我们可以用洋葱的皮层来表示词法环境:全局环境就是洋葱最外层的皮层,每一个子层都嵌套在它里面。
LexicalEnvironment = {
EnvironmentRecord: {
// Identifier bindings go here
}, // Reference to the outer environment
outer: < >
};
- “每次执行这样的代码就会创建一个新的词法环境”:每次一个封闭的外部函数被调用时,就会创建一个新的词法环境,这一点很重要——我们在文章最后将会再说到这点。(边注:函数不是唯一可以创建词法环境的方式,块语句和catch子句也可以创建词法环境,为了简单起见,在这篇文章中我们将只说函数创建的环境。)
作用域链
var x = 10; function foo() {
var y = 20; // free variable
function bar() {
var z = 15; // free variable
return x + y + z;
}
return bar;
}
动态作用域 VS 静态作用域
var x = 10; function foo() {
var y = x + 5;
return y;
} function bar() {
var x = 2;
return foo();
} function main() {
foo(); // Static scope: 15; Dynamic scope: 15
bar(); // Static scope: 15; Dynamic scope: 7
return 0;
}
当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(); //
基于我们对环境是如何工作的认识,我们可以说,上面例子中定义的环境看起来是这样的(注意,这个完全是伪代码):
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的工具
本文翻译youtube上的up主kudvenkat的javascript tutorial播放单 源地址在此: https://www.youtube.com/watch?v=PMsVM7rjupU& ...
- 学习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闭包导致的闭合变量问题以及解决方法
本文是翻译此文 预先阅读此文:闭合循环变量时被认为有害的(closing over the loop variable considered harmful) JavaScript也有同样的问题.考虑 ...
- 学习Javascript闭包(Closure) by 阮一峰
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...
随机推荐
- 恶意软件Mirai换了个马甲 瞄上我国2亿多台IoT设备
恶意软件Mirai换了个马甲 瞄上我国2亿多台IoT设备 想要起来时,一种沉重感阻碍着他,这是一种安全感:感觉到一张床为他铺好了,而且只属于他:想要静卧时,一种不安阻碍着他,把他从床上赶起来,这是 ...
- HTML5之appcache语法理解/HTML5应用程序缓存/manifest缓存文件官方用法翻译
习惯性的贴几个参考链接: W3School-HTML 5 应用程序缓存 官方 MDN window.applicationCache 接口文档 官方 MDN 用法示例 看所有的教程不如直接看最原始的官 ...
- NOR和NAND
NOR和NAND NOR和NAND是现在市场上两种主要的非易失闪存技术.Intel于1988年首先开发出NOR flash技术,彻底改变了原先由EPROM和EEPROM一统天下的局面.紧接着,198? ...
- 【NOIP模拟】jzoj5233概率博弈(树规)
Description 小A和小B在玩游戏.这个游戏是这样的: 有一棵
- Spring MVC 快捷定义 ViewController
WHY : 为什么我们需要快捷定义 ViewController ? 在项目开发过程中,经常会涉及页面跳转问题,而且这个页面跳转没有任何业务逻辑过程,只是单纯的路由过程 ...
- HTML的语法
1,什么是HTML标记语言,他是表示网页信息的符号标记语言,特点包括: a,可以设置文本的格式,比如标题,文号,文本颜色,段落等待 b,可以简历列表 c,可以插入图像和媒体 d,可以建立表格 e,超连 ...
- x86-64栈帧中的“红色区域” red zone of stack frame on x86-64
前几天看System V AMD64 ABI标准的时候发现栈帧的顶部后面有一块"red zone",在学cs:app3e/深入理解操作系统的时候并没有遇到这个,总结一下. 引用标准 ...
- R学习笔记 第五篇:数据变换和清理
在使用R的分组操作之前,首先要了解R语言包,包实质上是实现特定功能的,预先写好的代码库(library),R拥有大量的软件包,许多包都是由某一领域的专家编写的,但并不是所有的包都有很高的质量的,在使用 ...
- Linux分区规划与xshell使用排错
1.1 没有重要数据 /boot 200M 存放系统的引导信息 内核 swap 交换分区 防止内存用光了 临时的一个内存 如果你的内存小于8G swap是内存的1.5倍 如果你的 ...
- sass学习--安装ruby
1.下载ruby:https://rubyinstaller.org/downloads/ 2.安装完ruby之后,在开始菜单中,找到刚才我们安装的ruby,打开Start Command Promp ...