輕鬆理解javascript原型

輕鬆理解javascript原型

javascript原型真是一個令人望而生畏的字眼,很多時候,一談到原型,整個氣氛就瞬間嚴肅起來。的的確確,原型是這門語言的精華,但也是面試題目中的老戲骨——它對新手總是不那麼友好。在這篇文章中,作者以輕鬆幽默的語調,舉一反三,循序漸進的將javascript原型的相關知識依依爲你揭曉。但是,你不要斷章取義,因爲作者善於在後面說明前面事例的不足,更善於先用蹩腳的例子告訴你問題的本質,再提出優秀的解決方案。這是你在閱讀時需要注意的。

本章不涉及原型繼承的相關知識,因爲作者認爲應該把繼承單獨抽出組成一章,如果你對繼承的相關知識和作者的風格感興趣,繼續讀下一篇博客《輕鬆理解javascript繼承》。

這篇文章參考了《javascript語言精粹》、《javascript啓示錄》以及《javascript權威指南》的內容。

一切從函數開始

在javascript中,函數是對象,我們可以把函數存儲在一個變量中,也可以給函數添加屬性。JS中所有的函數都由一個叫做Function的構造器創建。當一個函數對象被創建時,Function構造器會”隱蔽地”給這個函數對象添加一個叫做prototype的屬性,其值是一個包含函數本身(constuctor)的對象:

this.prototype = {constructor : this}

其中,prototype就是“傳說中”的原型,而的this指的就是函數本身。javascript會“公平地”爲每個函數創建原型對象。無論這個函數以後是否用作構造函數。

下面的代碼是個很好的例子:

function sayHello () {

}

console.log(sayHello.prototype)  //=> { constuctor : sayHello(),  __proto__ : Object}

你會發現還有一個叫做__proto__的屬性,這又是什麼呢?先不要亂了陣腳,繼續向下看。

優秀的工匠——new

當函數“有志氣”成爲一名構造函數的時候,prototype屬性開始真正發揮作用。new運算符是一名優秀的“工匠”,它可以使用對象模具——構造函數產生一個個的實例對象。

當new運算符使用構造函數產生對象實例時,會“強制性地”在新對象中添加一個叫做__proto__的屬性作爲”隱祕連接“,它的值就等於它的構造函數prototype屬性的值,換句話說,使這它與其構造函數的prototype屬性指向同一個對象。

顯然,每一個javascript對象都會擁有一個叫做__proto__的屬性,因爲javascript中所有的對象都隱式或顯式地由構造函數new出,於是,也可以說在javscript中沒有真正意義上的空對象。

當然,我們的new運算符沒有忘記它的“老本行”:它會將構造函數中定義的實例屬性或方法(this.屬性)添加到新創建的對象中。

下面的代碼或許能夠幫助你理解:

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

// 爲構造器的prototype新增一個屬性
Student.prototype.age = 20;

var Tom = new Student("Tom");
console.log(Tom.name); // => Tom
console.log(Tom.__proto__.constructor); // =>function Student() {this.name = name}
console.log(Tom.__proto__.age); // =>20

簡而言之,原型prototype是javascript函數的一個屬性,當這個函數作爲構造器產生實例時,new運算符會獲得函數的prototype屬性的值並將其賦給對象實例的__proto__屬性,並以此作爲隱祕連接。因此,你在構造函數的prototype屬性中設置的值都會被該構造器的實例所擁有。

磐石——Object構造器

之所以還不說原型鏈,是因爲我想先試着不把事情變得那麼複雜:還是以上面的Student僞類爲例。Tom對象的__proto__屬性來自其構造器Student的prototype屬性,這個應該很好理解。但是,問題是Student的prototype也是一個對象,它有我們設置的age屬性,更有每個對象都擁有的__proto__屬性。那麼問題來了,Student的prototype對象是誰創建的呢,它的__proto__值從來自哪裏呢?

Object構造器是無名英雄——它創建所有以對象字面量表示的對象。Student的prototype對象正是由Object構造器創建的,它的__protot__值是在Object構造器的prototype屬性。

希望下面的例子能夠幫助你理解:

var obj = {};
console.log(obj.constructor); // =>function Object() {native code}
console.log('__proto__' in obj); // =>true

