Javascript中的的對象——原型模式(Prototype)

本文原文來源:《Object-Oriented JavaScript》By Stoyan Stefanov
本文翻譯來源:赤石俊哉 原創翻譯
版權申明: 若是您是原文的原做者而且不但願此文被公開,能夠聯繫做者刪除。本文翻譯由 赤石俊哉 翻譯整理,您能夠用於學習交流的目的,可是禁止用於其餘用途,因私自濫用引起的版權糾紛本人概不負責。web

原型模式(Prototype)

在這一章節中你將會學習使用「函數(function)」對象中的prototype屬性。在JavaScript的學習過程當中,理解prototype的工做原理是很重要的一個部分。畢竟,JavaScript被分類爲是一個基於原型模式對象模型的語言。其實原型模式並不難,可是它是一種新的觀念並且每每須要花些時間去理解。它是JavaScript中的一部分(閉包是另外一部分),一旦你「get「了他們,他們就會變得很容易理解也是頗有意義的。在本書的剩餘部分中,強烈建議多打多試這些示例。那樣會更加容易地學習和記住這些觀念。
本章中將會討論如下話題:數組

  • 每個函數都有一個prototype屬性,並且它包含了一個對象。瀏覽器

  • 向prototype中添加屬性。閉包

  • 使用向prototype中添加的屬性。app

  • 函數自身屬性以及原型屬性的區別。函數

  • 每個對象保存在prototype中的私密連接——__proto__學習

  • 方法:isPrototypeOf(),hasOwnProperty(),propertyIsEnumerable()測試

  • 如何增強內建對象,好比數組(array)和字符串(string)。this

原型屬性

JavaScript中的函數是對象,並且包含了方法和屬性。包括咱們常見的一些的方法,像apply()call()等,常見的屬性,像lengthconstructor等。還有一個屬性就是prototype編碼

當你定義了一個簡單的函數foo()以後,你能夠像其餘對象同樣,直接訪問這個函數的屬性:

>>> function foo(a, b){return a * b;}
>>> foo.length
2

>>> foo.constructor
Function()

prototype這個屬性在你定義函數的時候就建立好了。他的初始值是一個空對象。

>>> typeof foo.prototype
"object"

你可使用屬性和方法來擴充這個空對象。他們不會對foo()函數自己產生任何影響。他們只會在當你使用foo()做爲構造函數的時候被使用。

使用原型模式添加方法和屬性

在前面的章節中,已經學習過了如何定義一個構建新對象時使用的構造函數。最主要的思想是在函數中調用new時,訪問this變量,它包含了構建函數返回的對象。擴張(添加方法和屬性)this對象是在對象被建立時添加功能的一種方法。
讓咱們來看個例子,Gadget()構造方法中使用this來添加兩個屬性和一個方法到它建立的對象裏。

function Gadget(name, color){
    this.name = name;
    this.color = color;
    this.whatAreYou = function(){
        return 'I am a ' + this.color + ' ' + this.name;
    }
}

向構造函數的prototype中添加方法和屬性是在對象被建立的時候爲對象添加功能的另外一種方式。接下來再添加兩個屬性pricerating和一個getInfo()方法。由於prototype包含一個對象,因此你能夠像這樣添加:

Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;
Gadget.prototype.getInfo = function(){
    return 'Rating: ' + this.rating + ', Price: ' + this.price;
};

你也能夠經過另外一種方式達到一樣的目的,就是徹底覆蓋掉原型屬性,將它換成你選擇的對象:

Gadget.prototype = {
    price: 100,
    rating: 3,
    getInfo: function() {
        return `Rating: ` + this.rating + ', Price:' + this.price;
    }
};

使用原型屬性的方法和屬性

你添加到構造函數的原型屬性中的全部方法和屬性你都是能夠直接在使用這個構造函數構造新對象以後,直接使用的。好比,若是你使用Gadget()構建函數,建立了一個newtoy對象,你能夠直接訪問已經定義的全部方法和屬性。

>>> var newtoy = new Gadget('webcam', 'black');
>>> newtoy.name;
"webcam"

>>> newtoy.color;
"black"

>>> newtoy.whatAreYou();
"I am a black webcam"

>>> newtoy.price;
100

>>> newtoy.rating;
3

>>> newtoy.getInfo();
"Rating:3, Price: 100"

