這是 JS 原生方法原理探究系列的第三篇文章。本文會介紹如何模擬實現 new
操做符。關於 new
的具體用法,MDN 已經描述得很清楚了,這裏咱們不說廢話,直接講如何模擬實現。app
注意:下面展現的全部規範都是 ES5 版本的,與如今最新的規範有些區別
首先看一下根據規範的描述, new
操做符作了什麼事:函數
全是英文,不過不要緊,我簡單翻譯一下:post
我在使用 new
操做符的時候,後面跟着的構造函數可能帶參數,也可能不帶參數,若是不帶參數的話,好比說 new Fn()
,那麼這裏這個 Fn
就是一個 NewExpression
;若是帶參數,好比說 new Fn(name,age)
,那麼這裏的 Fn
就是一個 MemberExpression
。this
這兩種狀況下使用 new
操做符所進行的操做有點點不一樣,這裏拿帶參數的狀況說明一下:spa
Fn
這個 MemberExpression
求值,其結果是指向實際函數對象的一個引用,咱們把這個引用做爲 ref
GetValue(ref)
進行求值,獲得實際的函數對象,把這個對象做爲 constructor
Arguments
也就是傳進來的參數求值,獲得一個參數列表,做爲 argList
constructor
不是對象,則拋出類型錯誤constructor
沒有實現內部的 [[Constructor]]
方法,也拋出類型錯誤constructor
的 [[Constructor]]
方法,並將 argList
傳入做爲參數,返回調用結果從這些描述能夠看出,更多的實現細節放在函數的 [[Constructor]]
方法裏。那麼這個方法具體是作什麼用的呢?prototype
[[Constructor]]
的規範在 JS 中,函數有兩種調用方式,一種是正常調用,這將調用函數的內部方法 [[Call]]
,還有一種是經過 new 調用,此時的函數做爲一個構造函數,這將調用函數的另外一個內部方法 [[Consturct]]
。因此,要實現 new
操做的話,咱們得先搞懂 [[Construct]]
內部方法作了什麼事。翻譯
這裏繼續看規範是怎麼說的:code
簡單翻譯一下:對象
當經過可能爲空的參數列表調用函數 F
的內部方法 [[Construct]]
的時候,會執行以下步驟:rem
obj
做爲一個新建立的原生對象obj
設置全部內部方法obj
的內部屬性 [[Class]]
設置爲 Object
prototype
調用函數 F
的內部方法 [[Get]]
,獲取函數的原型對象,做爲 proto
proto
是對象,則將 obj
的內部屬性 [[Prototype]]
設置爲 proto
proto
不是對象,則將 obj
的內部屬性 [[Prototype]]
設置爲標準內建的 Object
的原型對象F
的內部方法 Call
, obj
做爲調用時的 this 值,此前傳給 [[Construct]]
的參數列表做爲調用時的參數。將調用後獲得的結果做爲 result
result
是對象,則將其返回obj
能夠說,規範已經講得很清楚了,簡單地說,在 new 一個構造函數的時候,具體會作下面的事情:
內部建立一個實例對象,並指定實例對象的原型:
__proto__
等於構造函數的 prototype
__proto__
等於 Object
的 prototype
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
進行判斷