动态

详情 返回 返回

JavaScript的對象、原型、類和繼承 - 动态 详情

前言

HTML萬物皆標籤。

CSS萬物皆盒。

JavaScript萬物皆對象。

對象

JavaScript對象的本質是數據和功能的集合,語法上表現為鍵值對的集合

對象的鍵可以理解為變量名。

對象的值的類型可以是任意數據類型。

鍵值對

鍵和值之間用:相連。

多組鍵值對之間用,分割。

let profile = {
    name: '吳彥祖',
    age: 48,
    charmingThenMe: false,
    works: ['特警新人類', '新警察故事', '門徒', '除暴'],
    bio: function () {
        alert('你很能打嗎?你會打有個屁用啊。')
    },
    hi() {
        this.bio()
        alert('出來混要有勢力,要有背景,你哪個道上的?')
    }
}

按照值是否為函數這一標準,進一步將鍵值對分為屬性(property)方法(method)

對象為數據和功能的集合,數據對應屬性,功能對應方法。

profile為例,前四個為屬性,後倆為方法。
hi() {
    ...
}

// 等價於

hi: function() {
    ...
}

// 上面的寫法是方法的短語法。

訪問

有兩種方式訪問對象的鍵值對,分別為點式訪問括號式訪問

// 點式訪問
profile.name // '吳彥祖'

// 括號式訪問
profile['age'] // 48

當你發現在一些特殊場景下使用點式訪問無法實現時,記得嘗試括號式訪問。

這些特殊場景大多出現於鍵產生於運行時。

比如:
當你在遍歷中需要從實參中獲取鍵。
或者你需要同時定義對象的鍵和值。

構造函數

實際開發中,若按照上面的方式使用對象,意味着每需要一個profile都需要手動寫出一個擁有相同鍵的對象,這會帶來災難性的後果:

  • 巨量的重複代碼
  • 一旦需要更新屬性或方法,則必須遍歷每一個對象

我們需要抽象

具體來説,我們需要一個函數,可以自動創建具有相同鍵的對象,而不是每次使用時,手動重寫一遍鍵。

// 鍵只需要在定義createProfile()時寫一次
function createProfile(name, age, charmingThenMe, works, bio, hi) {
    let o = {}
    o.name = name
    o.age = age
    o.charmingThenMe = charmingThenMe
    o.works = works
    o.bio = function () {
        alert(bio)
    }
    o.hi = function () {
        o.bio()
        alert(hi)
    }
    return o
}
// 後續生成對象時,只需要寫值,鍵會自動填充
let edisonChen = createProfile(
    '陳冠希',
    42,
    false,
    ['無間道', '頭文字D', '神槍手'],
    '在嗎拓海',
    '微信轉賬三百塊'
)

edisonChen.name
edisonChen.hi()

抽象實現。

createProfile()似乎有點冗餘,我們分析一下createProfile()內部都幹了些什麼:

  1. let o = {} 創建一個空對象
  2. o.foo = bar 為對象添加屬性和方法
  3. return o 返回新創建的對象

這不就是new的作用嗎!

當使用new調用一個函數(此處稱之為f())時,具體會有以下過程:

  1. 創建一個空對象o
  2. o的原型指向f()的prototype屬性
  3. this綁定到o,並執行f()
  4. 返回o

這意味着,如果我們使用new來調用生成對象的函數,我們只需要關注核心的業務邏輯即可,諸如生成空對象、綁定this、返回對象這種雜活兒直接委託出去。

function Profile(name, age, charmingThenMe, works, bio, hi) {
    this.name = name
    this.age = age
    this.charmingThenMe = charmingThenMe
    this.works = works
    this.bio = function () {
        alert(bio)
    }
    this.hi = function () {
        this.bio()
        alert(hi)
    }
}

Profile函數體內只有新對象所需的數據和方法,我們真正關注的也只是這一部分。

至於函數名為什麼從createProfile變成了Profile,這完全是依照慣例的約定俗成:

