模擬實現(xiàn) new 操作符(js)


title: 模擬實現(xiàn) new 操作符
date: 2019/10/22 21:30:25
categories:

  • 面試題
  • 前端

模擬實現(xiàn) new 操作符

本文參考:深度解析 new 原理及模擬實現(xiàn)

首先需要理解,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ù)生成的對象的原型處理

參考

francecil/leetcode:實現(xiàn) new 操作符

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刚梭,一起剝皮案震驚了整個濱河市肠阱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌朴读,老刑警劉巖屹徘,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異衅金,居然都是意外死亡噪伊,警方通過查閱死者的電腦和手機簿煌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鉴吹,“玉大人姨伟,你說我怎么就攤上這事《估” “怎么了夺荒?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長良蒸。 經(jīng)常有香客問我技扼,道長,這世上最難降的妖魔是什么嫩痰? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任剿吻,我火速辦了婚禮,結果婚禮上串纺,老公的妹妹穿的比我還像新娘丽旅。我一直安慰自己,他們只是感情好纺棺,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布榄笙。 她就那樣靜靜地躺著,像睡著了一般五辽。 火紅的嫁衣襯著肌膚如雪办斑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天杆逗,我揣著相機與錄音乡翅,去河邊找鬼。 笑死罪郊,一個胖子當著我的面吹牛蠕蚜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播悔橄,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼靶累,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了癣疟?” 一聲冷哼從身側(cè)響起挣柬,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎睛挚,沒想到半個月后邪蛔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡扎狱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年侧到,在試婚紗的時候發(fā)現(xiàn)自己被綠了勃教。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡匠抗,死狀恐怖故源,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情汞贸,我是刑警寧澤绳军,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站著蛙,受9級特大地震影響删铃,放射性物質(zhì)發(fā)生泄漏耳贬。R本人自食惡果不足惜踏堡,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咒劲。 院中可真熱鬧顷蟆,春花似錦、人聲如沸腐魂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛔屹。三九已至削樊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間兔毒,已是汗流浹背漫贞。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留育叁,地道東北人迅脐。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像豪嗽,于是被迫代替她去往敵國和親谴蔑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容

  • Reflect 對象與 Proxy 對象一樣龟梦,也是 ES6 為了操作對象而提供的新的API隐锭。Reflect 對象的...
    了凡和纖風閱讀 619評論 0 2
  • 第3章 基本概念 3.1 語法 3.2 關鍵字和保留字 3.3 變量 3.4 數(shù)據(jù)類型 5種簡單數(shù)據(jù)類型:Unde...
    RickCole閱讀 5,106評論 0 21
  • defineProperty() 學習書籍《ECMAScript 6 入門 》 Proxy Proxy 用于修改某...
    Bui_vlee閱讀 649評論 0 1
  • 第一章:塊級作用域綁定 塊級聲明 1.var聲明及變量提升機制:在函數(shù)作用域或者全局作用域中通過關鍵字var聲明的...
    BeADre_wang閱讀 825評論 0 0
  • 花生米吃凈了,白酒才下去了半瓶计贰。有的時候越想快點兒睡去钦睡,腦子卻越發(fā)的清醒。 看看表蹦玫,時間是23時53分18秒赎婚,想和...
    刀秋水閱讀 227評論 0 1