有一點很重要的是,原型屬性是」活的「,在JavaScript中對象的傳遞是經過引用來進行的。爲此,原型類型不是直接複製到新對象中的。這意味着什麼呢?這意味着,咱們能夠在任什麼時候間修改任何對象的原型屬性(甚至你均可以在新建對象以後進行修改),它們都是生效的。
讓咱們繼續來看一個例子,添加下面的方法到原型屬性裏面:

Gadget.prototype.get = function(what){
    return this[what];
};

儘管咱們在定義get()方法以前已經生成了newtoy對象,然而newtoy依舊能夠訪問這個新的方法:

>>> newtoy.get('price');
100

>>> newtoy.get('color');
"black"

「函數自身屬性」與「原型屬性」的對比

在上面的getInfo()例子中,使用了this來從內部指向對象自己,使用Gadget.prototype也能夠達到同樣的目的:

Gadget.prototype.getInfo = function(){
    return 'Rating: ' + Gadget.prototype.rating + ', Price: ' + Gadget.prototype.price;
};

這有啥不同呢?在回答這個問題以前,咱們先來測試一下看看原型屬性是怎麼工做的吧。
讓咱們再拿出咱們的newtoy對象:

>>> var newtoy = new Gadget('webcam', 'black');

當你嘗試訪問newtoy的一個屬性,使用表達式newtoy.name,JavaScript引擎將會瀏覽對象的全部屬性,尋找一個叫做name,若是找到它,它的值就會被返回。

>>> newtoy.name
'webcam'

什麼?你想嘗試着訪問rating屬性?JavaScript引擎會檢查newtoy中的全部屬性,而後沒有找到一個叫做rating的。而後腳本引擎就會鑑別出,構造函數中的原型屬性曾經嘗試着建立這個對象(就像你使用newtoy.constructor.prototype的時候同樣)。若是屬性在原型屬性中找到了這個屬性,就會使用原型屬性中的這個屬性。

>>> newtoy.rating
3

這和你直接訪問原型屬性同樣。每個對象都有一個構造函數的屬性,它是對建立該對象使用的構造函數的引用。因此,在這個例子中:

>>> newtoy.constructor
Gadget(name, color)

>>> newtoy.constructor.prototype.rating
3

如今,讓咱們再來看看第一步,每個對象都有一個構造函數。原型屬性是一個對象,因此,它也應該也有一個構造函數。進而它的構造函數又有一個原型屬性……

>>> newtoy.constructor.prototype.constructor
Gadget(name, color)
>>> newtoy.constructor.prototype.constructor.prototype
Object price=100 rating=3

這個循環將會持續下去,具體有多長取決於這個原型屬性鏈有多長。可是最後會終結於一個內建的Object()對象。它是最外層父類。在這個例子中,若是你嘗試着使用newtoy.toString(),而newtoy他沒有本身的toString()方法,並且它的原型屬性對象裏也沒有,他就會一直往上找,最後會調用Object對象的toString()方法。

>>> newtoy.toString()
"[object Object]"

使用函數自身的屬性覆蓋原型屬性的屬性

如上面所演示的,若是你的對象沒有一個確切的本身的屬性,可使用一個原型鏈上層的對象。若是對象和原型屬性裏面有相同名字的屬性,自身的屬性會被優先使用。
接下來咱們來模擬一個屬性同時存在於自身屬性和原型屬性中:

function Gadget(name){
    this.name = name;
}

>>> Gadget.prototype.name = 'foo';
"foo"

建立一個新對象,訪問它的name屬性,它會給你對象自身的name屬性。

>>> var toy = new Gadget('camera');
>>> toy.name;
"camera"

若是你刪除這個屬性,那麼原型屬性中使用相同名字的屬性就會「表現出來」:

>>> delete toy.name;
true

>>> toy.name;
"foo"

固然,你能夠從新建立它的自身屬性:

>>> toy.name = 'camera';
>>> toy.name;
"camera"

遍歷屬性

若是你但願列出一個對象的全部屬性,你可使用一個for-in循環。在第二章節中,學習瞭如何遍歷一個數組裏面的全部元素:

var a = [1, 2, 3];
for (var i in a)
{
    console.log(a[i]);
}

數組是一個對象,因此能夠推導出for-in遍歷對象的時候:

var o = {p1: 1, p2: 2};
for (var i in o) {
    console.log(i + '=' + o[i]);
}

這將會產生:

p1=1
p2=2

須要知道的幾個細節:

  • 不是全部的屬性都在for-in循環中顯示出來。好比,數組的length,以及constructor屬性就不會被顯示出來。被顯示出來的屬性叫作可枚舉的。你可使用每一個對象都能提供的propertyIsEnumerable()方法來檢查一個屬性是否是可枚舉的。

  • 原型鏈中原型屬性若是是可枚舉的,也會被顯示出來。你可使用hasOwnProperty()方法來檢查一個屬性是自身屬性仍是原型屬性。

  • propertyIsEnumerable()將會對全部原型屬性中的屬性返回false,儘管他們會在for-in循環中顯示出來,也是可枚舉的。

爲了看看這些函數的效果,咱們使用一個簡化版本的Gadget()

function Gadget(name, color)
{
    this.name = name;
    this.color = color;
    this.someMethod = function(){
        return 1;
    }
}
Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;

建立一個新的對象:

var newtoy = new Gadget('webcam', 'black');

若是你使用for-in循環,你能夠看到對象的全部屬性,包括那些原型屬性的:

for (var prop in newtoy){
    console.log(prop + ' = ' + newtoy[prop];
}

這個結果也包含對象的方法(那是由於方法是正好類型是函數的屬性):

name = webcam
color = black
someMethod = function(){ return 1;}
price = 100
rating = 3

若是你想區分對象自身屬性和原型屬性的屬性,使用hasOwnProperty(),試試這個:

>>> newtoy.hasOwnProperty('name')
true

>>> newtoy.hasOwnProperty('price')
false

讓咱們再來循環一次,可是此次只顯示自身的屬性:

for (var prop in newtoy){
    if (newtoy.hasOwnProperty(prop)){
        console.log(prop + '=' + newtoy[prop]);
    }
}

結果:

name=webcam
color=black
someMethod=function(){return 1;}

接下來讓咱們試試propertyIsEnumerable()。若是自身屬性不是內置的屬性,這個函數就會返回true:

>>> newtoy.propertyIsEnumerable('name')
true

>>> newtoy.propertyIsEnumerable('constructor')
false

任何從原型鏈上來的屬性都是不可枚舉的:

>>> newtoy.propertyIsEnumerable('price')
false

注意,雖然若是你獲取了包含在原型屬性中對象,而且調用了它的propertyIsEnumerable(),這個屬性是能夠枚舉的。

>>> newtoy.constructor.prototype.propertyIsEnumberable('price')
true

isPrototypeOf()

每個對象都有isPrototypeOf()方法。這個方法會告訴你指定的對象是誰的原型屬性。
咱們先寫一個簡單的對象monkey

var monkey = {
    hair: true,
    feeds: 'bananas',
    breathes: 'air'
};

接下來,讓咱們創建一個Human()構造函數,而後設定它的prototype屬性指向monkey

function Human(name){
    this.name = name;
}
Human.prototype = monkey;

若是你建立一個叫做georgeHuman對象,而後問它:「monkeygeorge的原型屬性嗎?」,你就會獲得true

>>> var george = new Human('George');
>>> monkey.isPrototypeOf(george)
true

祕密的__proto__連接

如你所知的,當你嘗試訪問一個不存在與當前對象的屬性時,它會查詢原型屬性的屬性。
讓咱們繼續使用monkey對象做爲Human()構造函數的原型屬性。

var monkey = {
    feeds: 'bananas',
    breathes: 'air'
};
function Human() {}
Human.prototype = monkey;

接下來建立一個developer對象,而後給他一些屬性:

var developer = new Human();
developer.feeds = 'pizza';
developer.hacks = 'JavaScript';

如今,咱們來作些查詢吧。hacksdeveloper的屬性:

>>> developer.hacks
"JavaScript"

feeds能夠在對象中被找到:

>>> developer.feeds
"pizza"

breathes不存在於developer對象中,因爲有一個祕密的連接指向原型類型對象,因此轉而查找原型類型。

>>> developer.breathes
"air"

能夠從developer對象中獲得原型屬性對象呢?固然,能夠啦。使用constructor做爲中間對象,就像developer.constructor.prototype指向monkey同樣。可是這並非十分可靠的。由於constructor大多時候用於提供信息的用途,並且是能夠隨時被覆蓋修改的。你甚至能夠用一個不是對象的東西覆蓋掉它。這樣作絲絕不會影響到原型鏈的功能。

讓咱們看一些字符串的構造屬性:

>>> developer.constructor = 'junk'
"junk"

看上去,prototype已經亂成一團了:

>>> typeof developer.constructor.prototype
"undefined"

可是事實卻並不是如此,由於開發者仍然呼吸着「空氣」(developerbreathes屬性仍然是air):

>>> developer.breathes
"air"

這表示原型屬性的祕密連接仍然存在。在火狐瀏覽器中公開的這個祕密連接是__proto__屬性(proto先後各加兩個下劃線)。

>>> developer._proto__
Object feeds = bananas breathes=air

你能夠在學習的過程當中使用這個祕密連接,可是實際編碼中不推薦使用。由於它不存在於Internet Explorer中,因此你的代碼將會變得難以移植。打個比方,若是你使用monkey建立了一堆對象,並且你如今想在全部的對象中更改一些東西。你能夠修改monkey,並且全部的實例都會繼承這些變化。

>>> monkey.test = 1
1

>>> developer.test
1

__proto__不是等效於prototype__proto__是實例的一個屬性,儘管prototype是構造函數的一個屬性。

>>> typeof developer.__proto__
"object"

>>> typeof developer.prototype
"undefined"

再次強調,你能夠在Debug或者是學習的時候使用__proto__,其餘時候不要。

擴充內建對象

內建的一些對象像構造函數Array,String,甚至是ObjectFunction()均可以經過他們的原型屬性來進行擴充。打個比方,你就能夠向Array原型屬性中添加新方法,並且它們能夠在全部的數組中被使用。讓咱們來試試。
在PHP中,有一個函數叫作in_array(),它會告訴你若是數組中是否存在某個值。在JavaScript中,沒有inArray()這樣的函數,因此咱們能夠實現它,並添加到Array.prototype中。

Array.prototype.inArray = function(needle) {
    for (var i = 0, len = this.length; i < len; i++) {
        if (this[i] === needle) {
            return true;
        }
    }   
    return false; 
}

如今,全部的數組就都有新的方法了。讓我試試:

>>> var a = ['red', 'green', 'blue']; 
>>> a.inArray('red');
true

>>> a.inArray('yellow');
false

真是簡單快捷!讓我再來作一個。想象一下你的程序可能常常須要反轉字符串吧,或許你會認爲字符串對象應該有一個內建的reverse()方法,畢竟數組有reverse()方法。你能夠輕鬆地添加reverse()方法給String的原型屬性。瀏覽Array.prototype.reverse()(這和第四章末尾的練習類似)。

String.prototype.reverse = function() {
    return Array.prototype.reverse.apply(this.split('')).join('');
}

這個代碼使用split()使用字符串生成了一個數組,而後調用了這個數組上的reverse()方法,生成了一個反轉的數組。而後再使用join()將反轉的數組變回了字符串。讓咱們試試新的方法:

>>> "Stoyan".reverse();
"nayotS"

擴充內建對象——討論

經過原型屬性來擴充內建對象是一項強力的技術,並且你能夠用它來將JavaScript塑形成你想要的樣子。你在使用這種強有力的方法以前都要完全地思考清楚你的想法。
看看一個叫作Prototype的JavaScript庫,它的做者太愛這個方法了,以致於連庫的名字都叫這個了。使用這個庫,你可使用一些JavaScript方法,讓使用JavaScript如Ruby語言同樣靈活。
YUI(雅虎用戶界面)庫是另外一個比較流行的JavaScript庫。它的做者則是明確地反對這個領域。他們不會以任何方式更改內建對象。無論你用的是什麼庫,修改核心對象都只會迷惑庫的使用者,並且形成意料以外的錯誤。
事實是,JavaScript發生了變化,瀏覽器也帶來了支持更多功能的新版本。如今你認爲須要擴充到原型屬性的缺失的功能,也許在明天就變成了內建的方法。所以你的方法可能就不被須要了。可是若是你使用這種方法已經寫了不少代碼並且你的方法又有些不一樣於內建的新內建實現呢?
最起碼來講你能作的是,在實現一個方法以前先去檢查一下它是否存在。咱們的上一個例子就應該像這樣:

if (!String.prototype.reverse) {
  String.prototype.reverse = function() {   
    return Array.prototype.reverse.apply(this.split('')).join(''); 
  } 
}

一些原型屬性的陷阱

在處理原型屬性的時候,這兩個現象是須要考慮在內的:

  • <!--The prototype chain is live with the exception of when you completely replace the prototype object.-->

  • prototype.constructor是不可靠的。

建立一個簡單的構建函數和兩個對象:

>>> function Dog(){ this.tail = true; }
>>> var benji = new Dog();
>>> var rusty = new Dog();

甚至在建立了對象以後,你仍能夠向原型屬性添加屬性,並且對象會使用新的屬性。讓咱們插進方法say()

>>> Dog.prototype.say = function(){ return 'Woof!';}

兩個對象都會使用新的方法:

>>> benji.say();
"Woof!"

>>> rusty.say();
"Woof!"

到此爲止,若是你詢問你的對象,用來建立他們的構建函數是什麼,他們還會正確地彙報:

>>> benji.constructor;
Dog();

>>> rusty.constructor;
Dog();

一個有趣的現象是若是你問原型屬性的構造函數是什麼,你仍然會獲得Dog(),他不算太準確。原型屬性是Object()建立的一個普通對象而已。使用Dog()構造的不含任何屬性的對象。

>>> benji.constructor.prototype.constructor
Dog()

>>> typeof benji.constructor.prototype.tail
"undefined"

如今咱們用一個全新的對象徹底覆蓋原型屬性對象:

>>> Dog.prototype = {paws: 4, hair: true};

這證實咱們的舊對象不能訪問新原型屬性的屬性。他們仍保持着與舊原型屬性對象的祕密連接。

>>> typeof benji.paws
"undefined"

>>> benji.say()
"Woof!"

>>> typeof benji.__proto__.say
"function"

>>> typeof benji.__proto__.paws
"undefined"

你再建立新的對象,將會使用更新後的原型屬性:

>>> var lucy = new Dog();
>>> lucy.say()
TypeError: lucy.say is not a function

>>> lucy.paws
4

指向新原型屬性的私密連接__proto__

>>> typeof lucy.__proto__.say
"undefined"

>>> typeof lucy.__proto__.paws
"number"

新對象的構建函數屬性再也不被正確地彙報出來了。原本應該指向Dog(),可是卻指向了Object()

>>> lucy.constructor
Object()
>>> benji.constructor
Dog()

最難區分的部分是當你查找構造函數的原型屬性時:

>>> typeof lucy.constructor.prototype.paws
"undefined"
>>> typeof benji.constructor.prototype.paws
"number"

下面的語句將會修復上面全部的意料以外的現象:

>>> Dog.prototype = {paws: 4, hair: true};
>>> Dog.prototype.constructor = Dog;

當你覆蓋原型屬性,推薦重置constructor屬性。

總結

讓咱們來總結一下這一章節中學習的幾個要點。

  • 全部的函數都有一個叫做prototype的屬性,初始狀況下,它包含一個空白的對象。

  • 你能夠向原型屬性中添加屬性和方法。你甚至能夠將它徹底替換成你選擇的對象。

  • 當你使用構造函數穿件對象(使用new),這個對象會有一個祕密連接指向它的原型屬性,並且能夠把原型屬性的屬性當成本身的來用。

  • 相比原型屬性的屬性,同名自身的屬性擁有更高的優先級。

  • 使用hasOwnProperty()方法來區分自身屬性和原型屬性的屬性。

  • 存在一個原型鏈:若是你的對象foo沒有屬性bar,當你使用foo.bar的時候,JavaScript會從它的原型屬性中去尋找bar屬性。若是沒有找到,它會繼續在原型屬性的原型屬性中找,而後是原型屬性的原型屬性的原型屬性,並且一步一步向上,直到最高層父類Object

  • 你能夠擴充內建構造函數。全部的對象均可以應用你的擴充。申明Array.prototype.flip,然後全部的數組都會立刻擁有一個flip()方法。[1,2,3].flip()。在擴充方法和屬性以前,檢查是否存在,爲你的代碼添加將來的保證。

相關文章
相關標籤/搜索