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

毛爺爺我男神不想說話發表於2019-01-02

1.拋磚引玉

宣告:本人看了冴羽一篇文章後,感覺那邊文章寫的很好,但是不夠詳細,所以在這裡豐富了一下,然後也希望分享給更多的人,一起學習

javascript 的this這個關鍵字我相信有很多人會像我一樣對他既陌生有熟悉。相信每一個學習JavaScript的前端工程師都做過這麼一個事那就是到某個搜尋引擎 key in ( javscript this),然後看過幾個部落格後感覺自己懂了this!

來來讓我們看看大部分部落格會怎麼寫:

  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的指代。

PS.在正式開始之前,本人先宣告以下的文章會囉嗦一點,如果你還不是很懂this,希望你有時間去耐心的讀下去,希望也可以給你一種新的認識。

2.this的前世今生

在深入瞭解 JavaScript 中的 this 關鍵字之前,有必要先退一步,看一下為什麼 this 關鍵字很重要。this 允許複用函式時使用不同的上下文。換句話說,“this” 關鍵字允許在呼叫函式或方法時決定哪個物件應該是焦點。

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的指向。

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了!!

那麼根據規範我們就要先去找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

引用

相關文章