重学JavaScript深入理解系列(六)

JavaScript深入理解—-闭包(Closures)

概要

本文将介绍一个在JavaScript经常会拿来讨论的话题 —— 闭包(closure)。闭包其实已经是个老生常谈的话题了; 有大量文章都介绍过闭包的内容,尽管如此,这里还是要试着从理论角度来讨论下闭包,看看ECMAScript中的闭包内部究竟是如何工作的。

正如在此前文章中提到的,这些文章都是系列文章,相互之间都是有关联的。因此,为了更好的理解本文要介绍的内容, 建议先去阅读下第四章 - 作用域链和 第二章 - 变量对象。

概论

在讨论ECMAScript闭包之前,先来介绍下函数式编程(与ECMA-262-3 标准无关)中一些基本定义。 然而,为了更好的解释这些定义,这里还是拿ECMAScript来举例。

众所周知,在函数式语言中(ECMAScript也支持这种风格),函数即是数据。就比方说,函数可以保存在变量中,可以当参数传递给其他函数,还可以当返回值返回等等。 这类函数有特殊的名字和结构。

定义

函数式参数(“Funarg”) —— 是指值为函数的参数。

如下例子:

function exampleFunc(funArg) {funArg();
}exampleFunc(function () {alert('funArg');
});
复制代码

上述例子中funarg的实参是一个传递给exampleFunc的匿名函数。

反过来,接受函数式参数的函数称为 高阶函数(high-order function 简称:HOF)。还可以称作:函数式函数 或者 偏数理的叫法:操作符函数。 上述例子中,exampleFunc 就是这样的函数。

此前提到的,函数不仅可以作为参数,还可以作为返回值。这类以函数为返回值的函数称为 _带函数值的函数(functions with functional value or function valued functions)。

(function selfApplicative(funArg) {if (funArg && funArg === selfApplicative) {alert('self-applicative');return;}selfApplicative(selfApplicative);})();
复制代码

以自己为返回值的函数称为 自复制函数(auto-replicative function 或者 self-replicative function)。 通常,“自复制”这个词用在文学作品中:

(function selfReplicative() {return selfReplicative;
})();
复制代码

在函数式参数中定义的变量,在“funarg”激活时就能够访问了(因为存储上下文数据的变量对象每次在进入上下文的时候就创建出来了):

