原文:https://zhehuaxuan.github.io/2019/02/21/JavaScript%E6%96%B0%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%A8%A1%E6%8B%9F/
作者:zhehuaxuan
寫在前面的話
前端的入門相對簡單,相對於其他方向天花板可能會相對較低。但是在市場上一個優秀的前端依舊是很搶手的。能夠站在金字塔上的人往往寥寥無幾。
目前前端也已經一年半了,在公司的知識棧相對落後,就業形勢不容樂觀,所以有必要自己琢磨,往中高階前端進階。後續我將推出《JavaScript進階系列》,一方面是一個監督自己學習的一個過程,另一方面也會給看到的童鞋一些啟發。
JavaScript新建物件的過程
在ES5中定義一個函式來建立物件,如下:
function Person(name){
this.name = name;
}
Person.prototype.getName = function(){
return name;
}
var person = new Person("xuan");
console.log(person.name);//輸出:xuan
console.log(person.getName());//輸出:xuan
複製程式碼
我們看到當我們新建一個物件,我們就可以訪問構造器中的指向this的屬性,還可以訪問原型中的屬性。我們不妨把JavaScript呼叫new的過程主要由下面四步組成:
- 新生成一個空物件
- 將空物件連結到原型中
- 繫結this
- 返回新物件
下面跟著我按照這個思路來建立物件:
function create(){
//Todo
}
person = create(Person,"xuan");//create(ObjectName,...arguments)
複製程式碼
我們使用如上所示的函式來模擬new關鍵字。
首先第一步新建一個物件:
function create(){
var obj = new Object();
return obj;
}
person = create(Person,"xuan");
複製程式碼
現在已經建立並返回一個物件,當然現在列印出來肯定是一個普通的物件,畢竟流程還沒有走完,我們接著往下看。
第二步連結到原型中:
function create(){
var obj = new Object();
var constructor = [].shift.call(arguments);
console.log(constructor);
console.log(arguments);
obj.__proto__ = constructor.prototype;
return obj;
}
person = create(Person,"xuan");
複製程式碼
現在把建構函式和引數都列印出來了。沒問題!
第三步繫結this,如下:
function create() {
let obj = new Object();
let constructor = [].shift.call(arguments)
obj.__proto__ = constructor.prototype
constructor.apply(obj, arguments);
console.log(obj);
return obj;
}
person = create(Person,"xuan");
複製程式碼
列印結果實現new物件的效果。
現在改一下建構函式程式碼:
function Person(name){
this.name = name;
return {
name:"abc"
}
}
var person = new Person("xuan");
console.log(person);
console.log(Object.prototype.toString.call(person));
複製程式碼
效果如下:
我們執行一下我們構建的函式效果如下:
發現不一致,所以我們要處理第三步繫結this中apply函式的返回值:
function create() {
let obj = new Object();
let constructor = [].shift.call(arguments)
obj.__proto__ = constructor.prototype
//constructor.apply(obj, arguments);
let res = constructor.apply(obj, arguments);
if(res){
return res;
}else{
return obj;
}
}
person = create(Person,"xuan");
複製程式碼
效果如下:
完美!
現在我們思考一下這裡的res返回值有三種情況:undefined,基本型別,物件。
如果res是undefined時,返回obj;
如果res是基本型別我們也返回obj;
如果res是物件我們返回res物件;
綜合一下:
如果返回的res物件是Object型別那麼返回res,否則返回obj。當然其他的判斷條件也是可以的。最後程式碼優化如下:
function create() {
let obj = new Object();
let constructor = [].shift.call(arguments)
obj.__proto__ = constructor.prototype
//constructor.apply(obj, arguments);
let res = constructor.apply(obj, arguments);
return res instanceof Object?res:obj;
}
person = create(Person,"xuan");
複製程式碼
幾個問題
現在的程式碼已經完美了麼?我們先來提幾個問題。
- new Object()建立的物件純淨麼?
- 為啥使用[].shift.call()來進行引數分割?arguments是一個陣列麼?
new Object()建立的物件純淨麼?
首先什麼是純淨?我們定義一個物件的__proto__
屬性為空的物件是一個純淨的物件。
在第二步的時候中已經改變的obj的原型鏈,所以無論它前面的原型鏈是咋樣的都無所謂,但是為了保證物件的純淨性,我們有必要引出Object.create()
,該方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__
。我們來看一下:
var person1 = Object.create({});
複製程式碼
列印如下:
我們看到person1的__proto__
指向了{}
物件,所以我們在上述程式碼中直接修改如下:
function create() {
let constructor = [].shift.call(arguments);
let obj = Object.create(constructor.prototype);
let res = constructor.apply(obj, arguments);
return res instanceof Object?res:obj;
}
person = create(Person,"xuan");
複製程式碼
為啥使用[].shift.call()來進行引數分割?arguments是一個陣列麼?
首先我們知道arguments是函式傳入的引數,那麼這個引數是陣列麼?我們列印一下便知:
console.log(arguments);
console.log(Object.prototype.toString.call(arguments));
console.log(arguments instanceof Array);
複製程式碼
結果如下
不是陣列。我們展開發現他跟陣列很像,查一下資料發現這個物件是類陣列。裡面沒有shift函式,直接呼叫shift會報錯。我們使用使用Array.from(arguments)將arguments轉成陣列,然後在呼叫shift函式也是一種思路。但是在這裡我們使用apply最適合。所以下述程式碼是模擬new Object()的最優程式碼:
function create() {
let constructor = [].shift.call(arguments);
let obj = Object.create(constructor.prototype);
let res = constructor.apply(obj, arguments);
return res instanceof Object?res:obj;
}
person = create(Person,"xuan");
複製程式碼
還有更優的實現方法,請大佬們不吝拍磚!