前戲

- 寫的比較短了,三分鐘看完應該是沒問題(嗯。。)。
- 當然最好再花半小時思考理解一下。
正文
構造函數(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版)》)吧,第六章關于原型鏈有相當詳細的講解。