详解JavaScript闭包


新钛云服已为您服务1013

 

在JavaScript中,闭包是通过引用将周围上下文的变量绑定起来的函数。

1

2

3

4

5

6

7

8

9

function getMeAClosure() {

    var canYouSeeMe = "here I am";

    return (function theClosure() {

        return {canYouSeeIt: canYouSeeMe ? "yes!": "no"};

    });

}

 

var closure = getMeAClosure();

closure().canYouSeeIt; //"yes!"

每个JavaScript函数都会在创建时形成一个闭包。稍后,我将解释原因,并逐步介绍创建闭包的过程。

然后,我将解决一些常见的误解,并完成一些实际应用。但是首先,我们需要知道:词汇范围和可变环境(VariableEnvironment)为您带来了JavaScript闭包……

词汇范围

词汇一词与单词或语言有关。因此,函数的词汇范围是由函数在书面源代码中的物理位置静态定义的。

考虑以下示例:

1

2

3

4

5

6

7

8

9

var x = "global";

 

function outer() {

    var y = "outer";   

 

    function inner() {

        var x = "inner";   

    }

}

函数inner在物理上被函数outer包围,而函数outer又被全局上下文包装。我们已经形成了词汇层次:整体、外部、内部

任何给定函数的外部词法范围由词法层次结构中的祖先定义。因此,函数的外部词汇范围inner包括全局对象和函数outer。


可变环境

全局对象具有关联的执行上下文。另外,每次调用函数都会建立并输入一个新的执行上下文。执行上下文是静态词汇范围的动态对应物。

每个执行上下文定义一个可变环境(VariableEnvironment),该变量环境是该上下文声明的变量的存储库。(ES 5 10.4,10.5)

[请注意,在EcmaScript 3中,函数的VariableEnvironment被称为ActivationObject,这也是我在一些较早的文章中使用的术语]

我们可以用伪代码表示VariableEnvironment…

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

//variableEnvironment: {x: undefined, etc.};

var x = "global";

//variableEnvironment: {x: "global", etc.};

 

function outer() {

    //variableEnvironment: {y: undefined};

    var y = "outer";

    //variableEnvironment: {y: "outer"};

 

    function inner() {

        //variableEnvironment: {x: undefined};

        var x = "inner";   

        //variableEnvironment: {x: "inner"};

    }

}

但是,事实证明这只是图片的一部分。每个VariableEnvironment也将继承其词法范围的VariableEnvironment。[英雄进入(左阶段)…。]


[[scope]]属性

当给定的执行上下文在代码中遇到函数定义时,将创建一个新的函数对象,该对象具有一个名为[[scope]]的内部属性(如词汇范围),该属性引用当前的VariableEnvironment。(ES 5 13.0-2)

每个函数都具有[[scope]]属性,并且在调用该函数时,scope属性的值将分配给其VariableEnvironment的外部词法环境引用(或externalLex)属性。

(ES 5 10.4.3.5-7)这样,每个VariableEnvironment都从其词法父级的VariableEnvironment继承。此范围链从全局对象开始,沿词法层次结构的长度运行。

让我们看看现在的伪代码如何:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

//VariableEnvironment: {x: undefined, etc.};

var x = "global";

//VariableEnvironment: {x: "global", etc.};

 

function outer() {

    //VariableEnvironment: {y: undefined, outerLex: {x: "global", etc.}};

    var y = "outer";   

    //VariableEnvironment: {y: "outer", outerLex: {x: "global", etc.}};

 

    function inner() {

        //VariableEnvironment: {x: undefined, outerLex: {y: "outer", outerLex: {x:"global", etc.}};

        var x = "inner";   

        //VariableEnvironment: {x: "inner", outerLex: {y: "outer", outerLex: {x:"global", etc.}};

    }

}