使用對象名並以大寫開頭作為該對象構造函數的名稱

let j = new Profile('周杰倫', 43, false, ['夜曲', '最偉大的作品'], '喔唷', '不錯哦')

j.works
j.hi()

這就是JavaScript的構造函數。

原型

JavaScript中的每個對象都有一個叫做原型的內建屬性。

原型也是一個對象,原型也有原型,逐級溯源,形成原型鏈

當一個對象的原型為null時,原型鏈結束。

一個對象,不僅能訪問自己獨有的屬性和方法,還可以訪問整個原型鏈上所有對象的屬性和方法。

這解釋了,為什麼你只是聲明瞭一個字符串,就可以調用一批字符串的內建方法。

// 在控制枱中執行以下代碼:
let o = {
  name: 'a',
  hi() {
    console.log(`hello world`);
  }
}

o;

點開控制枱返回的對象,你會發現,除了剛剛自定義的屬性name和方法hi(),還有一個長得很奇怪的 [[Prototype]],點開它你會發現另一個奇怪的鍵————__proto__

這就是對象o的原型,它不僅長得奇怪,甚至連名字都沒有。

是的,ECMAScript認為對象原型“不配擁有姓名”,儘管你可以通過o.__proto__訪問到它,但o.__proto__是不受標準認可的屬性,它只是各大瀏覽器內部的實現,並且已經被官方廢棄。

獲取原型

不要通過__proto__屬性去獲取對象的原型。

使用Object.getPrototypeOf()獲取。

let n = 123

do {
    n = Object.getPrototypeOf(n)
    console.log(n)
} while (n)

// Number
// Object
// null

設置原型

JavaScript中一般使用Object.create()或者constructors構造函數設置原型。

Object.create()

使用實參作為原型生成一個新對象。

let a = {
    hi() {
        console.log('hello world')
    }
}

let b = Object.create(a)

b.hi() // hello world

constructor

JavaScript中,所有函數都有一個叫prototype的屬性,當使用new關鍵字來調用一個構造函數來生成新對象時,構造函數的這個prototype屬性被設置為新生成對象的原型。

這個機制能夠保證:只要指定了構造函數的prototype屬性,所有由構造函數生成的新對象的原型都能保持一致。

// 聲明並初始化一個fruit對象,作為原型對象供構造函數使用
let fruit = {
    hi() {
        console.log(`吃個${this.name}${this.name}`)
    }
}

// 聲明Fruit(構造)函數
function Fruit(name) {
    this.name = name
}

// 設置構造函數的prototype屬性
Fruit.prototype.hi = fruit.hi
// 或者
// Fruit.prototype = fruit

let p = new Fruit('桃') // 生成新對象p

p.hi() // '吃個桃桃'

