面試官問(wèn):能否模擬實(shí)現(xiàn)JS的new操作符

前言

用過(guò)Vuejs的同學(xué)都知道,需要用new操作符來(lái)實(shí)例化。

new Vue({
    el: '#app',
    mounted(){},
});

那么面試官可能會(huì)問(wèn)是否想過(guò)new到底做了什么,怎么模擬實(shí)現(xiàn)呢。

附上之前寫(xiě)文章寫(xiě)過(guò)的一段話:已經(jīng)有很多模擬實(shí)現(xiàn)new操作符的文章,為什么自己還要寫(xiě)一遍呢。學(xué)習(xí)就好比是座大山,人們沿著不同的路登山,分享著自己看到的風(fēng)景。你不一定能看到別人看到的風(fēng)景,體會(huì)到別人的心情。只有自己去登山,才能看到不一樣的風(fēng)景,體會(huì)才更加深刻。

new 做了什么

先看簡(jiǎn)單例子1

// 例子1
function Student(){
}
var student = new Student();
console.log(student); // {}
// student 是一個(gè)對(duì)象。
console.log(Object.prototype.toString.call(student)); // [object Object]
// 我們知道平時(shí)聲明對(duì)象也可以用new Object(); 只是看起來(lái)更復(fù)雜
// 順便提一下 `new Object`(不推薦)和Object()也是一樣的效果
// 可以猜測(cè)內(nèi)部做了一次判斷,用new調(diào)用
/** if (!(this instanceof Object)) {
*    return new Object();
*  }
*/
var obj = new Object();
console.log(obj) // {}
console.log(Object.prototype.toString.call(student)); // [object Object]

typeof Student === 'function' // true
typeof Object === 'function' // true

從這里例子中,我們可以看出:一個(gè)函數(shù)用new操作符來(lái)調(diào)用后,生成了一個(gè)全新的對(duì)象。而且StudentObject都是函數(shù),只不過(guò)Student是我們自定義的,ObjectJS本身就內(nèi)置的。
再來(lái)看下控制臺(tái)輸出圖,感興趣的讀者可以在控制臺(tái)試試。

例子1 控制臺(tái)輸出圖

new Object() 生成的對(duì)象不同的是new Student()生成的對(duì)象中間還嵌套了一層__proto__,它的constructorStudent這個(gè)函數(shù)。

// 也就是說(shuō):
student.constructor === Student;
Student.prototype.constructor === Student;

小結(jié)1:從這個(gè)簡(jiǎn)單例子來(lái)看,new操作符做了兩件事:

  1. 創(chuàng)建了一個(gè)全新的對(duì)象。
  2. 這個(gè)對(duì)象會(huì)被執(zhí)行[[Prototype]](也就是__proto__)鏈接。

接下來(lái)我們?cè)賮?lái)看升級(jí)版的例子2

// 例子2
function Student(name){
    console.log('賦值前-this', this); // {}
    this.name = name;
    console.log('賦值后-this', this); // {name: '軒轅Rowboat'}
}
var student = new Student('軒轅Rowboat');
console.log(student); // {name: '軒轅Rowboat'}

由此可以看出:這里Student函數(shù)中的this指向new Student()生成的對(duì)象student。

小結(jié)2:從這個(gè)例子來(lái)看,new操作符又做了一件事:

  1. 生成的新對(duì)象會(huì)綁定到函數(shù)調(diào)用的this。

接下來(lái)繼續(xù)看升級(jí)版例子3

// 例子3
function Student(name){
    this.name = name;
    // this.doSth();
}
Student.prototype.doSth = function() {
    console.log(this.name);
};
var student1 = new Student('軒轅');
var student2 = new Student('Rowboat');
console.log(student1, student1.doSth()); // {name: '軒轅'} '軒轅'
console.log(student2, student2.doSth()); // {name: 'Rowboat'} 'Rowboat'
student1.__proto__ === Student.prototype; // true
student2.__proto__ === Student.prototype; // true
// __proto__ 是瀏覽器實(shí)現(xiàn)的查看原型方案。
// 用ES5 則是:
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true
例子3 控制臺(tái)輸出圖

關(guān)于JS的原型關(guān)系筆者之前看到這張圖,覺(jué)得很不錯(cuò),分享給大家。


JavaScript原型關(guān)系圖

小結(jié)3:這個(gè)例子3再一次驗(yàn)證了小結(jié)1中的第2點(diǎn)。

也就是這個(gè)對(duì)象會(huì)被執(zhí)行[[Prototype]](也就是__proto__)鏈接。并且通過(guò)new Student()創(chuàng)建的每個(gè)對(duì)象將最終被[[Prototype]]鏈接到這個(gè)Student.protytype對(duì)象上。

