標籤:
看了之後我總算對原型繼承有了更深刻的理解,做愛分享的姑娘,原文連結:理解Javascript
原型
我(個人)不喜歡的,就是講原型時上來就拿類做比較的,所以我不會這樣講。不過我的確講過構造器函數,在這方面和類多多少少有共通之處。我的建議是:忘掉類。有很多觀點認為“類”學的泛濫是物件導向的過度發展,是一種悲哀,以至於有太多的開發人員幾乎把物件導向和類划上了等號。在學習原型之前,我請你先記住並品味這句話:
物件導向設計的精髓在於“抽象”二字,類是實現實體抽象的一種手段,但不是唯一一種。
prototype 和
__proto__
事先聲明:永遠,永遠不要在真實的代碼裡使用 __proto__ 屬性,在本文裡用它純粹是用於研究!很快我們會講到它的替代品,抱歉請忍耐。
在 JavaScript 裡,函數是對象(等學完了這一篇,不妨研究一下函數究竟是怎麼就成了對象的?),對象嘛,毫無意外的就會有屬性(方法也是屬性),然後毫無意外的 prototype 就是函數的一個屬性,最後毫無意外的 prototype 屬性也是一個對象。瞧,多麼順理成章的事情:
function foo() {}foo.prototype; // 裡面有啥自己去看
好吧,那 prototype 有啥用?呃,如果你把函數就當做函數來用,那它壓根沒用。不過,若你把函數當作構造器來用的話,新產生的對象就可以直接存取到 prototype 對象裡的屬性。
// 要充當構造器了,按慣例把首字母大寫function Foo() {}var f = new Foo();f.constructor; // function Foo() {}
想一下,f 的 constructor 屬性哪裡來的?如果你想不明白,請用 console.dir(Foo.prototype) 一探究竟。
這說明了一個問題:
函數的原型屬性不是給函數自己用的,而是給用函數充當構造器建立的對象使用的。
令人疑惑的是,prototype 屬性存在於 Foo 函數對象內,那麼由 Foo 建立的執行個體對象 f 是怎麼訪問到 prototype 的呢?是通過複製 prototype 對象嗎?接著上面的代碼我們繼續來看:
f.__proto__; // Foo {}Foo.prototype; // Foo {}f.__proto__ === Foo.prototype; // true
哦~不是複製過來的,而是一個叫做 __proto__ 的屬性指向了構造器的 prototype 對象呀。
沒錯!這就是原型機制的精髓所在,讓我們來總結一下所有的細節(包括隱含在表象之下的):
- 函數擁有
prototype 屬性,但是函數自己不用它
- 函數充當構造器的時候可以建立出新的對象,這需要
new 操作符的配合。其工作原理我已經在第一篇做了大部分的闡述
- 我尚未提及的是:
new 在建立新對象的時候,會賦予新對象一個屬性指向構造器的 prototype 屬性。這個新的屬性在某些瀏覽器環境內叫做 __proto__
- 當訪問一個對象的屬性(包括方法)時,首先尋找這個對象自身有沒有該屬性,如果沒有就尋找它的原型(也就是
__proto__指向的 prototype 對象),如果還沒有就尋找原型的原型(prototype 也有它自己的 __proto__,指向更上一級的prototype 對象),依此類推一直找到 Object 為止
OK,上面的第四點事實上就是 JavaScript 的對象屬性尋找機制。由此可見:
原型的意義就在於為對象的屬性尋找機制提供一個方向,或者說一條路線
一個對象,它有許多屬性,其中有一個屬性指向了另外一個對象的原型屬性;而後者也有一個屬性指向了再另外一個對象的原型屬性。這就像一條一環套一環的鎖鏈一樣,並且從這條鎖鏈的任何一點尋找下去,最後都能找到鏈條的起點,即 Object;因此,我們也把這種機制稱作:原型鏈。
現在,我希望統一一下所使用的術語(至少在本文範圍內):
- 函數的
prototype 屬性:我們叫它 原型屬性 或 原型對象
- 對象的
__proto__ 屬性:我們叫它 原型
例如:
Foo 的原型屬性(或原型對象) = Foo.prototype
f 的原型 = f.__proto__
統一術語的原因在於,儘管 Foo.prototype 和 f.__proto__ 是等價的,但是 prototype 和 __proto__ 並不一樣。當考慮一個固定的對象時,它的 prototype 是給原型鏈的下方使用的,而它的 __proto__ 則指向了原型鏈的上方;因此,一旦我們說“原型屬性”或者“原型對象”,那麼就暗示著這是給它的子子孫孫們用的,而說“原型”則是暗示這是從它的父輩繼承過來的。
再換一種說法:對象的原型屬性或原型對象不是給自己用的,而對象的原型是可以直接使用的。
__proto__ 的問題
既然 __proto__ 可以訪問到對象的原型,那麼為什麼禁止在實際中使用呢?
這是一個設計上的失誤,導致 __proto__ 屬性是可以被修改的,同時意味著 JavaScript 的屬性尋找機制會因此而“癱瘓”,所以強烈的不建議使用它。
如果你確實要通過一個對象訪問其原型,ES5 提供了一個新方法:
Object.getPrototypeOf(f) // Foo {}
這是安全的,儘管放心使用。考慮到低版本瀏覽器的相容性問題,可以使用 es5-shim
自有屬性和原型屬性的區別
由於對象的原型是一個引用而不是賦值,所以更改原型的屬性會立刻作用於所有的執行個體對象。這一特性非常適用於為對象定義執行個體方法:
function Person(name) { this.name = name;}Person.prototype.greeting = function () { return "你好,我叫" + this.name;};var p1 = new Person("張三");var p2 = new Person("李四");p1.greeting(); // 你好,我叫張三p2.greeting(); // 你好,我叫李四/* 改變執行個體方法的行為:*/Person.prototype.greeting = function () { return "你好,我叫" + this.name + ",很高興認識你!";};/* 觀察其影響:*/p1.greeting(); // 你好,我叫張三,很高興認識你!p2.greeting(); // 你好,我叫李四,很高興認識你!
然而,改變自有屬性則不同,它只會對新建立的執行個體對象產生影響,接上例:
function Person(name) { this.name = "超人";}/* 不影響已存在的執行個體對象 */p1.greeting(); // 你好,我叫張三,很高興認識你!/* 隻影響新建立的執行個體對象 */var p3 = new Person("王五");p3.greeting(); // 你好,我叫超人,很高興認識你!
這個例子看起來有點無厘頭,沒啥大用,不過它的精神在於:在現實世界中,複雜物件的行為或許會根據情況對其進行重寫,但是我們不希望改變對象的內部狀態;或者,我們會實現繼承,去覆蓋父級對象的某些行為而不引向其他相同的部分。在這些情況下,原型會給予我們最大程度的靈活性。
我們如何知道屬性是自有的還是來自於原型的?上代碼~
p1.hasOwnProperty("name"); // truep1.hasOwnProperty("greeting"); // falsep1.constructor.prototype.hasOwnProperty("greeting"); // trueObject.getPrototypeOf(p1).hasOwnProperty("greeting"); // true
代碼很簡單,就不用過度解釋了,注意最後兩句實際上等價的寫法。
小心
constructor
剛才的這一句代碼:p1.constructor.prototype.hasOwnProperty("greeting");,其實暗含了一個有趣的問題。
對象 p1 能夠訪問自己的構造器,這要謝謝原型為它提供了 constructor 屬性。接著通過 constructor 屬性又可以反過來訪問到原型對象,這似乎是一個圈圈,我們來實驗一下:
p1.constructor === p1.constructor.prototype.constructor; // truep1.constructor === p1.constructor.prototype.constructor.prototype.constructor; // true
還真是!不過我們不是因為好玩才研究這個的。
儘管我們說:更改原型對象的屬性會立即作用於所有的執行個體對象,但是如果你完全覆蓋了原型對象,事情就變得詭異起來了:(閱讀接下來的例子,請一句一句驗證自己心中所想)
function Person(name) { this.name = name;}var p1 = new Person("張三");Person.prototype.greeting = function () { return "你好,我叫" + this.name;};p1.name; // 張三p1.greeting(); // 你好,我叫張三p1.constructor === Person; // true/* so far so good, but... */Person.prototype = { say: function () { return "你好,我叫" + this.name; }};p1.say(); // TypeError: Object #<Person> has no method ‘say‘p1.constructor.prototype; // Object { say: function }
呃?Person 的原型屬性裡明明有 say 方法呀?原型對象不是即時生效的嗎?
原型繼承
若是只為了建立一種對象,原型的作用就無法全部發揮出來。我們會進一步利用原型和原型鏈的特性來拓展我們的代碼,實現基於原型的繼承。
原型繼承是一個非常大的話題範圍,慢慢地你會發現,儘管原型繼承看起來沒有類繼承那麼的規整(相對而言),但是它卻更加靈活。無論是單繼承還是多繼承,甚至是 Mixin 及其他連名字都說不上來的繼承方式,原型繼承都有辦法實現,並且往往不止一種辦法。
不過讓我們先從簡單的開始:
function Person() { this.klass = ‘人類‘;}Person.prototype.toString = function () { return this.klass;};Person.prototype.greeting = function () { return ‘大家好,我叫‘ + this.name + ‘, 我是一名‘ + this.klass + ‘。‘;};function Programmer(name) { this.name = name; this.klass = ‘程式員‘;}Programmer.prototype = new Person();Programmer.prototype.constructor = Programmer;
這是一個非常好的例子,它向我們揭示了以下要點:
var someone = new Programmer(‘張三‘);someone.name; // 張三someone.toString(); // 程式員someone.greeting(); // 大家好,我叫張三, 我是一名程式員。
我來捋一遍:
- 倒數第二行,
new Person() 建立了對象,然後賦給了 Programmer.prototype 於是構造器的原型屬性就變成了 Person 的執行個體對象。
- 因為
Person 對象擁有重寫過的 toString() 方法,並且這個方法返回的是宿主對象的 klass 屬性,所以我們可以給Programmer 定義一個 greeting() 方法,並在其中使用繼承而來的 toString()。
- 當
someone 對象調用 toString() 方法的時候,this 指向的是它自己,所以能夠輸出 程式員 而不是 人類。
還沒完,繼續看:
// 因為 Programmer.prototype.constructor = Programmer; 我們才能得到:someone.constructor === Programmer; // true// 這些結果體現了何謂“鏈式”原型繼承someone instanceof Programmer; // truesomeone instanceof Person; // truesomeone instanceof Object; // true
方法重載
上例其實已經實現了對 toString() 方法的重載(這個方法的始祖對象是 Object.prototype),秉承同樣的精神,我們自己寫的子構造器同樣可以通過原型屬性來重載父構造器提供的方法:
Programmer.prototype.toString = function () { return this.klass + "(碼農)";}var codingFarmer = new Programmer("張三");codingFarmer.greeting(); // 大家好,我叫張三, 我是一名程式員(碼農)。
屬性尋找與方法重載的矛盾
思維活躍反應快的同學或許已經在想了:
為什麼一定要把父類的執行個體賦給子類的原型屬性,而不是直接用父類的原型屬性呢?
好問題!這個想法非常有道理,而且這麼一來我們還可以減少屬性尋找的次數,因為向上尋找的時候跳過了父類執行個體的 __proto__,直接找到了(如上例)Person.prototype。
然而不這麼做的理由也很簡單,如果你這麼做了:
Programmer.prototype = Person.prototype;
由於 Javascript 是引用賦值,因此等號兩端的兩個屬性等於指向了同一個對象,那麼一旦你在子類對方法進行重載,連帶著父類的方法也一起變化了,這就失去了重載的意義。因此只有在確定不需要重載的時候才可以這麼做。
原型和原型對象(__proto__和prototype)轉