console.log(Fruit.prototype === Object.getPrototypeOf(p)) // true
// 均為 fruit
  1. 使用字面量方式創建fruit對象,對象中定義了hi方法
  2. 聲明構造函數Fruit,通過thisname屬性和值添加到執行時產生的新對象上
  3. fruithi方法添加到構造函數函數的prototype上(實踐中原型往往具有多個屬性,此時使用直接賦值一次性添加)
  4. 使用new關鍵字生成新對象p
  5. 調用phi方法(p本身並沒有hi方法,而是繼承自fruits
  6. 驗證構造函數的prototype與新對象p的原型的一致性

自有屬性

可以看到,上面的示例中,由構造函數Peach生成的對象p具有兩個鍵:

  1. 一個是屬性name,定義在構造函數中
  2. 一個是方法hi,定義在原型中

那些直接定義在對象上,而非通過繼承獲得的屬性,屬於自有屬性

通過Object.hasOwn()判斷屬性是否為自有屬性:

console.log(Object.hasOwn(p, 'name')) // ture

console.log(Object.hasOwn(p, 'hi')) // false

console.log(Object.hasOwn(fruits, 'hi')) // true
嚴格來説,自有屬性應該被稱之為自有鍵,如果你一定要使用屬性和方法來區分鍵的話。
但屬性在很多語境下是不區分狹義的屬性和方法的,後者在標準中也未被定義。

原型小結

回顧上面的fruits示例,思考下面這個問題:

為什麼要將方法定義在原型中,而將屬性定義構造函數中呢?

因為這種行為與數據分離的機制恰好契合了類和實例。

對象間因具有相同的行為而被抽象為類,行為(方法)被 類(原型)定義。

對象間因數據的差異而成為一個又一個的實例,數據(屬性)被構造函數(返回實例)定義。

原型是JavaScript強大而靈活的特性之一,它使得代碼複用對象組合成為可能。

JavaScript提供了一種更加開發者友好的方式來實現類和實例 —— class

fruits為例:

class Fruits {
    name

    constructor(name) {
        this.name = name
    }

    hi() {
        console.log(`吃個${this.name}${this.name}`)
    }
}

let p = new Fruits('🍑')

p.hi() // 吃個🍑🍑

可以看出,class通過封裝:

  • 聲明並初始化原型對象
  • 聲明構造函數
  • 初始化構造函數的prototype屬性

等步驟,將基於原型鏈生成對象的語法,相較於純構造函數而言,進一步簡化。

省略屬性

你甚至可以省略屬性的聲明。

class Fruits {
    constructor(name) {
        this.name = name
    }

    hi() {
        console.log(`吃個${this.name}${this.name}`)
    }
}

let p = new Fruits('🍑')

p.hi() // 吃個🍑🍑
⚠️注意:實踐中不要省略,因為這會降低代碼的可讀性

屬性的默認值

屬性在初始化的時候,可以指定默認值。

class Fruits {
    name

    constructor(name) {
        this.name = name || '🍉'
    }

    hi() {
        console.log(`吃個${this.name}${this.name}`)
    }
}

let w = new Fruits()

w.hi() // 吃個🍉🍉

省略構造函數

如果沒有初始化的需求,則可以省略構造函數。

class Fruits {
    hi() {
        console.log(`吃個屁`)
    }
}

let p = new Fruits()

p.hi() // 吃個屁

繼承

原型有原型鏈,類有繼承。

以汽車舉例,先定義汽車父類:

class Vehicle {
    brand // 所有的車都有品牌

    constructor(b) {
        this.brand = b
    }

    // 所有的品牌都有標語
    slogan() {
        console.log(`This is ${this.brand}`)
    }
}

通過繼承汽車,定義電動汽車:

class EV extends Vehicle{
    batteryType // 電池類型
    remaining // 剩餘電量

    constructor(b, bt, r) {
        super(b)
        this.batteryType = bt
        this.remaining = r
    }

    charge() {
        let t = 0
        switch (this.batteryType) {
            case '三元鋰':
                t = (1-this.remaining) / 2
                break
            case '磷酸鐵鋰':
                t = 1- this.remaining
                break
            default:
                t = Math.random()
        }
        console.log(
            `尊貴的${this.brand}車主:` +
            `您的${this.batteryType || ''}電池` +
            `只需${Math.ceil(t*60)}分鐘即可充滿!`
        )
    }
}

使用extends從父類繼承屬性和方法,使用super()調用父類的方法。

let t = new EV('Tesla', '三元鋰', 0.3)
t.charge() // 尊貴的Tesla車主:您的三元鋰電池只需21分鐘即可充滿!


let w = new EV('WuLing', '磷酸鐵鋰', 0.5)
w.charge() // 尊貴的WuLing車主:您的磷酸鐵鋰電池只需30分鐘即可充滿!

通過繼承,子類可以使用父類的屬性和方法。

也可以定義子類自己的屬性和方法。

甚至,父類中已有的屬性和方法,也支持在子類中重新定義。

結語

雖然class看起來是個新東西,但本質上它還是原型鏈,或者説,它是原型鏈的語法糖。

JavaScript本質上不是傳統意義上面向對象的編程語言,JavaScript是基於原型的編程語言。

Add a new 评论

Some HTML is okay.