通常大家都是先學了 C/C++/Java 這類語言,后兩者面向對象的開發思路很相似,寫一個 class,然后 new 出相應的實例對象,這些對象有的屬性和方法是各自不同的,有的則是共享的類屬性、類方法。如果要一個 class 要繼承另一個,就直接用相應的語法繼承就行了。但這個套路到了 JavaScript 上就似乎不適用了,因為 JavaScript 采用了原型繼承的思路來實現面向對象的。
尤其是我們在剛學習 JavaScript 的時候一般是為了操作 DOM,甚至是直接使用了 jQuery 這樣的類庫,不大有面向對象開發的需求,于是也沒深入去研究過這個。直到要找工作的時候,發現這是必考基礎題,才開始查資料,發現和以前的套路有不同,更難以接受所謂的「基于原型的繼承機制」。
上帝說要有類,就有了「類」
且說 JavaScript 一開始設計的時候,只是非常簡易的腳本語言,但因為其中都是對象,所以需要一個機制來聯系它們,最后還是設計了繼承。但作者并沒有用「類」的形式來實現,而是通過 new 一個「構造函數」來生成一個對象。
例如 Ruby 中,是實實在在地用了 class 聲明了一個 Person 類,然后用 initialize 方法實例化一個對象,這個對象有自己的屬性 name 和 gender。
class Person
def initialize(name, gender)
@name = name
@gender = gender
end
end
someone = Person.new('Jason', 'male')
在 JavaScript 中,并沒有 class 的概念(ES6之后的語法糖暫且不提),而是使用了一個函數,運行 new 的時候便會根據這個 Person 函數生成一個對象,有其自己的屬性 name 和 gender, 這個對象還有一個特別的屬性,那就是__proto__
,它的值就是Person.prototype
的引用,后面再詳細說。
function Person(name, gender){
this.name = name;
this.gender = gender;
}
var a = new Person("Jason", 'male');
var b = new Person("Amy", 'female');
console.log(a.name); // Jason
console.log(b.gender); // female
共享屬性和方法
如上似乎實現了一個類,但實際上好像沒有類變量或者類方法?因為通過 new 運算符生成的對象都是各自獨立的,只不過它們的__proto__
屬性都是Person.prototype
罷了。
為了共享屬性和方法,就要用到這個見了幾萬遍的屬性——prototype了。其實每一個函數都有一個prototype屬性,通常是一個空的對象。如果給這個prototype對象里加點東西,那么由這個Person函數構造出來的對象便都能訪問到。
function Person(name, gender){
this.name = name;
this.gender = gender;
}
console.log(Person.prototype); // {} 是一個空對象
Person.prototype = {
country: 'China'
};
var a = new Person("Jason", 'male');
var b = new Person("Amy", 'female');
console.log(a.name); // Jason
console.log(b.name); // Amy
console.log(a.country); // China
console.log(b.country); // China
為什么呢?在之前已經說了,通過Person這個函數new出來的對象,都自帶了一個屬性__proto__
,它的值是指向Person.prototype
,是一個引用。意思就是說這個實例對象直接instanceof Person
的。而 Person.prototype.__proto__
也是一個對象呀,因為Person.prototype
是直接由Object生成的,所以 Person.prototype.__proto__ == Object.prototype
。
a.__proto__ == Person.prototype
a.__proto__.__proto__ == Person.prototype.__proto__ == Object.prototype
所以說,a對象是繼承自Person的原型對象( Person.prototype ),而Person的原型對象又繼承自Object的原型對象,這樣就構成了原型鏈。
反過來看原型鏈
當我調用a.country
的時候,會發現a對象里并沒有這個屬性,便開始根據原型鏈向上查找,也就是去找__proto__
所指向的原型對象,看看它有沒有country屬性,如果有,就得到了,沒有的話繼續向上查找,直到Object.prototype
為止,因為Object再往上就是null了。Object原型對象是最初始的原型了,沒有再繼承自別的地方了。
所以對象通常還自帶了一個方法hasOwnProperty
,用法如下:
a.hasOwnProperty('name'); // true
a.hasOwnProperty('gender'); // true
a.hasOwnProperty('country'); // false
可以得出,country
屬性并不是自己的屬性,而是通過原型鏈向上查找得到的,也就是所謂的繼承來的。當然啦,hasOwnProperty
這個方法顯然也不是在寫Person構造函數時自己開發的,是通過原型鏈向上查找,直到Object.prototype才有的,也就是實際上是調用了a.__proto__.__proto__.hasOwnProperty('name')
。
而instanceof
這個運算符則是通過原型鏈來計算的。
a instanceof Person // true
a instanceof Object // true
a instanceof Dog // false
a.__proto__ = Object.prototype // 改變原型鏈的關系,跳過Person,直接繼承自Object原型
a instanceof Person // false
a instanceof Object // true
寫在最后
我總算是搞懂這個原型鏈了,實在是艱辛。搞明白這個之后才覺得,原型繼承似乎更加好用。寫文之前參考了阮一峰的文章,醍醐灌頂,所以根據自己的理解寫下這些,希望能幫助更多的人理解原型鏈。