通過原型機制,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/