[[scope]]属性充当嵌套的VariableEnvironments之间的桥梁,并启用通过内部VariableEnvironments嵌入外部变量(并按词法优先级进行优先排序)的过程。[[scope]]属性还启用了闭包,因为如果没有闭包,将在外部函数返回后取消引用外部函数的变量并进行垃圾回收。

 因此,我们有了它–闭包不过是词汇范围界定的不可避免的副作用 

消除误解

既然我们知道了关闭是如何工作的,那么我们就可以开始处理一些与它们相关的更为谣言。

误解1:仅在返回内部函数之后才创建闭包

创建函数时,会为其分配一个[[scope]]属性,该属性引用外部词法范围的变量并防止对其进行垃圾回收。因此,闭包是在函数创建时形成的

不要求在函数成为闭包之前应将其返回。这是一个无需返回函数即可工作的闭包:

1

2

3

4

5

var callLater = function(fn, args, context) {

    setTimeout(function(){fn.apply(context, args)}, 2000);

}

 

callLater(alert,['hello']);

误区2:外部变量的值被复制或“放入”到闭包中

如我们所见,闭包引用变量而不是值。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

//Bad Example

//Create an array of functions that add 1,2 and 3 respectively

var createAdders = function() {

    var fns = [];

    for (var i=1; i<4; i++) {

        fns[i] = (function(n) {

            return i+n;

        });

    }

    return fns;

}

 

var adders = createAdders();

adders[1](7); //11 ??

adders[2](7); //11 ??

adders[3](7); //11 ??

所有三个加法器函数都指向同一个变量i。到这些函数中的任何一个被调用时,其值为i4。

一种解决方案是通过自调用函数传递每个参数。由于每个函数调用都在唯一的执行上下文中进行,因此我们保证参数变量在连续调用中具有唯一性。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

//Good Example

//Create an array of functions that add 1,2 and 3 respectively

var createAdders = function() {

    var fns = [];

    for (var i=1; i<4; i++) {

        (function(i) {

            fns[i] = (function(n) {

                return i+n;

            });

        })(i)   

    }

    return fns;

}

 

var adders = createAdders();

adders[1](7); //8 (-:

adders[2](7); //9 (-:

adders[3](7); //10 (-:

误解3:闭包仅适用于内部函数

可以肯定的是,由外部函数创建的闭包并不是很有趣,因为[[scope]]属性仅引用全局范围,在任何情况下它都是通用的。尽管如此,需要注意的是,每个函数的闭包创建过程都是相同的,并且每个函数都会创建一个闭包。

误解4:闭包仅适用于匿名函数

我已经在太多文章中看到了这种说法。

误解5:封闭导致内存泄漏

闭包本身不会创建循环引用。在我们最初的示例中,函数inner通过其[[scope]]属性引用外部变量,但引用的变量或函数均未outer引用函数inner或其局部变量。

IE的较旧版本因内存泄漏而臭名昭著,这些通常会归咎于闭包。一个典型的罪魁祸首是函数引用的DOM元素,而同一DOM元素的属性引用与该函数相同词法范围内的另一个对象。在IE6和IE8之间,这些循环引用大多已被驯服。


实际应用

功能模板

有时我们想定义一个函数的多个版本,每个版本都符合一个蓝图,但可以通过提供的参数进行修改。例如,我们可以创建一组标准函数来转换度量单位:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

function makeConverter(toUnit, factor, offset) {

    offset = offset || 0;

    return function(input) {

        return [((offset+input)*factor).toFixed(2), toUnit].join(" "); 

    }

}  

 

var milesToKm = makeConverter('km',1.60936);

var poundsToKg = makeConverter('kg',0.45460);

var farenheitToCelsius = makeConverter('degrees C',0.5556, -32);

 

milesToKm(10); //"16.09 km"

poundsToKg(2.5); //"1.14 kg"

farenheitToCelsius(98); //"36.67 degrees C"

如果像我一样喜欢函数抽象,那么下一个逻辑步骤就是简化该过程(请参见下文)。

功能性JavaScript

除了JavaScript函数是一流的对象之外,JavaScript函数的另一个最好的朋友是闭包。

