【2】this

this

說到this,需要明確三方面內容:

  • this何時被賦值
  • this被賦了什麼值
  • 內置函數如何使用this的

this何時被賦值

進入函數代碼

當控制流根據一個函數對象 F、調用者提供的 thisArg 以及調用者提供的 argumentList,進入函數代碼的執行環境時,執行以下步驟:

  1. 如果函數代碼是嚴格模式下的代碼,設 this 綁定 爲 thisArg。
  2. 否則如果 thisArg 是 null 或 undefined ,則設 this 綁定爲全局對象。
  3. ……
以上信息來源:進入函數代碼

從上訴信息中可以知道

  • this 與調用者提供的 thisArg 密切相關
  • this 在嚴格模式下爲 thisArg
  • this 在非嚴格模式下爲 thisArg 或 全局對象

那麼 thisArg 又是怎麼來的呢?下面來看下函數調用過程:

函數調用

  1. 令 ref 爲解釋執行 MemberExpression 的結果。
  2. 令 func 爲 GetValue(ref)。
  3. 令 argList 爲解釋執行 Arguments 的結果,產生參數值們的內部列表(參見 11.2.4)。
  4. 如果 Type(func) 不是 Object,拋出一個 TypeError 異常。
  5. 如果 IsCallable(func) 爲 false,拋出一個 TypeError 異常。
  6. 如果 Type(ref) 爲 Reference,那麼

    • 如果 IsPropertyReference(ref) 爲 true,

      • 那麼令 thisValue 爲 GetBase(ref)。
    • 否則,ref 的基值是一個環境記錄項。

      • 令 thisValue 爲調用 GetBase(ref) 的 ImplicitThisValue 具體方法的結果。
  7. 否則,Type(ref) 不是 Reference。

    • 令 thisValue 爲 undefined。
  8. 返回調用 func 的 [[Call]] 內置方法的結果,傳入 thisValue 作爲 this 值和列表 argList 作爲參數列表。
以上信息來源:函數調用

