JS?new操作原理及手寫函數(shù)模擬實現(xiàn)示例
引言
我們知道,在 ES6
之前(ES5
),JavaScript
中類的表現(xiàn)形式就是構(gòu)造函數(shù)。
JavaScript
中的構(gòu)造函數(shù)是怎么樣的?
- 構(gòu)造函數(shù)也是一個普通的函數(shù),從表現(xiàn)形式上看,和普通的函數(shù)沒有任何區(qū)別(除了按照慣例,構(gòu)造函數(shù)名稱的首字母通常會大寫);
- 但如果一個普通函數(shù)被 new 操作符調(diào)用了,那么這個函數(shù)就叫做構(gòu)造函數(shù);
原理
如果一個函數(shù)被 new
操作符調(diào)用了,那么它會執(zhí)行如下操作:
- 在內(nèi)存中創(chuàng)建一個新對象;
- 將這個新對象內(nèi)部的
[[Prototype]]
屬性賦值為構(gòu)造函數(shù)的prototype
屬性; - 將構(gòu)造函數(shù)內(nèi)部的
this
賦值為這個新對象(即this
指向新對象); - 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對象添加屬性);
- 如果構(gòu)造函數(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]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 obj.__proto__ = constructor.prototype // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對象添加屬性) var res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(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]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 obj.__proto__ = constructor.prototype // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對象添加屬性) var res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(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]]
屬性賦值為構(gòu)造函數(shù)的 prototype
屬性時,是通過給 obj
上的 __proto__
屬性賦值實現(xiàn)的(相當(dāng)于使用了 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]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 + Object.setPrototypeOf(obj, constructor.prototype) // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對象添加屬性) var res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(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]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 + var obj = Object.create(constructor.prototype) // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對象添加屬性) var res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(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]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 Object.setPrototypeOf(obj, constructor.prototype) // 或者使用 Object.create() 直接指定原型創(chuàng)建新對象 // const obj = Object.create(constructor.prototype) // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對象添加屬性) const res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(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)用的構(gòu)造方法或函數(shù)中,new.target
會返回一個指向構(gòu)造方法或函數(shù)的引用(參考鏈接:developer.mozilla.org/en-US/docs/… )。所以我們可以使用 new.target
來檢測函數(shù)或構(gòu)造方法是否是通過 new
操作符被調(diào)用的。那么我們還需要在自己實現(xiàn)的 useNewOperator
函數(shù)中添加相應(yīng)的代碼:
無注釋版本
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 }
以上,就是關(guān)于 new
操作背后的原理,以及手寫函數(shù)模擬實現(xiàn) new
操作過程的所有內(nèi)容啦,更多關(guān)于JS new操作手寫函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js字符串轉(zhuǎn)換為對象格式的三種方法總結(jié)
關(guān)于js里面的字符串轉(zhuǎn)對象,又或者是對象轉(zhuǎn)為字符串,都是平時開發(fā)應(yīng)用是經(jīng)常用到的知識點,下面這篇文章主要給大家介紹了關(guān)于js字符串轉(zhuǎn)換為對象格式的三種方法,需要的朋友可以參考下2022-12-12onbeforeunload與onunload事件異同點總結(jié)
本文對onbeforeunload與onunload事件的異同點、觸發(fā)于、可以用在哪些元素以及解決刷新頁面時不調(diào)用onbeforeunload等等,感興趣的朋友可以參考下哈2013-06-06