從冴羽的《JavaScript深刻之從ECMAScript規範解讀this》引發的思考

1.拋磚引玉

聲明:本人看了冴羽一篇文章後,感受那邊文章寫的很好,可是不夠詳細,因此在這裏豐富了一下,而後也但願分享給更多的人,一塊兒學習javascript

javascript 的this這個關鍵字我相信有不少人會像我同樣對他既陌生有熟悉。相信每個學習JavaScript的前端工程師都作過這麼一個事那就是到某個搜索引擎 key in ( javscript this),而後看過幾個博客後感受本身懂了this!html

來來讓咱們看看大部分博客會怎麼寫:前端

  1. MDN
  2. Javascript 的 this 用法--阮一峯
  3. [譯] this(他喵的)究竟是什麼 — 理解 JavaScript 中的 this、call、apply 和 bind -- 掘金
  4. The JavaScript this Keyword
    .....
    此外不就不列舉了。

反正基本上都第一步,先告訴你一個事實『this 是在函數被調用時發生的綁定,指向上下文對象,即被調用函數所處的環境,也就是說,this 在函數內部指向了調用函數的對象。』;第二步,而後要不經過綁定方式的角度給你從new 綁定,顯示綁定等多個角度告訴你this是誰,或者從函數調用狀況,構造函數狀況,bind調用等多個狀況給你分析一波。而後不知道你會不會想我同樣深深的記住調用對象是誰,this就是誰,而後把不太明白的特殊狀況緊緊的記下,作個筆記什麼的。而後以爲我終於弄明白this了,而後在真正使用的時候偶爾還會發生,怎麼又忘記這種狀況了!!!納尼?what?臉好疼有沒有?
而後在去看網上的博客,看看有沒有這種狀況的解釋,看了冴羽大大的這篇《JavaScript深刻之從ECMAScript規範解讀this》文章後我以爲我能夠從一個新的角度再去學習一下,讓咱們能夠更深一步的去理解this,甚至找出一種方法,找this的時候經過「公式」去找到this的指代。java

PS.在正式開始以前,本人先聲明如下的文章會囉嗦一點,若是你還不是很懂this,但願你有時間去耐心的讀下去,但願也能夠給你一種新的認識。git

2.this的前世此生

在深刻了解 JavaScript 中的 this 關鍵字以前,有必要先退一步,看一下爲何 this 關鍵字很重要。this 容許複用函數時使用不一樣的上下文。換句話說,「this」 關鍵字容許在調用函數或方法時決定哪一個對象應該是焦點。github

2.1 this 是在函數被調用時發生的綁定,那麼函數被調用的時候,JavaScript引擎都幹了啥?

【ECMAScript規範 10.4.3節 進入函數代碼】是這麼解釋的: 當控制流根據一個函數對象 F、調用者提供的 thisArg 以及調用者提供的 argumentList,進入 函數代碼 的執行環境時,執行如下步驟:面試

  1. 若是函數代碼是嚴格模式下的代碼 ,設 this 綁定爲 thisArg(調用者)。(嚴格等於調用者)
  2. 若是不是嚴格模式下的代碼, 斷定thisArg 是否是 null 或 undefined,是則設 this 綁定爲 全局對象 。
  3. 不然若是 Type(thisArg) 的結果不爲 Object,則設 this 綁定爲 ToObject(thisArg)。
  4. 不然設 this 綁定爲 thisArg。
  5. 以 F 的 [[Scope]] 內部屬性爲參數調用 NewDeclarativeEnvironment,並令 localEnv 爲調用的結果。
  6. 設詞法環境爲 localEnv。
  7. 設變量環境爲 localEnv。
  8. 令 code 爲 F 的 [[Code]] 內部屬性的值。
  9. 10.5 描述的方案,使用 函數代碼 code 和 argumentList 執行定義綁定初始化步驟。

由於咱們是研究this,因此咱們着重關注前4條就能夠了,簡單總結一下就是:當調用函數的時候會建立函數執行上下文,在建立的過程當中有一步就是建立this ,並根據是否在嚴格模式下,調用者是否是NUll或者undefined,調用者是否是對象決定this的指向。bash

2.2 咱們搞清楚在調用的時候的this的指向規則,裏面有個很扎眼的詞 thisArg,它是什麼鬼?

