

通過原型機制,JavaScript 中的對象從其他對象繼承功能特性;這種繼承機制與經典的面向對象編程語言的繼承機制不同。本文將探討這些差別,解釋原型鏈如何工作,並了解如何通過 prototype
屬性向已有的構造器添加方法
基於原型的語言?
JavaScript 常被描述為一種基於原型的語言 (prototype-based language)——每個對象擁有一個原型對象,對象以其原型為模板、從原型繼承方法和屬性。原型對象也可能擁有原型,並從中繼承方法和屬性,一層一層、以此類推。這種關系常被稱為原型鏈 (prototype chain),它解釋了為何一個對象會擁有定義在其他對象中的屬性和方法。
准確地說,這些屬性和方法定義在Object的構造器函數(constructor functions)之上的prototype
屬性上,而非對象實例本身。
在傳統的 OOP 中,首先定義“類”,此后創建對象實例時,類中定義的所有屬性和方法都被復制到實例中。在 JavaScript 中並不如此復制——而是在對象實例和它的構造器之間建立一個鏈接(它是__proto__屬性,是從構造函數的prototype
屬性派生的),之后通過上溯原型鏈,在構造器中找到這些屬性和方法。
注意: 理解對象的原型(可以通過Object.getPrototypeOf(obj)
或者已被棄用的__proto__
屬性獲得)與構造函數的prototype
屬性之間的區別是很重要的。前者是每個實例上都有的屬性,后者是構造函數的屬性。也就是說,Object.getPrototypeOf(new Foobar())
和Foobar.prototype
指向着同一個對象。
以上描述很抽象;我們先看一個例子。
在javascript中,函數可以有屬性。 每個函數都有一個特殊的屬性叫作原型(prototype)
,正如下面所展示的。請注意,下面的代碼是獨立的一段(在網頁中沒有其他代碼的情況下,這段代碼是安全的)。為了最好的學習體驗,你最好打開一個控制台 (在Chrome和Firefox中,可以按Ctrl+Shift+I來打開)切換到"控制台" 選項卡, 復制粘貼下面的JavaScript代碼,然后按回車來運行
function doSomething(){} console.log( doSomething.prototype ); // It does not matter how you declare the function, a // function in javascript will always have a default // prototype property. var doSomething = function(){}; console.log( doSomething.prototype );
正如上面所看到的, doSomething
函數有一個默認的原型屬性,它在控制台上面呈現了出來. 運行這段代碼之后,控制台上面應該出現了像這樣的一個對象.
{ constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() } }
現在,我們可以添加一些屬性到 doSomething 的原型上面,如下所示.
function doSomething(){} doSomething.prototype.foo = "bar"; console.log( doSomething.prototype );
結果:
{ foo: "bar", constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() } }
然后,我們可以使用 new 運算符來在現在的這個原型基礎之上,創建一個 doSomething
的實例。正確使用 new 運算符的方法就是在正常調用函數時,在函數名的前面加上一個 new
前綴. 通過這種方法,在調用函數前加一個 new
,它就會返回一個這個函數的實例化對象. 然后,就可以在這個對象上面添加一些屬性:
function doSomething(){} doSomething.prototype.foo = "bar"; // add a property onto the prototype var doSomeInstancing = new doSomething(); doSomeInstancing.prop = "some value"; // add a property onto the object console.log( doSomeInstancing );
結果:
{ prop: "some value", __proto__: { foo: "bar", constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() } } }
就像上面看到的doSomeInstancing
的 __proto__
屬性就是doSomething.prototype
. 但是這又有什么用呢?
好吧,當你訪問 doSomeInstancing
的一個屬性, 瀏覽器首先查找 doSomeInstancing
是否有這個屬性. 如果 doSomeInstancing
沒有這個屬性, 然后瀏覽器就會在 doSomeInstancing
的 __proto__
中查找這個屬性(也就是 doSomething.prototype). 如果 doSomeInstancing 的 __proto__
有這個屬性, 那么 doSomeInstancing 的 __proto__
上的這個屬性就會被使用. 否則, 如果 doSomeInstancing 的 __proto__
沒有這個屬性, 瀏覽器就會去查找 doSomeInstancing 的 __proto__
的 __proto__
,看它是否有這個屬性.
默認情況下, 所有函數的原型屬性的 __proto__
就是 window.Object.prototype
. 所以 doSomeInstancing 的 __proto__
的 __proto__
(也就是 doSomething.prototype 的 __proto__
(也就是 Object.prototype
)) 會被查找是否有這個屬性. 如果沒有在它里面找到這個屬性, 然后就會在 doSomeInstancing 的 __proto__
的 __proto__
的 __proto__
里面查找. 然而這有一個問題: doSomeInstancing 的 __proto__
的 __proto__
的 __proto__
不存在. 最后, 原型鏈上面的所有的 __proto__
都被找完了, 瀏覽器所有已經聲明了的 __proto__
上都不存在這個屬性,然后就得出結論,這個屬性是 undefined
.
function doSomething(){} doSomething.prototype.foo = "bar"; var doSomeInstancing = new doSomething(); doSomeInstancing.prop = "some value"; console.log("doSomeInstancing.prop: " + doSomeInstancing.prop); console.log("doSomeInstancing.foo: " + doSomeInstancing.foo); console.log("doSomething.prop: " + doSomething.prop); console.log("doSomething.foo: " + doSomething.foo); console.log("doSomething.prototype.prop: " + doSomething.prototype.prop); console.log("doSomething.prototype.foo: " + doSomething.prototype.foo);
結果:
doSomeInstancing.prop: some value doSomeInstancing.foo: bar doSomething.prop: undefined doSomething.foo: undefined doSomething.prototype.prop: undefined doSomething.prototype.foo: bar
理解原型對象
讓我們回到 Person()
構造器的例子。請把下面代碼例子依次寫入瀏覽器控制台。。
本例中我們首先將定義一個構造器函數:
function Person(first, last, age, gender, interests) { // 屬性與方法定義 };
然后在控制台創建一個對象實例:
var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
在 JavaScript 控制台輸入 "person1.
",你會看到,瀏覽器將根據這個對象的可用的成員名稱進行自動補全:
在這個列表中,你可以看到定義在 person1
的原型對象、即 Person()
構造器中的成員—— name
、age
、gender
、interests
、bio
、greeting
。同時也有一些其他成員—— watch
、valueOf
等等——這些成員定義在 Person()
構造器的原型對象、即 Object
之上。下圖展示了原型鏈的運作機制。
那么,調用 person1
的“實際定義在 Object
上”的方法時,會發生什么?比如:
person1.valueOf()
這個方法僅僅返回了被調用對象的值。在這個例子中發生了如下過程:
- 瀏覽器首先檢查,
person1
對象是否具有可用的valueOf()
方法。 - 如果沒有,則瀏覽器檢查
person1
對象的原型對象(即Person
構造函數的prototype屬性所指向的對象)是否具有可用的valueof()
方法。 - 如果也沒有,則瀏覽器檢查
Person()
構造函數的prototype屬性所指向的對象的原型對象(即Object
構造函數的prototype屬性所指向的對象)是否具有可用的valueOf()
方法。這里有這個方法,於是該方法被調用。
注意:必須重申,原型鏈中的方法和屬性沒有被復制到其他對象——它們被訪問需要通過前面所說的“原型鏈”的方式。
注意:沒有官方的方法用於直接訪問一個對象的原型對象——原型鏈中的“連接”被定義在一個內部屬性中,在 JavaScript 語言標准中用 [[prototype]]
表示(參見 ECMAScript)。然而,大多數現代瀏覽器還是提供了一個名為 __proto__
(前后各有2個下划線)的屬性,其包含了對象的原型。你可以嘗試輸入 person1.__proto__
和 person1.__proto__.__proto__
,看看代碼中的原型鏈是什么樣的!
prototype 屬性:繼承成員被定義的地方
那么,那些繼承的屬性和方法在哪兒定義呢?如果你查看 Object
參考頁,會發現左側列出許多屬性和方法——大大超過我們在 person1
對象中看到的繼承成員的數量。某些屬性或方法被繼承了,而另一些沒有——為什么呢?
原因在於,繼承的屬性和方法是定義在 prototype
屬性之上的(你可以稱之為子命名空間 (sub namespace) )——那些以 Object.prototype.
開頭的屬性,而非僅僅以 Object.
開頭的屬性。prototype
屬性的值是一個對象,我們希望被原型鏈下游的對象繼承的屬性和方法,都被儲存在其中。
於是 Object.prototype.watch()、
Object.prototype.valueOf()
等等成員,適用於任何繼承自 Object()
的對象類型,包括使用構造器創建的新的對象實例。
Object.is()
、Object.keys()
,以及其他不在 prototype
對象內的成員,不會被“對象實例”或“繼承自 Object()
的對象類型”所繼承。這些方法/屬性僅能被 Object()
構造器自身使用。
注意:這看起來很奇怪——構造器本身就是函數,你怎么可能在構造器這個函數中定義一個方法呢?其實函數也是一個對象類型。
-
- 你可以檢查已有的
prototype
屬性。回到先前的例子,在 JavaScript 控制台輸入:Person.prototype
- 輸出並不多,畢竟我們沒有為自定義構造器的原型定義任何成員。缺省狀態下,構造器的
prototype
屬性初始為空白。現在嘗試:Object.prototype
- 你可以檢查已有的
你會看到 Object
的 prototype
屬性上定義了大量的方法;如前所示,繼承自 Object
的對象都可以使用這些方法。
JavaScript 中到處都是通過原型鏈繼承的例子。比如,你可以嘗試從 String
、Date
、Number
和 Array
全局對象的原型中尋找方法和屬性。它們都在原型上定義了一些方法,因此當你創建一個字符串時:
var myString = 'This is my string.';
myString
立即具有了一些有用的方法,如 split()
、indexOf()
、replace()
等。
重要:prototype
屬性大概是 JavaScript 中最容易混淆的名稱之一。你可能會認為,this
關鍵字指向當前對象的原型對象,其實不是(還記得么?原型對象是一個內部對象,應當使用 __proto__
訪問)。prototype
屬性包含(指向)一個對象,你在這個對象中定義需要被繼承的成員。
create()
Object.create()
方法可以創建新的對象實例。
-
- 例如,在上個例子的 JavaScript 控制台中輸入:
var person2 = Object.create(person1);
create()
實際做的是從指定原型對象創建一個新的對象。這里以person1
為原型對象創建了person2
對象。在控制台輸入:person2.__proto__
- 例如,在上個例子的 JavaScript 控制台中輸入:
結果返回對象person1
。
constructor 屬性
每個實例對象都從原型中繼承了一個constructor屬性,該屬性指向了用於構造此實例對象的構造函數。
-
- 例如,繼續在控制台中嘗試下面的指令:
person1.constructor person2.constructor
都將返回
Person()
構造器,因為該構造器包含這些實例的原始定義。一個小技巧是,你可以在
constructor
屬性的末尾添加一對圓括號(括號中包含所需的參數),從而用這個構造器創建另一個對象實例。畢竟構造器是一個函數,故可以通過圓括號調用;只需在前面添加new
關鍵字,便能將此函數作為構造器使用。- 在控制台中輸入:
var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);
- 現在嘗試訪問新建對象的屬性,例如:
person3.name.first person3.age person3.bio()
- 在控制台中輸入:
- 例如,繼續在控制台中嘗試下面的指令:
正常工作。通常你不會去用這種方法創建新的實例;但如果你剛好因為某些原因沒有原始構造器的引用,那么這種方法就很有用了。
此外,constructor
屬性還有其他用途。比如,想要獲得某個對象實例的構造器的名字,可以這么用:
instanceName.constructor.name
具體地,像這樣:
person1.constructor.name
修改原型
從我們從下面這個例子來看一下如何修改構造器的 prototype
屬性。
向構造器的 prototype
添加了一個新的方法:
function Person(first, last, age, gender, interests) { // 屬性與方法定義 }; var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']); Person.prototype.farewell = function() { alert(this.name.first + ' has left the building. Bye for now!'); }
但是 farewell()
方法仍然可用於 person1
對象實例——舊有對象實例的可用功能被自動更新了。這證明了先前描述的原型鏈模型。這種繼承模型下,上游對象的方法不會復制到下游的對象實例中;下游對象本身雖然沒有定義這些方法,但瀏覽器會通過上溯原型鏈、從上游對象中找到它們。這種繼承模型提供了一個強大而可擴展的功能系統。
你很少看到屬性定義在 prototype 屬性中,因為如此定義不夠靈活。比如,你可以添加一個屬性:
Person.prototype.fullName = 'Bob Smith';
但這不夠靈活,因為人們可能不叫這個名字。用 name.first
和 name.last
組成 fullName
會好很多:
Person.prototype.fullName = this.name.first + ' ' + this.name.last;
然而,這么做是無效的,因為本例中 this
引用全局范圍,而非函數范圍。訪問這個屬性只會得到 undefined undefined
。但這個語句若放在 先前定義在 prototype
上的方法中則有效,因為此時語句位於函數范圍內,從而能夠成功地轉換為對象實例范圍。你可能會在 prototype
上定義常屬性 (constant property) (指那些你永遠無需改變的屬性),但一般來說,在構造器內定義屬性更好。
注:關於 this
關鍵字指代(引用)什么范圍/哪個對象,這個問題超出了本文討論范圍。事實上,這個問題有點復雜,如果現在你沒能理解,也不用擔心。
事實上,一種極其常見的對象定義模式是,在構造器(函數體)中定義屬性、在 prototype
屬性上定義方法。如此,構造器只包含屬性定義,而方法則分裝在不同的代碼塊,代碼更具可讀性。例如:
// 構造器及其屬性定義 function Test(a,b,c,d) { // 屬性定義 }; // 定義第一個方法 Test.prototype.x = function () { ... } // 定義第二個方法 Test.prototype.y = function () { ... } // 等等……
本文介紹了 JavaScript 對象原型,包括原型鏈如何允許對象之間繼承特性、prototype
屬性、如何通過它來向構造器添加方法。
如果大家有任何疑問即可留言反饋,會在第一時間回復反饋,謝謝大家!
文章參考來源:MDN文檔
本文為Tz張無忌文章,讀后有收獲可以請作者喝杯咖啡,轉載請文章注明出處:https://www.cnblogs.com/zhaohongcheng/