面試官問:能否模擬實現JS的new操作符

若川發表於2018-11-05

前言

用過Vuejs的同學都知道,需要用new操作符來例項化。

new Vue({
    el: '#app',
    mounted(){},
});
複製程式碼

那麼面試官可能會問是否想過new到底做了什麼,怎麼模擬實現呢。

附上之前寫文章寫過的一段話:已經有很多模擬實現new操作符的文章,為什麼自己還要寫一遍呢。學習就好比是座大山,人們沿著不同的路登山,分享著自己看到的風景。你不一定能看到別人看到的風景,體會到別人的心情。只有自己去登山,才能看到不一樣的風景,體會才更加深刻。

new 做了什麼

先看簡單例子1

// 例子1
function Student(){
}
var student = new Student();
console.log(student); // {}
// student 是一個物件。
console.log(Object.prototype.toString.call(student)); // [object Object]
// 我們知道平時宣告物件也可以用new Object(); 只是看起來更復雜
// 順便提一下 `new Object`(不推薦)和Object()也是一樣的效果
// 可以猜測內部做了一次判斷,用new呼叫
/** 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
複製程式碼

從這裡例子中,我們可以看出:一個函式用new操作符來呼叫後,生成了一個全新的物件。而且StudentObject都是函式,只不過Student是我們自定義的,ObjectJS本身就內建的。 再來看下控制檯輸出圖,感興趣的讀者可以在控制檯試試。

例子1 控制檯輸出圖
new Object() 生成的物件不同的是new Student()生成的物件中間還巢狀了一層__proto__,它的constructorStudent這個函式。

// 也就是說:
student.constructor === Student;
Student.prototype.constructor === Student;
複製程式碼

小結1:從這個簡單例子來看,new操作符做了兩件事:

  1. 建立了一個全新的物件。
  2. 這個物件會被執行[[Prototype]](也就是__proto__)連結。

接下來我們再來看升級版的例子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函式中的this指向new Student()生成的物件student

小結2:從這個例子來看,new操作符又做了一件事:

  1. 生成的新物件會繫結到函式呼叫的this

接下來繼續看升級版例子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__ 是瀏覽器實現的檢視原型方案。
// 用ES5 則是:
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true
複製程式碼

例子3 控制檯輸出圖
關於JS的原型關係筆者之前看到這張圖,覺得很不錯,分享給大家。
JavaScript原型關係圖

小結3:這個例子3再一次驗證了小結1中的第2點。也就是這個物件會被執行[[Prototype]](也就是__proto__)連結。並且通過new Student()建立的每個物件將最終被[[Prototype]]連結到這個Student.protytype物件上。

細心的同學可能會發現這三個例子中的函式都沒有返回值。那麼有返回值會是怎樣的情形呢。 那麼接下來請看例子4

// 例子4
function Student(name){
    this.name = name;
    // Null(空) null
    // Undefined(未定義) undefined
    // Number(數字) 1
    // String(字串)'1'
    // Boolean(布林) true
    // Symbol(符號)(第六版新增) symbol
    
    // Object(物件) {}
        // Function(函式) function(){}
        // Array(陣列) []
        // Date(日期) new Date()
        // RegExp(正規表示式)/a/
        // Error (錯誤) new Error() 
    // return /a/;
}
var student = new Student('軒轅Rowboat');
console.log(student); {name: '軒轅Rowboat'}
複製程式碼

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

由此得出 小結4:

  1. 如果函式沒有返回物件型別Object(包含Functoin, Array, Date, RegExg, Error),那麼new表示式中的函式呼叫會自動返回這個新的物件。

結合這些小結,整理在一起就是:

  1. 建立了一個全新的物件。
  2. 這個物件會被執行[[Prototype]](也就是__proto__)連結。
  3. 生成的新物件會繫結到函式呼叫的this
  4. 通過new建立的每個物件將最終被[[Prototype]]連結到這個函式的prototype物件上。
  5. 如果函式沒有返回物件型別Object(包含Functoin, Array, Date, RegExg, Error),那麼new表示式中的函式呼叫會自動返回這個新的物件。

new 模擬實現

知道了這些現象,我們就可以模擬實現new操作符。直接貼出程式碼和註釋

/**
 * 模擬實現 new 操作符
 * @param  {Function} ctor [建構函式]
 * @return {Object|Function|Regex|Date|Error}      [返回結果]
 */
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    // ES6 new.target 是指向建構函式
    newOperator.target = ctor;
    // 1.建立一個全新的物件,
    // 2.並且執行[[Prototype]]連結
    // 4.通過`new`建立的每個物件將最終被`[[Prototype]]`連結到這個函式的`prototype`物件上。
    var newObj = Object.create(ctor.prototype);
    // ES5 arguments轉成陣列 當然也可以用ES6 [...arguments], Aarry.from(arguments);
    // 除去ctor建構函式的其餘引數
    var argsArr = [].slice.call(arguments, 1);
    // 3.生成的新物件會繫結到函式呼叫的`this`。
    // 獲取到ctor函式返回結果
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    // 小結4 中這些型別中合併起來只有Object和Function兩種型別 typeof null 也是'object'所以要不等於null,排除null
    var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    // 5.如果函式沒有返回物件型別`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那麼`new`表示式中的函式呼叫會自動返回這個新的物件。
    return newObj;
}
複製程式碼

最後用模擬實現的newOperator函式驗證下之前的例子3

// 例子3 多加一個引數
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__ 是瀏覽器實現的檢視原型方案。
// 用ES5 則是:
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true
複製程式碼

可以看出,很符合new操作符。讀者發現有不妥或可改善之處,歡迎指出。 回顧這個模擬new函式newOperator實現,最大的功臣當屬於Object.create()這個ES5提供的API

Object.create() 用法舉例

筆者之前整理的一篇文章中也有講過,可以翻看JavaScript 物件所有API解析

MDN Object.create()

Object.create(proto, [propertiesObject]) 方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__。 它接收兩個引數,不過第二個可選引數是屬性描述符(不常用,預設是undefined)。

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

對於不支援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();
    };
}
複製程式碼

到此,文章就基本寫完了。感謝讀者看到這裡。

最後總結一下:

  1. new做了什麼:
  1. 建立了一個全新的物件。
  2. 這個物件會被執行[[Prototype]](也就是__proto__)連結。
  3. 生成的新物件會繫結到函式呼叫的this
  4. 通過new建立的每個物件將最終被[[Prototype]]連結到這個函式的prototype物件上。
  5. 如果函式沒有返回物件型別Object(包含Functoin, Array, Date, RegExg, Error),那麼new表示式中的函式呼叫會自動返回這個新的物件。
  1. 怎麼模擬實現
// 去除了註釋
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;
}
複製程式碼

讀者發現有不妥或可改善之處,歡迎指出。另外覺得寫得不錯,可以點個贊,也是對筆者的一種支援。

關於

作者:常以軒轅Rowboat若川為名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
個人部落格
segmentfault個人主頁
掘金個人主頁
知乎
github

相關文章