看了冴羽的文章“JavaScript深入之从ECMAScript规范解读this”,决定读一读this
的规范定义,梳理一下this。
this存在的场景
this
存在的场景肯定是函数执行的时候,那么有以下几种调用函数的方式:
- 直接调用:
foo()
,foo
中this
的值将会是window
或undeifined
,取决于是否是严格模式。 - 作为对象的方法调用:
obj.foo()
,this
的值将是obj
。 - 作为
new
操作符的一部分调用:new foo()
。 - 通过
apply
、call
、bind
调用:this
的值将被绑定为指定值。
然而,有特殊情况的存在:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1
所以,有必要从规范解析this
到底是如何被指定的。
函数被调用时的this
直接看规范12.3.4 Function Calls 的过程:
12.3.4.1 Runtime Semantics: Evaluation
CallExpression : CallExpression Arguments
…
Let ref be the result of evaluating CallExpression.
Let func be ? GetValue(ref).
…
Return ? EvaluateCall(func, ref, Arguments, tailCall).
12.3.4.2.EvaluateCall ( func, ref, arguments, tailPosition )
If Type(ref) is Reference, then
a. If IsPropertyReference(ref) is true, then
i. Let thisValue be GetThisValue(ref).
b. Else the base of ref is an Environment Record,
i. Let refEnv be GetBase(ref).
ii. Let thisValue be refEnv.WithBaseObject().Else Type(ref) is not Reference,
a. Let thisValue be undefined.……
可以看到EvaluateCall
根据ref
确定了this
的值。
总结一下:
- 计算
CallExpression
的结果赋给ref
- 若
Type(ref)
为reference
则:- 若
IsPropertyReference(ref)
为true
,则:- 令
this
为GetThisValue(ref)
的值
- 令
- 否则令
ref
的base
为EnvironmentRecord
- 令
this
为GetBase(ref).WithBaseObject()
- 令
- 若
- 否则令
this
为undefined
所以,关键在于:
如何根据
CallExpression
计算ref
,ref
的类型很重要,如果ref
的类型不是reference
,那么this
直接为undefined
。- 如果
ref
的类型是reference
,那么需要知道IsPropertyReference(ref)
、GetThisValue(ref)
、GetBase(ref)
分别干了什么才能确定this
。
- 如果
CallExpression
第一步计算CallExpression
赋给ref
Let ref be the result of evaluating CallExpression.
在12.3 Left-Hand-Side Expressions中,MemberExpression包含了以下几种类型:
- PrimaryExpression:原始表达式,如:1.1、undefined、变量foo
- MemberExpression[ Expression]:属性访问表达式
- MemberExpression . IdentifierName:属性访问表达式
- MemberExpression TemplateLitera
- SuperProperty:super属性表达式
- MetaProperty
- new MemberExpression Arguments:new表达式
比如foo.bar就是一个MemberExpression。
函数调用涉及到了CallExpression:
- CoverCallExpressionAndAsyncArrowHead
- SuperCall
- CallExpression Arguments:函数调用表达式
- CallExpression [ Expression ]:函数
- CallExpression . IdentifierName:函数表达式的属性访问
CallExpression TemplateLiteral:略
比如foo.bar()
中的foo.bar
是CallExpression。astexplorer将foo
描述为标识符(identifier)。
为什么foo.bar是MemberExpression,又是CallExpression呢?
答案是:看整体foo.bar()
,由两部分组成:CallExpression和Arguments,左边的foo.bar是CallExpression,右边的()
是Arguments;而foo.bar这个CallExpression可以细分成MemberExpression . IdentifierName,foo是MemberExpression,bar是IdentifierName,中间.
是属性访问符。
所以,当我们调用foo.bar
,计算CallExpression
赋给ref
时,ref
便为foo.bar这个CallExpression
的计算结果。
Type
第二步判断ref
是否为reference
。
Type(ref)
用于获取ref
的类型。
规范将ECMAScript的类型分为了:语言类型(language types)和规范类型(specification types)。
- 语言类型就是描述变量、供开发者直接操作、开发JS的,包括:
Undefined, Null, Boolean, String, Symbol, Number,
和Object
。 - 规范类型用于在算法层面描述JS语言的结构以及语言类型的,开发者无法接触,包括:
Reference, List, Completion, Property Descriptor, Lexical Environment, Environment Record,
和Data Block.
。这个Reference
便和this
有极大关联。
那么Reference
类型是:
A Reference is a resolved name or property binding.
The Reference type is used to explain the behaviour of such operators as delete, typeof, the assignment operators,
the super keyword and other language features.For example, the left-hand operand of an assignment is expected to
produce a reference.
翻译:Reference
是一个可解析的名称,如foo;或是属性绑定,这里的属性便是对象的属性,如foo.bar,Reference
提供了描述这个名称或属性的信息。Reference
类型是用来解释delete
、typeof
、assignment operators
、关键字super
以及其他语言的行为的,举个栗子:赋值符号的左操作数便被视为产生了一个Reference
。
Reference
由三部分组成:base value
、referenced name
、strict reference
。
其中这三部分的取值:
base value
可以是语言类型(String
、Object
等等)或Environment Record
。referenced name
可以是String
或Symbol
值。strict reference
可以是true
或false
。
而GetBase( V )
、GetReferencedName ( V )
、IsStrictReference( V )
便用来分别获取reference
的base value
、referenced name
和strict reference
。
举个reference
的栗子:
1 | // 简单来讲,这个赋值操作会经历这个过程:...获得lref: GetReferencedName(foo) 获得rval:GetValue(1) => 对lref进行赋值PutValue(lref,rval)... |
IsPropertyReference( V )
用于根据GetBase( V )
是否为对象或基本类型的对象(Boolean, String, Symbol, or Number
简单来说就是对象)返回true
或false
,简述为如果base value是对象,则true,或者说如果这个reference是属性绑定如foo.bar,则是true。
EnviromentRecord
可以粗略地理解为本地作用域的记录,记录了标识符的映射(@待勘误):
An Environment Record records the identifier bindings that are created within the scope of its associated Lexical
Environment.
所以,reference
除了用于描述那些可解析的名称,如变量、函数等等之外,还可能用于描述属性。如果是属性,那么GetBase(ref)
有可能是一个对象,否则是EnviromentRecord。
GetValue
GetValue( V )
用于根据GetBase( V )
的值获取V
的真正的值(V是一个reference),它返回的始终是一个语言类型的值,而不是规范类型的值,如:
1 | let foo = 1; |
计算过程如下:
- ReturnIfAbrupt(V).
- If Type(V) is not Reference, return V.
- Let base be GetBase(V).
- If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
- If IsPropertyReference(V) is true, then
a. If HasPrimitiveBase(V) is true, then
i. Assert: In this case, base will never be undefined or null.
ii. Set base to ! ToObject(base).
b. Return ? base.[[Get]](GetReferencedName(V), GetThisValue(V)).- Else base must be an Environment Record,
a. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V)) (see 8.1.1).
确定this
回到代码中:
1 | var value = 1; |
确定this的过程:
计算
CallExpression
赋给ref
若
Type(ref)
为reference
则:2.1 若
IsPropertyReference(ref)
为true
,则:2.2令
this
为GetThisValue(ref)
的值2.3否则令
ref
的base
为EnvironmentRecord
2.4令
this
为GetBase(ref).WithBaseObject()
否则令
this
为undefined
第二步Type(ref)
则能判定为reference
了,且IsPropertyReference(ref)
为true,所以计算GetThisValue(ref)
赋值给this
。
GetThisValue的过程:
6.2.4.10 GetThisValue ( V )
- Assert: IsPropertyReference(V) is true.
- If IsSuperReference(V) is true, then
a. Return the value of the thisValue component of the reference V.- Return GetBase(V).
简单理解为如果不是Super调用,则返回GetBase(ref)
。
示例1:foo.bar
1 | //示例1 |
前面说到:foo.bar()的形式中,ref
是foo.bar这个CallExpression的计算结果。
这里根据.
属性访问符计算结果,所以查看property accessorsd的计算过程,直接看最后一步:
MemberExpression : MemberExpression . IdentifierName // foo.bar foo:MemberExpression bar:IdentifierName
Return a value of type Reference whose base value component is bv, whose referenced name component is
propertyNameString, and whose strict reference flag is strict.
.
运算符返回了一个reference,这个reference形如:
1 | foo.barReference: { |
所以Type(ref)
为true,继续执行下一步判断:
2.1 若
IsPropertyReference(ref)
为true
ref的baseValue为foo,是对象,所以这一步为true,继续下一步:
2.2 令
this
为GetThisValue(ref)
的值
上面说了,GetThisValue(ref)
在这里相当于GetBase(ref)
,而ref的baseValue为foo,所以this为foo。
示例2:(foo.bar)
(foo.bar)是CallExpression,ref
是(foo.bar)这个CallExpression的计算结果。
这里根据()
和.
运算符计算CallExpression,看12.2.10 The Grouping Operator的计算过程:
ParenthesizedExpression : ( Expression )
// (foo.bar) foo.bar: Expression: MemberExpression . IdentifierName
- Return the result of evaluating Expression. This may be of type Reference.
也就是返回了foo.bar的计算结果,那么结果和示例1一样,this为foo。
示例3:(foo.bar = foo.bar)
1 | //示例3 |
(foo.bar = foo.bar)是CallExpression,ref
是(foo.bar = foo.bar)这个CallExpression的计算结果。
根据=
和()
操作符计算CallExpression,所以这里只看=
操作符返回的结果:
12.15.4 Runtime Semantics: Evaluation
AssignmentExpression : LeftHandSideExpression = AssignmentExpression
….
i. Let rref be the result of evaluating AssignmentExpression.
ii. Let rval be ? GetValue(rref).
e. Perform ? PutValue(lref, rval).
f. Return rval.
….
最终返回了GetValue(rref),我们不管它确切返回了什么值(结果应该是function iamfunc),重点是GetValue(rref)返回了一个语言类型的值,不是reference,那么第二步:
2.
Type(ref)
为reference
,则:
肯定是false的,所以执行下一步:
否则令
this
为undefined
所以this为undefined(非严格模式下this是window)。
示例4:(false || foo.bar)
1 | //示例4 |
(false || foo.bar)是CallExpression,ref
是(false || foo.bar)这个CallExpression的计算结果。
根据||
和()
操作符计算CallExpression,所以这里只看||
操作符返回的结果:
LogicalORExpression : LogicalORExpression || LogicalANDExpression
- Let lref be the result of evaluating LogicalORExpression.
- Let lval be ? GetValue(lref).
- Let lbool be ToBoolean(lval).
- If lbool is true, return lval.
- Let rref be the result of evaluating LogicalANDExpression.
- Return ? GetValue(rref).
最终返回了GetValue(rref)=>function iamfunc,是一个语言类型的值,所以Type(ref)为false,this是undefined。
示例5:(foo.bar, foo.bar)
1 | //示例5 |
(foo.bar, foo.bar)是CallExpression,ref
是(foo.bar, foo.bar)这个CallExpression的计算结果。
根据,
和()
操作符计算CallExpression,所以这里只看||
操作符返回的结果:
Expression : Expression , AssignmentExpression
- Let lref be the result of evaluating Expression.
- Perform ? GetValue(lref).
- Let rref be the result of evaluating AssignmentExpression.
- Return ? GetValue(rref).
最终返回了GetValue(rref)=>function iamfunc,是一个语言类型的值,所以Type(ref)为false,this是undefined。
之前错误地以为(foo.bar, foo.bar)会返回foo.bar,其实不是,返回的是GetValue(rref of foo.bar)=>function iamfunc,只是返回了一个语言类型的值。
总结
至此,归纳一下确定this的值的重点。
在函数调用的地方,有CallExpression arguments的形式
首先计算CallExpression的值赋给ref,计算CallExpression就是确定CallExpression的返回值:
.
操作符的返回值是reference,
、||
、=
等操作符会返回GetValue(v),是语言类型值,非reference
返回值赋值给ref,如果ref非reference,那么this直接为undefined;如果是reference:
IsPropertyReference
判定ref是否是属性绑定,若是,this则为GetThisValue(ref)
,即GetBase(ref)
,为ref的baseValue,如bar.foo中,ref的baseValue为foo。- 若不是属性绑定,ref就是一个可解析名称,那么this肯定就为
with object
和undefined其中一者。
- 若不是属性绑定,ref就是一个可解析名称,那么this肯定就为
画一张图做总结:
参考
- “JavaScript深入之从ECMAScript规范解读this”
- ECMA-262 edition 10th