title: 模擬實現(xiàn) new 操作符
date: 2019/10/22 21:30:25
categories:
- 面試題
- 前端
模擬實現(xiàn) new 操作符
首先需要理解,JavaScript 中的構造函數(shù)跟 Java 中的構造函數(shù)性質(zhì)是不一樣的。js 不是基于 class 這種靜態(tài)類模式,而是基于原型對象的模式苛秕。
所以褥紫,在 js 中柏副,new 操作符,其實可以通俗的理解成一個輔助工具序六,用來輔助函數(shù)構造出一個新對象报破。所以悠就,我們才能夠來模擬實現(xiàn)它,因為它其實通俗理解泛烙,就是一個工具函數(shù)理卑。
得先明確這點翘紊,才能知道蔽氨,的確是可以模擬 new 操作符的。
new 的職責
function A() {
this.a = 1;
}
A.prototype.b = 1;
var a = new A(); // {a: 1}
a.b; // 1
所以,以上這種場景的 new 操作符其實就是做了幾件事:
- 創(chuàng)建一個繼承自 A.prototype 的空對象
- 讓空對象作為函數(shù) A 的上下文鹉究,并調(diào)用 A
- 返回這個空對象
這是基本的 new 使用的場景宇立,那么我們要來模擬實現(xiàn)的話,這幾件事就得自己處理:
function _new(Fn, ...args) {
// 1. 創(chuàng)建一個繼承構造函數(shù).prototype的空對象
var obj = Object.create(Fn.prototype);
// 2. 讓空對象作為函數(shù) A 的上下文自赔,并調(diào)用 A
Fn.call(obj, ...args);
// 3. 返回這個空對象
return obj;
}
這樣就結束了嗎妈嘹?并沒有
要模擬實現(xiàn)一個完整的 new 操作符,就還得將它的其他使用場景都考慮進去:
- 當構造函數(shù)有返回值時
- 判斷一個函數(shù)是否能夠作為構造函數(shù)使用
先來考慮第一種:
function A() {
this.a = 1;
return {b: 1};
}
new A(); // {b: 1}
function B() {
this.b = 1;
return 1;
}
new B(); // {b:1}
所以绍妨,當構造函數(shù)返回一個對象時润脸,那么就以這個對象作為構造函數(shù)生成的對象;當構造函數(shù)返回基本類型數(shù)據(jù)時他去,當做沒有返回值處理毙驯,內(nèi)部新建個對象返回。
套用 MDN 對 new 的說明:
new 運算符創(chuàng)建一個用戶定義的對象類型的實例或具有構造函數(shù)的內(nèi)置對象的實例灾测。 ——(來自于MDN)
其實這句解釋就把 new 操作符的所有職責或者說所有使用場景覆蓋了:
- 用戶定義的對象類型 ==> 當構造函數(shù)有返回值時
- 具有構造函數(shù)的內(nèi)置對象 ==> 當前函數(shù)可用來作為構造函數(shù)爆价,那么返回內(nèi)部創(chuàng)建的新對象
所以,要完整模擬一個 new 的工作媳搪,還得完成上面兩點铭段,先來看看對返回值的處理,很簡單:
function _new(Fn, ...args) {
// 1. 創(chuàng)建一個繼承構造函數(shù).prototype的空對象
var obj = Object.create(Fn.prototype);
// 2. 讓空對象作為函數(shù) A 的上下文秦爆,并調(diào)用 A序愚,同時獲取它的返回值
let result = Fn.call(obj, ...args);
// 3. 如果構造函數(shù)返回一個對象,那么直接 return 它等限,否則返回內(nèi)部創(chuàng)建的新對象
return result instanceof Object ? result : obj;
}
接下去就剩最后一個處理了:判斷一個函數(shù)是否可以作為構造函數(shù)
如何判斷函數(shù)是否可以作為構造函數(shù)
我們通過 function 定義的普通函數(shù)都可以結合 new 來作為構造函數(shù)使用展运,那么到底如何判斷一個函數(shù)能否作為構造函數(shù)呢?
網(wǎng)上有些文章里說了:
每個函數(shù)都有一些內(nèi)部屬性精刷,如: [[Construct]] 表示可以用來作為構造函數(shù)使用拗胜,[[Call]] 表示可以用來作為普通函數(shù)使用
所以,當一個函數(shù)沒有 [[Construct]] 內(nèi)部屬性時怒允,它就不能用來作為構造函數(shù)
???
沒錯埂软,從引擎角度來看,的確是這樣處理纫事,但這些內(nèi)部屬性我們并沒有辦法看到的啊勘畔,那對于我們這些寫 js 的來說,如何判斷一個函數(shù)是否能夠作為構造函數(shù)呢丽惶?靠經(jīng)驗積累炫七?
那就先來說說靠經(jīng)驗積累好了:
- 箭頭函數(shù)不能作為構造函數(shù)使用(每篇介紹箭頭函數(shù)的文章里基本都會說明)
- Generator 函數(shù)不能作為構造函數(shù)使用(俗稱 * 函數(shù),如
function *A(){}
) - 對象的簡寫方法不能作為構造函數(shù)使用(
{ A(){} }
) - 內(nèi)置方法不能作為構造函數(shù)使用(如 Math.min)
靠經(jīng)驗積累只能是這樣一條條去羅列钾唬,末尾鏈接的文章里有這么一句話:
[除非特別說明万哪,es6+ 實現(xiàn)的特定函數(shù)都沒有實現(xiàn) [Construct]] 內(nèi)置方法
簡單的說侠驯,特定函數(shù)設計之初肯定不是為了用來構造的
這大佬是直接去閱讀的 ECMA 規(guī)范,可靠性很強
那么奕巍,經(jīng)驗積累的方式更多是用于面試的場景吟策,但模擬實現(xiàn) new 是得從代碼層面去判斷,所以的止,還有其他方式可以用來判斷函數(shù)是否能夠作為構造函數(shù)嗎檩坚?
有的,末尾鏈接的文章里诅福,大佬給出了很多種思路匾委,大致列一下:
- 通過構造函數(shù)是否有該屬性判斷 Fn.prototype.constructor,但有局限性氓润,無法處理手動修改的場景
- 通過拋異常方式剩檀,局限性是依賴于原有 new 操作符,而且會導致構造函數(shù)邏輯被先行處理
- 通過 Reflect.construct旺芽,加上 Symbol 的特殊處理后沪猴,就沒有局限性,推薦方案
每種思路采章,文章都有講解运嗜,感興趣可以直接去看看,這里就只挑最后一種來講講:
- 通過
Reflect.construct()
來判斷一個函數(shù)是否能夠作為構造函數(shù)
// 代碼來自文末的鏈接
function is_constructor(f) {
// 特殊判斷悯舟,Symbol 能通過檢測
if (f === Symbol) return false;
try {
Reflect.construct(String, [], f);
} catch (e) {
return false;
}
return true;
}
其實本質(zhì)上也是用拋異常方式來判斷担租,但與直接 new A() 的拋異常方式不同的是,它不會觸發(fā)構造函數(shù)的執(zhí)行抵怎。這就得來看看 Reflect.construct 了:
Reflect.construct 方法等同于 new target(...args)奋救,提供了一種不使用 new 來調(diào)用構造函數(shù)的方法:
function A() {
this.a = 1;
}
new A(); // {a: 1}
// 等價于
Reflect.construct(A, []); // {a: 1}
有的可能就好奇了,既然這樣反惕,那就直接用 Reflect.construct 來模擬實現(xiàn) new 不就好了尝艘,還需要自己寫上面那么多代碼,處理那么多場景么姿染?
emmm背亥,你說的很有道理,是可以這樣沒錯悬赏,但這樣狡汉,不就學不到 new 的職責原理了嗎,不就回答不了面試官的提問了嗎闽颇?
Reflect.construct 還可以接收一個可選的第三個參數(shù):
Reflect.construct(target, argumentsList[, newTarget])
- target: 被調(diào)用的構造函數(shù)
- argumentsList:參數(shù)列表盾戴,類數(shù)組類型數(shù)據(jù)
- new Target:可選,當有傳入時兵多,使用 newTarget.prototype 來作為實例對象的 prototype尖啡,否則使用 target.prototype
- 當 target 或者 newTarget 不能作為構造函數(shù)時橄仆,拋出 TypeError 異常
那么,我們可以怎樣來利用這些特性呢可婶?先看使用原始 new 的方式:
function A(){
console.log(1);
}
B = () => {
console.log(2);
}
new A(); // 輸出1
new B(); // TypeError沿癞,拋異常
// 使用拋異常方式來判斷某個函數(shù)能否作為構造函數(shù)時援雇,如果可以矛渴,那么構造函數(shù)就會被先執(zhí)行一遍,如果剛好在構造函數(shù)內(nèi)處理一些業(yè)務代碼惫搏,那么可能就會有副作用影響了
function isConstructor(Fn) {
try {
new A(); // 能夠判斷出 A 可以作為構造函數(shù)具温,但 A 會被先執(zhí)行一次
// new B(); // 能夠判斷出 B 不能作為構造函數(shù)
} catch(e) {
return false;
}
return true;
}
那么,該如何來使用 Reflect.construct 呢筐赔?
關鍵在于它的第三個參數(shù)铣猩,是用來指定構造函數(shù)生成的對象的 prototype,并不會去執(zhí)行它茴丰,但卻會跟第一個參數(shù)構造函數(shù)一起經(jīng)過能否作為構造函數(shù)([[Construct]])檢查达皿,看看用法:
function A(){
console.log(1);
}
A.prototype.a = 1;
function B() {
console.log(2);
}
B.prototype.a = 2;
var a = Reflect.construct(A, []); // 輸出 1
a.a; // 1,繼承自 A.prototype
var b = Reflect.construct(A, [], B); // 輸出 1
b.a; // 2, 繼承自 B.prototype;
我們來大概寫一下 Reflect.construct 傳入三個參數(shù)時的偽代碼:
Reflect.construct = function(target, args, newTarget) {
check target has [[Construct]]
check newTarget has [[Construct]]
var obj = Object.create(newTarget ? newTarget.prototype : target.prototype)
var result = target.call(obj, ...args);
return result instanceof Object ? result : obj;
}
第一個參數(shù) target 和第三個參數(shù) newTarget 都會進行是否能作為構造函數(shù)使用的檢查贿肩,雖然 target 會被作為構造函數(shù)而調(diào)用峦椰,但我們可以把待檢查的函數(shù)傳給第三個參數(shù),而第一個參數(shù)隨便傳入一個無關但可用來作為構造函數(shù)使用不就好了汰规,所以汤功,代碼是這樣:
// 代碼來自文末的鏈接
function is_constructor(f) {
// 特殊處理,因為 Symbol 能通過 Reflect.construct 對參數(shù)的檢測
if (f === Symbol) return false;
try {
// 第一個 target 參數(shù)傳入無關的構造函數(shù)溜哮,第三個參數(shù)傳入待檢測函數(shù)
Reflect.construct(String, [], f);
} catch (e) {
return false;
}
return true;
}
// 當 f 可作為構造函數(shù)使用滔金,Reflect.construct 就會正常執(zhí)行,那么此時:
// Reflect.construct(String, [], f) 其實相當于執(zhí)行了:
// var a = new String();
// a.__proto__ = f.prototype
// 既不會讓被檢測函數(shù)先行執(zhí)行一遍茂嗓,又可以達到利用引擎層面檢測函數(shù)是否能作為構造函數(shù)的目的
總結
最終餐茵,模擬 new 的實現(xiàn)代碼:
function _new(Fn, ...args) {
function is_constructor(f) {
if (f === Symbol) return false;
try {
Reflect.construct(String, [], f);
} catch (e) {
return false;
}
return true;
}
// 1. 參數(shù)判斷檢測
let isFunction = typeof Fn === 'function';
if (!isFunction || !is_constructor(Fn)) {
throw new TypeError(`${Fn.name || Fn} is not a constructor`);
}
// 2. 創(chuàng)建一個繼承構造函數(shù).prototype的空對象
var obj = Object.create(Fn.prototype);
// 3. 讓空對象作為函數(shù) A 的上下文,并調(diào)用 A述吸,同時獲取它的返回值
let result = Fn.call(obj, ...args);
// 4. 如果構造函數(shù)返回一個對象钟病,那么直接 return 它,否則返回內(nèi)部創(chuàng)建的新對象
return result instanceof Object ? result : obj;
}
幾個關鍵點理清就可以寫出來了:
- 如何判斷某個函數(shù)能否作為構造函數(shù)
- 構造函數(shù)有返回值時的處理
- 構造函數(shù)生成的對象的原型處理