靈魂連接——原型鏈

好的,原型鏈在我們試圖從某個對象獲取某個屬性(或方法)時發揮作用。如果那個屬性剛好像下面這樣存在於這個對象之中,那無需多慮,直接返回即可。

var student = {name : 'Jack'}
student.name // =>Jack

但是,如果這個屬性不直接存在於這個對象中,那麼javascript會在這個對象的構造器的prototype屬性,也就是這個對象的__proto__屬性中進行查找。

由於訪問__proto__並非官方ECMA標準的一部分,所以後面我們都說”其構造函數的prototype屬性”,而不說“這個對象的__proto__屬性“了。

好吧,如果找到,則直接返回,否則,繼續這個循環,因爲prototype的值也是對象:繼續在 /該對象的構造器的prototype對象/ 的構造器的prototype屬性中尋找……。

所以你該知道,由於prototype屬性一定是一個對象,因此原型鏈或者說查找中的最後一站是Object.prototype。如果查找到這裏仍然沒有發現,則循環結束,返回undefined。

因爲這種鏈查找機制的存在,上面的代碼得到了簡化,這也是Javascript中繼承的基石:

console.log(Tom.__proto__.age); // =>20
console.log(Tom.age); // =>20

好吧,我希望通過下面的例子帶你拉通走一遍:

var arr = [];
console.log(arr.foo); //=>undefined

首先,當JS得知要訪問arr的foo屬性時,他首先會在arr對象裏查找foo屬性,但是結局令人失望。之後,它會去查找arr的構造函數即Array的prototype屬性,看是否能在這裏查找到什麼線索,結果也沒有。最後,它會去查找Array的prototype對象的構造函數——Object的prototype屬性——仍然沒有找到,搜索結束,返回undefined。

之所以舉一個原生的構造函數的例子是因爲我一直害怕因爲使用自定義的例子而給大家帶來一種只有自定義的構造函數纔可以這樣的錯覺。你要知道,這篇文章所講述的道理適合一切的構造器。

好了,讓我們看一個自定義的構造器並在原型鏈上查找到屬性的”好“例子:

Object.prototype.foo = "some foo";

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

// 爲構造器的prototype新增一個屬性
Student.prototype.age = 20;


var Tom = new Student("Tom");
console.log(Tom.name); // => Tom
console.log(Tom.age); // =>20
console.log(Tom.foo); // =>some foo

這裏要說明的是,原型鏈在查找時,會使用它查找到的第一個值;一旦找到,立即返回,不會再往下進行尋找。

私有與共享

當屬性涉及到對象時問題變得棘手起來,你知道,對象的傳遞與複製時引用傳遞,也就是說,我在實例的構造器prototype上設置的屬性如果是對象,那麼所有實例實際上都將共享這一個對象:

var pocketMoney = {
    num: 5,
    value:10
};

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

// 爲構造器的prototype新增一個屬性
Student.prototype.pocketMoney = pocketMoney;

var Tom = new Student("Tom");
var Jack = new Student("Jack");

Tom.pocketMoney.num = 4;

console.log(Tom.pocketMoney.num); // =>4
console.log(Jack.pocketMoney.num); // =>4

Tom去遊戲廳打遊戲花費了一張10元的紙幣,但Jack的零花錢卻也因此減少了,這顯然是不可接受的。

所以,爲了避免這種情況的發生,請將屬性寫在構造器內部,更改後,事情纔看上去像回事:

function Student(name) {
    this.name = name;
    this.pocketMoney = {
        num: 5,
        value: 10
    };
}

var Tom = new Student("Tom");
var Jack = new Student("Jack");

Tom.pocketMoney.num = 4;

console.log(Tom.pocketMoney.num); // =>4
console.log(Jack.pocketMoney.num); // =>5

我們常常把方法寫在prototype中,而不是屬性。

階段總結

到此,原型的基礎知識我們就已經基本講完了。如果你覺得有很多東西沒有提到,你是正確的,因爲我已經把繼承單獨抽出來,組成了新的一章,裏面會涉及大量有關原型的知識,我也會介紹給你一種全新的,備受推崇的繼承方式——應用模塊的差異化繼承。我強烈推薦你趁熱打鐵,現在就去讀我博客中的《輕鬆理解javascript繼承》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章