理解 JavaScript(四)

第四篇拖了好久了,真是有點很差意思。實話實說,拖延好久的緣由主要是沒想好怎麼寫,由於這一篇的主題比較有挑戰性:原型和基於原型的繼承——啊~我終於說出口了,這下沒借口拖延了==javascript

原型

我(我的)不喜歡的,就是講原型時上來就拿類作比較的,因此我不會這樣講。不過個人確講過構造器函數,在這方面和類多多少少有共通之處。個人建議是:忘掉類。有不少觀點認爲「類」學的泛濫是面向對象的過分發展,是一種悲哀,以致於有太多的開發者幾乎把面向對象和類劃上了等號。在學習原型以前,我請你先記住並品味這句話:java

面向對象設計的精髓在於「抽象」二字,類是實現實體抽象的一種手段,但不是惟一一種。

prototype__proto__

事先聲明:永遠,永遠不要在真實的代碼裏使用 __proto__ 屬性,在本文裏用它純粹是用於研究!很快咱們會講到它的替代品,抱歉請忍耐。git

在 JavaScript 裏,函數是對象(等學完了這一篇,不妨研究一下函數到底是怎麼就成了對象的?),對象嘛,毫無心外的就會有屬性(方法也是屬性),而後毫無心外的 prototype 就是函數的一個屬性,最後毫無心外的 prototype 屬性也是一個對象。瞧,多麼瓜熟蒂落的事情:程序員

function foo() {}
foo.prototype;    // 裏面有啥本身去看

好吧,那 prototype 有啥用?呃,若是你把函數就當作函數來用,那它壓根沒用。不過,若你把函數看成構造器來用的話,新生成的對象就能夠直接訪問到 prototype 對象裏的屬性。github

// 要充當構造器了,按慣例把首字母大寫
function Foo() {}
var f = new Foo();
f.constructor;    // function Foo() {}

想一下,fconstructor 屬性哪裏來的?若是你想不明白,請用 console.dir(Foo.prototype) 一探究竟。segmentfault

這說明了一個問題:瀏覽器

函數的原型屬性不是給函數本身用的,而是給用函數充當構造器建立的對象使用的。

使人疑惑的是,prototype 屬性存在於 Foo 函數對象內,那麼由 Foo 建立的實例對象 f 是怎麼訪問到 prototype 的呢?是經過複製 prototype 對象嗎?接着上面的代碼咱們繼續來看:安全

f.__proto__;                      // Foo {}
Foo.prototype;                    // Foo {}
f.__proto__ === Foo.prototype;    // true

哦~不是複製過來的,而是一個叫作 __proto__ 的屬性指向了構造器的 prototype 對象呀。函數

沒錯!這就是原型機制的精髓所在,讓咱們來總結一下全部的細節(包括隱含在表象之下的):學習

  1. 函數擁有 prototype 屬性,可是函數本身不用它
  2. 函數充當構造器的時候能夠建立出新的對象,這須要 new 操做符的配合。其工做原理我已經在第一篇作了大部分的闡述
  3. 我還沒有說起的是:new 在建立新對象的時候,會賦予新對象一個屬性指向構造器的 prototype 屬性。這個新的屬性在某些瀏覽器環境內叫作 __proto__
  4. 當訪問一個對象的屬性(包括方法)時,首先查找這個對象自身有沒有該屬性,若是沒有就查找它的原型(也就是 __proto__ 指向的 prototype 對象),若是尚未就查找原型的原型(prototype 也有它本身的 __proto__,指向更上一級的 prototype 對象),依此類推一直找到 Object 爲止

OK,上面的第四點事實上就是 JavaScript 的對象屬性查找機制。因而可知:

原型的意義就在於爲對象的屬性查找機制提供一個方向,或者說一條路線

一個對象,它有許多屬性,其中有一個屬性指向了另一個對象的原型屬性;然後者也有一個屬性指向了再另一個對象的原型屬性。這就像一條一環套一環的鎖鏈同樣,而且從這條鎖鏈的任何一點尋找下去,最後都能找到鏈條的起點,即 Object;所以,咱們也把這種機制稱做:原型鏈

如今,我但願統一一下所使用的術語(至少在本文範圍內):

  • 函數的 prototype 屬性:咱們叫它 原型屬性原型對象
  • 對象的 __proto__ 屬性:咱們叫它 原型

例如:

  • Foo 的原型屬性(或原型對象) = Foo.prototype
  • f 的原型 = f.__proto__

統一術語的緣由在於,儘管 Foo.prototypef.__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");        // true
p1.hasOwnProperty("greeting");    // false

p1.constructor.prototype.hasOwnProperty("greeting");     // true
Object.getPrototypeOf(p1).hasOwnProperty("greeting");    // true

代碼很簡單,就不用過分解釋了,注意最後兩句實際上等價的寫法。

當心 constructor

剛纔的這一句代碼:p1.constructor.prototype.hasOwnProperty("greeting");,其實暗含了一個有趣的問題。

對象 p1 可以訪問本身的構造器,這要謝謝原型爲它提供了 constructor 屬性。接着經過 constructor 屬性又能夠反過來訪問到原型對象,這彷佛是一個圈圈,咱們來試驗一下:

p1.constructor === p1.constructor.prototype.constructor;    // true
p1.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.toString() + '。';
};

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();    // ‌你們好,我叫張三, 我是一名程序員。

我來捋一遍:

  1. 倒數第二行,new Person() 建立了對象,而後賦給了 Programmer.prototype 因而構造器原型屬性就變成了 Person 的實例對象。
  2. 由於 Person 對象擁有重寫過的 toString() 方法,而且這個方法返回的是宿主對象的 klass 屬性,因此咱們能夠給 Programmer 定義一個 greeting() 方法,並在其中使用繼承而來的 toString()
  3. someone 對象調用 toString() 方法的時候,this 指向的是它本身,因此可以輸出 程序員 而不是 人類

還沒完,繼續看:

// 由於 Programmer.prototype.constructor = Programmer; 咱們才能獲得:
someone.constructor === Programmer;    ‌// true

// 這些結果體現了何謂「鏈式」原型繼承
‌‌someone instanceof Programmer;         ‌// true
‌‌someone instanceof Person;             //‌ true
‌‌someone 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 是引用賦值,所以等號兩端的兩個屬性等於指向了同一個對象,那麼一旦你在子類對方法進行重載,連帶着父類的方法也一塊兒變化了,這就失去了重載的意義。所以只有在肯定不須要重載的時候才能夠這麼作。

相關文章
相關標籤/搜索