那咱們來看看【ECMAScript規範 11.2.3節 函數調用】前端工程師

  1. 令 ref 爲解釋執行 MemberExpression 的結果 .
  2. 令 func 爲 GetValue(ref).
  3. 令 argList 爲解釋執行 Arguments 的結果 , 產生參數值們的內部列表 (see 11.2.4).
  4. 若是 Type(func) is not Object ,拋出一個 TypeError 異常 .
  5. 若是 IsCallable(func) is false ,拋出一個 TypeError 異常 .
  6. 若是 Type(ref) 爲 Reference,那麼 若是 IsPropertyReference(ref) 爲 true,那麼 令 thisValue 爲 GetBase(ref). 若是 Type(ref) 不是 Reference , ref 的基值是一個環境記錄項,令 thisValue 爲調用 GetBase(ref) 的 ImplicitThisValue 具體方法的結果
  7. 不然 , 假如 Type(ref) 不是 Reference. 令 thisValue 爲 undefined.
  8. 返回調用 func 的 [[Call]] 內置方法的結果 , 傳入 thisValue 做爲 this 值和列表 argList 做爲參數列表

這裏的thisValue的值其實就是咱們要找的thisArg了!!app

那麼根據規範咱們就要先去找MemberExpression 的結果是什麼了?

那咱們看一看MemberExpression的語法

  • PrimaryExpression // 原始表達式 能夠參見《JavaScript權威指南第四章》
  • FunctionExpression // 函數定義表達式
  • MemberExpression [ Expression ] // 屬性訪問表達式
  • MemberExpression . IdentifierName // 屬性訪問表達式
  • new MemberExpression Arguments // 對象建立表達式

原來MemberExpression 的結果就是:執行函數名部分表達式的結果(簡單理解 MemberExpression 其實就是()左邊的部分表達式的結果)。

下面咱們要看看Reference是什麼了?

Reference type:按字面翻譯就是引用類型,可是它並非咱們常說的JavaScript中的引用類型,它是一個規範類型(實際並不存在),也就是說是爲了解釋規範某些行爲而存在的,好比delete、typeof、賦值語句等。規範類型設計用於解析命名綁定的(A Reference is a resolved name binding.),它由三部分組成:

  • 基 (base) 值,
  • 引用名稱(referenced name)
  • 布爾值 嚴格引用 (strict reference) 標誌。

基值就是屬性所在的對象或者就是 EnvironmentRecord,基值是 undefined, 一個 Object, 一個 Boolean, 一個 String, 一個 Number, 一個 environment record 中的任意一個。基值是 undefined 表示此引用能夠不解決一個名字的綁定(A base value of undefined indicates that the reference could not be resolved to a binding.)。

PS.最後一句話個人理解是這個引用不須要一個名字和他關聯起來,好比咱們建立的匿名函數,他的base應該就是undefined。

舉個栗子🌰:

var foo = 1;

// 對應的Reference是:
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

var foo = {
    bar: function () {
        return this;
    }
};
 
foo.bar(); // foo

// bar對應的Reference是:
var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};
複製代碼

IsPropertyReference(ref),GetBase(ref)和ImplicitThisValue()是什麼?

  1. IsPropertyReference(V)。 若是引用的基值是個對象或 HasPrimitiveBase(V) 是 true,那麼返回 true;不然返回 false。(Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.)
  2. GetBase(V)。 返回引用值 V 的基值組件。(Returns the base value component of the reference V.)
  3. ImplicitThisValue()。 環境記錄分爲聲明式環境記錄和對象式環境記錄,不一樣的略有不一樣。
    • 聲明式環境記錄項永遠將 undefined 做爲其 ImplicitThisValue 返回。
    • 對象式環境記錄項的 ImplicitThisValue 一般返回 undefined,除非其 provideThis 標識的值爲 true。
      • 令 envRec 爲函數調用時對應的聲明式環境記錄項。
      • 若是 envRec 的 provideThis 標識的值爲 true,返回 envRec 的綁定對象。
      • 不然返回 undefined。

2.3 this指代計算僞代碼

把2.1 和2.2的知識進行總結能夠寫出以下僞代碼:

FUNCTION.this = {
    ref =  MemberExpression 的結果;                        //'()'左邊的表達式的結果 
    IF Type(ref) == Reference THEN                        //若是左邊的表達式的結果是引用
        IF IsPropertyReference(ref) THEN                    //斷定引用的基值是否是一個對象
            thisArg = ref.base                              //若是是,thisArg是這個引用的基值
        ELSE                
            thisArg = ref.base.ImplicitThisValue()        //若是引用的基值不是對象,說明是個環境記錄,thisArg是
                                                          //它的ImplicitThisValue,絕大部分狀況下是Undefined
    ELSE
        thisArg = undefined;                            //若是左邊的表達式的結果不是引用,thisArg是undefined
    
   IF thisArg == undefined                          
        IF 'using strict' THEN                      //thisArg若是是undefined
            return   undefined                       //在嚴格模式下,函數的this就是undefined
        ELSE
            return   全局對象                       //在嚴格模式下,函數的this就是undefined
    ELSE
        return  thisArg                             //thisArg不是undefined,函數的this就是這個基值對象
} 
複製代碼

