首页 > 代码库 > js闭包
js闭包
* 原文Jim Jey的《Javascipt Closures》
介绍
闭包
“闭包”是一个表达式(通常是一个函数),可以将自由变量与绑定这些变量的环境(即“闭包”表达式)一起使用。 闭包是ECMAScript(javascript)最强大的功能之一,但是在不了解的情况下它们不能被资源利用。然而,它们相对容易创建,即使是意外,并且它们的创建具有潜在的有害后果,特别是在一些相对普遍的Web浏览器环境中。为了避免意外遇到缺陷并利用他们提供的好处,有必要了解其机制。这在很大程度上取决于范围链在标识符解析中的作用,以及对对象上属性名称的解析。
闭包的简单说明是ECMAScript允许内部函数;功能定义和功能表达式在其他功能的功能范围内。并且这些内部函数可以访问其外部函数内的所有局部变量,参数和声明的内部函数。当这些内部功能中的一个使得在其包含的功能之外可访问时,形成闭合,使得其可以在外部功能返回之后被执行。在这一点上,它仍然可以访问其外部函数的局部变量,参数和内部函数声明。那些局部变量,参数和函数声明(最初)具有返回外部函数并且可以通过内部函数进行交互的值。
不幸的是,正确理解闭包需要了解其背后的机制,以及相当多的技术细节。虽然在以下解释的早期部分ECMA 262指定的算法已被刷过,但是很多都不能被忽略或容易地简化。熟悉对象属性名称解析的人可能会跳过该部分,但只有已经熟悉闭包的人才能够跳过以下部分,现在可以停止阅读并重新利用它们。
对象属性名词解析
ECMAScript识别两类对象“Native Object”和“Host Object”,其中包含一个称为“内置对象”(ECMA 262 3rd Ed Section 4.3)的本机对象的子类别。本地对象属于语言,并且主机对象由环境提供,并且可以是例如文档对象,DOM节点等。
本机对象是松散和动态的命名属性(一些实现在内置对象子类别中不是动态的,尽管通常并不重要)。对象的定义的命名属性将保存一个值,该值可以是对另一个Object的引用(函数也是此意义上的对象)或原始值:String,Number,Boolean,Null或Undefined。 Undefined原始类型有点奇怪,因为可以为对象的属性分配一个Undefined值,但这样做不会从该对象中删除该属性;它仍然是一个定义的命名属性,它只保存值undefined。
以下简要描述如何在最大程度地刷新内部细节的对象上读取和设置属性值。
值的分配
可以通过为该命名属性分配一个值来创建对象的命名属性或设置在现有命名属性上的值。 所以给出:
var objectRef = new Object(); //create a generic javascript object.
名称为“testNumber”的属性可以创建为:
objectRef.testNumber = 5; /* - or:- */ objectRef["testNumber"] = 5;
对象在分配之前没有“testNumber”属性,但是在分配时创建一个。 任何后续任务不需要创建属性,它只是重新设置其值:
objectRef.testNumber = 8; /* - or:- */ objectRef["testNumber"] = 8;
Javascript对象具有自己可以是对象的原型,如稍后将描述的那样,原型可能具有命名属性。 但这在任务中没有任何作用。 如果分配了一个值,并且实际对象没有具有相应名称的属性,则会创建该名称的属性,并为其分配该值。 如果它有属性,那么它的值被重新设置。
读值
它正在从对象属性读取原型的值。 如果对象具有属性访问器中使用的属性名称的属性,则返回该属性的值:
/* Assign a value to a named property. If the object does not have a property with the corresponding name prior to the assignment it will have one after it:- */ objectRef.testNumber = 8; /* Read the value back from the property:- */ var val = objectRef.testNumber; /* and - val - now holds the value 8 that was just assigned to the named property of the object. */
但是所有的对象都可能有原型,而原型是对象,因此它们反过来可能具有可能具有原型的原型,以及形成所谓的原型链的原型。 当链中的一个对象具有空原型时,原型链结束。 Object构造函数的默认原型有一个null原型,所以:
var objectRef = new Object(); //create a generic javascript object.
使用原型Object.prototype创建一个对象,该原型本身具有空原型。 所以objectRef的原型链只包含一个对象:Object.prototype。 然而:
/* A "constructor" function for creating objects of a - MyObject1 - type. */ function MyObject1(formalParameter){ /* Give the constructed object a property called - testNumber - and assign it the value passed to the constructor as its first argument:- */ this.testNumber = formalParameter; } /* A "constructor" function for creating objects of a - MyObject2 - type:- */ function MyObject2(formalParameter){ /* Give the constructed object a property called - testString - and assign it the value passed to the constructor as its first argument:- */ this.testString = formalParameter; } /* The next operation replaces the default prototype associated with all MyObject2 instances with an instance of MyObject1, passing the argument - 8 - to the MyObject1 constructor so that its - testNumber - property will be set to that value:- */ MyObject2.prototype = new MyObject1( 8 ); /* Finally, create an instance of - MyObject2 - and assign a reference to that object to the variable - objectRef - passing a string as the first argument for the constructor:- */ var objectRef = new MyObject2( "String_Value" );
objectRef变量引用的MyObject2实例有一个原型链。 该链中的第一个对象是创建并分配给MyObject2构造函数的prototype属性的MyObject1实例。 MyObject1的实例有一个原型,通过实现将对象分配给函数MyObject1的prototype属性。 该对象有一个原型,与Object.prototype引用的对象对应的默认对象原型。 Object.prototype有一个null原型,所以原型链在这一点结束。
当属性访问器尝试读取一个命名属性窗体时,由变量objectRef引用的对象,整个原型链可以进入该过程。 在简单的情况下:
var val = objectRef.testString;
- 由objectRef引用的MyObject2实例具有名称为“testString”的属性,因此该属性的值设置为“String_Value”,分配给变量val。 然而:-
var val = objectRef.testNumber;
- 不能从MyObject2本身的实例中读取一个命名属性,因为它没有这样的属性,但变量val设置为值8而不是未定义,因为找不到对象本身的对应的命名属性,解释器然后检查 对象就是它的原型。 它的原型是MyObject1的实例,它是使用名为“testNumber”的属性创建的,赋值为该属性,因此属性访问器的值为8. MyObject1或MyObject2都没有定义一个toString方法,但如果 属性访问器尝试从objectRef读取toString属性的值: -
var val = objectRef.toString;
- val变量被分配给一个函数的引用。 该函数是Object.prototype的toString属性,并返回,因为检查objectRef的原型的过程,当objectRef不具有“toString”属性时,它是作用于一个对象,所以当发现该原型缺少 该产品的原型依次检查。 它的原型是Object.prototype,它具有一个toString方法,因此它是对返回的该函数对象的引用。
最后:-
var val = objectRef.madeUpProperty;
- 返回undefined,因为在处理原型链的过程中,找不到任何具有名称“madeUpPeoperty”的对象的属性,它最终将转到Object.prototype的原型,该原型为null,并且进程结束返回未定义。
对命名属性的读取返回在对象上或从其原型链中找到的第一个值。如果没有相应的属性已经存在,则将一个值分配给对象上的命名属性将在对象本身上创建一个属性。
这意味着如果将值分配为objectRef.testNumber = 3,则将在MyObject2本身的实例上创建一个“testNumber”属性,并且任何后续尝试读取该值将检索该对象上设置的值。不再需要检查原型链来解析属性访问器,但是将赋值给其“testNumber”属性的值为8的MyObject1实例未改变。对objectRef对象的赋值掩盖其原型链中的相应属性。
注意:ECMAScript定义内部Object类型的内部[[prototype]]属性。该属性不能直接通过脚本访问,但它是在属性访问器解析中使用的内部[[prototype]]属性引用的对象链;对象的原型链。存在一个公共原型属性,允许与内部[[prototype]]属性相关联的原型的分配,定义和操作。 ECMA 262(第3版)中描述了两种关系的细节,超出了本讨论范围。
标识符解析,执行上下文和范围链
执行上下文
执行上下文是ECMSScript规范(ECMA 262第3版)用于定义ECMAScript实现所需行为的抽象概念。该规范没有说明执行上下文是如何实现的,但执行上下文具有引用规范定义结构的关联属性,因此它们可能被构想(甚至实现)为具有属性的对象,但不是公共属性
。
所有JavaScript代码都在执行上下文中执行。全局代码(内联执行的代码通常作为JS文件或HTML页面加载)在全局执行上下文中执行,并且函数(可能作为构造函数)的每次调用都具有关联的执行上下文。使用eval函数执行的代码也会得到一个不同的执行上下文,但是由于JavaScript程序员通常不会使用eval,所以这里不再赘述。执行上下文的具体细节见ECMA 262第10.2节(第3版)。
当调用javascript函数时,它会进入一个执行上下文,如果另一个函数被调用(或递归地使用相同的函数),则创建一个新的执行上下文,并且在函数调用的持续时间内执行进入该上下文。当被调用函数返回时返回到原始执行上下文。因此,运行JavaScript代码形成一堆执行上下文。
当执行上下文被创建时,许多事情以定义的顺序发生。首先,在函数的执行上下文中,创建“激活”对象。激活对象是另一种规范机制。它可以被认为是一个对象,因为它最终具有可访问的命名属性,但它不是一个普通对象,因为它没有原型(至少不是一个定义的原型),它不能被javascript代码直接引用。
为函数调用创建执行上下文的下一步是创建一个参数对象,它是一个数组类对象,整数索引成员与传递给函数调用的参数相对应。它还具有长度和被调用属性(与此讨论无关,详见规范)。使用名称“arguments”创建Activation对象的属性,并将对参数对象的引用分配给该属性。
接下来,为执行上下文分配一个作用域。范围由对象的列表(或链)组成。每个函数对象都有一个内部[[scope]]属性(我们将在稍后详细介绍),它也由对象的列表(或链)组成。分配给函数调用执行上下文的范围由相应函数对象的[[scope]]属性引用的列表组成,激活对象添加在链前面(或列表顶部) )。
然后,使用ECMA 262引用的对象作为“变量”对象的“变量实例化”过程进行。但是,Activation对象用作Variable对象(请注意,重要的是它们是相同的对象)。为每个函数的形式参数创建Variable对象的命名属性,如果函数调用的参数与这些参数对应,那些参数的值将分配给属性(否则分配的值未定义)。内部函数定义用于创建分配给Variable对象的属性的函数对象,其名称与函数声明中使用的函数名称相对应。变量实例化的最后一个阶段是创建与对象在函数中声明的所有局部变量的Variable对象的命名属性。
在变量对象中创建的对应于声明的局部变量的属性最初在变量实例化过程中被分配了未定义的值,局部变量的实际初始化不会发生,直到在执行函数体代码时对相应的赋值表达式进行评估。
事实上,具有其参数属性的Activation对象和具有与函数局部变量相对应的命名属性的Variable对象是相同的对象,允许将标识符参数视为是一个函数局部变量。
最后,分配一个值用于此关键字。如果分配的值指的是对象,则属性访问器以该关键字为前缀,引用该对象的属性。如果分配的值(内部)为空,则该关键字将引用全局对象。
全局执行上下文的处理方式略有不同,因为它没有参数,因此不需要定义的激活对象来引用它们。全局执行上下文确实需要一个范围,其范围链只包含一个对象,即全局对象。全局执行上下文通过变量
全局执行上下文还使用对该对象的全局对象的引用。
范围链和[范围]
函数调用的执行上下文的作用域链是通过将执行上下文的“激活/变量”对象添加到在函数对象的[[scope]]属性中保存的作用域链的前端而构建的,因此了解内部 [[scope]]属性被定义。
在ECMAScript函数中是对象,它们在函数声明的变量实例化过程中创建,在函数表达式的评估过程中或通过调用Function构造函数。
使用Function构造函数创建的函数对象始终具有引用仅包含全局对象的作用域链的[[scope]]属性。
使用函数声明或函数表达式创建的函数对象具有分配给其内部[[scope]]属性的执行上下文的作用域链。
在全局函数声明的最简单的情况下,如:
function exampleFunction(formalParameter){ ... // function body code }
- 在全局执行上下文的变量实例化过程中创建相应的函数对象。 全局执行上下文具有仅由全局对象组成的范围链。 因此,通过名称为“exampleFunction”的全局对象的属性创建并引用的函数对象被分配一个引用仅包含全局对象的作用域链的内部[[scope]]属性。
当在全局上下文中执行函数表达式时,将分配一个类似的范围链:
var exampleFuncRef = function(){ ... // function body code }
- 除了在这种情况下,在全局执行上下文的变量实例化期间创建全局对象的命名属性,但不创建函数对象,并将其分配给全局对象的命名属性的引用,直到赋值表达式为评估。 但是,在全局执行上下文中仍然发生函数对象的创建,因此创建的函数对象的[[scope]]属性仍然只包含所分配的作用域链中的全局对象。
内部函数声明和表达式导致在函数的执行上下文中创建的函数对象,因此可以获得更精细的范围链。 考虑下面的代码,它定义了一个内部函数声明的函数,然后执行外部函数:
function exampleOuterFunction(formalParameter){ function exampleInnerFuncitonDec(){ ... // inner function body } ... // the rest of the outer function body. } exampleOuterFunction( 5 );
与外部函数声明对应的函数对象在全局执行上下文中的变量实例化过程中创建,因此其[[scope]]属性包含一个仅具有全局对象的项目作用域链。
当全局代码执行对exampleOuterFunction的调用时,将为该函数调用和一个“激活/变量”对象创建一个新的执行上下文。新的执行上下文的范围变成由新的激活对象组成的链,后面是由外部函数对象的[[scope]]属性(仅仅是全局对象)引用的链。该新执行上下文的变量实例化导致创建与内部函数定义相对应的函数对象,并且该函数对象的[[scope]]属性从其创建的执行上下文中分配范围的值。包含激活对象的作用域链,后跟全局对象。
到目前为止,这一切都是自动的,由源代码的结构和执行控制。执行上下文的范围链定义了创建的函数对象的[[scope]]属性,函数对象的[[scope]]属性定义了其执行上下文的范围(以及相应的激活对象)。但是ECMAScript提供了with语句作为修改范围链的一种手段。
with语句评估一个表达式,如果该表达式是一个对象,它将被添加到当前执行上下文(在Activation / Variable对象之前)的作用域链中。然后,with语句执行另一个语句(可能本身就是一个块语句),然后将执行上下文的范围链恢复到之前的内容。
函数声明不会受到with语句的影响,因为它们导致在变量实例化过程中创建函数对象,但可以在一个with语句中对函数表达式进行求值:
/* create a global variable - y - that refers to an object:- */ var y = {x:5}; // object literal with an - x - property function exampleFuncWith(){ var z; /* Add the object referred to by the global variable - y - to the front of he scope chain:- */ with(y){ /* evaluate a function expression to create a function object and assign a reference to that function object to the local variable - z - :- */ z = function(){ ... // inner function expression body; } } ... } /* execute the - exampleFuncWith - function:- */ exampleFuncWith();
当调用exampleFuncWith函数时,生成的执行上下文具有由其激活对象组成的范围链,后跟全局对象。 在评估函数表达式期间,with语句的执行将全局变量y引用的对象添加到该作用域链的前端。 通过对函数表达式的求值创建的函数对象被分配一个[[scope]]属性,该属性与创建它的执行上下文的范围相对应。 由对象y组成的作用域链,后跟来自外部函数调用的执行上下文的Activation对象,后跟全局对象。
当与with语句关联的块语句终止时,执行上下文的范围将被还原(y对象被删除),但是该函数对象已经在该点被创建,并且其[[scope]]属性赋予一个作用域的引用 链条与y对象在其头。
标识符解析
标识符是针对范围链进行解析的。 ECMA 262将其分类为关键字而不是标识符,这并不是不合理的,因为它始终根据使用它的执行上下文中的此值进行解析,而不引用范围链。
标识符解析以范围链中的第一个对象开始。检查它是否具有与标识符对应的名称的属性。因为范围链是一个对象链,所以这个检查包含该对象的原型链(如果有的话)。如果在作用域链中的第一个对象上没有找到对应的值,则搜索进行到下一个对象。等等,直到链中的一个对象(或其原型之一)具有一个与标识符对应的名称的属性或作用域链用尽。
对标识符的操作与上述对象上使用属性访问器的方式相同。在作用域链中标识为具有相应属性的对象取代了属性访问器中的对象,并且该标识符作为该对象的属性名称。全局对象始终位于作用域链的末尾。
由于与函数调用相关联的执行上下文将在链的前端具有“激活/变量”对象,因此有效地首先检查函数体中使用的标识符,以查看它们是否与形式参数,内部函数声明名称或局部变量相对应。那些将被解析为激活/变量对象的命名属性。
闭包
自动垃圾收集
ECMAScript使用自动垃圾收集。 该规范没有定义细节,留给实施者进行整理,并且已知一些实现给他们的垃圾收集操作提供了非常低的优先级。 但是一般的想法是,如果一个对象变得不可引用(通过没有剩余的引用来执行代码),它将可用于垃圾收集,并且在将来的某个时间点被销毁,并且它消耗的任何资源被释放并返回 到系统重新使用。
通常在退出执行上下文时会出现这种情况。 范围链结构,激活/变量对象以及在执行上下文中创建的任何对象(包括函数对象)将不再可访问,因此可用于垃圾回收。
形成闭包
通过返回在该函数调用的函数调用的执行上下文中创建的函数对象并将该内部函数的引用分配给另一对象的属性来形成闭包。 或者通过将例如全局变量,全局可访问对象的属性或作为引用传递的对象作为参数直接分配给这样的函数对象的引用到外部函数调用。 例如:-
function exampleClosureForm(arg1, arg2){ var localVar = 8; function exampleReturned(innerArg){ return ((arg1 + arg2)/(innerArg + localVar)); } /* return a reference to the inner function defined as - exampleReturned -:- */ return exampleReturned; } var globalVar = exampleClosureForm(2, 4);
现在,在调用exampleClosureForm的执行上下文中创建的函数对象不能被垃圾回收,因为它被全局变量引用,并且仍然可以访问,甚至可以使用globalVar(n)执行。
但是,由于globalVar引用的函数对象是使用[[scope]]属性创建的,该属性指的是包含属于其创建的执行上下文的激活/变量对象的范围链(和全局对象)。现在,Activation / Variable对象不能被垃圾回收,因为globalVar引用的函数对象的执行将需要将整个范围链从其[[scope]]属性添加到为每个调用创建的执行上下文的范围它。
形成封闭物。内部函数对象具有自由变量,函数范围链上的激活/变量对象是绑定它们的环境。
激活/变量对象通过在分配给globalVar变量现在引用的函数对象的内部[[scope]]属性的作用域链中被引用。激活/变量对象与其状态一起保存;其属性的值。对内部函数调用的执行上下文中的范围分辨率将解析与该激活/变量对象的命名属性相对应的标识符作为该对象的属性。即使创建它们的执行上下文已经退出,这些属性的值仍然可以被读取和设置。
在上述示例中,激活/变量对象具有表示形式参数,内部函数定义和局部变量的值的状态,当外部函数返回(退出其执行上下文)时。 arg1属性的值为2,arg2属性值为4,localVar为值8,以及一个exampleReturned属性,该属性是从外部函数返回的内部函数对象的引用。 (为了方便起见,在后面的讨论中,将这个激活/变量对象称为“ActOuter1”)。
如果exam??pleClosureForm函数再次被调用为: -
var secondGlobalVar = exampleClosureForm(12, 3);
- 将创建一个新的执行上下文以及一个新的激活对象。并且将返回一个新的函数对象,其自己的distinct [[scope]]属性引用了包含Activation对象的范围链,该第二个执行上下文,其中arg1为12,arg2为3。激活/变量对象作为后续讨论中的“ActOuter2”,为方便起见)
通过第二次执行exampleClosureForm形成了第二个不同的闭包。
通过执行被分配给全局变量globalVar和secondGlobalVar的引用的exampleClosureForm创建的两个函数对象返回表达式((arg1 + arg2)/(innerArg + localVar))。它将各种运算符应用于四个标识符。这些标识符如何解决对关闭的使用和价值至关重要。
考虑globalVar引用的函数对象的执行为globalVar(2)。创建一个新的执行上下文和一个激活对象(我们称之为“ActInner1”),它被添加到被执行的函数对象的[[scope]]属性的范围链的头部。 ActInner1被赋予一个名为innerArg的属性,其形式参数和参数值2分配给它。此新执行上下文的范围链是:ActInner1-> ActOuter1->全局对象。
标识符解析是针对范围链进行的,所以为了返回表达式((arg1 + arg2)/(innerArg + localVar))的值,将通过查找属性来确定标识符的值,名称与标识符对应,依次在范围链中的每个对象上。
链中的第一个对象是ActInner1,它的名称为innerArg,属性值为2.所有其他3个标识符与ActOuter1的命名属性相对应。 arg1为2,arg2为4,localVar为8.函数调用返回((2 + 4)/(2 + 8))。
将其与第二个GlobalVar引用的其他相同的函数对象的执行作为secondGlobalVar(5)进行比较。为这个新的执行上下文“ActInner2”调用激活对象,范围链变为:ActInner2-> ActOuter2->全局对象。 ActInner2将innerArg返回为5,ActOuter2分别返回arg1,arg2和localVar为12,3和8。返回的值为((12 + 3)/(5 + 8))。
再次执行secondGlobalVar,并且一个新的激活对象将出现在作用域链的前面,但ActOuter2仍将是链中的下一个对象,并且其命名属性的值将再次用于标识符arg1,arg2和localVar的解析。
这是ECMAScript内部函数如何获得并维护访问其创建的执行上下文的形式参数,声明的内部函数和局部变量。而且闭包的形成允许这样一个功能对象在持续存在的情况下继续引用这些值,读取和写入它们。来自内部函数创建的执行上下文中的激活/变量对象保留在函数对象的[[scope]]属性所引用的作用域链上,直到对内部函数的所有引用都释放并且使函数对象可用用于垃圾收集(以及其范围链上的任何现在不需要的对象)。
内部函数本身可以具有内部函数,并且从执行函数返回以形成闭包的内部函数本身可以返回内部函数并形成自己的闭包。通过每次嵌套,作用域链将获得额外的激活对象,该对象源自内部函数对象被创建的执行上下文。 ECMAScript规范要求范围链是有限的,但对其长度没有限制。实施可能确实施加了一些实际的限制,但没有具体的规模。嵌套内部功能的潜力似乎远远超过了任何人编写代码的愿望.
我们可以用闭包来做什么?
奇怪的是,这个答案似乎是任何东西和一切。 我被告知闭包使ECMAScript可以模拟任何东西,所以限制是构思和实现仿真的能力。 这是一个有点深奥,最好从一些更实用的东西开始。
示例1:使用函数引用的setTimeout
闭包的常见用途是在执行该功能之前为函数的执行提供参数。例如,当一个函数作为web浏览器环境中常见的setTimout函数的第一个参数被提供时。
setTimeout在以毫秒为单位表示的间隔(作为其第二个参数)之后调度函数的执行(或JavaScript源代码的字符串,但不在本上下文中)作为其第一个参数。如果一段代码想要使用setTimeout,它将调用setTimeout函数,并将作为第一个参数的函数对象的引用和毫秒的间隔作为第二个参数,但是对函数对象的引用不能提供该调度执行的参数功能。
然而,代码可以调用另一个返回对内部函数对象的引用的函数,该内部函数对象通过引用传递给setTimeout函数。用于执行内部函数的参数通过对返回它的函数的调用传递。 setTimout在不传递参数的情况下执行内部函数,但内部函数仍然可以访问返回它的外部函数的调用提供的参数:
function callLater(paramA, paramB, paramC){ /* Return a reference to an anonymous inner function created with a function expression:- */ return (function(){ /* This inner function is to be executed with - setTimeout - and when it is executed it can read, and act upon, the parameters passed to the outer function:- */ paramA[paramB] = paramC; }); } ... /* Call the function that will return a reference to the inner function object created in its execution context. Passing the parameters that the inner function will use when it is eventually executed as arguments to the outer function. The returned reference to the inner function object is assigned to a local variable:- */ var functRef = callLater(elStyle, "display", "none"); /* Call the setTimeout function, passing the reference to the inner function assigned to the - functRef - variable as the first argument:- */ hideMenu=setTimeout(functRef, 500);
示例2:将函数与对象实例方法相关联
还有许多其他情况,当分配一个对一个函数对象的引用,以便它将在将来的某个时间被执行,在这个时候,它有助于为执行该函数提供参数,这在执行时不容易获得,但是不能直到作业当天才知道。
一个例子可能是一个javascript对象,旨在封装与特定DOM元素的交互。它具有doOnClick,doMouseOver和doMouseOut方法,并希望在DOM元素上触发相应的事件时执行这些方法,但是可能会有任何数量的与不同DOM元素相关联的JavaScript对象的实例,并且单个对象实例不会知道他们将如何被实例化的代码使用。对象实例不知道如何全局引用它们,因为它们不知道哪些全局变量(如果有的话)将被分配给它们的实例的引用。
所以问题是执行与JavaScript对象的特定实例有关联的事件处理函数,并且知道要调用该对象的方法。
以下示例使用将对象实例与元素事件处理程序相关联的小型通用闭包函数。安排事件处理程序的执行调用对象实例的指定方法,将事件对象和对相关元素的引用传递给对象方法并返回方法的返回值。
/* A general function that associates an object instance with an event handler. The returned inner function is used as the event handler. The object instance is passed as the - obj - parameter and the name of the method that is to be called on that object is passed as the - methodName - (string) parameter. */ function associateObjWithEvent(obj, methodName){ /* The returned inner function is intended to act as an event handler for a DOM element:- */ return (function(e){ /* The event object that will have been parsed as the - e - parameter on DOM standard browsers is normalised to the IE event object if it has not been passed as an argument to the event handling inner function:- */ e = e||window.event; /* The event handler calls a method of the object - obj - with the name held in the string - methodName - passing the now normalised event object and a reference to the element to which the event handler has been assigned using the - this - (which works because the inner function is executed as a method of that element because it has been assigned as an event handler):- */ return obj[methodName](e, this); }); } /* This constructor function creates objects that associates themselves with DOM elements whose IDs are passed to the constructor as a string. The object instances want to arrange than when the corresponding element triggers onclick, onm ouseover and onm ouseout events corresponding methods are called on their object instance. */ function DhtmlObject(elementId){ /* A function is called that retrieves a reference to the DOM element (or null if it cannot be found) with the ID of the required element passed as its argument. The returned value is assigned to the local variable - el -:- */ var el = getElementWithId(elementId); /* The value of - el - is internally type-converted to boolean for the - if - statement so that if it refers to an object the result will be true, and if it is null the result false. So that the following block is only executed if the - el - variable refers to a DOM element:- */ if(el){ /* To assign a function as the element‘s event handler this object calls the - associateObjWithEvent - function specifying itself (with the - this - keyword) as the object on which a method is to be called and providing the name of the method that is to be called. The - associateObjWithEvent - function will return a reference to an inner function that is assigned to the event handler of the DOM element. That inner function will call the required method on the javascript object when it is executed in response to events:- */ el.onclick = associateObjWithEvent(this, "doOnClick"); el.onmouseover = associateObjWithEvent(this, "doMouseOver"); el.onmouseout = associateObjWithEvent(this, "doMouseOut"); ... } } DhtmlObject.prototype.doOnClick = function(event, element){ ... // doOnClick method body. } DhtmlObject.prototype.doMouseOver = function(event, element){ ... // doMouseOver method body. } DhtmlObject.prototype.doMouseOut = function(event, element){ ... // doMouseOut method body. }
所以DhtmlObject的任何实例都可以将自己与他们感兴趣的DOM元素联系起来,而不需要知道其他代码如何使用它们,影响全局命名空间或冒着与DhtmlObject的其他实例冲突的风险。
示例3:封装相关功能
闭包可用于创建其他范围,可用于将相关联和依赖代码分组,以最小化意外交互的风险。假设一个函数是构建一个字符串,并避免重复的连接操作(和创建大量的中间字符串)的愿望是使用数组来顺序存储字符串的部分,然后使用Array.prototype输出结果.join方法(以空字符串为参数)。该数组将作为输出的缓冲区,但将其本地定义为该函数将导致其在每次执行该函数时重新创建,如果该数组的唯一变量内容将被重新生成,则该数组可能不是必需的在每个函数调用时分配。
一种方法可能使数组成为一个全局变量,以便可以在不重新创建的情况下重新使用该变量。但是后果将是,除了引用使用缓冲区数组的函数的全局变量之外,还有一个引用数组本身的第二个全局属性。效果是使代码不易管理,因为如果要在其他地方使用,则其作者必须记住包括函数定义和数组定义。它也使代码不易于与其他代码集成,因为不必仅仅确保函数名称在全局命名空间中是唯一的,因此有必要确保它所依赖的Array使用的名称在全局范围内是唯一的命名空间。
关闭允许缓冲区数组与依赖于它的函数相关联(并整齐地打包),同时保留从全局命名空间分配的缓冲区数组的属性名称,并且没有名称冲突和意外的风险互动。
这里的诀窍是通过在线执行函数表达式来创建一个额外的执行上下文,并使该函数表达式返回一个内部函数,该函数将是外部代码使用的函数。然后将缓冲区数组定义为在线执行的函数表达式的局部变量。这只会发生一次,所以数组只创建一次,但是可用于依赖它的功能重复使用。
以下代码创建一个函数,该函数将返回一个HTML字符串,其中大部分是常量,但这些常量字符序列需要插入作为函数提供的可变信息作为函数调用。
对内部函数对象的引用从函数表达式的在线执行中返回并分配给全局变量,以便可以将其作为全局函数调用。缓冲区数组定义为外部函数表达式中的局部变量。它不会在全局命名空间中公开,并且每当调用使用它的函数时都不需要重新创建它。
/* A global variable - getImgInPositionedDivHtml - is declared and assigned the value of an inner function expression returned from a one-time call to an outer function expression. That inner function returns a string of HTML that represents an absolutely positioned DIV wrapped round an IMG element, such that all of the variable attribute values are provided as parameters to the function call:- */ var getImgInPositionedDivHtml = (function(){ /* The - buffAr - Array is assigned to a local variable of the outer function expression. It is only created once and that one instance of the array is available to the inner function so that it can be used on each execution of that inner function. Empty strings are used as placeholders for the date that is to be inserted into the Array by the inner function:- */ var buffAr = [ ‘<div id="‘, ‘‘, //index 1, DIV ID attribute ‘" style="position:absolute;top:‘, ‘‘, //index 3, DIV top position ‘px;left:‘, ‘‘, //index 5, DIV left position ‘px;width:‘, ‘‘, //index 7, DIV width ‘px;height:‘, ‘‘, //index 9, DIV height ‘px;overflow:hidden;\"><img src=http://www.mamicode.com/"‘, ‘‘, //index 11, IMG URL ‘\" width=\"‘, ‘‘, //index 13, IMG width ‘\" height=\"‘, ‘‘, //index 15, IMG height ‘\" alt=\"‘, ‘‘, //index 17, IMG alt text ‘\"><\/div>‘ ]; /* Return the inner function object that is the result of the evaluation of a function expression. It is this inner function object that will be executed on each call to - getImgInPositionedDivHtml( ... ) -:- */ return (function(url, id, width, height, top, left, altText){ /* Assign the various parameters to the corresponding locations in the buffer array:- */ buffAr[1] = id; buffAr[3] = top; buffAr[5] = left; buffAr[13] = (buffAr[7] = width); buffAr[15] = (buffAr[9] = height); buffAr[11] = url; buffAr[17] = altText; /* Return the string created by joining each element in the array using an empty string (which is the same as just joining the elements together):- */ return buffAr.join(‘‘); }); //:End of inner function expression. })(); /*^^- :The inline execution of the outer function expression. */
如果一个函数依赖于一个(或多个)其他函数,但这些其他函数并不期望任何其他代码直接使用,则可以使用相同的技术将这些函数与公开暴露的函数进行分组。 将复杂的多功能过程复制到易于携带和封装的代码单元中。
其他例子
可能是最着名的闭包应用之一是Douglas Crockford在ECMAScript对象中仿真私有实例变量的技术。 哪些可以扩展到各种范围的结构,包含嵌套的可访问性/可见性,包括ECMAScript对象的私有静态成员的仿真。
关闭的可能应用是无止境的,了解它们如何工作可能是实现如何使用它们的最佳指南。
意外闭包
渲染任何可在其创建的函数体内访问的内部函数将形成闭包。这使得关闭非常容易创建,其中一个后果是不喜欢闭包作为语言特征的javascript作者可以观察内部函数对各种任务的使用,并采用内部函数,没有明显的后果,没有意识到闭包是被创造或者这样做的含义是什么。
意外创建闭包可能会产生有害的副作用,如下面的IE内存泄漏问题描述,但它们也可能影响代码的效率。它不是封闭的,而是经过精心的使用,可以为创建高效的代码做出重大贡献。它是使用可以影响效率的内在功能。
常见的情况是使用内部函数作为DOM元素的事件处理程序。例如,以下代码可能用于向链接元素添加onclick处理程序:
/* Define the global variable that is to have its value added to the - href - of a link as a query string by the following function:- */ var quantaty = 5; /* When a link passed to this function (as the argument to the function call - linkRef -) an onclick event handler is added to the link that will add the value of a global variable - quantaty - to the - href - of that link as a query string, then return true so that the link will navigate to the resource specified by the - href - which will by then include the assigned query string:- */ function addGlobalQueryOnClick(linkRef){ /* If the - linkRef - parameter can be type converted to true (which it will if it refers to an object):- */ if(linkRef){ /* Evaluate a function expression and assign a reference to the function object that is created by the evaluation of the function expression to the onclick handler of the link element:- */ linkRef.onclick = function(){ /* This inner function expression adds the query string to the - href - of the element to which it is attached as an event handler:- */ this.href += (‘?quantaty=‘+escape(quantaty)); return true; }; } }
每当调用addGlobalQueryOnClick函数时,都会创建一个新的内部函数(并且通过其赋值形成闭包)。 从效率的角度来看,如果addGlobalQueryOnClick函数只被调用一次或两次,那么这个函数是非常重要的,但是如果函数被大量使用,那么将会创建许多不同的函数对象(一个用于内部函数表达式的每个评估)。
上述代码没有利用内部函数在它们被创建的函数之外变得可访问的事实(或所产生的闭包)。 因此,通过定义要用作事件处理程序的函数,然后将该函数的引用分配给事件处理属性,可以实现完全相同的效果。 将只创建一个函数对象,并且使用该事件处理程序的所有元素将共享一个函数的引用:
/* Define the global variable that is to have its value added to the - href - of a link as a query string by the following function:- */ var quantaty = 5; /* When a link passed to this function (as the argument to the function call - linkRef -) an onclick event handler is added to the link that will add the value of a global variable - quantaty - to the - href - of that link as a query string, then return true so that the link will navigate to the resource specified by the - href - which will by then include the assigned query string:- */ function addGlobalQueryOnClick(linkRef){ /* If the - linkRef - parameter can be type converted to true (which it will if it refers to an object):- */ if(linkRef){ /* Assign a reference to a global function to the event handling property of the link so that it becomes the element‘s event handler:- */ linkRef.onclick = forAddQueryOnClick; } } /* A global function declaration for a function that is intended to act as an event handler for a link element, adding the value of a global variable to the - href - of an element as an event handler:- */ function forAddQueryOnClick(){ this.href += (‘?quantaty=‘+escape(quantaty)); return true; }
由于第一个版本中的内部函数不被用于利用其使用所产生的闭包,因此不会使用内部函数更为有效,因此不会重复创建许多基本相同的函数对象的过程。
类似的考虑也适用于对象构造函数。 看到类似于以下框架构造函数的代码并不罕见:
unction ExampleConst(param){ /* Create methods of the object by evaluating function expressions and assigning references to the resulting function objects to the properties of the object being created:- */ this.method1 = function(){ ... // method body. }; this.method2 = function(){ ... // method body. }; this.method3 = function(){ ... // method body. }; /* Assign the constructor‘s parameter to a property of the object:- */ this.publicProp = param; }
每次使用构造函数创建一个对象,使用新的ExampleConst(n),将创建一组新的函数对象作为其方法。因此,创建的对象实例越多,创建的函数对象越多,就可以与它们一起使用。
Douglas Crockford用于模拟javascript对象上的私有成员的技术利用了封闭结果,从内部函数对象的引用到构造函数内的构造对象的公共属性。但是,如果对象的方法没有利用它们在构造函数中形成的闭包,则为每个对象实例化创建多个函数对象将使实例化过程变得更慢,并且将使用更多的资源来容纳创建的额外的函数对象。
在这种情况下,创建函数对象一次会更有效,并将引用分配给构造函数原型的相应属性,以便它们可以由使用该构造函数创建的所有对象共享:
function ExampleConst(param){ /* Assign the constructor‘s parameter to a property of the object:- */ this.publicProp = param; } /* Create methods for the objects by evaluating function expressions and assigning references to the resulting function objects to the properties of the constructor‘s prototype:- */ ExampleConst.prototype.method1 = function(){ ... // method body. }; ExampleConst.prototype.method2 = function(){ ... // method body. }; ExampleConst.prototype.method3 = function(){ ... // method body. };
Internet Explorer内存泄漏问题
Internet Explorer Web浏览器(在版本4至6上进行验证(6)在写入时为当前)在其垃圾收集系统中有故障,防止垃圾收集ECMAScript和某些主机对象,如果这些主机对象组成“循环”参考。所涉及的主机对象是任何DOM节点(包括文档对象及其后代)和ActiveX对象。如果形成包括一个或多个循环引用的循环引用,那么在浏览器关闭之前,所涉及的对象都不会被释放,并且消耗的内存在系统发生之前将不可用。
循环引用是指两个或多个对象以可以遵循的方式相互指向并返回到起始点。诸如对象1具有引用对象2的属性,对象2具有引用对象3的属性,并且对象3具有回溯到对象1的属性。一旦没有其他对象引用任何其他对象,则具有纯ECMAScript对象对象1,2或3,它们只是相互引用的事实被识别,并且它们可用于垃圾收集。但是在Internet Explorer中,如果这些对象恰好是DOM节点或ActiveX对象,则垃圾收集不能看到它们之间的循环关系与系统的其余部分隔离,并将其释放。相反,它们都保留在内存中,直到浏览器关闭。
封闭在形成循环引用方面非常好。如果将形成闭包的函数对象作为例如DOM节点上的事件处理程序分配,并将对该节点的引用分配给其作用域链中的一个激活/变量对象,则循环引用将存在。 DOM_Node.onevent - > function_object。[[scope]] - > scope_chain - > Activation_object.nodeRef - > DOM_Node。这是很容易做到的,并且围绕一个在每个页面共同的代码中形成这样一个参考的站点的浏览可能消耗大部分系统内存(可能全部)。
- Written by Richard Cornford. March 2004.
- Translated by vavv july 2017
- With corrections and suggestions by:-
- Martin Honnen.
- Yann-Erwan Perio (Yep).
- Lasse Reichstein Nielsen.
- Mike Scirocco.
- Dr John Stockton.
- Garrett Smith.
js闭包