JS?new操作原理及手寫函數(shù)模擬實(shí)現(xiàn)示例
引言
我們知道,在 ES6 之前(ES5),JavaScript 中類的表現(xiàn)形式就是構(gòu)造函數(shù)。
JavaScript 中的構(gòu)造函數(shù)是怎么樣的?
- 構(gòu)造函數(shù)也是一個(gè)普通的函數(shù),從表現(xiàn)形式上看,和普通的函數(shù)沒有任何區(qū)別(除了按照慣例,構(gòu)造函數(shù)名稱的首字母通常會(huì)大寫);
- 但如果一個(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ì)象;
手寫函數(shù)模擬 new
下面,我們就根據(jù)上面的原理,嘗試自己手寫一個(gè)函數(shù),模擬實(shí)現(xiàn) new 操作符的功能。
v1 基本實(shí)現(xiàn)
我們先用 ES5 的語(yǔ)法來進(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)能跑但還存在問題,即沒有考慮傳入第一個(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í),是通過給 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() 直接指定原型來創(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è)賮硎褂?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 屬性,在通過使用 new 操作符被調(diào)用的構(gòu)造方法或函數(shù)中,new.target 會(huì)返回一個(gè)指向構(gòu)造方法或函數(shù)的引用(參考鏈接:developer.mozilla.org/en-US/docs/… )。所以我們可以使用 new.target 來檢測(cè)函數(shù)或構(gòu)造方法是否是通過 new 操作符被調(diào)用的。那么我們還需要在自己實(shí)現(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ù)模擬實(shí)現(xiàn) new 操作過程的所有內(nèi)容啦,更多關(guān)于JS new操作手寫函數(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-10
javascript 自動(dòng)轉(zhuǎn)到命名錨記
javascript 自動(dòng)轉(zhuǎn)到命名錨記,方面業(yè)內(nèi)控制導(dǎo)航等信息2009-01-01
js字符串轉(zhuǎn)換為對(duì)象格式的三種方法總結(jié)
關(guān)于js里面的字符串轉(zhuǎn)對(duì)象,又或者是對(duì)象轉(zhuǎn)為字符串,都是平時(shí)開發(fā)應(yīng)用是經(jīng)常用到的知識(shí)點(diǎn),下面這篇文章主要給大家介紹了關(guān)于js字符串轉(zhuǎn)換為對(duì)象格式的三種方法,需要的朋友可以參考下2022-12-12
js遍歷對(duì)象key和value實(shí)戰(zhàn)舉例
這篇文章主要給大家介紹了關(guān)于js遍歷對(duì)象key和value的相關(guān)資料,隨著JavaScript在web應(yīng)用程序中的廣泛使用,遍歷對(duì)象的key和value成為了編寫復(fù)雜代碼所必需的技能,需要的朋友可以參考下2023-07-07
onbeforeunload與onunload事件異同點(diǎn)總結(jié)
本文對(duì)onbeforeunload與onunload事件的異同點(diǎn)、觸發(fā)于、可以用在哪些元素以及解決刷新頁(yè)面時(shí)不調(diào)用onbeforeunload等等,感興趣的朋友可以參考下哈2013-06-06

