前幾天無意間發現了剛開始學JavaScript時在知乎寫的一些回答,有一個就是講new操作符到底幹了什麼。從現在的視角看我當時的回答雖然是正確的,但是在對原理的剖析和細節的理解上還相去甚遠。所以藉此機會,就想重新梳理一下這一年多來對new操作符理解的變化與成長。
模擬new操作符
第一次去了解new操作符,是在看《JS高階程式設計(第三版)》這本書時,P145是這樣寫到的。
要建立Person的新例項,必須使用new操作符。以這種方式呼叫建構函式實際上會經歷以下4個步驟:
- 建立一個新物件;
- 將建構函式的作用域賦給新物件(因此this就指向了這個新物件);
- 執行建構函式中的程式碼(為這個新物件新增屬性);
- 返回新物件;
儘管書中是這樣描述,但是並沒有給出實踐的程式碼,所以我就按照這個步驟自己去實踐模擬一個new操作符。程式碼如下:
var mockNew = function (constructor) {
var o = {} // 建立一個新物件
constructor.apply(o, Array.prototype.slice.call(arguments, 1)) // 賦作用域 執行程式碼
return o // 返回新物件
}
複製程式碼
然後我們用這個模擬new操作符的函式來創造一個物件試試。
var Person = function (name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function () {
console.log(this.name)
}
var person1 = mockNew(Person, 'MeloGuo', 22)
console.log(person1.name) // 'MeloGuo'
console.log(person1.age) // 22
person1.sayName() // Uncaught TypeError: person1.sayName is not a function
複製程式碼
看起來我的模擬函式雖然能訪問例項中的屬性,但是卻不能訪問sayName方法,而且當我使用instanceof
操作符檢測時卻得到了這樣的結果。
console.log(person1 instanceof Person) // false
console.log(person1 instanceof Object) // true
複製程式碼
改進的模擬函式
可見person1並不是Person的例項。當時的我還不知道問題出在哪裡,直到學習到原型鏈時我才直到mockNew函式缺少了一個步驟,即繫結建構函式原型。所以person1例項是無法訪問到Person原型中的sayName方法,同時instanceof
操作符的結果也為false
。因為instanceof
操作符是用來檢測一個物件在其原型鏈中是否存在一個建構函式的prototype屬性的,而person1的原型鏈中並不存在Person.prototype,所以返回值為false
。因此,我們改造mockNew函式如下:
var mockNew = function (constructor) {
var o = {}
o.__proto__ = constructor.prototype // 繫結建構函式原型,但是生產程式碼中千萬別用.__proto__
constructor.apply(o, Array.prototype.slice.call(arguments, 1))
return o
}
複製程式碼
這時我們再建立例項,然後使用instanceof
操作符檢測一下,同時呼叫下sayName方法。
var person1 = mockNew(Person, 'MeloGuo', 22)
console.log(person1 instanceof Person) // true
console.log(person1 instanceof Object) // true
person1.sayName() // 'MeloGuo'
複製程式碼
這時看似mockNew函式已經完全模擬了new操作符了,但是當我們嘗試下面這種情況時,又出現了問題。
function Person (name) {
this.name = name
return { age: 22 }
}
var person1 = new Person('MeloGuo')
var person2 = mockNew(Person, 'MeloGuo')
console.log(person1) // {age: 22}
console.log(person2) // Person {name: "MeloGuo"}
console.log(person1 instanceof Person) // false
console.log(person2 instanceof Person) // true
複製程式碼
什麼!!!用new操作符呼叫的Person建構函式並沒有按照預期返回帶有name屬性並且在Person.prototype上的物件,而是返回了我們手動return的帶有age屬性的物件,但是我們的mockNew函式是正常返回了。所以我們的mockNew函式中肯定又丟失了一些細節,為了弄清楚,只好硬著頭皮去讀ECMA-262規範了。看到規範中的steps後才恍然大悟了new操作符的整個執行流程。
完善模擬函式
簡單來說我們在返回物件前缺失了判斷返回值型別的步驟。
- 如果建構函式的返回值是值型別,那麼就丟棄掉,依然返回建構函式建立的例項。
- 如果建構函式的返回值是引用型別,那麼就返回這個引用型別,丟棄建構函式建立的例項。
注:如果沒有顯示return,那麼相當於隱式返回了
undefined
,則丟棄它。
吸取了規範中的內容,並且加入ES6語法後,我們新的mockNew函式如下:
function mockNew (constructor, ...args) {
const isPrimitive = result => {
// 如果result為值型別則返回true
// 如果result為引用型別則返回false
}
const o = Object.create(constructor.prototype)
const result = constructor.apply(o, args)
return isPrimitive(result) ? o : result
}
複製程式碼
這時的mockNew函式可以說是較好的模擬了new操作符的功能。
總結
其實new操作符就是一個語法糖。在傳統的面向類的語言中,“建構函式”是類中的一些特殊方法,使用new初始化類時會呼叫類中的建構函式。而JavaScript中的new其實是用來告訴函式,我要以“構造”的方式呼叫你了,從而向mockNew函式中的流程一樣,得到我們的例項。所以本質上來說JS是沒有所謂建構函式
的,有的應該是構造呼叫
。這樣的稱呼能讓我們更清楚認識JS中的new操作符。