從上訴信息中可以知道

  • thisArg 即 thisValue
  • thisValue 與 ref 的類型密切相關
  • 如果 ref 的類型是 Reference(引用規範類型

    • 如果 ref 是屬性引用,通過 GetBase(ref)(返回引用值ref的基值部分) 獲取 thisValue
    • 否則,通過 ImplicitThisValue 方法獲取 thisValue
  • 否則,thisValue 爲 undefined

那麼,瞭解到這裏可能有許多新的疑問,比如:

  • Reference 是怎樣的類型
  • ref 是怎麼來的
  • ref 什麼時候是 Reference,什麼時候不是。
  • GetBase(ref) 和 ImplicitThisValue 是如何產生結果的

Reference 是怎樣的類型

首先先解釋下 Reference。

其實ES中的類型分爲ECMAScript語言類型規範類型

ECMAScript語言類型對應的是程序員使用 ECMAScript 語言直接操作的值,如 Undefined、Null、Boolean、String、Number、Object等。
規範類型可用來描述 ECMAScript 表達式運算的中間結果,但這樣的值不能儲存爲對象的屬性或 ECMAScript 語言的變量值。引用尤雨溪的解釋:

這裏的 Reference 是一個 Specification Type,也就是 “只存在於規範裏的抽象類型”。它們是爲了更好地描述語言的底層行爲邏輯才存在的,但並不存在於實際的 js 代碼中。

ref 是怎麼來的

從上訴函數調用中可以知道,ref 是解釋執行 MemberExpression 的結果。
下面詳細看下 MemberExpression 的解析過程:

產生式 CallExpression : MemberExpression Arguments 按照下面的過程執行 :

  1. 令 baseReference 爲解釋執行 MemberExpression 的結果。
  2. 令 baseValue 爲 GetValue(baseReference)。
  3. 令 propertyNameReference 爲解釋執行 Expression 的結果。
  4. 令 propertyNameValue 爲 GetValue(propertyNameReference)。
  5. 調用 CheckObjectCoercible(baseValue)。
  6. 令 propertyNameString 爲 ToString(propertyNameValue)。
  7. 如果正在執行中的語法產生式包含在嚴格模式代碼當中,令 strict 爲 true,否則令 strict 爲 false。
  8. 返回一個值類型的引用,其基值爲 baseValue 且其引用名爲 propertyNameString,嚴格模式標記爲 strict。

從上訴信息中分析可以知道

  • 解釋執行 MemberExpression 的結果是一個引用規範類型(Reference)
  • 這個引用規範類型包含三部分信息:

    • baseValue
    • propertyNameString
    • strict
  • thisValue 的值取決於 baseValue
  • baseValue 是調用 GetValue 獲得的。
  • GetValue 得到的基值是 undefined、Object、Boolean、String、Number、環境記錄項中的任意一個(詳見:引用規範類型),而不是引用規範類型。
GetValue 詳細過程見:GetValue

ref 什麼時候是 Reference,什麼時候不是 Reference

一般來說,ref 是MemberExpression解析的結果,都將是 Reference。
但是,如果 MemberExpression 是其函數表達式的一部分,則可能將改變最終解析結果的類型。
而改變解析結果類型的主要原因取決於是否調用了 GetValue 方法,如果調用了 GetValue ,函數中間值 ref 將是 Object 類型。

那麼哪些表達式不使用 GetValue 呢?

更多表達式詳見:表達式

GetBase(ref) 和 ImplicitThisValue 是如何產生結果的

  • GetBase:返回引用值ref的基值部分
  • ImplicitThisValue : 聲明式環境記錄項永遠將 undefined 作爲其 ImplicitThisValue 返回。

this被賦了什麼值

其實在 this何時被賦值 部分已經介紹了 this被賦了什麼值。下面總結三種賦值過程:

第一種this賦值過程:

var v = 1; 
var obj = {
    v: 2,
    fn: function(){
        console.log(this.v);
    }
}
obj.fn(); // 2
(obj.fn)(); // 2
  1. 調用表達式解析 obj.fn ,返回引用規範類型,baseValue 爲 obj。
  2. 函數調用,由於 1 過程返回的爲引用規範類型,且爲屬性引用,調用 GetBase 將 baseValue (obj) 作爲返回值,返回給 thisValue。
  3. 進入函數代碼,thisArg 爲 obj,將其賦值給 this。

如果對步驟1中,baseValue 爲 obj 有疑問,詳見屬性訪問, 產生式 MemberExpression : MemberExpression [ Expression ] 執行過程。
相當於有兩個過程:

  1. 解析obj,baseValue 爲聲明式環境記錄項。
  2. 解析obj.fn,baseValue 爲 obj。

第二種this賦值過程:

function foo(){
    console.log(this);
}
foo(); // Window
  1. 調用表達式解析 foo, 返回引用規範類型,baseValue 爲聲明式環境記錄項(函數聲明時綁定)。
  2. 函數調用,由於 1 過程返回的爲引用規範類型,且不爲屬性引用,調用 ImplicitThisValue 方法,返回 undefined 。
  3. 進入函數代碼,thisArg 爲 undefined,非嚴格模式下將 this 賦值爲全局對象。

第三種this賦值過程:

var v = 1; 
var obj = {
    v: 2,
    fn: function(){
        console.log(this.v);
    }
}
var fn2 = obj.fn;
fn2(); // 1
(obj.fn, obj.fn)(); // 1
  1. 調用表達式解析 fn2,(obj.fn, obj.fn) ,由於賦值表達式、逗號表達式都使用了 GetValue 方法,返回函數(Object 類型)。
  2. 函數調用,由於 1 過程返回的不是引用規範類型,所以 thisValue 爲 undefined`。
  3. 進入函數代碼,thisArg 爲 undefined,非嚴格模式下將 this 賦值爲全局對象。

內置函數如何使用this的

內置函數修改 this 是通過給 func 的 [[Call]] 內置方法傳遞 thisArg 來實現的。

內置方法

內置方法模擬實現

使用成員表達式模擬內置方法[[Call]]的效果:

apply

Function.prototype.apply_ = function (context, arr) { 
    var context = Object(context) || window;
    var result;

    // 臨時記錄需要調用的function 
    context.fn = this;
      
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        // 使用成員表達式指定context.fn執行時this爲context 
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

call

Function.prototype.call_ = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    // 獲取參數列表 
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    } 
    // 使用成員表達式指定context.fn執行時this爲context  
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}  

bind

Function.prototype.bind_  = function (context) {
    // 記錄bind的函數 
    var self = this;
    var args = [];
    // 獲取綁定的參數列表 
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    } 

    // 創建新函數 
    var fbound = function () {
        // 獲取未綁定的參數列表 
        var bindArgs = Array.prototype.slice.call(arguments);
        // fbound被當做構造函數使用,this指向實例。否則,指向 context
        self.apply(this instanceof self ? this : context, args.concat(bindArgs));
    } 
    // 維護原型關係   
    fbound.prototype = self.prototype || new Function().prototype ;
    return fbound;
}

參考文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章