三分鐘看完JavaScript原型與原型鏈

clancysong發表於2018-02-27

前戲

三分鐘看完JavaScript原型與原型鏈

  • 寫的比較短了,三分鐘看完應該是沒問題(嗯。。)。
  • 當然最好再花半小時思考理解一下。

正文

建構函式與原型

與大部分面嚮物件語言不同,JavaScript中並沒有引入類(class)的概念,但JavaScript仍然大量地使用了物件,為了保證物件之間的聯絡,JavaScript引入了原型與原型鏈的概念。

在Java中,宣告一個例項的寫法是這樣的:

ClassName obj = new ClassName()
複製程式碼

為了保證JavaScript“看起來像Java”,JavaScript中也加入了new操作符:

var obj = new FunctionName()
複製程式碼

可以看到,與Java不同的是,JavaScript中的new操作符後面跟的並非類名而是函式名,JavaScript並非通過類而是直接通過建構函式來建立例項。

function Dog(name, color) {
    this.name = name
    this.color = color
    this.bark = () => {
        console.log('wangwang~')
    }
}

const dog1 = new Dog('dog1', 'black')
const dog2 = new Dog('dog2', 'white')
複製程式碼

上述程式碼就是宣告一個建構函式並通過建構函式建立例項的過程,這樣看起來似乎有點物件導向的樣子了,但實際上這種方法還存在一個很大的問題。

在上面的程式碼中,有兩個例項被建立,它們有自己的名字、顏色,但它們的bark方法是一樣的,而通過建構函式建立例項的時候,每建立一個例項,都需要重新建立這個方法,再把它新增到新的例項中。這無疑造成了很大的浪費,既然例項的方法都是一樣的,為什麼不把這個方法單獨放到一個地方,並讓所有的例項都可以訪問到呢。


這裡就需要用到原型(prototype)

  • 每一個建構函式都擁有一個prototype屬性,這個屬性指向一個物件,也就是原型物件。當使用這個建構函式建立例項的時候,prototype屬性指向的原型物件就成為例項的原型物件。
  • 原型物件預設擁有一個constructor屬性,指向指向它的那個建構函式(也就是說建構函式和原型物件是互相指向的關係)。
  • 每個物件都擁有一個隱藏的屬性[[prototype]],指向它的原型物件,這個屬性可以通過 Object.getPrototypeOf(obj)obj.__proto__ 來訪問。
  • 實際上,建構函式的prototype屬性與它建立的例項物件的[[prototype]]屬性指向的是同一個物件,即 物件.__proto__ === 函式.prototype
  • 如上文所述,原型物件就是用來存放例項中共有的那部分屬性。
  • 在JavaScript中,所有的物件都是由它的原型物件繼承而來,反之,所有的物件都可以作為原型物件存在。
  • 訪問物件的屬性時,JavaScript會首先在物件自身的屬性內查詢,若沒有找到,則會跳轉到該物件的原型物件中查詢。

那麼可以將上述程式碼稍微做些修改,這裡把bark方法放入Dog建構函式的原型中:

function Dog(name, color) {
    this.name = name
    this.color = color
}

Dog.prototype.bark = () => {
    console.log('wangwang~')
}
複製程式碼

接著再次通過這個建構函式建立例項並呼叫它的bark方法:

const dog1 = new Dog('dog1', 'black')
dog1.bark()  //'wangwang~'
複製程式碼

可以看到bark方法能夠正常被呼叫。這時再建立另一個例項並重寫它的bark方法,然後再次分別呼叫兩個例項的bark方法並觀察結果:

const dog2 = new Dog('dog2', 'white')
dog2.bark() = () => {
    console.log('miaomiaomiao???')
}
dog1.bark()  //'wangwang~'
dog2.bark()  //'miaomiaomiao???'
複製程式碼

這裡dog2重寫bark方法並沒有對dog1造成影響,因為它重寫bark方法的操作實際上是為自己新增了一個新的方法使原型中的bark方法被覆蓋了,而並非直接修改了原型中的方法。若想要修改原型中的方法,需要通過建構函式的prototype屬性:

Dog.prototype.bark = () => {
    console.log('haha~')
}
dog1.bark()  //'haha~'
dog2.bark()  //'haha~'
複製程式碼

這樣看起來就沒什麼問題了,將例項中共有的屬性放到原型物件中,讓所有例項共享這部分屬性。如果想要統一修改所有例項繼承的屬性,只需要直接修改原型物件中的屬性即可。而且每個例項仍然可以重寫原型中已經存在的屬性來覆蓋這個屬性,並且不會影響到其他的例項。

原型鏈與繼承

上文提到,JavaScript中所有的物件都是由它的原型物件繼承而來。而原型物件自身也是一個物件,它也有自己的原型物件,這樣層層上溯,就形成了一個類似連結串列的結構,這就是原型鏈(prototype chain)

所有原型鏈的終點都是Object函式的prototype屬性,因為在JavaScript中的物件都預設由Object()構造。Objec.prototype指向的原型物件同樣擁有原型,不過它的原型是null,而null則沒有原型。

通過原型鏈就可以在JavaScript中實現繼承,JavaScript中的繼承相當靈活,有多種繼承的實現方法,這裡只介紹一種最常用的繼承方法也就是組合繼承

function Dog(name, color) {
    this.name = name
    this.color = color
}

Dog.prototype.bark = () => {
    console.log('wangwang~')
}

function Husky(name, color, weight) {
    Dog.call(this, name, color)
    this.weight = weight
}

Husky.prototype = new Dog()
複製程式碼

這裡宣告瞭一個新的建構函式Husky,通過call方法繼承Dog中的屬性(call方法的作用可以簡單理解為將Dog中的屬性新增到Husky中,因為還涉及到其他的知識點所以不多贅述),並新增了一個weight屬性。然後用Dog函式建立了一個例項作為Husky的原型物件賦值給Husky.prototype以繼承方法。這樣,通過Husky函式建立的例項就擁有了Dog中的屬性和方法。

結語

如果想要深入瞭解關於JavaScript中的物件和原型鏈的話,無腦推薦紅寶書(《JavaScript高階程式設計(第3版)》)吧,第六章關於原型鏈有相當詳細的講解。

相關文章