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

前戲

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

正文

構造函數與原型

與大部分面嚮對象語言不一樣,JavaScript中並無引入類(class)的概念,但JavaScript仍然大量地使用了對象,爲了保證對象之間的聯繫,JavaScript引入了原型與原型鏈的概念。函數

在Java中,聲明一個實例的寫法是這樣的:ui

ClassName obj = new ClassName()
複製代碼

爲了保證JavaScript「看起來像Java」,JavaScript中也加入了new操做符:this

var obj = new FunctionName()
複製代碼

能夠看到,與Java不一樣的是,JavaScript中的new操做符後面跟的並不是類名而是函數名,JavaScript並不是經過類而是直接經過構造函數來建立實例。spa

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')
複製代碼

上述代碼就是聲明一個構造函數並經過構造函數建立實例的過程,這樣看起來彷佛有點面向對象的樣子了,但實際上這種方法還存在一個很大的問題。prototype

在上面的代碼中,有兩個實例被建立,它們有本身的名字、顏色,但它們的bark方法是同樣的,而經過構造函數建立實例的時候,每建立一個實例,都須要從新建立這個方法,再把它添加到新的實例中。這無疑形成了很大的浪費,既然實例的方法都是同樣的,爲何不把這個方法單獨放到一個地方,並讓全部的實例均可以訪問到呢。設計


這裏就須要用到原型(prototype)code

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

那麼能夠將上述代碼稍微作些修改,這裏把bark方法放入Dog構造函數的原型中:cdn

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方法並觀察結果:blog

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版)》)吧,第六章關於原型鏈有至關詳細的講解。

相關文章
相關標籤/搜索