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

前戲

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

正文

構造函數(shù)與原型

與大部分面向對象語言不同,JavaScript中并沒有引入類(class)的概念,但JavaScript仍然大量地使用了對象,為了保證對象之間的聯(lián)系,JavaScript引入了原型與原型鏈的概念。

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

ClassName obj = new ClassName()

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

var obj = new FunctionName()

可以看到,與Java不同的是,JavaScript中的new操作符后面跟的并非類名而是函數(shù)名,JavaScript并非通過類而是直接通過構造函數(shù)來創(chuàng)建實例。

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')

上述代碼就是聲明一個構造函數(shù)并通過構造函數(shù)創(chuàng)建實例的過程,這樣看起來似乎有點面向對象的樣子了,但實際上這種方法還存在一個很大的問題。

在上面的代碼中,有兩個實例被創(chuàng)建,它們有自己的名字、顏色,但它們的bark方法是一樣的,而通過構造函數(shù)創(chuàng)建實例的時候,每創(chuàng)建一個實例,都需要重新創(chuàng)建這個方法,再把它添加到新的實例中。這無疑造成了很大的浪費,既然實例的方法都是一樣的,為什么不把這個方法單獨放到一個地方,并讓所有的實例都可以訪問到呢。


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

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

那么可以將上述代碼稍微做些修改,這里把bark方法放入Dog構造函數(shù)的原型中:

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

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

接著再次通過這個構造函數(shù)創(chuàng)建實例并調用它的bark方法:

const dog1 = new Dog('dog1', 'black')
dog1.bark()  //'wangwang~'

可以看到bark方法能夠正常被調用。這時再創(chuàng)建另一個實例并重寫它的bark方法,然后再次分別調用兩個實例的bark方法并觀察結果:

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

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

Dog.prototype.bark = () => {
    console.log('haha~')
}
dog1.bark()  //'haha~'
dog2.bark()  //'haha~'

這樣看起來就沒什么問題了,將實例中共有的屬性放到原型對象中,讓所有實例共享這部分屬性。如果想要統(tǒng)一修改所有實例繼承的屬性,只需要直接修改原型對象中的屬性即可。而且每個實例仍然可以重寫原型中已經(jīng)存在的屬性來覆蓋這個屬性,并且不會影響到其他的實例。

原型鏈與繼承

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

所有原型鏈的終點都是Object函數(shù)的prototype屬性,因為在JavaScript中的對象都默認由Object()構造。Objec.prototype指向的原型對象同樣擁有原型,不過它的原型是null,而null則沒有原型。

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

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()

這里聲明了一個新的構造函數(shù)Husky,通過call方法繼承Dog中的屬性(call方法的作用可以簡單理解為將Dog中的屬性添加到Husky中,因為還涉及到其他的知識點所以不多贅述),并添加了一個weight屬性。然后用Dog函數(shù)創(chuàng)建了一個實例作為Husky的原型對象賦值給Husky.prototype以繼承方法。這樣,通過Husky函數(shù)創(chuàng)建的實例就擁有了Dog中的屬性和方法。

結語

如果想要深入了解關于JavaScript中的對象和原型鏈的話,無腦推薦紅寶書(《JavaScript高級程序設計(第3版)》)吧,第六章關于原型鏈有相當詳細的講解。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容