• QQ咨詢:4001806960
  • 咨詢熱線:400-180-6960

彎道超車,5分鍾快速理解構造器函數與原型對象之間的關系

作者:日期:2019-04-20 12:22:56 點擊:132 構造函數,原型鏈

如果按面向對象的思路去講 JavaScript 的 new,還是很難去理解,我們可以從另一個方向去理解一下它。

你這些人類

我是一名程序員,也是一個人,我可能:

  • 有一個響亮亮的名稱
  • 在某一天出生
  • 是個男人
  • 我能行走
  • 我還能跑步
  • 還能跳躍
  • 能說話
  • 我還能寫代碼

那麽,在 JavaScript 中,我們可能像下面這樣表達我:

 const me = {     name:'大胡子農同工潘半仙',
birth:'1998-08-08',
sex:'male',
walk:function (speed, direction, duration){
//以 speed 的速度向 direction 方向行走 duration長的時間
},
run: function (speed, direction, duration){
//像跑步一樣的速度
},
jump: function (high, direction, angle){
//以 angle 角度向 direction 方向跳 high 高
},
speak: function (letters){
//說出letters 這些詞
},
coding: function (language, line){
//寫程序呢
}}

你們這些人類

當然,這個世界上不可能只有我一個程序員,更不可能只有我一個人,就像我們這個小公司,就有七八百人,似乎所有這些人的數據都保存在數據庫裏面:

name sex birth
潘韬 male 1988-08-08
高超 male 1985-08-09
春雨 male 1999-08-08

我們從數據庫中查詢出上面這幾條記錄,在 JavaScript 可能表示爲一個二維數據,然後要創建出這三個人來,可能是下面這樣的:

 const people = DBDB.query()
//people = [['潘濤','male','1988-08-08'],[...],[...]]
for(let i = 0; i < people.length; i++){
let [name, sex, birth] = people[i];
people[i] = {
name,
sex,
birth,
walk:function (){},
run:function (){},
jump:function (){},
speak:function (){},
coding:function (){}
}}

重複的資源占用

上面大家已經發現,像上面這樣去創建三個對象, walk、run、jump、speak、coding 這五件能做的事情(方法),其實做法都一樣,但是我們卻重複的去描述該怎麽做了,其實就占用了很多資源,所以,我們可能會像下面這樣改進一下:

 const walk = function walk (){}; const run = function run (){}; const jump = function jump (){}; const speak = function speak (){}; const coding = function coding (){};  for(let i = 0; i < people.length; i++){
let [name, sex, birth] = people[i];
people[i] = {
name,
sex,
birth,
walk,
run,
jump,
speak,
coding
}}

不同的人共用相同的資源(方法)

但是這個世界不止有人類

對,人類相比于這個世界上的其它生物來講,數量根本就值得一提,如果像上面這樣,可能各種不同物種能做的事情都會要定義出不同的函數,蠕動肯定不是人類會去做的事情,但很多別的生物會做,那麽爲了代碼管理方便,我們把人能做的所有事情都放在一個對象裏面,這樣就相當于有了一個命名空間了,不會再跟別的物種相沖突:

 const whatPeopleCanDo = {
walk:function (){},
run:function (){},
jump:function (){},
speak:function (){},
coding:function (){}
} for(let i = 0; i < people.length; i++){
let[name, sex, birth] = people[i];
people[i] = {
name,
sex,
birth,
...whatPeopleCanDo
}
}

原型

但是,有的人可能我們並不知道他的 sex 信息是多少,有的也有可能不知道 birth 是多少,但是我們希望在創建這個人的時候,能給不知道的數據一些初始數據,所以, whatPeopleCanDo 並不能完全的表達出一個人,我們再改進:

 const peopleLike = {
name: '',
sex: 'unknown',
birth: '',
walk:function (){},
run:function (){},
jump:function (){},
speak:function (){},
coding:function (){}
}
for(let i = 0; i < people.length; i++){
let[name, sex, birth] = people[i];
people[i] = {
...peopleLike,
name: name || peopleLike.name,
sex:sex || peopleLike.sex,
birth: birth || peopleLike.birth
}}

這樣一來,我們就可以爲不知道的屬性加一些默認值,我們稱 peopleLike 這個東東就爲原型,它表示了像人類這樣的物種有哪些屬性,能幹什麽事情。

 const peoplePrototype = {
name: '',
sex: 'unknown',
birth: '',
walk:function (){},
run:function (){},
jump:function (){},
speak:function (){},
coding:function (){}
}
for(let i = 0; i < people.length; i++){
let[name, sex, birth] = people[i];
people[i] = {
...peoplePrototype,
name: name || peoplePrototype.name,
sex:sex || peoplePrototype.sex,
birth: birth || peoplePrototype.birth,
__proto__:peoplePrototype}}

