问题提出
设有一个js代码:
也就是在某个函数作用域中调用obj
对象上的名为omethod
方法,传入的参数是args
。
通过相关的文档资料可知,这个步骤分为两个部分:获得方法、调用方法。从这个意义上来说,我们可能认为这句代码等价为
显然这是错误的。事实上,问题在于 #1 行,func
获得的是对象方法的引用,此后这个func
就与obj
无关了,也就是说func
中的的this
并不指向obj
,在非严格模式下,它指向全局对象。因此在 #2 行直接调用func
,如果其内部使用了this
,那么其结果就会与预期不相符。
正确的应该是如下所示
或
对于obj.omethod(args)
语句,执行引擎应该将其认为是一句代码(而不是我们在源代码中拆分的两句)而执行的。一个值得思考的问题是,在执行过程中,引擎获得了omethod
的方法后,该方法已经绑定了obj
吗?如果拿到的方法尚未绑定obj
,那么是在什么时候、如何将obj
作为omethod
的this
而绑定了呢?
术语解释
要解答这个问题,我们必须要明白其内部的原理。具体的实现可能各不相同,而一个很好的参考对象就是语言的规格文档。在这里,我参阅的是ECMAScript® Language Specification (Standard ECMA-262 5.1 Edition / June 2011)。
注意,下文所述的一些数据结构只是概念性的,是规格文档用于说明原理而使用的,它并不与具体实现相关联。也就是说,我们可以通过它们来了解运作原理而不依赖于具体实现。
有以下几个名词术语是必须要了解的:
- Reference
- Lexical Environments
- Environment Records
- Object Environment Records
Reference
A Reference is a resolved name binding. A Reference consists of three components,the base value,the referenced name and the Boolean valued strict reference flag. The base value is either undefined,an Object,a Boolean,a String,a Number,or an environment record (10.2.1). A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.
Reference我们可以理解为一个携带了某些元信息的数据结构。规格文档在描述一些产生式的解析算法时,常常会把产生式的词法单元计算(evaluate)为一个Reference,它含有词法单元所依赖的的上下文环境、词法单元的名称等信息。也即是说,通过Reference,我们可以使用这个词法单元。
Reference由三部分组成:
-
base
:可以理解为Reference的环境或者基础,即建立在base
上的Reference是有意义的。这个base
可以是undefined
、Object
、Boolean
、String
、Number
、ER
(即Environment Record)。如果base
是undefined
说明Reference是无效的,即无法被解析为一个绑定。 -
reference name
:Reference的名字,是一个String
-
strict reference flag
:布尔类型,是指示是否是严格模式的标志。
针对一个 Reference V
,有以下抽象方法:
- Get方法:
GetBase(V)
、GetReferencedName(V)
、IsStrictReference(V);
- IsUnresolvableReference(V):
return AssertEqual(base,undefined);
- HasPrimitiveBase(V):
return AssertOneOf(base,[Boolean,String,Number]);
判断词法单元依赖的上下文是否是基础类型。例如,Number.isNaN
,那么词法单元isNaN
被包装为一个Reference时,reference name 就是 " isNaN ",而它依赖的上下文( base )就是Number
,从而这个Reference的base就是一个Primitive-Type( Number )。 - IsPropertyReference(V):
return AssertEqual(base,Object) AND HasPrimitiveBase(V);
当Reference的base是一个对象类型时,该方法就返回true
。显然如果一个Reference的上下文是一个对象,那么这个Reference就是对象的属性,这也是这个方法为什么叫这个名字。
我们注意到无论是HasPrimitiveBase(V)
还是IsPropertyReference(V)
,它们返回true
的条件中,不包含base是ER的情况。这是因为ER是一个很特殊的(概念性的)对象,在一些方法中(如GetValue
)中一般要将ER和其他的base类型分别操作。
注意这些方法只做概念说明使用,并不定义在具体实现中。
Lexical Environments
Lexical Environment可以称为“词法环境”。文档的一节中,对Lexical Environments的定义是:
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code.
Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment. Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration,a WithStatement,or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated.
An Environment Record records the identifier bindings that are created within the scope of its associated Lexical Environment.
可以看出,词法环境是一个说明类型(specification type),即只是规格文档的一个用于说明的抽象概念。它是用于定义 哪些标识符 对应于 哪些变量和函数。词法环境是可以嵌套的,外部词法环境定义的变量在内部词法环境可以被访问到,但如果内部词法环境定义了一个与外部词法环境同名的量就会产生屏蔽现象。
一个词法环境包括了一个环境记录(Environment Record,ER),以及一个可能为null
指向外部词法环境的引用。词法环境通常和一些ECMAScript代码结构有关,如函数声明、With语句、Try语句的Catch子句,当这些结构被执行时,就会产生一个新的词法环境。
A Lexical Environment may serve as the outer environment for multiple inner Lexical Environments. For example,if a FunctionDeclaration contains two nested FunctionDeclarations then the Lexical Environments of each of the nested functions will have as their outer Lexical Environment the Lexical Environment of the current execution of the surrounding function.
Environment Records
Environment Record是“环境记录”,写为ER。在Stack Overflow上有一篇解答很好的说明了如何理解ER。
ER,顾名思义它是一种用于对当前环境存在的某些量进行 记录 的数据结构,每一个词法环境都包含一个ER。ER可以想象为一个映射或者关系表结构,执行引擎可以利用ER进行变量的查找操作。
ER上有一些抽象方法(see also:Table 17——Abstract Methods of Environment Records),其中和本问题有关的是两个:
- GetBindingValue(N,S):用于从ER中获取绑定的量,我们可以想象为从映射表中根据键
N
来获得值。N
是想要获得的值的名字,它是字符串类型;S
是指示是否是严格模式。如果是严格模式,且所请求的绑定的量不存在或未初始化,那么将抛出ReferenceError
异常 - ImplicitThisValue():返回一个用作函数对象调用时的 this 值,这些函数对象是从该ER中获得的。这个方法在DER中总返回
undefined
,在OER的行为见“Object Environment Records”节。
在JS中有两类ER:Declarative Environment Record(DER)和Object Environment Record(OER)。若把ER比喻为一个抽象类,那么DER和OER就像是是两个具体实现类。
DER比较容易理解,通过它的名字可见,它是一种声明型的ER,在程序中声明的变量、函数、对象、Catch子句的用于接收异常的变量,都会被当前作用域环境当中的ER所记录。
例如,考虑如下代码:
在f
这个函数作用域中,产生了一个词法环境 LE_O ,LE_O 中的ER就是一个DER,它的逻辑结构可以想象为一个表:
DER of the Lexical Environment within function scope
————————————————————————————————
binding name binding value
-------------------------------------------------------------
foo 1
bar function
baz object
-------------------------------------------------------------
所以,当f
内部执行bar()
时,bar
内部也有一个词法环境 LE_I,执行输出语句时,首先在LE_I 上的ER中寻找foo
,显然没有,于是根据 LE_I 上的指向外部词法环境的指针 就找到了 LE_O,然后通过 LE_O 上的 DER,我们就找到了foo
的值。
那么OER如何理解呢?
仍然要注意,ER的只是为了更好的说明某些原理而提出的概念,它是概念性的事物,与具体实现无关。
Object Environment Records
OER也是用于记录当前环境中定义的某些量,以便执行引擎可以查找,而“当前环境”指的就是就是 对象环境(Object Environment)。
换言之,如果我们给一个对象上定义成员变量和成员函数,那么该对象便会和一个OER绑定,这个对象就称作OER的 绑定对象(binding object),而定义的那些变量、函数就会成为该OER的 绑定值(binding value) 。
当我们在OER中进行变量操作、函数调用时,这些变量、函数就是在OER的 binding object 上去寻找的。
为了更好的理解,考虑下面的代码,它使用了操作符是with
:
say(); // #1------I define pi as 3.1415
global.say(); //#2------[In Node.js] I define pi as 3.1415
with(obj){
say(); //#3------I define pi as 4
}
obj.say(); //#4------I define pi as 4
第一个say
函数执行时的环境是全局环境(此时是非严格模式),于是它的ER是OER(而不是DER),OER的绑定对象是全局对象(在Nodejs中全局对象是global
,而Chrome控制台中全局对象是window
。在下文中统称为Global
),于是say()
的全称是Global.say()
。this
绑定为全局对象Global
,全局对象的pi
是3.1415
。
第二个say
显式地指定了global
,于是say
的ER就是OER,绑定对象是global
。
:
The global environment’s Environment Record is an object environment record whose binding object is the global object.The global environment’s outer environment reference is null.
而在 with 语句块中,新的OER被创建,OER被置为 obj
,所以其中的say
会在obj
上寻找(可以省略obj
前缀),于是say()
的全称是obj.say()
。
函数代码中的this
被也绑定为obj
,或者说,对于say
中的pi
,执行引擎会在obj
这个OER中去寻找(retrieve bingding value from binding object。)
第4个say
显式地指定了obj
,于是say
的ER就是OER,绑定对象是obj
。
The with statement adds an object environment record for a computed object to the lexical environment of the current execution context. It then executes a statement using this augmented lexical environment. Finally,it restores the original lexical environment.
既然全局环境的ER是OER,而在with
中也是OER,在obj.method(...)
中method
的ER也是OER,那么什么时候可以用DER呢?
解决方案
现在我们来看看代码
根据规格文档的文法产生式(EMCAScript设计的JavaScript是一个上下文无关文法),它的规约过程如下:
obj.omethod(args)
=> Identifier.omethod(args)
=> [MemberExpression] . omethod(args)
=> [MemberExpression . IdentifierName](args)
=> [MemberExpression] (args)
=> [MemberExpression Arguments]
=> CallExpression
我们通过自底向上的方式来分别说明。
Identifier:obj
解析为 Reference
在中说到:
An Identifier is evaluated by performing Identifier Resolution as specified in 10.3.1. The result of evaluating an Identifier is always a value of type Reference.
即对一个标识符Identifier的计算结果总是一个Reference类型,计算方法是。
Identifier Resolution算法 是将 标识符 识别为 当前运行的执行上下文 中的 词法环境 中的某个量。例如在函数作用域中定义了一个obj
对象,那么obj
就被绑定到 当前词法作用域 中的 DER 当中。现在遇到一个名为obj
的标识符,那么该算法就可将这个标识符识别为DER中的obj
对象,即我们要引用这个对象。
Identifier Resolution算法的过程是:
- 令env是当前运行的执行上下文的词法环境
- 如果语句处于严格模式下,令strict为
true
,否则为false
- 返回
GetIdentifierReference(env,Identifier,strict)
的结果,该结果总是一个Reference类型,其中的 Referenced Name 就是标识符字符串。
那么GetIdentifierReference
是什么呢?
方法是抽象方法,接受的三个参数分别是词法环境lex、标识符字符串name、严格模式指示符strict。它的执行步骤是
- 如果lex是
null
,那么返回Reference { base:undefined,referenced-name: name,strict:strict }
- 否则,令envRec是lex中的环境记录ER
- 令exists是
envRec.HasBinding(name)
的执行结果。这个函数表示envRec是否有绑定名为name的量- 如果exists是
true
,那么返回Reference { base:envRec,strict:strict }
- 否则,令outer 是 lex 的外部词法环境
- 递归调用并返回
GetIdentifierReference(outer,strict)
的结果
可见它先会在当前词法作用域中寻找标识符定义,如果没找到那就到外部词法环境中寻找,再到外部的外部……以此类推,形成一个 链 的结构。
在我们的例子中,obj.omethod(args)
是定义在一个函数作用域中的,我们假定在 词法环境链 中 已经定义了 obj
对象。那么 对 obj
标识符的计算结果是:
// The result of evaluating Identifier 'obj' is a Reference
Reference:{
base: someDER,ref-name: 'obj',strict: true or false,depending on concrete code
}
注意这个base是某个DERsomeDER
,因为obj
是定义在函数作用域的词法环境链中的(而不是某个对象中),所以ER应该是一个DER,而不是OER。
obj.omethod
的解析
obj.omethod
被规约为 MemberExpression . IdentifierName 。这实际上是一个的语句,在规格文件中提到:
MemberExpression . IdentifierName is identical in its behaviour to MemberExpression[ identifier-name-string ]
也就是说,obj.omethod
实际上和obj["omethod"]
是等价的,因此我们来看看后者是如何计算的。
对于产生式MemberExpression -> MemberExpression [ Expression],计算方法是:
- 令baseReference 是计算MemberExpression的结果
- 令baseValue是
GetValue(baseReference)
的结果- 令propertyNameReference是Expression的计算结果
- 令propertyNameValue是
GetValue(propertyNameReference)
的计算结果- 调用
CheckObjectCoercible(baseValue)
,这个函数会抛出一个错误,如果参数无法使用ToObject
转为一个对象的话。- 令propertyNameString 是
ToString(propertyNameValue)
- 如果是严格模式,令strict为
true
,否则为false
- 返回
Reference { base:baseValue,referenced-name: propertyNameString,strict:strict }
这里出现了GetValue(V)函数,这个函数的算法是:
- 如果V的类型不是Reference,则直接返回 V
- 现在V是一个Reference,令base 是
GetBase(V)
的结果- 如果
IsUnresolvableReference(V)===true
,则抛出ReferenceError
异常如果
IsPropertyReference(V)
(即V的base不是ER也不是undefined
),则
- 如果
HasPrimitiveBase(V)===false
,即V的base不是一个基础类型(String、Number、Boolean)的对象,那么令get是base的[[Get]]
内部方法,否则get按照下文构造(这里‘下文’参见文档原文,这里不再列出)- 返回
base.[[Get]]( GetReferencedName(V) )
- 否则,base一定是一个ER,返回
base.GetBindingValue( GetReferencedName(V),GetReferencedName(V))
简单来说,如果
GetValue
的参数V是一个Reference,那么就从base中获得名为 V的ReferencedName 的绑定量。
现在回到例子中来,对于obj["omethod"]
:
-
baseReference 是 对
obj
的计算结果(obj
-> Identifier -> PrimaryExpression -> MemberExpression),通过上一节我们知道,它的结果是Reference:{ base: someDER,strict: ...}
-
baseValue是
GetValue(baseReference)
的结果,通过上面的算法(其中base:someDER是一个ER),我们知道baseValue 就是obj
对象本身 -
propertyNameReference是对Expression的计算结果,Expression在这里是简单的字符序列
omethod
,因此propertyNameReference就是omethod
(在文档中搜索“PropertyName : IdentifierName”即可找到说明)(如果是类似于obj[bar.foo().getPropName()]
之类的复杂表达式,那么propertyNameReference可能就不是简单字符序列了) -
propertyNameValue是对
GetValue(propertyNameReference)
的计算结果,由于propertyNameReference是字符序列不是Reference,所以一开始就返回,propertyNameValue是'omethod'
- 调用
CheckObjectCoercible(baseValue)
通过 - 令propertyNameString 是
ToString(propertyNameValue)
,得到的还是'omethod'
- 如果是严格模式,令strict为
true
,否则为false
- 返回
Reference { base:obj,referenced-name: 'omethod',strict:strict }
函数调用的算法:Function Calls
对于产生式 CallExpression : MemberExpression Arguments 的操作如下:
- 令 ref 为对MemberExpression 的计算结果。
- 令
func=GetValue(ref)
- 令
argList
为Arguments的计算结果,它就是函数调用的参数列表。- 若
Type(func)
是Object
,抛出TypeError
异常- 若
IsCallable(func)
是false
,则抛出TypeError
异常如果
Type(ref)
是Reference,则:
- 如果
IsPropertyReference(ref)
是true
,则令thisValue
为GetBase(ref)
- 否则 ref 的base就是一个Environment Record,则令
thisValue
是GetBase(ref).ImplicitThisValue
- Else,
Type(ref)
不是Reference,则令thisValue
是undefined
- 返回
func.[[Call]](thisValue,argList)
对于obj.omethod(args)
而言,obj.omethod
已经规约为MemberExpression,(args)
被规约为Arguments:
- 令 ref 为对obj.method 的计算结果。通过上一节可知,ref为
Reference { base:obj,strict:strict }
- 令
func=GetValue(ref)
。所以func
得到的是obj
上名为的omethod
的属性,它是一个方法(it's callable)。 - 令
argList
为Arguments的计算结果,在这里是args
-
Type(ref)
是Reference,由于base是一个object(注意这里base是obj,它是Object类型,而不是ER!),从而IsPropertyReference(ref)
是true
,则令thisValue
是GetBase(ref)
,也就是obj
。 - 返回
func.[[Call]](obj,args)
总结
从上面可以看出,对于在函数作用域中的obj.omethod(args)
而言,经过如下几步: