理解引用

Know thy reference

(原文:know thy reference - kangax)javascript

1、前言

翻譯好不是件容易的事兒,我儘可能講得通順,一些術語會保留原詞,翻出一篇拗口的文章實在是得不償失。前端

2、用大量的抽象來解釋"this"

以前的一個星期天的早上,我躺牀上看HackerNews,有一篇叫「This in JavaScript」的文章,我稍微掃了兩眼。不出意外,就是函數調用、方法調用、顯式綁定、構造函數實例化這檔子事。這篇文章特別長,我越看就越以爲,這一大堆的解釋和例子會給一個不瞭解this機制的人帶來多大的心理陰影啊。java

我想起來幾年前我第一次看DC的「JavaScript The Good Parts」,當時看完書裏的相關總結以後以爲無比清晰,書裏簡要地列出了這幾條:git

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.程序員

只由調用方式決定,並且只有四種狀況。看看,這說得多簡單。github

因而我去評論裏看有沒有人說 HackerNews 的這篇文章講得太複雜了。果真,不少人都搬出了「JavaScript The Good Parts」裏的總結,其中一我的提煉了一下:express

  • 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)app

  • A few functions change the behavior of this - bind,call and applyide

  • The 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的規範容易得多了。

1、關於引用

老實說,看到關於引用的討論那麼少我也多多少少能夠理解,畢竟這也並非語言自己的一部分。引用只是一種機制,用來描述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(){ ... }}

「好吧......我服了。」

2、理論

ECMAScript把引用定義成「resolved name binding」。這是由三個部分組成的抽象實體 - base, namestrict 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, namestrict三個屬性。下面舉兩個例子:

//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的指向有什麼用呢?

3、函數調用

看看函數調用的時候發生了什麼

  1. Let ref be the result of evaluating MemberExpression.

  2. Let func be GetValue(ref).

  3. Let argList be the result of evaluating Arguments, producing an internal list of argument values (see 11.2.4).

  4. If Type(func) is not Object, throw a TypeError exception.

  5. If IsCallable(func) is false, throw a TypeError exception.

  6. If Type(ref) is Reference, then

    1. If IsPropertyReference(ref) is true, then

      1. Let thisValue be GetBase(ref).

    2. Else, the base of ref is an Environment Record

      1. Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).

  7. Else, Type(ref) is not Reference.

    1. Let thisValue be undefined.

  8. 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四條裏面的一、2兩條:

//foo.bar()
- `foo.bar`是個屬性引用嗎?
- 是的
- 那取它的base,也就是`foo`做爲`this`吧

//foo()
- `foo`是個屬性引用嗎?
- 不是
- 那你的base就是undefined

//(function(){})()
- 什麼?你連引用都不是啊,那不用看了,undefined

4、賦值,逗號,圓括號運算符

有了前面的瞭解,咱們看看能不能解釋得了下面這幾個函數調用的this指向。

  • (f = foo.bar)()

  • (1, foo.bar)()

  • (foo.bar)()

從第一個賦值運算提及,括號裏是一個簡單賦值操做,若是咱們看看簡單賦值作了些什麼的話,咱們可能能夠看出點端倪:

  1. Let lref be the result of evaluating LeftHandSideExpression.

  2. Let rref be the result of evaluating AssignmentExpression.

  3. Let rval be GetValue(rref).

  4. 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"

  5. Call PutValue(lref, rval).

  6. Return rval.

注意到右邊的表達式在賦值以前經過內部的GetValue()求值。在咱們的例子裏面,foo.bar引用被轉換成了一個函數對象,那麼以非引用方式調用函數的話,this就指向了undefined。因此深刻剖析的話,比起來foo.bar(),(f = foo.bar)()其實更像是(function(){})()。就是說,它是個求過值的表達式,而不是一個擁有base的引用。

第二個逗號運算就相似了:

  1. Let lref be the result of evaluating Expression.

  2. Call GetValue(lref).

  3. Let rref be the result of evaluating AssignmentExpression.

  4. Return GetValue(rref).

經過了GetValue,引用轉換成了函數對象,this指向了undefined.

最後是圓括號運算符:

  1. Return the result of evaluating Expression. This may be of type Reference.

  2. 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.

5、typeof運算符

既然都聊到這兒了,乾脆聊一聊別的。看看typeof運算符的說法:

  1. Let val be the result of evaluating UnaryExpression.

  2. If Type(val) is Reference, then

    1. If IsUnresolvableReference(val) is true, return "undefined".

    2. Let val be GetValue(val).

  3. Return a String determined by Type(val) according to Table 20.

這也就是爲何咱們對一個沒法解析的引用使用typeof操做符的時候並不會報錯。可是若是不用typeof運算符,直接作一個聲明呢:

Expression Statement:

  1. Let exprRef be the result of evaluating Expression.

  2. Return (normal, GetValue(exprRef), empty).

GetValue():

  1. If Type(V) is not Reference, return V.

  2. Let base be the result of calling GetBase(V).

  3. If IsUnresolvableReference(V), throw a ReferenceError exception.

看到了吧,過不了GetValue這一關,因此說出現了無法解析的聲明直接就報錯了。

6、delete運算符

長話短說:

  • 若是不是個引用,返回true(delete 1,delete /x/)

  • 若是是無法解析的引用(delete iDontExist)

    • 嚴格模式,報錯

    • 不然返回true

  • 若是確實是個屬性引用,那就刪了它,返回true

  • 若是是全局對象做爲base的屬性

    • 嚴格模式,報錯

    • 不然,刪除,返回true

3、後記

這篇文章都是基於ES5的,ES2015可能會有些變化。

另外,結果我仍是翻出來一篇拗口的文章,Oops!

相關文章
相關標籤/搜索