bind,curry,partial和compose的典型实现都依赖于闭包为新函数提供对原始函数和参数的引用。

例如,这是咖喱:

1

2

3

4

5

6

7

8

9

10

Function.prototype.curry = function() {

    if (arguments.length<1) {

        return this; //nothing to curry with - return function

    }

    var __method = this;

    var args = toArray(arguments);

    return function() {

        return __method.apply(this, args.concat([].slice.apply(null, arguments)));

    }

}

这是我们先前使用咖喱做的例子:

1

2

3

4

5

6

7

8

9

10

11

12

function converter(toUnit, factor, offset, input) {

    offset = offset || 0;

    return [((offset+input)*factor).toFixed(2), toUnit].join(" "); 

}

 

var milesToKm = converter.curry('km',1.60936,undefined);

var poundsToKg = converter.curry('kg',0.45460,undefined);

var farenheitToCelsius = converter.curry('degrees C',0.5556, -32);

 

milesToKm(10); //"16.09 km"

poundsToKg(2.5); //"1.14 kg"

farenheitToCelsius(98); //"36.67 degrees C"

还有许多其他使用闭包的漂亮函数修饰符。这颗小宝石由Oliver Steele提供。

1

2

3

4

5

6

7

8

9

10

11

/**

 * Returns a function that takes an object,

  * and returns the value of its 'name' property erty

 */

var pluck = function(name) {

    return function(object) {

        return object[name];

    }

}

 

var getLength = pluck('length');

getLength("SF Giants are going to the World Series!"); //40



模块模式

这种众所周知的技术使用闭包来维护对外部作用域变量的私有排他引用。在这里,我使用模块模式制作“猜数字”游戏。请注意,在此示例中,闭包(guess)对secretNumber变量具有独占访问权,而responses对象在创建时引用了变量值的副本。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

var secretNumberGame = function() {

    var secretNumber = 21;

 

    return {

        responses: {

            true: "You are correct! Answer is " + secretNumber,

            lower: "Too high!",

            higher: "Too low!"

        },

 

        guess: function(guess) {

            var key =

                (guess == secretNumber) ||

                    (guess < secretNumber ?

"higher": "lower");

            alert(this.responses[key])

        }

    }

}

 

var game = secretNumberGame();

game.guess(45); //"Too high!"

game.guess(18); //"Too low!"

game.guess(21); //"You are correct! Answer is 21"


结论:用编程术语来说,闭包代表优雅和精致的高度。它们使代码更紧凑,更易读,更美观,并促进了功能的重用。 

*本文翻译自https://javascriptweblog.wordpress.com/2010/10/25/understanding-javascript-closures/,如有侵权请联系删除。

了解新钛云服

新钛云服荣膺第四届FMCG零售消费品行业CIO年会「年度数字化服务最值得信赖品牌奖」

新钛云服三周岁,公司月营收超600万元,定下百年新钛的发展目标

当IPFS遇见云服务|新钛云服与冰河分布式实验室达成战略协议

新钛云服正式获批工信部ISP/IDC(含互联网资源协作)牌照

深耕专业,矗立鳌头,新钛云服获千万Pre-A轮融资

新钛云服,打造最专业的Cloud MSP+,做企业业务和云之间的桥梁

新钛云服一周年,完成两轮融资,服务五十多家客户

上海某仓储物流电子商务公司混合云解决方案

往期技术干货

Kubernetes扩容到7,500节点的历程

低代码开发,全民开发,淘汰职业程序员!

国内主流公有云VPC使用对比及总结

万字长文:云架构设计原则|附PDF下载

刚刚,OpenStack 第 19 个版本来了,附28项特性详细解读!

Ceph OSD故障排除|万字经验总结

七个用于Docker和Kubernetes防护的安全工具

运维人的终身成长,从清单管理开始|万字长文!

OpenStack与ZStack深度对比:架构、部署、计算存储与网络、运维监控等

什么是云原生?

IT混合云战略:是什么、为什么,如何构建?


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值