javaScript中的闭包原理 (译)
这篇文章通过javaScript代码解释了闭包的原理,来让编程人员理解闭包。它不是写给大牛或使用功能性语言进行编程的程序员的。一旦意会了其核心概念,闭包理解起来并不难。然而,你不可能通过阅读任何有关闭包的学术文章或学术类的指导信息来弄明白它!
这篇文章是为有一些主流语言的编程经验,并且能读懂下面这段javaScript代码的编程人员准备的:
function sayHello(name) {
var text = 'Hello ' + name;
var say = function() { console.log(text); }
say();
}
闭包举例 |
两句话总结:
- 闭包是一个函数的局部变量,函数返回后不会消失,或
- 闭包是函数返回后一个未释放的堆栈帧(就像一个“堆栈帧”被malloc分配,而不是放在堆栈上。
下面的代码返回了一个函数引用:
function sayHello2(name) {
var text = 'Hello ' + name; // Local variable
var say = function() { console.log(text); }
return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"
上面的代码中,大多数javaScript程序员都可以理解如何将一个函数引用返回给一个变量(say2)。如果你不理解的话,那么在你学习闭包前需要先理解它。一个C程序员会认为函数sayHello2会将函数say的指针返回给say2,并且变量say和say2是两个指向目标函数的不同指针。
在这里,C函数指针和javaScript函数引用之间有着关键的区别。在javaScript中,你可以将函数引用变量看成既有一个指向函数的指针,又有一个指向闭包的隐藏指针。上面的代码中,由于在函数sayHello2中声明了另外一个匿名函数function(){console.log(text);},因此有一个闭包。在javaScript中,如果在一个函数中再使用function关键字,那你就是在创建一个闭包。
在C和其他大多数常用语言中,当一个函数返回后,由于栈帧被销毁,所有的局部变量将不能再被访问。在javaScript中,如果在一个函数中声明另一个函数,则当局部变量从你调用的函数返回后仍然可以访问。这点在上面已经证明了,因为在从sayHello2()返回后又调用了函数say2()。注意,我们称之为引用变量的代码text,是函数sayHello2()的一个局部变量。
function() { console.log(text); } // Output of say2.toString();
通过观察say2.toString()的输出,我们可以看到,该代码是指变量text。由于sayHello2()的局部变量仍保留在闭包中,因此匿名函数可以引用值为“Hello Bob”的变量text。
神奇之处在于,在javaScript中,函数引用也有一个对创建它的闭包的隐蔽引用——类似于委托(delegates)是一个方法指针加对一个对象的隐蔽引用。
更多的例子 |
出于某种原因,当你通过某些资料了解闭包的时候它们似乎很难理解,但是当你看一些例子的时候,你可以实际点击并了解它们的工作原理(这花了我一段时间来理解)。我建议你仔细研究一些有关闭包的例子,直到你能理解它们的工作原理。如果你在没有完全弄明白闭包工作原理的情况下就开始使用它的话,那你将会很快遇到一些非常奇怪的bug。
例3 |
这个例子说明,局部变量没有被复制——它们被引用保留了。就像当外部函数退出后仍然在内存中保留着一个栈帧。
function say667() {
// Local variable that ends up within closure
var num = 666;
var say = function() { console.log(num); }
num++;
return say;
}
var sayNumber = say667();
sayNumber(); // logs 667
例4 |
因为都在一个setupSomeGlobals()的调用中声明,所以三个全局函数对同一个闭包有一个共同的引用。
var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
// Local variable that ends up within closure
var num = 666;
// Store some references to functions as global variables
gLogNumber = function() { console.log(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
} setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); //
gSetNumber(5);
gLogNumber(); // var oldLog = gLogNumber; setupSomeGlobals();
gLogNumber(); // oldLog() //
当setupSomeGlobals()中定义三个函数时,这三个函数共享访问该闭包相同的局部变量。
注意上面的例子,如果你再次调用setupSomeGlobals()的时候,就会创建一个新的闭包(堆栈帧)。已有的gLogNumber,gIncreaseNumber,gSetNumber旧变量会被新闭包的函数重新覆盖。(在javaScript中,当你在函数中声明一个内部函数,每次调用外部函数的时候都会重新创建一次内部函数)。
例5 |
这个例子对很多人来理解起来回比较麻烦,所以你需要弄懂它。如果你在一个循环体中定义一个函数的时候要非常小心:闭包中的局部变量并不会像你刚开始想象的样子起作用。
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
} function testList() {
var fnlist = buildList([1,2,3]);
// Using j only to help prevent confusion -- could use i.
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
} result.push( function() {console.log(item + ' ' + list[i])}行向result数组添加了三次对匿名函数的引用。如果你对匿名函数不是太熟悉的话,可以这样理解:
pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);
注意,当你运行这个例子的时候,将会出现三次“item2 undefined”警告!这是因为,就像前面的例子,这里只有一个局部变量buildList的闭包。当在fnList[j]()行中调用匿名函数的时候,每次都使用了同样的单个闭包,并且使用了对应闭包中i和item的当前值(由于已经循环完,因此i的值为3,item的值为item2)。注意我们是从0开始索引的,因此item的值为item2,i++会将i的值增加到3。
例6 |
这个例子说明,在退出前,闭包中包含了所有在外部函数内声明的局部变量。注意,变量alice实际上是在匿名函数后声明的。首先定义了匿名函数,因为alice和匿名函数在同一个范围(javaScript变量提前声明),所以当调用函数的时候能访问alice变量。同时,sayAlice()()只需要直接调用从sayAlice()返回的函数引用——这非常像之前完成(what was done previously),但是没有临时变量。
function sayAlice() {
var say = function() { console.log(alice); }
// Local variable that ends up within closure
var alice = 'Hello Alice';
return say;
}
sayAlice()();
同时注意,变量say也在闭包中,可能会被在sayAlice()中声明的其他函数访问,或者在内部函数中被递归访问。
例7 |
最后一个例子表明,函数的每次调用都会为局部变量创建一个单独的闭包。函数声明时不会有单独的闭包,每次调用函数时才会有单独的闭包。
function newClosure(someNum, someRef) {
// Local variables that end up within closure
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
console.log('num: ' + num +
'\nanArray ' + anArray.toString() +
'\nref.someVar ' + ref.someVar);
}
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
总结 |
如果你对某件事不是完全明白,那最好的方法就是举些例子来理解它。读一些学术性的解释远远没有切实地理解一些例子来的容易。我对闭包和堆栈帧等概念的解释在技术上不完全科学——这些解释总的来说只是为了来帮助理解这些概念。一旦掌握了基本思想,之后你可以再具体理解细节部分。
最后观点 |
l 当你在一个函数中声明另一个函数,就用到了闭包。
l 当你在一个函数中使用了eval(),就用到了闭包。eval中的代码可以引用这个函数的局部变量,在eval中你甚至可以通过eval(‘var foo = …’)创建新的变量。
l 当你在一个函数中使用new Function(…)(函数构造器)的时候,这时并没有创建闭包。(new出来的函数不能引用外部函数中的局部变量。)
l javaScript中的闭包就像一个保存着所有局部变量的副本,变量值为函数返回时对应的值。
l 最好这样想,总是在进入一个函数的时候就创建了闭包,并且向该闭包中加入了局部变量。
l 每次调用包含闭包的函数的时候,都会对应有一个新的局部变量集合(假设这个函数中包含了一个内部函数,并且返回了这个内部函数的引用或者以某种方式存在的该内部函数的外部引用)。
l 两个函数可能看起来有相同的代码,但是由于它们隐藏的闭包,因此有着完全不同的行为。我不认为javaScript代码真的可以发现一个函数引用是否有闭包。
l 如果你正在尝试动态修改源代码(例如:myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));),如果myFunction是一个闭包的话,该修改将不会起 作用(当然,你肯定不会在代码运行的时候进行源代码的修改,但是…)。
l 你可能在函数中的函数声明中获得函数声明,同时你可以在多个级别获得闭包。
l 我认为,通常来说,一个闭包是函数和其包含的变量的术语。注意,在本文我没有用到这个概念。
l 我怀疑javaScript中的闭包和那些在功能性语言中的闭包有所不同。
译自:http://stackoverflow.com/questions/111102/how-do-javascript-closures-work
翻译的不太好,对英文和对技术还有很大的提升空间,后续如果发现某个句子有更好的翻译方法会继续改进。
javaScript中的闭包原理 (译)的更多相关文章
- [译]Javascript中的闭包(closures)
本文翻译youtube上的up主kudvenkat的javascript tutorial播放单 源地址在此: https://www.youtube.com/watch?v=PMsVM7rjupU& ...
- javascript中的闭包解析
学习javaScript已经有一段时间了,在这段时间里,已经感受到了JavaScript的种种魅力,这是一门神奇的语言,同时也是一门正在逐步完善的语言,相信在大家的逐步修改中,这门语言会逐步的完善下去 ...
- JavaScript中的闭包理解
原创文章,转载请注明:JavaScript中的闭包理解 By Lucio.Yang 1.JavaScript闭包 在小学期开发项目的时候,用node.js开发了服务器,过程中遇到了node.js的第 ...
- 一篇文章把你带入到JavaScript中的闭包与高级函数
在JavaScript中,函数是一等公民.JavaScript是一门面向对象的编程语言,但是同时也有很多函数式编程的特性,如Lambda表达式,闭包,高阶函数等,函数式编程时一种编程范式. funct ...
- 让你分分钟学会Javascript中的闭包
Javascript中的闭包 前面的话: 闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它 ...
- 难道这就是JavaScript中的"闭包"
其实对于JavaScript中的"闭包"还没真正理解,这次在实际Coding中似乎遇到了"闭包"的问题,仅此摘录,以待深究. 表现为jQuery的post方法回 ...
- 浅谈JavaScript中的闭包
浅谈JavaScript中的闭包 在JavaScript中,闭包是指这样一个函数:它有权访问另一个函数作用域中的变量. 创建一个闭包的常用的方式:在一个函数内部创建另一个函数. 比如: functio ...
- 【JS】JavaScript中的闭包
在JavaScript中,闭包指的是有权访问另一个函数作用域中的变量的函数:创建闭包最常见的方式就是在一个函数内创建另一个函数.如下例子: function A(propertyName){ retu ...
- JavaScript中new实现原理
JavaScript中new实现原理 1.创建一个空对象 obj 2.将该对象 obj 的原型链 __proto__ 指向构造函数的原型 prototype, 并且在原型链 __proto__ 上设置 ...
随机推荐
- regular
regular.py import re # . # 只能匹配一个字母,而不是2个或0个 # \ # 转义 # 'abc\\.com' r'abc\.com' # 字符集[] # 匹配他所包括的任意字 ...
- Microsoft Speech SDK开发包 使用
下载开发包.我们首先从微软的官网上面下载开发包,下载地址如下: http://www.microsoft.com/en-us/download/details.aspx?id=10121我们主要下载三 ...
- 如何用vs2013开发人员命令提示工具执行一个方法(一个简单的demo)
在任何一个编辑器中编写一个静态的Main方法,必须是静态且名为Main的方法,并将Main方法所在的类文件命名为yang.cs(这个名字随便命名),如图-1. 图-1 打开你的vs2013开发人员命令 ...
- vuex本地存储
vuex与localstorage 区别:vuex数据存储的内存,localstorage的数据存储在本地 应用场景:vuex用于组件之间的传值,localstorage用于不同页面之间的传值 永久性 ...
- android调试之adb
ADB 其实大部分的PC开发机与Android设备的操作都是通过adb(android debug bridge)技术完成的,这是一个C/S架构的命令行工具,主要由三个部分组成 运行在PC开发机上的命 ...
- 创建Sitemap文件供搜索引擎使用
以下内容转载自 http://www.cnblogs.com/webtrados/archive/2009/12/29/1635305.html 如何创建Sitemap文件 Sitemap的格式有XM ...
- CodeForces Gym 100685I Innovative Business (贪心)
题意:给定一条路的长和宽,然后给你瓷砖的长和宽,你只能横着或者竖着铺,也可以切成片,但是每条边只能对应一条边,问你最少要多少瓷砖. 析:先整块整块的放,然后再考虑剩下部分,剩下的再分成3部分,先横着, ...
- 深入理解Java中方法的参数传递机制
形参和实参 我们知道,在Java中定义方法时,是可以定义参数的,比如: public static void main(String[] args){ } 这里的args就是一个字符串数组类型的参数. ...
- 51nod1639(组合数学)
题目链接:http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1639 题意:中文题诶- 思路:组合数学 n根鞋带要组成一个环, ...
- sed 删除指定行
参考:http://blog.sina.com.cn/s/blog_4ba5b45e0102e7l2.html