JS?new操作原理及手寫(xiě)函數(shù)模擬實(shí)現(xiàn)示例
引言
我們知道,在 ES6
之前(ES5
),JavaScript
中類的表現(xiàn)形式就是構(gòu)造函數(shù)。
JavaScript
中的構(gòu)造函數(shù)是怎么樣的?
- 構(gòu)造函數(shù)也是一個(gè)普通的函數(shù),從表現(xiàn)形式上看,和普通的函數(shù)沒(méi)有任何區(qū)別(除了按照慣例,構(gòu)造函數(shù)名稱的首字母通常會(huì)大寫(xiě));
- 但如果一個(gè)普通函數(shù)被 new 操作符調(diào)用了,那么這個(gè)函數(shù)就叫做構(gòu)造函數(shù);
原理
如果一個(gè)函數(shù)被 new
操作符調(diào)用了,那么它會(huì)執(zhí)行如下操作:
- 在內(nèi)存中創(chuàng)建一個(gè)新對(duì)象;
- 將這個(gè)新對(duì)象內(nèi)部的
[[Prototype]]
屬性賦值為構(gòu)造函數(shù)的prototype
屬性; - 將構(gòu)造函數(shù)內(nèi)部的
this
賦值為這個(gè)新對(duì)象(即this
指向新對(duì)象); - 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對(duì)象添加屬性);
- 如果構(gòu)造函數(shù)返回了非空對(duì)象,則返回該對(duì)象;否則,返回剛創(chuàng)建的新對(duì)象;
手寫(xiě)函數(shù)模擬 new
下面,我們就根據(jù)上面的原理,嘗試自己手寫(xiě)一個(gè)函數(shù),模擬實(shí)現(xiàn) new
操作符的功能。
v1 基本實(shí)現(xiàn)
我們先用 ES5
的語(yǔ)法來(lái)進(jìn)行實(shí)現(xiàn):
function useNewOperator() { var constructor = arguments[0] var args = [].slice.call(arguments, 1) // 1. 在內(nèi)存中創(chuàng)建一個(gè)新對(duì)象 var obj = {} // 2. 這個(gè)新對(duì)象內(nèi)部的 [[Prototype]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 obj.__proto__ = constructor.prototype // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個(gè)新對(duì)象(即 this 指向新對(duì)象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對(duì)象添加屬性) var res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(shù)返回非空對(duì)象,則返回該對(duì)象;否則,返回剛才創(chuàng)建的新對(duì)象 if (res != null && (typeof res === 'object' || typeof res === 'function')) { return res } return obj } // 測(cè)試 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ù)類型
上面的基本實(shí)現(xiàn)能跑但還存在問(wèn)題,即沒(méi)有考慮傳入第一個(gè)參數(shù)是否為函數(shù)類型,如果第一個(gè)參數(shù)傳入的不是函數(shù),那么在執(zhí)行 constructor.apply(obj, args)
這行代碼調(diào)用 constructor()
時(shí)就會(huì)報(bào)錯(cuò)了。所以我們需要加上判斷,如果第一個(gè)參數(shù)傳入的不是一個(gè)函數(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)建一個(gè)新對(duì)象 var obj = {} // 2. 這個(gè)新對(duì)象內(nèi)部的 [[Prototype]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 obj.__proto__ = constructor.prototype // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個(gè)新對(duì)象(即 this 指向新對(duì)象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對(duì)象添加屬性) var res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(shù)返回非空對(duì)象,則返回該對(duì)象;否則,返回剛才創(chuàng)建的新對(duì)象 if (res != null && (typeof res === 'object' || typeof res === 'function')) { return res } return obj } // 測(cè)試 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__ 的替代方案
前面我們?cè)趯⑿聦?duì)象內(nèi)部的 [[Prototype]]
屬性賦值為構(gòu)造函數(shù)的 prototype
屬性時(shí),是通過(guò)給 obj
上的 __proto__
屬性賦值實(shí)現(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)建一個(gè)新對(duì)象 var obj = {} // 2. 這個(gè)新對(duì)象內(nèi)部的 [[Prototype]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 + Object.setPrototypeOf(obj, constructor.prototype) // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個(gè)新對(duì)象(即 this 指向新對(duì)象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對(duì)象添加屬性) var res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(shù)返回非空對(duì)象,則返回該對(duì)象;否則,返回剛才創(chuàng)建的新對(duì)象 if (res != null && (typeof res === 'object' || typeof res === 'function')) { return res } return obj } // 測(cè)試 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()
直接指定原型來(lái)創(chuàng)建新對(duì)象:
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)建一個(gè)新對(duì)象 - var obj = {} - // 2. 這個(gè)新對(duì)象內(nèi)部的 [[Prototype]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 + var obj = Object.create(constructor.prototype) // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個(gè)新對(duì)象(即 this 指向新對(duì)象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對(duì)象添加屬性) var res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(shù)返回非空對(duì)象,則返回該對(duì)象;否則,返回剛才創(chuàng)建的新對(duì)象 if (res != null && (typeof res === 'object' || typeof res === 'function')) { return res } return obj } // 測(cè)試 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 語(yǔ)法實(shí)現(xiàn)
下面,我們?cè)賮?lái)使用 ES6
語(yǔ)法(剩余參數(shù)(rest parameters
)、const
)進(jìn)行實(shí)現(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)建一個(gè)新對(duì)象 const obj = {} // 2. 這個(gè)新對(duì)象內(nèi)部的 [[Prototype]] 指針被賦值為構(gòu)造函數(shù)(constructor)的 prototype 屬性 Object.setPrototypeOf(obj, constructor.prototype) // 或者使用 Object.create() 直接指定原型創(chuàng)建新對(duì)象 // const obj = Object.create(constructor.prototype) // 3. 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個(gè)新對(duì)象(即 this 指向新對(duì)象) // 4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對(duì)象添加屬性) const res = constructor.apply(obj, args) // 5. 如果構(gòu)造函數(shù)返回非空對(duì)象,則返回該對(duì)象;否則,返回剛才創(chuàng)建的新對(duì)象 if (res != null && (typeof res === 'object' || typeof res === 'function')) { return res } return obj } // 測(cè)試 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 檢測(cè)
最后,還有一個(gè)點(diǎn)需要考慮,就是 ES6
新增的 new.target
屬性,在通過(guò)使用 new
操作符被調(diào)用的構(gòu)造方法或函數(shù)中,new.target
會(huì)返回一個(gè)指向構(gòu)造方法或函數(shù)的引用(參考鏈接:developer.mozilla.org/en-US/docs/… )。所以我們可以使用 new.target
來(lái)檢測(cè)函數(shù)或構(gòu)造方法是否是通過(guò) new
操作符被調(diào)用的。那么我們還需要在自己實(shí)現(xiàn)的 useNewOperator
函數(shù)中添加相應(yīng)的代碼:
無(wú)注釋版本
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
操作背后的原理,以及手寫(xiě)函數(shù)模擬實(shí)現(xiàn) new
操作過(guò)程的所有內(nèi)容啦,更多關(guān)于JS new操作手寫(xiě)函數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Phaser.js實(shí)現(xiàn)簡(jiǎn)單的跑酷游戲附源碼下載
這篇文章主要介紹了Phaser.js實(shí)現(xiàn)簡(jiǎn)單的跑酷游戲附源碼下載,需要的朋友可以參考下2018-10-10javascript 自動(dòng)轉(zhuǎn)到命名錨記
javascript 自動(dòng)轉(zhuǎn)到命名錨記,方面業(yè)內(nèi)控制導(dǎo)航等信息2009-01-01js字符串轉(zhuǎn)換為對(duì)象格式的三種方法總結(jié)
關(guān)于js里面的字符串轉(zhuǎn)對(duì)象,又或者是對(duì)象轉(zhuǎn)為字符串,都是平時(shí)開(kāi)發(fā)應(yīng)用是經(jīng)常用到的知識(shí)點(diǎn),下面這篇文章主要給大家介紹了關(guān)于js字符串轉(zhuǎn)換為對(duì)象格式的三種方法,需要的朋友可以參考下2022-12-12js遍歷對(duì)象key和value實(shí)戰(zhàn)舉例
這篇文章主要給大家介紹了關(guān)于js遍歷對(duì)象key和value的相關(guān)資料,隨著JavaScript在web應(yīng)用程序中的廣泛使用,遍歷對(duì)象的key和value成為了編寫(xiě)復(fù)雜代碼所必需的技能,需要的朋友可以參考下2023-07-07onbeforeunload與onunload事件異同點(diǎn)總結(jié)
本文對(duì)onbeforeunload與onunload事件的異同點(diǎn)、觸發(fā)于、可以用在哪些元素以及解決刷新頁(yè)面時(shí)不調(diào)用onbeforeunload等等,感興趣的朋友可以參考下哈2013-06-06