面試官問:可否模擬實現JS的new操做符

前言

用過Vuejs的同窗都知道,須要用new操做符來實例化。前端

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

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

附上以前寫文章寫過的一段話:已經有不少模擬實現 new操做符的文章,爲何本身還要寫一遍呢。學習就比如是座大山,人們沿着不一樣的路爬山,分享着本身看到的風景。你不必定能看到別人看到的風景,體會到別人的心情。只有本身去爬山,才能看到不同的風景,體會才更加深入。

new 作了什麼

先看簡單例子1github

// 例子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()生成的對象studentsegmentfault

小結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對象上。app

細心的同窗可能會發現這三個例子中的函數都沒有返回值。那麼有返回值會是怎樣的情形呢。
那麼接下來請看例子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表達式中的函數調用會自動返回這個新的對象。

2.怎麼模擬實現:

// 去除了註釋
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,歡迎follow~

相關文章
相關標籤/搜索