首頁 > 軟體

JS物件建立與繼承的彙總梳理

2022-07-08 14:06:39

引言

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

這些文章是:

驀然回首,“工廠、構造、原型”設計模式,正在燈火闌珊處

JS精粹,原型鏈繼承和建構函式繼承的 “毛病”

“工廠、構造、原型” 設計模式與 JS 繼承

JS 高階程式設計 4:class 繼承的重點

JS class 並不只是簡單的語法糖!

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

物件建立

不難發現,每一篇都離不開工廠、構造、原型這 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")}
}

這樣寫:

  • 寫起來麻煩,重複的程式碼量大;
  • 不利於修改,比如當 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 個相像的工廠,顯得太蠢了。。。

建構函式

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

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 繫結來找不行嗎?

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

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

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

工廠+構造+原型

但,總有人在追求極致。

上述的組合繼承,父類別建構函式被呼叫了兩次,一次是 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 物件和函數的關係:

即使是這樣宣告一個物件,let obj = {} ,它一樣是由建構函式 Object 構造而來的:

let obj = {} 
obj.__proto__ === Object.prototype // true

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

對應程式碼中的意思:

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

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

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

對於本篇來說,繼承,其實都是父子建構函式在繼承,然後再由建構函式範例化物件,以此來實現物件的繼承。

到底是誰在繼承?函數?物件?都是吧~~

小結

本篇由建立物件說起,講了工廠函數,它可以做一層最基本的封裝;

再到,對工廠的拓展,演進為建構函式;

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

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

再講到簡化書寫的 Es6 class ;

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

更多關於JS物件建立繼承的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com