細(xì)心的同學(xué)可能會(huì)發(fā)現(xiàn)這三個(gè)例子中的函數(shù)都沒(méi)有返回值。那么有返回值會(huì)是怎樣的情形呢。
那么接下來(lái)請(qǐng)看例子4

// 例子4
function Student(name){
    this.name = name;
    // Null(空) null
    // Undefined(未定義) undefined
    // Number(數(shù)字) 1
    // String(字符串)'1'
    // Boolean(布爾) true
    // Symbol(符號(hào))(第六版新增) symbol
    
    // Object(對(duì)象) {}
        // Function(函數(shù)) function(){}
        // Array(數(shù)組) []
        // Date(日期) new Date()
        // RegExp(正則表達(dá)式)/a/
        // Error (錯(cuò)誤) new Error() 
    // return /a/;
}
var student = new Student('軒轅Rowboat');
console.log(student); {name: '軒轅Rowboat'}

筆者測(cè)試這七種類(lèi)型后MDN JavaScript類(lèi)型,得出的結(jié)果是:前面六種基本類(lèi)型都會(huì)正常返回{name: '軒轅Rowboat'},后面的Object(包含Functoin, Array, Date, RegExg, Error)都會(huì)直接返回這些值。

由此得出 小結(jié)4:

  1. 如果函數(shù)沒(méi)有返回對(duì)象類(lèi)型Object(包含Functoin, Array, Date, RegExg, Error),那么new表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新的對(duì)象。

結(jié)合這些小結(jié),整理在一起就是:

  1. 創(chuàng)建了一個(gè)全新的對(duì)象。
  2. 這個(gè)對(duì)象會(huì)被執(zhí)行[[Prototype]](也就是__proto__)鏈接。
  3. 生成的新對(duì)象會(huì)綁定到函數(shù)調(diào)用的this。
  4. 通過(guò)new創(chuàng)建的每個(gè)對(duì)象將最終被[[Prototype]]鏈接到這個(gè)函數(shù)的prototype對(duì)象上。
  5. 如果函數(shù)沒(méi)有返回對(duì)象類(lèi)型Object(包含Functoin, Array, Date, RegExg, Error),那么new表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新的對(duì)象。

new 模擬實(shí)現(xiàn)

知道了這些現(xiàn)象,我們就可以模擬實(shí)現(xiàn)new操作符。直接貼出代碼和注釋

/**
 * 模擬實(shí)現(xiàn) new 操作符
 * @param  {Function} ctor [構(gòu)造函數(shù)]
 * @return {Object|Function|Regex|Date|Error}      [返回結(jié)果]
 */
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    // ES6 new.target 是指向構(gòu)造函數(shù)
    newOperator.target = ctor;
    // 1.創(chuàng)建一個(gè)全新的對(duì)象,
    // 2.并且執(zhí)行[[Prototype]]鏈接
    // 4.通過(guò)`new`創(chuàng)建的每個(gè)對(duì)象將最終被`[[Prototype]]`鏈接到這個(gè)函數(shù)的`prototype`對(duì)象上。
    var newObj = Object.create(ctor.prototype);
    // ES5 arguments轉(zhuǎn)成數(shù)組 當(dāng)然也可以用ES6 [...arguments], Aarry.from(arguments);
    // 除去ctor構(gòu)造函數(shù)的其余參數(shù)
    var argsArr = [].slice.call(arguments, 1);
    // 3.生成的新對(duì)象會(huì)綁定到函數(shù)調(diào)用的`this`。
    // 獲取到ctor函數(shù)返回結(jié)果
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    // 小結(jié)4 中這些類(lèi)型中合并起來(lái)只有Object和Function兩種類(lèi)型 typeof null 也是'object'所以要不等于null,排除null
    var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    // 5.如果函數(shù)沒(méi)有返回對(duì)象類(lèi)型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新的對(duì)象。
    return newObj;
}

最后用模擬實(shí)現(xiàn)的newOperator函數(shù)驗(yàn)證下之前的例子3

// 例子3 多加一個(gè)參數(shù)
function Student(name, age){
    this.name = name;
    this.age = age;
    // this.doSth();
    // return Error();
}
Student.prototype.doSth = function() {
    console.log(this.name);
};
var student1 = newOperator(Student, '軒轅', 18);
var student2 = newOperator(Student, 'Rowboat', 18);
// var student1 = new Student('軒轅');
// var student2 = new Student('Rowboat');
console.log(student1, student1.doSth()); // {name: '軒轅'} '軒轅'
console.log(student2, student2.doSth()); // {name: 'Rowboat'} 'Rowboat'

student1.__proto__ === Student.prototype; // true
student2.__proto__ === Student.prototype; // true
// __proto__ 是瀏覽器實(shí)現(xiàn)的查看原型方案。
// 用ES5 則是:
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true

