JavaScript中调用对象方法时,this指针是如何绑定的

前端之家收集整理的这篇文章主要介绍了JavaScript中调用对象方法时,this指针是如何绑定的前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

问题提出

设有一个js代码

也就是在某个函数作用域中调用obj对象上的名为omethod方法,传入的参数是args

通过相关的文档资料可知,这个步骤分为两个部分:获得方法调用方法。从这个意义上来说,我们可能认为这句代码等价为

显然这是错误。事实上,问题在于 #1 行,func获得的是对象方法的引用,此后这个func就与obj无关了,也就是说func中的的this并不指向obj,在非严格模式下,它指向全局对象。因此在 #2 行直接调用func,如果其内部使用了this,那么其结果就会与预期不相符。

正确的应该是如下所示

对于obj.omethod(args)语句,执行引擎应该将其认为是一句代码(而不是我们在源代码中拆分的两句)而执行的。一个值得思考的问题是,在执行过程中,引擎获得了omethod方法后,该方法已经绑定了obj吗?如果拿到的方法尚未绑定obj,那么是在什么时候、如何将obj作为omethodthis而绑定了呢?

术语解释

要解答这个问题,我们必须要明白其内部的原理。具体的实现可能各不相同,而一个很好的参考对象就是语言的规格文档。在这里,我参阅的是ECMAScript® Language Specification (Standard ECMA-262 5.1 Edition / June 2011)

注意,下文所述的一些数据结构只是概念性的,是规格文档用于说明原理而使用的,它并不与具体实现相关联。也就是说,我们可以通过它们来了解运作原理而不依赖于具体实现。

有以下几个名词术语是必须要了解的:

  • Reference
  • Lexical Environments
  • Environment Records
  • Object Environment Records

Reference

文档的一节中,对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可以是undefinedObjectBooleanStringNumberER(即Environment Record)。如果baseundefined说明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所记录。

例如,考虑如下代码:

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,全局对象的pi3.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算法的过程是:

  1. env是当前运行的执行上下文的词法环境
  2. 如果语句处于严格模式下,令stricttrue,否则为false
  3. 返回GetIdentifierReference(env,Identifier,strict)的结果,该结果总是一个Reference类型,其中的 Referenced Name 就是标识符字符串。

那么GetIdentifierReference是什么呢?

方法是抽象方法,接受的三个参数分别是词法环境lex、标识符字符串name、严格模式指示符strict。它的执行步骤是

  1. 如果lexnull,那么返回Reference { base:undefined,referenced-name: name,strict:strict }
  2. 否则,令envReclex中的环境记录ER
  3. existsenvRec.HasBinding(name)的执行结果。这个函数表示envRec是否有绑定名为name的量
  4. 如果existstrue,那么返回 Reference { base:envRec,strict:strict }
  5. 否则,令outerlex 的外部词法环境
  6. 递归调用并返回 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],计算方法是:

  1. baseReference 是计算MemberExpression的结果
  2. baseValueGetValue(baseReference)的结果
  3. propertyNameReferenceExpression的计算结果
  4. propertyNameValueGetValue(propertyNameReference)的计算结果
  5. 调用CheckObjectCoercible(baseValue),这个函数会抛出一个错误,如果参数无法使用ToObject转为一个对象的话。
  6. propertyNameStringToString(propertyNameValue)
  7. 如果是严格模式,令stricttrue,否则为false
  8. 返回 Reference { base:baseValue,referenced-name: propertyNameString,strict:strict }

这里出现了GetValue(V)函数,这个函数的算法是:

  1. 如果V的类型不是Reference,则直接返回 V
  2. 现在V是一个Reference,令baseGetBase(V)的结果
  3. 如果IsUnresolvableReference(V)===true,则抛出ReferenceError异常
  4. 如果IsPropertyReference(V)(即V的base不是ER也不是undefined),则

    • 如果HasPrimitiveBase(V)===false,即V的base不是一个基础类型(String、Number、Boolean)的对象,那么令get是base的[[Get]]内部方法,否则get按照下文构造(这里‘下文’参见文档原文,这里不再列出)
    • 返回 base.[[Get]]( GetReferencedName(V) )
  5. 否则,base一定是一个ER,返回base.GetBindingValue( GetReferencedName(V),GetReferencedName(V))

简单来说,如果GetValue的参数V是一个Reference,那么就从base中获得名为 V的ReferencedName 的绑定量。

现在回到例子中来,对于obj["omethod"]

  1. baseReference 是 对obj的计算结果(obj-> Identifier -> PrimaryExpression -> MemberExpression),通过上一节我们知道,它的结果是Reference:{ base: someDER,strict: ...}
  2. baseValueGetValue(baseReference)的结果,通过上面的算法(其中base:someDER是一个ER),我们知道baseValue 就是 obj对象本身
  3. propertyNameReference是对Expression的计算结果,Expression在这里是简单的字符序列omethod,因此propertyNameReference就是omethod(在文档中搜索“PropertyName : IdentifierName”即可找到说明)(如果是类似于obj[bar.foo().getPropName()]之类的复杂表达式,那么propertyNameReference可能就不是简单字符序列了)
  4. propertyNameValue是对GetValue(propertyNameReference)的计算结果,由于propertyNameReference是字符序列不是Reference,所以一开始就返回,propertyNameValue'omethod'
  5. 调用CheckObjectCoercible(baseValue)通过
  6. propertyNameStringToString(propertyNameValue),得到的还是'omethod'
  7. 如果是严格模式,令stricttrue,否则为false
  8. 返回 Reference { base:obj,referenced-name: 'omethod',strict:strict }

函数调用的算法:Function Calls

函数调用的算法:

对于产生式 CallExpression : MemberExpression Arguments 的操作如下:

  1. ref 为对MemberExpression 的计算结果。
  2. func=GetValue(ref)
  3. argListArguments的计算结果,它就是函数调用的参数列表。
  4. Type(func)Object,抛出TypeError异常
  5. IsCallable(func)false,则抛出TypeError异常
  6. 如果Type(ref)是Reference,则:

    • 如果IsPropertyReference(ref)true,则令thisValueGetBase(ref)
    • 否则 ref 的base就是一个Environment Record,则令thisValueGetBase(ref).ImplicitThisValue
  7. Else,Type(ref)不是Reference,则令thisValueundefined
  8. 返回func.[[Call]](thisValue,argList)

对于obj.omethod(args)而言,obj.omethod已经规约为MemberExpression(args)被规约为Arguments

  1. ref 为对obj.method 的计算结果。通过上一节可知,refReference { base:obj,strict:strict }
  2. func=GetValue(ref)。所以func得到的是obj上名为的omethod属性,它是一个方法(it's callable)。
  3. argListArguments的计算结果,在这里是args
  4. Type(ref)是Reference,由于base是一个object(注意这里base是obj,它是Object类型,而不是ER!),从而IsPropertyReference(ref)true,则令thisValueGetBase(ref),也就是obj
  5. 返回func.[[Call]](obj,args)

总结

从上面可以看出,对于在函数作用域中的obj.omethod(args)而言,经过如下几步:

  1. obj被包装为一个Reference:ref-1,它的base是函数作用域链中的某个DER,Type(ref-1.base)==ER
  2. obj.omethod被包装为一个Reference:ref-2,它的base是obj对象,Type(ref-2.base)==Object
  3. ref-2提取方法,传入参数列表,并将ref-2的base作为this,调用方法

猜你在找的JavaScript相关文章