知其然,而知其所以然,JS 對象創建與繼承【彙總梳理】

語言: CN / TW / HK

theme: awesome-green

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第 30 天,點擊查看活動詳情

小序

在 6 月更文中零零散散講了 JS 的對象創建和對象繼承,有工友對此還是表示疑惑,要注意:這是兩個不同但又相關的東西,千萬別搞混了!

這些文章是:

本篇作為彙總篇,來一探究竟!!沖沖衝

image.png

對象創建

不難發現,每一篇都離不開工廠、構造、原型這 3 種設計模式中的至少其一!

讓人不禁想問:JS 為什麼非要用到這種 3 種設計模式了呢??

正本溯源,先從對象創建講起:

我們本來習慣這樣聲明對象(不用任何設計模式)

let car= { price:100, color:"white", run:()=>{console.log("run fast")} }

當有兩個或多個這樣的對象需要聲明時,是不可能一直複製寫下去的:

``` let car1 = { price:100, color:"white", run:()=>{console.log("run fast")} }

let car2 = { price:200, color:"balck", run:()=>{console.log("run slow")} }

let car3 = { price:300, color:"red", run:()=>{console.log("broken")} } ```

這樣寫:

  1. 寫起來麻煩,重複的代碼量大;
  2. 不利於修改,比如當 car 對象要增刪改一個屬性,需要多處進行增刪改;

工廠函數

肯定是要封裝啦,第一個反應,可以 藉助函數 來幫助我們批量創建對象~

於是乎:

``` function makeCar(price,color,performance){ let obj = {} obj.price = price obj.color= color obj.run = ()=>{console.log(performance)} return obj }

let car1= makeCar("100","white","run fast") let car2= makeCar("200","black","run slow") let car3= makeCar("300","red","broken") ```

這就是工廠設計模式在 JS 創建對象時應用的由來~

到這裏,對於【對象創建】來説,應該夠用了吧?是,在不考慮擴展的情況下,基本夠用了。

但這個時候來個新需求,需要創建 car4、car5、car6 對象,它們要在原有基礎上再新增一個 brand 屬性,會怎麼寫?

第一反應,直接修改 makeCar

``` function makeCar(price,color,performance,brand){ let obj = {} obj.price = price obj.color= color obj.run = ()=>{console.log(performance)} obj.brand = brand return obj }

let car4= makeCar("400","white","run fast","benz") let car5= makeCar("500","black","run slow","audi") let car6= makeCar("600","red","broken","tsl") ```

這樣寫,不行,會影響原有的 car1、car2、car3 對象;

那再重新寫一個 makeCarChild 工廠函數行不行?

``` function makeCarChild (price,color,performance,brand){ let obj = {} obj.price = price obj.color= color obj.run = ()=>{console.log(performance)} obj.brand = brand return obj }

let car4= makeCarChild("400","white","run fast","benz") let car5= makeCarChild("500","black","run slow","audi") let car6= makeCarChild("600","red","broken","tsl") ```

行是行,就是太麻煩,全量複製之前的屬性,建立 N 個相像的工廠,顯得太蠢了。。。

image.png

構造函數

於是乎,在工廠設計模式上,發展出了:構造函數設計模式,來解決以上覆用(也就是繼承)的問題。

``` function MakeCar(price,color,performance){ this.price = price this.color= color this.run = ()=>{console.log(performance)} }

function MakeCarChild(brand,...args){ MakeCar.call(this,...args) this.brand = brand }

let car4= new MakeCarChild("benz","400","white","run fast") let car5= new MakeCarChild("audi","500","black","run slow") let car6= new MakeCarChild("tsl","600","red","broken") ```

構造函數區別於工廠函數: * 函數名首字母通常大寫; * 創建對象的時候要用到 new 關鍵字(new 的過程這裏不再贅述了,之前文章有); * 函數沒有 return,而是通過 this 綁定來實現尋找屬性的;

到此為止,工廠函數的複用也解決了。

構造+原型

新的問題在於,我們不能通過查找原型鏈從 MakeCarChild 找到 MakeCar

``` car4.proto===MakeCarChild.prototype // true

MakeCarChild.prototype.proto === MakeCar.prototype // false MakeCarChild.proto === MakeCar.prototype // false ```

無論在原型鏈上怎麼找,都無法從 MakeCarChild 找到 MakeCar

這就意味着:子類不能繼承父類原型上的屬性

這裏提個思考問題:為什麼“要從原型鏈查找到”很重要?為什麼“子類要繼承父類原型上的屬性”?就靠 this 綁定來找不行嗎?

image.png

