Know thy reference
(原文:know thy reference - kangax)
一、前言
翻译好不是件容易的事儿,我尽量讲得通顺,一些术语会保留原词,翻出一篇拗口的文章实在是得不偿失。
二、用大量的抽象来解释"this"
之前的一个星期天的早上,我躺床上看HackerNews,有一篇叫「This in JavaScript」的文章,我稍微扫了两眼。不出意外,就是函数调用、方法调用、显式绑定、构造函数实例化这档子事。这篇文章特别长,我越看就越觉得,这一大堆的解释和例子会给一个不了解this
机制的人带来多大的心理阴影啊。
我想起来几年前我第一次看DC的「JavaScript The Good Parts」,当时看完书里的相关总结之后觉得无比清晰,书里简要地列出了这几条:
The
this
parameter is very important in object oriented programming, and its value is determined by the invocation pattern.There are four patterns of invocation in JavaScript:the method invocation pattern,the function invocation pattern,the constructor invocation pattern and the apply invocation pattern.The patterns differ in how the bonus parameter this is initialized.
只由调用方式决定,而且只有四种情况。看看,这说得多简单。
于是我去评论里看有没有人说 HackerNews 的这篇文章讲得太复杂了。果然,很多人都搬出了「JavaScript The Good Parts」里的总结,其中一个人提炼了一下:
The keyword
this
refers to whatever is left of the dot at call-time.If there's nothing to the left of the dot,then
this
refers to the root scope(e.g. Window)A few functions change the behavior of
this
- bind,call and applyThe keyword
new
binds this to the object just created
简直精辟。但是我注意到里面的一句话-"whatever is left of the dot at call-time"。乍一看很有道理嘛,比方说foo.bar()
,this
指向foo
;又比方说foo.bar.baz()
,this
指向foo.bar
。但是(f = foo.bar)()
呢?在这里所谓的「Whatever is left of the dot at call-time」就是foo
,那this
就指向foo
咯?
为了拯救前端程序员于水火之中,我留言说,所谓的「句号左边的东西」可能没这么简单,要真的理解this,
你可能需要理解引用和它的base values
。
也是这一次经历我才真的意识到引用的概念其实很少被提到。我去搜了一下"JavaScript reference",结果出来一些关于"pass-by-reference vs. pass-by-value"的讨论。不行,我得出来救场了。
这就是为什么我要来写这篇博客。
我会解释ECMAScript里面神秘的引用,一旦你理解了引用,你就会明白通过引用来了解this
的绑定是多么轻松,你也会发现读ECMAScript的规范容易得多了。
一、关于引用
老实说,看到关于引用的讨论那么少我也多多少少可以理解,毕竟这也并不是语言本身的一部分。引用只是一种机制,用来描述ECMAScript里的特定行为。它对于解释引擎的实现至关重要,但是它们在代码里是看不见摸不着的。
当然,理解它对于写代码完完全全是必要的。
回到我们之前的问题:
foo.bar()
(f = foo.bar)()
到底为什么第一个的this
指向foo
,而第二个指向全局对象呢?
你可能会说,“括号左边的表达式里面完成了一次对 f 的赋值,赋值完了之后就相当于调用 f() ,这样的话就是一次函数调用,而不是方法调用了。”
好的,那这个呢:
(1, foo.bar)()
“噢,这是个圆括号运算符嘛!它完成从左边到右边的求值,所以它肯定和 foo.bar() 是一样的,所以它的this
指向foo
。”
var foo = {
bar: function() {
'use strict'
return this
}
}
(1, foo.bar)() //undefined
“呃......真是奇怪啊”
那这个呢:
(foo.bar)()
“呃,考虑到上一个例子,肯定也是undefined
吧,应该是圆括号搞了什么鬼。”
(foo.bar)() //{bar: function(){ ... }}
“好吧......我服了。”
二、理论
ECMAScript把引用定义成「resolved name binding」。这是由三个部分组成的抽象实体 - base
, name
和strict flag
,第三个好懂,现在咱们聊前两个就够了。
创建引用有两种情况:
Identifier resolution
property access
比方说吧,foo
创建了一个引用,foo.bar
也创建了一个引用。而像1
, "foo"
, /x/
, { }
, [ 1,2,3 ]
这些字面量值和函数表达式(function(){})
就没有。
Example | Reference? | Notes |
---|---|---|
"foo" | NO | |
123 | NO | |
/x/ | NO | |
({}) | NO | |
(function(){}) | NO | |
foo | YES | Could be unresolved reference if foo is not defined |
foo.bar | YES | Property reference |
(123).toString | YES | Property reference |
(function(){}).toString | YES | Property reference |
(1, foo.bar)() | NO | Already evaluated, BUT see grouping operator exception |
(f = foo.bar)() | NO | Already evaluated, BUT see grouping operator exception |
(foo) | YES | Grouping operator does not evaluate reference |
(foo.bar) | YES | Grouping operator does not evaluate reference |
先别管后面四个,我们待会再看。
每次一个引用创建的时候,它的组成部分base
,name
,strict
都被赋上值。name
就是解析的标识符或者属性名,base
就是属性对象或者环境对象。
可能把引用理解成一个没有原型的JavaScript对象会比较好,它就只有base
, name
和strict
三个属性。下面举两个例子:
//when foo is defined earlier
foo
var Reference = {
base: Environment,
name: "foo",
strict: false
}
----------------
foo.bar
//这就是所谓的「Property Reference」
var Reference = {
base: foo,
name: "bar",
strict: false
}
还有第三种情况,即不可解析引用。如果在作用域里找不到标识符,引用的base
就会设为undefined
:
//when foo is not defined
foo
var Reference = {
base: undefined,
name: "foo",
strict: false
}
你肯定见过,解析不了的引用可能会导致引用错误-("foo is not defined").
本质上来说,引用就是一种代表名称绑定的简单机制,它把对象的属性解析和变量解析抽象出一个类似对象的数据结构:
var reference = {
base: Object or Environment,
name: name
}
现在我们知道ECMAScript底层做了什么了,但是这对解释this
的指向有什么用呢?
三、函数调用
看看函数调用的时候发生了什么:
Let ref be the result of evaluating MemberExpression.
Let func be GetValue(ref).
Let argList be the result of evaluating Arguments, producing an internal list of argument values (see 11.2.4).
If Type(func) is not Object, throw a TypeError exception.
If IsCallable(func) is false, throw a TypeError exception.
-
If Type(ref) is Reference, then
-
If IsPropertyReference(ref) is true, then
Let thisValue be GetBase(ref).
-
Else, the base of ref is an Environment Record
Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).
-
-
Else, Type(ref) is not Reference.
Let thisValue be undefined.
Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values.
加粗的第六步基本上就解释了DC四条里面的1、2两条:
//foo.bar()
- `foo.bar`是个属性引用吗?
- 是的
- 那取它的base,也就是`foo`作为`this`吧
//foo()
- `foo`是个属性引用吗?
- 不是
- 那你的base就是undefined
//(function(){})()
- 什么?你连引用都不是啊,那不用看了,undefined
四、赋值,逗号,圆括号运算符
有了前面的了解,我们看看能不能解释得了下面这几个函数调用的this
指向。
(f = foo.bar)()
(1, foo.bar)()
(foo.bar)()
从第一个赋值运算说起,括号里是一个简单赋值操作,如果我们看看简单赋值做了些什么的话,我们可能可以看出点端倪:
Let lref be the result of evaluating LeftHandSideExpression.
Let rref be the result of evaluating AssignmentExpression.
Let rval be GetValue(rref).
-
Throw a SyntaxError exception if the following conditions are all true:
Type(lref) is Reference is true
IsStrictReference(lref) is true
Type(GetBase(lref)) is Environment Record
GetReferencedName(lref) is either "eval" or "arguments"
Call PutValue(lref, rval).
Return rval.
注意到右边的表达式在赋值之前通过内部的GetValue()
求值。在我们的例子里面,foo.bar
引用被转换成了一个函数对象,那么以非引用方式调用函数的话,this
就指向了undefined
。所以深入剖析的话,比起来foo.bar()
,(f = foo.bar)()
其实更像是(function(){})()
。就是说,它是个求过值的表达式,而不是一个拥有base
的引用。
第二个逗号运算就类似了:
Let lref be the result of evaluating Expression.
Call GetValue(lref).
Let rref be the result of evaluating AssignmentExpression.
Return GetValue(rref).
通过了GetValue
,引用转换成了函数对象,this
指向了undefined
.
最后是圆括号运算符:
Return the result of evaluating Expression. This may be of type Reference.
NOTE This algorithm does not apply GetValue to the result of evaluating Expression. The principal motivation for this is so that operators such as delete and typeof may be applied to parenthesised expressions.
那很明白了,圆括号运算符没有对引用进行转换,这也就是为什么它的this
指向了foo
.
五、typeof运算符
既然都聊到这儿了,干脆聊一聊别的。看看typeof运算符的说法:
Let val be the result of evaluating UnaryExpression.
-
If Type(val) is Reference, then
If IsUnresolvableReference(val) is true, return "undefined".
Let val be GetValue(val).
Return a String determined by Type(val) according to Table 20.
这也就是为什么我们对一个无法解析的引用使用typeof
操作符的时候并不会报错。但是如果不用typeof
运算符,直接做一个声明呢:
Expression Statement:
Let exprRef be the result of evaluating Expression.
Return (normal, GetValue(exprRef), empty).
GetValue():
If Type(V) is not Reference, return V.
Let base be the result of calling GetBase(V).
If IsUnresolvableReference(V), throw a ReferenceError exception.
看到了吧,过不了GetValue
这一关,所以说出现了没法解析的声明直接就报错了。
六、delete运算符
长话短说:
如果不是个引用,返回
true
(delete 1,delete /x/)-
如果是没法解析的引用(delete iDontExist)
严格模式,报错
否则返回
true
如果确实是个属性引用,那就删了它,返回
true
-
如果是全局对象作为
base
的属性严格模式,报错
否则,删除,返回
true
三、后记
这篇文章都是基于ES5的,ES2015可能会有些变化。
另外,结果我还是翻出来一篇拗口的文章,Oops!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。