JS 原生方法原理探究(三):從規範解讀如何實現 new 操做符

這是 JS 原生方法原理探究系列的第三篇文章。本文會介紹如何模擬實現 new 操做符。關於 new 的具體用法,MDN 已經描述得很清楚了,這裏咱們不說廢話,直接講如何模擬實現。app

new 操做符的規範

注意:下面展現的全部規範都是 ES5 版本的,與如今最新的規範有些區別

首先看一下根據規範的描述, new 操做符作了什麼事:函數

全是英文,不過不要緊,我簡單翻譯一下:post

我在使用 new 操做符的時候,後面跟着的構造函數可能帶參數,也可能不帶參數,若是不帶參數的話,好比說 new Fn(),那麼這裏這個 Fn 就是一個 NewExpression;若是帶參數,好比說 new Fn(name,age),那麼這裏的 Fn 就是一個 MemberExpressionthis

這兩種狀況下使用 new 操做符所進行的操做有點點不一樣,這裏拿帶參數的狀況說明一下:spa

  1. 首先會對 Fn 這個 MemberExpression 求值,其結果是指向實際函數對象的一個引用,咱們把這個引用做爲 ref
  2. 接着調用 GetValue(ref) 進行求值,獲得實際的函數對象,把這個對象做爲 constructor
  3. Arguments 也就是傳進來的參數求值,獲得一個參數列表,做爲 argList
  4. 若是 constructor 不是對象,則拋出類型錯誤
  5. 若是 constructor 沒有實現內部的 [[Constructor]] 方法,也拋出類型錯誤
  6. 調用 constructor[[Constructor]]方法,並將 argList 傳入做爲參數,返回調用結果

從這些描述能夠看出,更多的實現細節放在函數的 [[Constructor]] 方法裏。那麼這個方法具體是作什麼用的呢?prototype

[[Constructor]] 的規範

在 JS 中,函數有兩種調用方式,一種是正常調用,這將調用函數的內部方法 [[Call]],還有一種是經過 new 調用,此時的函數做爲一個構造函數,這將調用函數的另外一個內部方法 [[Consturct]]。因此,要實現 new 操做的話,咱們得先搞懂 [[Construct]] 內部方法作了什麼事。翻譯

這裏繼續看規範是怎麼說的:code

簡單翻譯一下:對象

當經過可能爲空的參數列表調用函數 F 的內部方法 [[Construct]] 的時候,會執行以下步驟:rem

  1. obj 做爲一個新建立的原生對象
  2. 按照規範指定的,爲 obj 設置全部內部方法
  3. obj 的內部屬性 [[Class]] 設置爲 Object
  4. 傳參 prototype 調用函數 F 的內部方法 [[Get]],獲取函數的原型對象,做爲 proto
  5. 若是 proto 是對象,則將 obj 的內部屬性 [[Prototype]] 設置爲 proto
  6. 若是 proto 不是對象,則將 obj 的內部屬性 [[Prototype]] 設置爲標準內建的 Object 的原型對象
  7. 調用函數 F 的內部方法 Callobj 做爲調用時的 this 值,此前傳給 [[Construct]] 的參數列表做爲調用時的參數。將調用後獲得的結果做爲 result
  8. 若是 result 是對象,則將其返回
  9. 不然,返回 obj

能夠說,規範已經講得很清楚了,簡單地說,在 new 一個構造函數的時候,具體會作下面的事情:

  • 內部建立一個實例對象,並指定實例對象的原型:

    • 若是構造函數的原型是對象,則讓實例的 __proto__ 等於構造函數的 prototype
    • 若是構造函數的原型不是對象,則讓實例的 __proto__ 等於 Objectprototype
  • 將實例對象綁定爲構造函數中的 this,此前傳遞進來的參數做爲參數,並執行一遍構造函數
  • 若是構造函數返回了對象,則將其做爲返回值,不然將實例對象做爲返回值

代碼實現

ES3 版本的實現以下:

function myNew(Fn){
    if(typeof Fn != 'function'){
        throw new TypeError(Fn + 'is not a constructor')
    }
    myNew.target = Fn
    var instance = {}
    // 檢測構造函數原型是否是對象
    instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype 
    const returnValue = Fn.apply(instance,Array.prototype.slice.call(arguments,1))
    if(typeof returnValue === 'object' && returnValue !== null || typeof returnValue === 'function'){
        return returnValue
    } else {
        return instance
    }
}

ES6 版本的實現以下:

function myNew(Fn,...args){
    if(typeof Fn != 'function'){
        throw new TypeError(Fn + 'is not a constructor')
    }
    myNew.target = Fn
    const instance = {}
    // 檢測構造函數原型是否是對象
    instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype 
    const returnValue = Fn.call(instance,...args)
    return returnValue instanceof Object ? returnValue : instance
}

注意幾個要點:

1)當函數是經過 new 調用的時候,new.target 會指向函數自身,這個「指向」的操做在代碼裏就是經過 myNew.target = Fn 體現的

2)爲何不直接使用 const instance = Object.create(Fn.prototype) 建立實例呢?根據規範,咱們在實現 new 的時候,須要檢測構造函數的原型是否是對象,若是不是對象,好比說是 null,那麼實例的 __proto__ 會從新連接到 Object.prototype,而這裏若是使用了 Object.create,則會致使實例的 __proto__ 仍然指向 null。網上不少 new 的模擬實現直接使用了 Object.create,或者根本沒有對構造函數的原型進行類型檢查,這是不夠嚴謹的(注意,我沒有說這是錯誤的,只是不夠貼近原生 [[Consturct]] 的實現細節)。具體能夠閱讀我以前寫的另外一篇文章

3)若是沒法使用 instanceof,咱們也能夠改用 typeof Fn.prototype === 'Object' && Fn.prototype !== null 進行判斷

相關文章
相關標籤/搜索