於是乎,構造函數設計模式 + 原型設計模式 的 【組合繼承】應運而生

``` function MakeCar(price,color,performance){ this.price = price this.color= color this.run = ()=>{console.log(performance)} }

function MakeCarChild(brand,...args){ MakeCar.call(this,...args) this.brand = brand }

MakeCarChild.prototype = new MakeCar() // 原型繼承父類的構造器

MakeCarChild.prototype.constructor = MakeCarChild // 重置 constructor

let car4= new MakeCarChild("benz","400","white","run fast") ```

現在再找原型,就找的到啦:

car4.__proto__ === MakeCarChild.prototype // true MakeCarChild.prototype.__proto__ === MakeCar.prototype // true

其實,能到這裏,就已經很很優秀了,該有的都有了,寫法也不算是很複雜。

工廠+構造+原型

但,總有人在追求極致。

image.png

上述的組合繼承,父類構造函數被調用了兩次,一次是 call 的過程,一次是原型繼承 new 的過程,如果每次實例化,都重複調用,肯定是不可取的,怎樣避免?

工廠 + 構造 + 原型 = 寄生組合繼承 應運而生

核心是,通過工廠函數新建一箇中間商 F( ),複製了一份父類的原型對象,再賦給子類的原型;

``` function object(o) { // 工廠函數 function F() {} F.prototype = o; return new F(); // new 一個空的函數,所佔內存很小 }

function inherit(child, parent) { // 原型繼承 var prototype = object(parent.prototype) prototype.constructor = child child.prototype = prototype }

function MakeCar(price,color,performance){ this.price = price this.color= color this.run = ()=>{console.log(performance)} }

function MakeCarChild(brand,...args){ // 構造函數 MakeCar.call(this,...args) this.brand = brand }

inherit(MakeCarChild,MakeCar)

let car4= new MakeCarChild("benz","400","white","run fast") ```

``` car4.proto === MakeCarChild.prototype // true

MakeCarChild.prototype.proto === MakeCar.prototype // true ```

ES6 class

再到後來,ES6 的 class 作為寄生組合繼承的語法糖:

``` class MakeCar { constructor(price,color,performance){ this.price = price this.color= color this.performance=performance } run(){ console.log(console.log(this.performance)) } }

class MakeCarChild extends MakeCar{ constructor(brand,...args){ super(brand,...args); this.brand= brand; } }

let car4= new MakeCarChild("benz","400","white","run fast") ```

``` car4.proto === MakeCarChild.prototype // true

MakeCarChild.prototype.proto === MakeCar.prototype // true ```

有興趣的工友,可以看下 ES6 解析成 ES5 的代碼:原型與原型鏈 - ES6 Class的底層實現原理 #22

對象與函數

最後本瓜想再談談關於 JS 對象和函數的關係:

image.png

即使是這樣聲明一個對象,let obj = {} ,它一樣是由構造函數 Object 構造而來的:

``` let obj = {}

obj.proto === Object.prototype // true ```

在 JS 中,萬物皆對象,對象都是有函數構造而來,函數本身也是對象。

對應代碼中的意思:

  1. 所有的構造函數的隱式原型都等於 Function 的顯示原型,函數都是由 Function 構造而來,Object 構造函數也不例外;
  2. 所有構造函數的顯示原型的隱式原型,都等於 Object 的顯示原型,Function 也不例外;

``` // 1. Object.proto === Function.prototype // true

// 2. Function.prototype.proto === Object.prototype // true ```

這個設計真的就一個大無語,大糾結,大麻煩。。。

image.png

只能先按之前提過的歪理解記着先:Function 就是上帝,上帝創造了萬物;Object 就是萬物。萬物由上帝創造(對象由函數構造而來),上帝本身也屬於一種物質(函數本身卻也是對象);

對於本篇來説,繼承,其實都是父子構造函數在繼承,然後再由構造函數實例化對象,以此來實現對象的繼承。

到底是誰在繼承?函數?對象?都是吧~~


小結

本篇由創建對象説起,講了工廠函數,它可以做一層最基本的封裝;

再到,對工廠的拓展,演進為構造函數;

再基於原型特點,構造+原型,得出組合繼承;

再追求極致,講到寄生組合;

再講到簡化書寫的 Es6 class ;

以及最後對對象與函數的思考。

就先到這吧~~

OK,以上便是本篇分享。點贊關注評論,為好文助力👍

我是掘金安東尼 🤠 100 萬人氣前端技術博主 💥 INFP 寫作人格堅持 1000 日更文 ✍ 關注我,安東尼陪你一起度過漫長編程歲月 🌏