3.複習鞏固

舉幾個栗子🌰:

var value = 1;

function foo() {
    console.log(this.value)
}

var fooObj = {
  value: 2,
  bar: function () {
    return this.value;
  },
  fooIn:{
      value:3,
      foo:foo
  }
}
//示例1
console.log(foo());
//示例2
console.log(fooObj.bar());
//示例3
console.log((fooObj.bar)());
//示例4
console.log((fooObj.bar = fooObj.bar)());
//示例5
console.log((false || fooObj.bar)());
//示例6
console.log((fooObj.bar, fooObj.bar)());
//示例7
console.log(fooObj.fooIn.foo());
//輸出結果是:1 2 2 1 1 1 3
複製代碼
  1. foo() 的MemberExpression 是 foo,他的base是聲明式環境記錄,聲明式環境記錄的隱性的this值(ImplicitThisValue)是undefined ,在非嚴格模式下因此this指向window。

  2. fooObj.bar()的MemberExpression 是fooObj.bar是個引用,他的base 值是 fooObj,因此this指向 fooObj。

  3. (fooObj.bar)()的MemberExpression 是(fooObj.bar) 一個分組表達式,查看11.1.6分組表達式的規範:

    The production PrimaryExpression : ( Expression ) is evaluated as follows: Return the result of evaluating Expression. This may be of type Reference

    咱們要這個表達式的結果,結果是:fooObj.bar,因此和2是同樣的。

  4. (fooObj.bar = fooObj.bar)()的MemberExpression 是(fooObj.bar = fooObj.bar) 咱們分組表達式的返回值是 fooObj.bar = fooObj.bar,咱們就要計算這個表達式的結果,查看11.13.1簡單賦值的規範:

    令 lref 爲解釋執行 LeftH 和 SideExpression 的結果 .
    令 rref 爲解釋執行 AssignmentExpression 的結果 .
    令 rval 爲 GetValue(rref).
    拋出一個 SyntaxError 異常,當如下條件都成立 : Type(lref) 爲 Reference IsStrictReference(lref) 爲 true Type(GetBase(lref)) 爲環境記錄項 GetReferencedName(lref) 爲 "eval" 或 "arguments"
    調用 PutValue(lref, rval).
    返回 rval.

    由藍色部分能夠知道(fooObj.bar = fooObj.bar) 不是一個Reference,因此thisArg是一個undefined,非嚴格模式是window.

  5. (false || fooObj.bar)()的MemberExpression 是(false || fooObj.bar) ,類比4查看 11.11 二元邏輯運算符 ,咱們知道(false || fooObj.bar) 是個值不是Reference因此this也是指向window

  6. (fooObj.bar, fooObj.bar)()的MemberExpression 是(false || fooObj.bar) ,類比4查看11.14 逗號運算符,,咱們知道(false || fooObj.bar) 是個值不是Reference因此this也是指向window。

  7. fooObj.fooIn.foo()的MemberExpression 是fooObj.fooIn.foo,他是個Reference,基值是fooObj.fooIn,因此this指向fooObj.fooIn

總結:

看了本篇文章後應該能夠解決絕大多數的this的指向問題,還有傳說中的 構造函數調用模式 , 箭頭函數調用模式call、apply、bind 調用模式,你們應該也是均可以經過規範解釋的清楚,在這裏再也不一一列舉。

問題

var name = "The Window";
  var object = {
    name : "My Object",

    getNameFunc : function(){
      return function(){
        return this.name;
      };

    }
  };
  alert(object.getNameFunc()());//The Window

複製代碼

這個我沒有在規範裏面找到,因此只能當個問題給你們寫出來,若是寫的不對,還但願有大神在評論中指出來。
object.getNameFunc()()的MemberExpression 是object.getNameFunc(),這個函數的運行結果是return的匿名函數的指針,是個Reference可是它的base不是對象,因此是一個環境記錄項,沒有被bind,this,call改變因此ImplicitThisValue()爲undefined 因此非嚴格模式下就是window

引用

相關文章
相關標籤/搜索