我們不再把人類原型裏面的所有方法都綁定到某個人身上,而是像上面這樣,用一個特殊的字段 __proto__ 來指定:我的原型是 peoplePrototype 這個對象,同時,我們還制定了一個規則:如果你想請求我的某個方法,在我自己身上沒有,那就去我的原型上面找吧,如果我的原型上面沒有,那就去我的原型的原型上面去找,直到某個位置,沒有更上層的原型爲止

像上面這樣創建的 people 對象,有自己的屬性,但是當我們去訪問 people.speak() 方法的時候,其實訪問的是 people.__proto__.speak(),這是我們的規則。

更優雅的創建新新人類

我們總不能在需要創建新人的時候,都像上面這樣,自己去寫一個對象,然後再手工指定它的原型是什麽,所以,我們可以創建一個函數,專門用來生成人類的:

 const peoplePrototype = {
name: '',
sex: 'unknown',
birth: '',
walk:function (){},
run:function (){},
jump:function (){},
speak:function (){},
coding:function (){}
}
for(let i = 0; i < people.length; i++){
let[name, sex, birth] = people[i];
people[i] = {
...peoplePrototype,
name: name || peoplePrototype.name,
sex:sex || peoplePrototype.sex,
birth: birth || peoplePrototype.birth,
__proto__:peoplePrototype}}

現在這樣我們只需要引入 makePeople 這個函數就可以隨時隨地創建新人了。

更優雅一點的改進

顯然,上面這樣並不是最好的辦法,定義了一個原型,又定義了一個原型對象,我們可以把這兩個合並到一起,所以,就可以有下面這樣的實現了:

 const People = function People (name, sex, birth){
let people = {};
people.name = name || people.prototype.name;
people.sex = sex || people.prototype.sex;
people.birth = birth || people.prototype.birth;
people.__proto__ = People.prototype;
return people;
} People.prototype = {
name: '',
sex: 'unknown',
birth: '',
walk:function (){},
run:function (){},
jump:function (){},
speak:function (){},
coding:function (){}}

我們直接把創建人類的那個函數叫作 People,這個函數有一個屬性叫 prototype,它表示用我這個函數創建的對象的原型是什麽,這個函數做的事情還是以前那些事兒,創建臨時對象,設置對象的屬性,綁定一下原型,然後返回。

神奇的 this

我們除了人,還有別的動物,比如 Tiger、Fish等,按上面的方式,在 Tiger() 或者 Fish() 函數裏面都會建立不同的 tiger 或者 fish 名稱的臨時對象,這樣太麻煩,我們把這種函數創建出來的對象,都可以統一叫作“這個對象” ,也就是 this object,不在關心是人是鬼,統一把所有的臨時對象都叫 thisObject 或者更簡單的就叫作:這個,即 this。

 const People = function People (name, sex, birth){
let this = {};
this.name = name || people.prototype.name;
this.sex = sex || people.prototype.sex;
this.birth = birth || people.prototype.birth;
this.__proto__ = People.prototype;
return this;
}

當然,上面的這一段代碼是有問題的,只是假想一樣,這樣是不是可行。

new

到現在爲止,我們發現了整個代碼的演變,是時候引出這個 new 了,它來幹什麽呢?它後面接一個類似上面這種 People 的函數,表示我需要創建一個 People 的實例,它的發明就是爲了解決上面這些所有重複的事情。

有了 new 之後,我們不需要再每一次定義一個臨時對象,在 new 的上下文關系中,會在 People 函數體內自動爲創建一個臨時變量 this,這個就表示即將被創建出來的對象。同時,對于使用 new 創建的實例,會自動的綁定到創建函數的 prototype 作爲原型,還會自動爲 People 創建一個 constructor 函數,表示這個原型的創建函數是什麽,所以,我們可以改成下面這樣的了:

 const People = function People (name, sex, birth){
this.name = name || people.prototype.name;
this.sex = sex || people.prototype.sex;
this.birth = birth || people.prototype.birth;
}
People.prototype.name = '';
People.prototype.sex = 'unknown';
People.prototype.birth = '';
People.prototype.walk = function (){};
People.prototype.jump = function (){};
People.prototype.speak = function (){};
People.prototype.coding = function (){};

people = people.map(p => new People(...p));

總結

new 到底幹了什麽?當 new People() 的時候

  • 1、創建臨時變量 this,並將 this 綁定到 People 函數體裏
  • 2、執行 People.prototype.constructor = People
  • 3、執行 this.__proto__ = People.prototype
  • 4、執行 People 函數體中的自定義
  • 5、返回新創建的對象

本文來自前端社,可以掃描進行關注

上一篇: webpack4 打包優化策略(圖文)

下一篇: 如何利用 JS 的 Set 對象讓你的代碼運行的更快