可以看出,很符合new操作符。讀者發(fā)現(xiàn)有不妥或可改善之處,歡迎指出。
回顧這個(gè)模擬new函數(shù)newOperator實(shí)現(xiàn),最大的功臣當(dāng)屬于Object.create()這個(gè)ES5提供的API。

Object.create() 用法舉例

筆者之前整理的一篇文章中也有講過(guò),可以翻看JavaScript 對(duì)象所有API解析

MDN Object.create()

Object.create(proto, [propertiesObject])
方法創(chuàng)建一個(gè)新對(duì)象,使用現(xiàn)有的對(duì)象來(lái)提供新創(chuàng)建的對(duì)象的proto。
它接收兩個(gè)參數(shù),不過(guò)第二個(gè)可選參數(shù)是屬性描述符(不常用,默認(rèn)是undefined)。

var anotherObject = {
    name: '軒轅Rowboat'
};
var myObject = Object.create(anotherObject, {
    age: {
        value:18,
    },
});
// 獲得它的原型
Object.getPrototypeOf(anotherObject) === Object.prototype; // true 說(shuō)明anotherObject的原型是Object.prototype
Object.getPrototypeOf(myObject); // {name: "軒轅Rowboat"} // 說(shuō)明myObject的原型是{name: "軒轅Rowboat"}
myObject.hasOwnProperty('name'); // false; 說(shuō)明name是原型上的。
myObject.hasOwnProperty('age'); // true 說(shuō)明age是自身的
myObject.name; // '軒轅Rowboat'
myObject.age; // 18;

對(duì)于不支持ES5的瀏覽器,MDN上提供了ployfill方案。

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;

        return new F();
    };
}

到此,文章就基本寫(xiě)完了。感謝讀者看到這里。

最后總結(jié)一下:

1.new做了什么:

  1. 創(chuàng)建了一個(gè)全新的對(duì)象。
  2. 這個(gè)對(duì)象會(huì)被執(zhí)行[[Prototype]](也就是__proto__)鏈接。
  3. 生成的新對(duì)象會(huì)綁定到函數(shù)調(diào)用的this
  4. 通過(guò)new創(chuàng)建的每個(gè)對(duì)象將最終被[[Prototype]]鏈接到這個(gè)函數(shù)的prototype對(duì)象上。
  5. 如果函數(shù)沒(méi)有返回對(duì)象類(lèi)型Object(包含Functoin, Array, Date, RegExg, Error),那么new表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新的對(duì)象。

2.怎么模擬實(shí)現(xiàn):

// 去除了注釋
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    newOperator.target = ctor;
    var newObj = Object.create(ctor.prototype);
    var argsArr = [].slice.call(arguments, 1);
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    return newObj;
}

讀者發(fā)現(xiàn)有不妥或可改善之處,歡迎指出。另外覺(jué)得寫(xiě)得不錯(cuò),可以點(diǎn)個(gè)贊,也是對(duì)筆者的一種支持。

關(guān)于

作者:常以軒轅Rowboat為名混跡于江湖。前端路上 | PPT愛(ài)好者 | 所知甚少,唯善學(xué)。
個(gè)人博客
segmentfault個(gè)人主頁(yè)
掘金個(gè)人主頁(yè)
知乎
github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 Object 對(duì)象 教程:https://wangdoc.com/javascript/stdlib/obje...
    智勇雙全的小六閱讀 2,488評(píng)論 0 0
  • 學(xué)習(xí)目標(biāo): 理解面向?qū)ο箝_(kāi)發(fā)思想 掌握 JavaScript 面向?qū)ο箝_(kāi)發(fā)相關(guān)模式 掌握在 JavaScript ...
    金桔檸檬加冰閱讀 435評(píng)論 0 0
  • JS中原型鏈,說(shuō)簡(jiǎn)單也簡(jiǎn)單。 首先明確: 函數(shù)(Function)才有prototype屬性,對(duì)象(除Object...
    前小白閱讀 4,066評(píng)論 0 9
  • 百度云剛推出時(shí)我就開(kāi)始使用,雖然那時(shí)的傳輸速度、內(nèi)容都不盡如人意,但是經(jīng)過(guò)幾年的改進(jìn),它在我的生活中也開(kāi)始占據(jù)了一...
    江南雪閱讀 510評(píng)論 0 0
  • 豆豆是個(gè)熱情美麗的女孩子,對(duì)于這個(gè)熱情又美麗的女孩發(fā)出的5月小組組長(zhǎng)邀請(qǐng)任何人是拒絕不了的,我也不例外。事實(shí)證明,...
    七七粑粑閱讀 177評(píng)論 0 3

友情鏈接更多精彩內(nèi)容