前言
用過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
操作符來呼叫後,生成了一個全新的物件。而且Student
和Object
都是函式,只不過Student
是我們自定義的,Object
是JS
本身就內建的。
再來看下控制檯輸出圖,感興趣的讀者可以在控制檯試試。
new Object()
生成的物件不同的是new Student()
生成的物件中間還巢狀了一層__proto__
,它的constructor
是Student
這個函式。
// 也就是說:
student.constructor === Student;
Student.prototype.constructor === Student;
複製程式碼
小結1:從這個簡單例子來看,new
操作符做了兩件事:
- 建立了一個全新的物件。
- 這個物件會被執行
[[Prototype]]
(也就是__proto__
)連結。
接下來我們再來看升級版的例子2:
// 例子2
function Student(name){
console.log('賦值前-this', this); // {}
this.name = name;
console.log('賦值後-this', this); // {name: '若川'}
}
var student = new Student('若川');
console.log(student); // {name: '若川'}
複製程式碼
由此可以看出:這裡Student
函式中的this
指向new Student()
生成的物件student
。
小結2:從這個例子來看,new
操作符又做了一件事:
- 生成的新物件會繫結到函式呼叫的
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('川');
console.log(student1, student1.doSth()); // {name: '若'} '若'
console.log(student2, student2.doSth()); // {name: '川'} '川'
student1.__proto__ === Student.prototype; // true
student2.__proto__ === Student.prototype; // true
// __proto__ 是瀏覽器實現的檢視原型方案。
// 用ES5 則是:
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true
複製程式碼
關於JS的原型關係筆者之前看到這張圖,覺得很不錯,分享給大家。
小結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('若川');
console.log(student); {name: '若川'}
複製程式碼
筆者測試這七種型別後MDN JavaScript型別,得出的結果是:前面六種基本型別都會正常返回{name: '若川'}
,後面的Object
(包含Functoin
, Array
, Date
, RegExg
, Error
)都會直接返回這些值。
由此得出 小結4:
- 如果函式沒有返回物件型別
Object
(包含Functoin
,Array
,Date
,RegExg
,Error
),那麼new
表示式中的函式呼叫會自動返回這個新的物件。
結合這些小結,整理在一起就是:
- 建立了一個全新的物件。
- 這個物件會被執行
[[Prototype]]
(也就是__proto__
)連結。 - 生成的新物件會繫結到函式呼叫的
this
。 - 通過
new
建立的每個物件將最終被[[Prototype]]
連結到這個函式的prototype
物件上。 - 如果函式沒有返回物件型別
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, '川', 18);
// var student1 = new Student('若');
// var student2 = new Student('川');
console.log(student1, student1.doSth()); // {name: '若'} '若'
console.log(student2, student2.doSth()); // {name: '川'} '川'
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解析
Object.create(proto, [propertiesObject])
方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__。
它接收兩個引數,不過第二個可選引數是屬性描述符(不常用,預設是undefined
)。
var anotherObject = {
name: '若川'
};
var myObject = Object.create(anotherObject, {
age: {
value:18,
},
});
// 獲得它的原型
Object.getPrototypeOf(anotherObject) === Object.prototype; // true 說明anotherObject的原型是Object.prototype
Object.getPrototypeOf(myObject); // {name: "若川"} // 說明myObject的原型是{name: "若川"}
myObject.hasOwnProperty('name'); // false; 說明name是原型上的。
myObject.hasOwnProperty('age'); // true 說明age是自身的
myObject.name; // '若川'
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();
};
}
複製程式碼
到此,文章就基本寫完了。感謝讀者看到這裡。
最後總結一下:
new
做了什麼:
- 建立了一個全新的物件。
- 這個物件會被執行
[[Prototype]]
(也就是__proto__
)連結。- 生成的新物件會繫結到函式呼叫的
this
。- 通過
new
建立的每個物件將最終被[[Prototype]]
連結到這個函式的prototype
物件上。- 如果函式沒有返回物件型別
Object
(包含Functoin
,Array
,Date
,RegExg
,Error
),那麼new
表示式中的函式呼叫會自動返回這個新的物件。
- 怎麼模擬實現
// 去除了註釋
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;
}
複製程式碼
讀者發現有不妥或可改善之處,歡迎指出。另外覺得寫得不錯,可以點個贊,也是對筆者的一種支援。
關於
作者:常以若川為名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
個人部落格
segmentfault
前端視野專欄,開通了前端視野專欄,歡迎關注~
掘金專欄,歡迎關注~
知乎前端視野專欄,開通了前端視野專欄,歡迎關注~
github blog,求個star
^_^~