function testFn(funArg) {// 激活funarg, 本地变量localVar可访问funArg(10); // 20funArg(20); // 30}testFn(function (arg) {var localVar = 10;alert(arg + localVar);});
复制代码

然而,我们知道(特别在第四章中提到的),在ECMAScript中,函数是可以封装在父函数中的,并可以使用父函数上下文的变量。 这个特性会引发 funarg问题。

Funarg问题

在面向堆栈的编程语言中,函数的本地变量都是保存在 堆栈上的, 每当函数激活的时候,这些变量和函数参数都会压栈到该堆栈上。

当函数返回的时候,这些参数又会从堆栈中移除。这种模型对将函数作为函数式值使用的时候有很大的限制(比方说,作为返回值从父函数中返回)。 绝大部分情况下,问题会出现在当函数有 自由变量的时候。

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

如下所示:

function testFn() {var localVar = 10;function innerFn(innerParam) {alert(innerParam + localVar);}return innerFn;
}var someFn = testFn();
someFn(20); // 30
复制代码

上述例子中,对于innerFn函数来说,localVar就属于自由变量。

对于采用 面向堆栈模型来存储局部变量的系统而言,就意味着当testFn函数调用结束后,其局部变量都会从堆栈中移除。 这样一来,当从外部对innerFn进行函数调用的时候,就会发生错误(因为localVar变量已经不存在了)。

而且,上述例子在 面向堆栈实现模型中,要想将innerFn以返回值返回根本是不可能的。 因为它也是testFn函数的局部变量,也会随着testFn的返回而移除。

还有一个函数对象问题和当系统采用动态作用域,函数作为函数参数使用的时候有关。

看如下例子(伪代码):

var z = 10;function foo() {alert(z);
}foo(); // 10 – 静态作用域和动态作用域情况下都是(function () {var z = 20;foo(); // 10 – 静态作用域情况下, 20 – 动态作用域情况下})();// 将foo函数以参数传递情况也是一样的(function (funArg) {var z = 30;funArg(); // 10 – 静态作用域情况下, 30 – 动态作用域情况下})(foo);
复制代码

我们看到,采用动态作用域,变量(标识符)处理是通过动态堆栈来管理的。 因此,自由变量是在当前活跃的动态链中查询的,而不是在函数创建的时候保存起来的静态作用域链中查询的。

这样就会产生冲突。比方说,即使Z仍然存在(与之前从堆栈中移除变量的例子相反),还是会有这样一个问题: 在不同的函数调用中,Z的值到底取哪个呢(从哪个上下文,哪个作用域中查询)?

上述描述的就是两类 funarg问题 —— 取决于是否将函数以返回值返回(第一类问题)以及是否将函数当函数参数使用(第二类问题)。

为了解决上述问题,就引入了 闭包的概念。

闭包

闭包是代码块和创建该代码块的上下文中数据的结合。

让我们来看下面这个例子(伪代码):

var x = 20;function foo() {alert(x); // 自由变量 "x" == 20
}// foo的闭包
fooClosure = {call: foo // 对函数的引用lexicalEnvironment: {x: 20} // 查询自由变量的上下文
};
复制代码

上述例子中,“fooClosure”部分是伪代码。对应的,在ECMAScript中,“foo”函数已经有了一个内部属性——创建该函数上下文的作用域链。

这里“lexical”是不言而喻的,通常是省略的。上述例子中是为了强调在闭包创建的同时,上下文的数据就会保存起来。 当下次调用该函数的时候,自由变量就可以在保存的(闭包)上下文中找到了,正如上述代码所示,变量“z”的值总是10。

定义中我们使用的比较广义的词 —— “代码块”,然而,通常(在ECMAScript中)会使用我们经常用到的函数。 当然了,并不是所有对闭包的实现都会将闭包和函数绑在一起,比方说,在Ruby语言中,闭包就有可能是: 一个程序对象(procedure object), 一个lambda表达式或者是代码块。

对于要实现将局部变量在上下文销毁后仍然保存下来,基于堆栈的实现显然是不适用的(因为与基于堆栈的结构相矛盾)。 因此在这种情况下,上层作用域的闭包数据是通过 动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。 这种实现方式比基于堆栈的实现性能要低,然而,任何一种实现总是可以优化的: 可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 —— 是将数据存放在堆栈中还是堆中。

ECMAScript闭包的实现

讨论完理论部分,接下来让我们来介绍下ECMAScript中闭包究竟是如何实现的。 这里还是有必要再次强调下:ECMAScript只使用静态(词法)作用域(而诸如Perl这样的语言,既可以使用静态作用域也可以使用动态作用域进行变量声明)。
var x = 10;function foo() {alert(x);
}(function (funArg) {var x = 20;// funArg的变量 "x" 是静态保存的,在该函数创建的时候就保存了funArg(); // 10, 而不是 20})(foo);
复制代码

从技术角度来说,创建该函数的上层上下文的数据是保存在函数的内部属性 [[Scope]]中的。 如果你还不了解什么是[[Scope]],建议你先阅读第四章, 该章节对[[Scope]]作了非常详细的介绍。如果你对[[Scope]]和作用域链的知识完全理解了的话,那对闭包也就完全理解了。

根据函数创建的算法,我们看到 在ECMAScript中,所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链(除开异常的情况) (不管这个函数后续是否会激活 —— [[Scope]]在函数创建的时候就有了)

var x = 10;function foo() {alert(x);
}// foo is a closure
foo: <FunctionObject> = {[[Call]]: <code block of foo>,[[Scope]]: [global: {x: 10}],... // other properties
};
复制代码

正如此前提到过的,出于优化的目的,当函数不使用自由变量的时候,实现层可能就不会保存上层作用域链。 然而,ECMAScript-262-3标准中并未对此作任何说明;因此,严格来说 —— 所有函数都会在创建的时候将上层作用域链保存在[[Scope]]中。

有些实现中,允许对闭包作用域直接进行访问。比如Rhino,针对函数的[[Scope]]属性,对应有一个非标准的 __parent__属性,在第二章中作过介绍:

var global = this;
var x = 10;var foo = (function () {var y = 20;return function () {alert(y);};})();foo(); // 20
alert(foo.__parent__.y); // 20foo.__parent__.y = 30;
foo(); // 30// 还可以操作作用域链
alert(foo.__parent__.__parent__ === global); // true
alert(foo.__parent__.__parent__.x); // 10
复制代码

“万能”的[[Scope]]

这里还要注意的是:在ECMAScript中,同一个上下文中创建的闭包是共用一个[[Scope]]属性的。 也就是说,某个闭包对其中的变量做修改会影响到其他闭包对其变量的读取:
var firstClosure;
var secondClosure;function foo() {var x = 1;firstClosure = function () { return ++x; };secondClosure = function () { return --x; };x = 2; // 对AO["x"]产生了影响, 其值在两个闭包的[[Scope]]中alert(firstClosure()); // 3, 通过 firstClosure.[[Scope]]
}foo();alert(firstClosure()); // 4
alert(secondClosure()); // 3
复制代码

正因为这个特性,很多人都会犯一个非常常见的错误: 当在循环中创建了函数,然后将循环的索引值和每个函数绑定的时候,通常得到的结果不是预期的(预期是希望每个函数都能够获取各自对应的索引值)。

var data = [];for (var k = 0; k < 3; k++) {data[k] = function () {alert(k);};
}data[0](); // 3, 而不是 0
data[1](); // 3, 而不是 1
data[2](); // 3, 而不是 2
复制代码

上述例子就证明了 —— 同一个上下文中创建的闭包是共用一个[[Scope]]属性的。因此上层上下文中的变量“k”是可以很容易就被改变的。

如下所示:

activeContext.Scope = [... // higher variable objects{data: [...], k: 3} // activation object
];data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;
复制代码

这样一来,在函数激活的时候,最终使用到的k就已经变成了3了。

如下所示,创建一个额外的闭包就可以解决这个问题了:

var data = [];for (var k = 0; k < 3; k++) {data[k] = (function _helper(x) {return function () {alert(x);};})(k); // 将 "k" 值传递进去
}// 现在就对了
data[0](); // 0
data[1](); // 1
data[2](); // 2
复制代码

上述例子中,函数“_helper”创建出来之后,通过参数“k”激活。其返回值也是个函数,该函数保存在对应的数组元素中。 这种技术产生了如下效果: 在函数激活时,每次“_helper”都会创建一个新的变量对象,其中含有参数“x”,“x”的值就是传递进来的“k”的值。 这样一来,返回的函数的[[Scope]]就成了如下所示:

data[0].[[Scope]] === [... // 更上层的变量对象上层上下文的AO: {data: [...], k: 3},_helper上下文的AO: {x: 0}
];data[1].[[Scope]] === [... // 更上层的变量对象上层上下文的AO: {data: [...], k: 3},_helper上下文的AO: {x: 1}
];data[2].[[Scope]] === [... // 更上层的变量对象上层上下文的AO: {data: [...], k: 3},_helper上下文的AO: {x: 2}
];
复制代码

我们看到,这个时候函数的[[Scope]]属性就有了真正想要的值了,为了达到这样的目的,我们不得不在[[Scope]]中创建额外的变量对象。 要注意的是,在返回的函数中,如果要获取“k”的值,那么该值还是会是3。

顺便提下,大量介绍JavaScript的文章都认为只有额外创建的函数才是闭包,这种说法是错误的。 实践得出,这种方式是最有效的,然而,从理论角度来说,在ECMAScript中所有的函数都是闭包。

然而,上述提到的方法并不是唯一的方法。通过其他方式也可以获得正确的“k”的值,如下所示:

var data = [];for (var k = 0; k < 3; k++) {(data[k] = function () {alert(arguments.callee.x);}).x = k; // 将“k”存储为函数的一个属性
}// 同样也是可行的
data[0](); // 0
data[1](); // 1
data[2](); // 2
复制代码

Funarg和return

另外一个特性是从闭包中返回。在ECMAScript中,闭包中的返回语句会将控制流返回给调用上下文(调用者)。 而在其他语言中,比如,Ruby,有很多中形式的闭包,相应的处理闭包返回也都不同,下面几种方式都是可能的:可能直接返回给调用者,或者在某些情况下——直接从上下文退出。

ECMAScript标准的退出行为如下:

function getElement() {[1, 2, 3].forEach(function (element) {if (element % 2 == 0) {// 返回给函数"forEach",// 而不会从getElement函数返回alert('found: ' + element); // found: 2return element;}});return null;
}alert(getElement()); // null, 而不是 2
复制代码

然而,在ECMAScript中通过try catch可以实现如下效果:

var $break = {};function getElement() {try {[1, 2, 3].forEach(function (element) {if (element % 2 == 0) {// 直接从getElement"返回"alert('found: ' + element); // found: 2$break.data = element;throw $break;}});} catch (e) {if (e == $break) {return $break.data;}}return null;
}alert(getElement()); // 2
复制代码

理论版本

通常,程序员会错误的认为,只有匿名函数才是闭包。其实并非如此,正如我们所看到的 —— 正是因为作用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE,NFE,FD都是闭包), 这里只有一类函数除外,那就是通过Function构造器创建的函数,因为其[[Scope]]只包含全局对象。 为了更好的澄清该问题,我们对ECMAScript中的闭包作两个定义(即两种闭包):

ECMAScript中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量

闭包实践

实际使用的时候,闭包可以创建出非常优雅的设计,允许对funarg上定义的多种计算方式进行定制。 如下就是数组排序的例子,它接受一个排序条件函数作为参数:
[1, 2, 3].sort(function (a, b) {... // 排序条件
});
复制代码

同样的例子还有,数组的map方法(并非所有的实现都支持数组map方法,SpiderMonkey从1.6版本开始有支持),该方法根据函数中定义的条件将原数组映射到一个新的数组中

[1, 2, 3].map(function (element) {return element * 2;
}); // [2, 4, 6]
复制代码

使用函数式参数,可以很方便的实现一个搜索方法,并且可以支持无穷多的搜索条件

someCollection.find(function (element) {return element.someProperty == 'searchCondition';
});
复制代码

还有应用函数,比如常见的forEach方法,将funarg应用到每个数组元素:

[1, 2, 3].forEach(function (element) {if (element % 2 != 0) {alert(element);}
}); // 1, 3
复制代码

顺便提下,函数对象的 apply 和 call方法,在函数式编程中也可以用作应用函数。 apply和call已经在讨论“this”的时候介绍过了;这里,我们将它们看作是应用函数 —— 应用到参数中的函数(在apply中是参数列表,在call中是独立的参数)

(function () {alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);
复制代码

闭包还有另外一个非常重要的应用 —— 延迟调用:

var a = 10;
setTimeout(function () {alert(a); // 10, 一秒钟后
}, 1000);
复制代码

也可以用于回调函数:

...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {// 当数据就绪的时候,才会调用;// 这里,不论是在哪个上下文中创建,变量“x”的值已经存在了alert(x); // 10
};
..
复制代码

还可以用于封装作用域来隐藏辅助对象:

var foo = {};// initialization
(function (object) {var x = 10;object.getX = function _getX() {return x;};})(foo);alert(foo.getX()); // get closured "x" – 10
复制代码

总结

本文介绍了更多关于ECMAScript-262-3的理论知识,而我认为,这些基础的理论有助于理解ECMAScript中闭包的概念。

原文地址
译文地址

重学系列传送门

重学JavaScript深入理解系列(一)
重学JavaScript深入理解系列(二)
重学JavaScript深入理解系列(三)
重学JavaScript深入理解系列(四)
重学JavaScript深入理解系列(五)

转载于:https://juejin.im/post/5ce78710f265da1bca51b5ef

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/275081.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

EXT.NET复杂布局(四)——系统首页设计(上)

很久没有发帖了&#xff0c;很是惭愧&#xff0c;因此给各位使用EXT.NET的朋友献上一份礼物。 本篇主要讲述页面设计与效果&#xff0c;下篇将讲述编码并提供源码下载。 系统首页设计往往是个难点&#xff0c;因为往往要考虑以下因素&#xff1a; 重要通知系统功能菜单快捷操作…

figma设计_在Figma中使用隔片移交设计

figma设计I was quite surprised by how much the design community resonated with the concept of spacers since I published my 自从我发表论文以来&#xff0c;设计界对间隔件的概念产生了多少共鸣&#xff0c;我感到非常惊讶。 last story. It encouraged me to think m…

axios源码中的10多个工具函数,值得一学~

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。本文来自读者Ethan01投稿…

安装jenkins时出现 No such plugin: cloudbees-folder的解决办法

今天安装了一下jenkins&#xff0c;在初始化安装插件时出现“ No such plugin: cloudbees-folder”错误&#xff0c;根据网上的教程&#xff1a; 1、打开链接“http://ftp.icm.edu.pl/packages/jenkins/plugins/cloudbees-folder/”&#xff0c;在最下面找到并打开“latest”目…

寄充气娃娃怎么寄_我如何在5小时内寄出新设计作品集

寄充气娃娃怎么寄Over the Easter break, I challenged myself to set aside an evening rethinking the structure, content and design of my portfolio in Notion with a focus on its 在复活节假期&#xff0c;我挑战自己&#xff0c;把一个晚上放在一边&#xff0c;重新思…

基于Hbase的用户评分协同过滤推荐算法

基于Hbase的用户评分协同过滤推荐算法 作者&#xff1a; 张保维 2012-1-3 一、 概述 本文为推荐引擎设计的基础篇&#xff0c;介绍基于hbase 存储方式用户评分的方式进行推荐的主体算法及在分布式平台环境下的实现。由于推荐算法分支众多&#xff0c;我们先从简单及实用的算法…

最全 JavaScript Array 方法 详解

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。我们在日常开发中&#…

[译] React Hooks: 没有魔法,只是数组

[译] React Hooks: 没有魔法&#xff0c;只是数组 原文链接&#xff1a; medium.com/ryardley/r… 我是 React 新特性 Hooks 的粉丝。但是&#xff0c;在你使用 React Hooks的过程中&#xff0c;有一些看上去 很奇怪的限制 。在本文里&#xff0c;对于那些还在为了理解这些限制…

管理沟通中移情的应用_移情在设计中的重要性

管理沟通中移情的应用One of the most important aspects of any great design is the empathetic understanding of and connection to the user. If a design is ‘selfish’, as in when a product designed with the designer in mind and not the user, it will ultimatel…

网易前端进阶特训营,邀你免费入营!一举解决面试晋升难题!

网易等大厂的前端岗位一直紧缺&#xff0c;特别是资深级。最近一位小哥面进网易&#xff0c;定级P4&#xff08;资深&#xff09;&#xff0c;总包60W&#xff0c;给大家带来真实面经要点分享。网易的要求有&#xff1a;1.对性能优化有较好理解&#xff0c;熟悉常用调试工具2.熟…

Feign的构建过程及自定义扩展功能

spring-cloud-openfeign-core-2.1.1.RELEASE.jar 中 HystrixFeign 的详细构建过程&#xff1a; EnableFeignClients -> FeignClientsRegistrar 扫描 Feign注解的类 -> FeignClientFactoryBean通过Targeter生产FeignClient -> Targeter通过Feign.Builder构建Feign ->…

angelica类似_亲爱的当归(Angelica)是第一个让我哭泣的VR体验

angelica类似It was a night just like any other night. I finished work for the day and closed my laptop. I had dinner and after an hour, I put on my Oculus Quest headset in order to begin my VR workout.就像其他任何夜晚一样&#xff0c; 这 是一个夜晚。 我完成…

面试官:请手写一个带取消功能的延迟函数,axios 取消功能的原理是什么

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。本文仓库 https://githu…

关于base64编码的原理及实现

我们的图片大部分都是可以转换成base64编码的data&#xff1a;image。 这个在将canvas保存为img的时候尤其有用。虽然除ie外&#xff0c;大部分现代浏览器都已经支持原生的基于base64的encode和decode&#xff0c;例如btoa和atob。&#xff08;将canvas画布保存成img并强制改变…

Django web开发系列(五)模板

一 前言在上一节了解到视图函数处理后&#xff0c;会将结果渲染到创建的html页面&#xff0c;但html如何接收并显示视图函数返回的动态数据呢&#xff1f;最常用的做法就是使用模板(Template)&#xff0c;本节将简单介绍一下模板的作用和用法。 可以这样简单的理解模板的概念&a…

facebook 面试_如何为您的Facebook产品设计面试做准备

facebook 面试重点 (Top highlight)Last month, I joined Facebook to work on Instagram DMs and as a way to pay it forward, I 上个月&#xff0c;我加入了Facebook&#xff0c;从事Instagram DM的工作&#xff0c;作为一种支付方式&#xff0c;我 offered to help anyone…

8年了,开始写点东西了

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。今天分享一位大佬的文章…

荒径 弗罗斯特_弗罗斯特庞克,颠覆性城市建设者

荒径 弗罗斯特Most gamers are familiar with Will Wright’s famous SimCity series. It created the city building genre and there have been many attempts over the years to ape it. But few developers have been bold enough to completely deconstruct the formula; …

2012年1月份第2周51Aspx源码发布详情

WP7手指画图应用源码 2012-01-14 [VS2010] 游戏介绍&#xff1a;Windows Phone 7手指画图应用 – FingerPaint&#xff0c;您通过此游戏可以随心画一些感兴趣的东西&#xff0c;陶冶情操。操作简单&#xff0c;页面简洁。适合新手学习参考。 WP7 Car Bloke(交通工具开销记录)源…

Gitee 如何自动部署博客 Pages?推荐用这个GitHub Actions!

大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。前段时间我把自己的博客…