引言
闭包
“闭包”是一种表达式(通常来说是一个function
),它有各种变量以及自己的上下文,而这些变量就是绑定在这个上下文的
闭包是ECMAScript
众多强大特性之一,但是在没有很好的理解它的来龙去脉之前,想要驾驭它是相当困难的。然而,闭包相对来说是容易去构造的,甚至可能有时候偶然的机会你就会造出一个闭包,不过闭包的出现可能会带来潜在的有害的结果,特别是在一些常用的浏览器环境中的时候。要避免偶遇到闭包的缺陷,同时又充分利用它提供优点,就必须充分理解闭包的运作机制。闭包功能在标识符解析以及Javascript对象属性的解析过程中,重度依赖于作用域链。
对于闭包比较简单的解释就是ECMAScript
允许内部方法;方法的定义以及方法的表达式都在另一个方法的方法体内。并且内部方法运行访问所有局部变量,参数以及定义在同一父方法内的所有其他方法。当存在与一个方法里的内部方法开始访问其依赖的外部方法时,一个闭包就形成了,所以很可能在外部方法返回之后,内部方法才会被执行。而此时,内部方法依然可以访问局部变量,参数以及其父方法内部的其他内部方法。那些局部变量,参数以及其他内部方法在最初声明的时候就已经有值了,当外部方法返回之后,它们还依旧可能被内部方法在激活使用。
不妙的是,要正确理解闭包,就需要理解它背后的运行机制以及其他许多的技术细节。不过在ECMA 262
的解释中,以及有一些特定的算法被梳理过了,大多数不能够一带而过,需要去细细品啄。那些已经Javascript对象属性的解析了然于心的小伙伴,其实是可以那个部分。但是只有那些已经对闭包很熟悉的同志才可以跳过下面的这个部分,并且完全可以停止往下看,回到编辑器尽情的玩耍闭包吧。
Javascript对象的属性解析
ECMScript
识别两类对象,Native Object
和Host Object
,而Host Object
有个自雷本地对象,叫Built-in Object
内置对象(ECMA 262 3rd Ed Section 4.3)。Native objects
属于语言本身的对象,Host Objects
是语言所处的环境提供的对象,比如document objects
,DOM
节点之类的。
Native objects
通常没那么严格,是一包动态的名称属性(对于一些子类的耳机对象,一些实现方式并没有那么的动态,尽管这也没啥大碍)。对象的预定义的名称属性会维护一个值,这个值可能是指向另一个对象的引用(这时候方法也会是对象)或者是基本数据类的值,比如字符串、数值、布尔值、空或者是未定义。 未定义这种基本类型有点奇怪,有时候会将 未定义赋予一个对象的属性,但是这样做并不会将原先的属性给从原对象移除。它依然还是一个名称属性,只是值已经变成了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对象本身有原型,本身就是对象,而这个对象也有许多的属性,稍微我们会看下。但是原型本身和赋值没什么关系。如果要对一个对象进行赋值,且这个对象实际没有那个名称的属性,那便会创建这个属性并赋值。如果有这个属性,那就是重新赋值。
读值
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. */
但是所有的对象都有原型,而原型又都是对象,所以原型还可能会有原型等等等等,这就是所谓的原型链。原型链在其中一级的对象的prototype
属性为null
时结束。默认的Object
的构造函数有一个空的prototype
,所以:
var objectRef = new Object(); //create a generic javascript object.
由Object.prototype
创建的对象它自己就有一个空的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
的实例有原型链。这个原型链中的第一个对象就是MyObject1
的实例,它被赋值给了MyObject2
的构造函数。MyObject1
的实例有一个原型,它就是当时在实现方法中被赋值给MyObject1
的原型的对象。这个对象有一个原型,就是Object.prototype
。Object.prototype
自己本身有一个空的原型,因此原型链在这个节点就结束了。
当属性存储器通过变量objectRef
来读取某个属性的时候,正原型链就可以进入到这个过程。如下:
var val = objectRef.testString;
由objectRef
指向的MyObject2
的实例有个testString
的属性,那它就是那个属性的值,被设置为了字符串值。而这个属性的值又被赋值给val
这个变量。
var val = objectRef.testNumber;
而不能从MyObject2
的实例来读取这个属性,因它根本没有这个属性,但是变量val
被赋值为8
而不是undefined
,因为在对象自己本身的属性里面没有找到这个属性,那解释器就回去它的原型来找。它的原型是MyObject1
的实例,而它当时被创建的时候就有个属性名字为testNumber
且值为8
。所以属性存储器就读取出了值8
。MyObject1
与MyObject2
都没有去定义toString
方法,但是如果属性存储器试图去以toStirng
的方式来读取objectRef
的属性:
var val = objectRef.toString;
变量val
被指向了一个方法。这个方法就是属于Object.prototype
的属性方法,并且也返回了因为处理器检查了objectRef
的原型,当objectRef
被证明没有toString
这个属性,就会去检查它的原型了。它的原型就是Object.prototype
,而它就拥有toString
方法,所以最终还是会返回这个方法。
最后
var val = objectRef.madeUpProperty;
会返回undefined
,因为处理器在这个对象的原型链上找半天都没有找到madeUpPeoperty
属性。它最终会在Object.prototype
上找,而且肯定找不到,所以最终就返回了undefined
。
对象的属性值读取总是会返回一个被找到的值,不管是从对象本身还是它的原型链上。而对象的属性赋值会在对象本身上创建一个属性(这个对象本身没有这个属性的话)。
这就意味着,当objectRef.testNumber=3
被执行时,·testNumber·这个属性就会在MyObject2
的实例上创建这个属性。之后任何地方去读取这个值都会从当初赋值到这个对象的属性去读取。原型链不需要再被检查,但是MyObject1
的实例上testNumber
属性是不会发生变化的。objectRef
的赋值会将其原型链上相关属性挡在后面。
注意:ECMAScript
在Object
类型上定义了一个内部的[[prototype]]属性。这个属性无法直接通过脚本来访问,其实它就是属性存储器在读取原型链中采用的解决办法。对象的原型链。公共的原型的存在允许赋值,定义以及计算内部原型。这两者的内部关系在ECMA 262(3rd edition)
中有描述,这里就不讨论了。