JS?new操作原理及手寫函數(shù)模擬實現(xiàn)示例
引言
我們知道,在 ES6 之前(ES5),JavaScript 中類的表現(xiàn)形式就是構造函數(shù)。
JavaScript 中的構造函數(shù)是怎么樣的?
- 構造函數(shù)也是一個普通的函數(shù),從表現(xiàn)形式上看,和普通的函數(shù)沒有任何區(qū)別(除了按照慣例,構造函數(shù)名稱的首字母通常會大寫);
- 但如果一個普通函數(shù)被 new 操作符調(diào)用了,那么這個函數(shù)就叫做構造函數(shù);
原理
如果一個函數(shù)被 new 操作符調(diào)用了,那么它會執(zhí)行如下操作:
- 在內(nèi)存中創(chuàng)建一個新對象;
- 將這個新對象內(nèi)部的
[[Prototype]]屬性賦值為構造函數(shù)的prototype屬性; - 將構造函數(shù)內(nèi)部的
this賦值為這個新對象(即this指向新對象); - 執(zhí)行構造函數(shù)內(nèi)部的代碼(給新對象添加屬性);
- 如果構造函數(shù)返回了非空對象,則返回該對象;否則,返回剛創(chuàng)建的新對象;
手寫函數(shù)模擬 new
下面,我們就根據(jù)上面的原理,嘗試自己手寫一個函數(shù),模擬實現(xiàn) new 操作符的功能。
v1 基本實現(xiàn)
我們先用 ES5 的語法來進行實現(xiàn):
function useNewOperator() {
var constructor = arguments[0]
var args = [].slice.call(arguments, 1)
// 1. 在內(nèi)存中創(chuàng)建一個新對象
var obj = {}
// 2. 這個新對象內(nèi)部的 [[Prototype]] 指針被賦值為構造函數(shù)(constructor)的 prototype 屬性
obj.__proto__ = constructor.prototype
// 3. 構造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象)
// 4. 執(zhí)行構造函數(shù)內(nèi)部的代碼(給新對象添加屬性)
var res = constructor.apply(obj, args)
// 5. 如果構造函數(shù)返回非空對象,則返回該對象;否則,返回剛才創(chuàng)建的新對象
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
return res
}
return obj
}
// 測試
function Person(name, age) {
this.name = name
this.age = age
// return undefined
// return null
// return {}
// return 123
// return ''
// return String(123)
// return new String(123)
// return { name: 'wy' }
}
Person.prototype.sayName = function() {
console.log(this.name);
}
const p1 = new Person('zhj', 20)
console.log(p1); // Person {name: 'zhj', age: 20}
p1.sayName() // zhj
const p2 = useNewOperator(Person, 'zhj', 20)
console.log(p2); // Person {name: 'zhj', age: 20}
p2.sayName() // zhj
v2 考慮參數(shù)類型
上面的基本實現(xiàn)能跑但還存在問題,即沒有考慮傳入第一個參數(shù)是否為函數(shù)類型,如果第一個參數(shù)傳入的不是函數(shù),那么在執(zhí)行 constructor.apply(obj, args) 這行代碼調(diào)用 constructor() 時就會報錯了。所以我們需要加上判斷,如果第一個參數(shù)傳入的不是一個函數(shù),就直接拋出異常:
function useNewOperator() {
var constructor = arguments[0]
+
+ if (typeof constructor !== 'function') {
+ throw new TypeError('the first argument to useNewOperator function must be a function')
+ }
+
var args = [].slice.call(arguments, 1)
// 1. 在內(nèi)存中創(chuàng)建一個新對象
var obj = {}
// 2. 這個新對象內(nèi)部的 [[Prototype]] 指針被賦值為構造函數(shù)(constructor)的 prototype 屬性
obj.__proto__ = constructor.prototype
// 3. 構造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象)
// 4. 執(zhí)行構造函數(shù)內(nèi)部的代碼(給新對象添加屬性)
var res = constructor.apply(obj, args)
// 5. 如果構造函數(shù)返回非空對象,則返回該對象;否則,返回剛才創(chuàng)建的新對象
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
return res
}
return obj
}
// 測試
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() {
console.log(this.name);
}
const obj = {}
const p2 = useNewOperator(obj, 'zhj', 20) // Uncaught TypeError: the first argument to useNewOperator function must be a function
console.log(p2);
p2.sayName()
v3 Object.prototype.__proto__ 的替代方案
前面我們在將新對象內(nèi)部的 [[Prototype]] 屬性賦值為構造函數(shù)的 prototype 屬性時,是通過給 obj 上的 __proto__ 屬性賦值實現(xiàn)的(相當于使用了 Object.prototype.__proto__),雖然可以,但不推薦使用 Object.prototype.__proto__,更推薦使用 Object.getPrototypeOf/Reflect.getPrototypeOf 和 Object.setPrototypeOf/Reflect.setPrototypeOf(參考鏈接:developer.mozilla.org/zh-CN/docs/… )。所以我們做如下修改:
function useNewOperator() {
var constructor = arguments[0]
if (typeof constructor !== 'function') {
throw new TypeError('the first argument to useNewOperator function must be a function')
}
var args = [].slice.call(arguments, 1)
// 1. 在內(nèi)存中創(chuàng)建一個新對象
var obj = {}
// 2. 這個新對象內(nèi)部的 [[Prototype]] 指針被賦值為構造函數(shù)(constructor)的 prototype 屬性
+ Object.setPrototypeOf(obj, constructor.prototype)
// 3. 構造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象)
// 4. 執(zhí)行構造函數(shù)內(nèi)部的代碼(給新對象添加屬性)
var res = constructor.apply(obj, args)
// 5. 如果構造函數(shù)返回非空對象,則返回該對象;否則,返回剛才創(chuàng)建的新對象
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
return res
}
return obj
}
// 測試
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() {
console.log(this.name);
}
const p1 = new Person('zhj', 20)
console.log(p1); // Person {name: 'zhj', age: 20}
p1.sayName() // zhj
const p2 = useNewOperator(Person, 'zhj', 20)
console.log(p2); // Person {name: 'zhj', age: 20}
p2.sayName() // zhj
或者我們還可以使用 Object.create() 直接指定原型來創(chuàng)建新對象:
function useNewOperator() {
var constructor = arguments[0]
if (typeof constructor !== 'function') {
throw new TypeError('the first argument to useNewOperator function must be a function')
}
var args = [].slice.call(arguments, 1)
// 1. 在內(nèi)存中創(chuàng)建一個新對象
- var obj = {}
-
// 2. 這個新對象內(nèi)部的 [[Prototype]] 指針被賦值為構造函數(shù)(constructor)的 prototype 屬性
+ var obj = Object.create(constructor.prototype)
// 3. 構造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象)
// 4. 執(zhí)行構造函數(shù)內(nèi)部的代碼(給新對象添加屬性)
var res = constructor.apply(obj, args)
// 5. 如果構造函數(shù)返回非空對象,則返回該對象;否則,返回剛才創(chuàng)建的新對象
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
return res
}
return obj
}
// 測試
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() {
console.log(this.name);
}
const p1 = new Person('zhj', 20)
console.log(p1); // Person {name: 'zhj', age: 20}
p1.sayName() // zhj
const p2 = useNewOperator(Person, 'zhj', 20)
console.log(p2); // Person {name: 'zhj', age: 20}
p2.sayName() // zhj
v4 使用 ES6 語法實現(xiàn)
下面,我們再來使用 ES6 語法(剩余參數(shù)(rest parameters)、const)進行實現(xiàn):
function useNewOperator(constructor, ...args) {
if (typeof constructor !== 'function') {
throw new TypeError('the first argument to useNewOperator function must be a function')
}
// 1. 在內(nèi)存中創(chuàng)建一個新對象
const obj = {}
// 2. 這個新對象內(nèi)部的 [[Prototype]] 指針被賦值為構造函數(shù)(constructor)的 prototype 屬性
Object.setPrototypeOf(obj, constructor.prototype)
// 或者使用 Object.create() 直接指定原型創(chuàng)建新對象
// const obj = Object.create(constructor.prototype)
// 3. 構造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象)
// 4. 執(zhí)行構造函數(shù)內(nèi)部的代碼(給新對象添加屬性)
const res = constructor.apply(obj, args)
// 5. 如果構造函數(shù)返回非空對象,則返回該對象;否則,返回剛才創(chuàng)建的新對象
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
return res
}
return obj
}
// 測試
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() {
console.log(this.name);
}
const p1 = new Person('zhj', 20)
console.log(p1); // Person {name: 'zhj', age: 20}
p1.sayName() // zhj
const p2 = useNewOperator(Person, 'zhj', 20)
console.log(p2); // Person {name: 'zhj', age: 20}
p2.sayName() // zhj
v5 考慮 ES6 的 new.target 檢測
最后,還有一個點需要考慮,就是 ES6 新增的 new.target 屬性,在通過使用 new 操作符被調(diào)用的構造方法或函數(shù)中,new.target 會返回一個指向構造方法或函數(shù)的引用(參考鏈接:developer.mozilla.org/en-US/docs/… )。所以我們可以使用 new.target 來檢測函數(shù)或構造方法是否是通過 new 操作符被調(diào)用的。那么我們還需要在自己實現(xiàn)的 useNewOperator 函數(shù)中添加相應的代碼:
無注釋版本
function useNewOperator(constructor, ...args) {
if (typeof constructor !== 'function') {
throw new TypeError('the first argument to useNewOperator function must be a function')
}
useNewOperator.target = constructor
const obj = {}
Object.setPrototypeOf(obj, constructor.prototype)
// const obj = Object.create(constructor.prototype)
const res = constructor.apply(obj, args)
if (res != null && (typeof res === 'object' || typeof res === 'function')) {
return res
}
return obj
}
以上,就是關于 new 操作背后的原理,以及手寫函數(shù)模擬實現(xiàn) new 操作過程的所有內(nèi)容啦,更多關于JS new操作手寫函數(shù)的資料請關注腳本之家其它相關文章!
相關文章
onbeforeunload與onunload事件異同點總結
本文對onbeforeunload與onunload事件的異同點、觸發(fā)于、可以用在哪些元素以及解決刷新頁面時不調(diào)用onbeforeunload等等,感興趣的朋友可以參考下哈2013-06-06

