深度剖析前端JavaScript中的原型(JS的對象原型)


      
 
 
              這張圖片有點勸退了,哈哈哈~ 

 

 

 
 

通過原型機制,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() 構造器中的成員—— nameagegenderinterestsbiogreeting。同時也有一些其他成員—— watchvalueOf 等等——這些成員定義在 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() 構造器自身使用。

 

 

注意:這看起來很奇怪——構造器本身就是函數,你怎么可能在構造器這個函數中定義一個方法呢?其實函數也是一個對象類型

 

    1. 你可以檢查已有的 prototype 屬性。回到先前的例子,在 JavaScript 控制台輸入:
      Person.prototype
    2. 輸出並不多,畢竟我們沒有為自定義構造器的原型定義任何成員。缺省狀態下,構造器的 prototype 屬性初始為空白。現在嘗試:
      Object.prototype
      

        

 

你會看到 Object prototype 屬性上定義了大量的方法;如前所示,繼承自 Object 的對象都可以使用這些方法。

 

 

JavaScript 中到處都是通過原型鏈繼承的例子。比如,你可以嘗試從 StringDateNumberArray 全局對象的原型中尋找方法和屬性。它們都在原型上定義了一些方法,因此當你創建一個字符串時:

    var myString = 'This is my string.';

  

myString 立即具有了一些有用的方法,如 split()indexOf()replace() 等。

重要:prototype 屬性大概是 JavaScript 中最容易混淆的名稱之一。你可能會認為,this 關鍵字指向當前對象的原型對象,其實不是(還記得么?原型對象是一個內部對象,應當使用 __proto__ 訪問)。prototype 屬性包含(指向)一個對象,你在這個對象中定義需要被繼承的成員。

 

create()

Object.create() 方法可以創建新的對象實例。

    1. 例如,在上個例子的 JavaScript 控制台中輸入:
      var person2 = Object.create(person1);
      

        

    2. create() 實際做的是從指定原型對象創建一個新的對象。這里以 person1 為原型對象創建了 person2 對象。在控制台輸入:
      person2.__proto__
      

        

 

結果返回對象person1

 


 

constructor 屬性

每個實例對象都從原型中繼承了一個constructor屬性,該屬性指向了用於構造此實例對象的構造函數。

    1. 例如,繼續在控制台中嘗試下面的指令:
              person1.constructor
              person2.constructor
      

        

      都將返回 Person() 構造器,因為該構造器包含這些實例的原始定義。

      一個小技巧是,你可以在 constructor 屬性的末尾添加一對圓括號(括號中包含所需的參數),從而用這個構造器創建另一個對象實例。畢竟構造器是一個函數,故可以通過圓括號調用;只需在前面添加 new 關鍵字,便能將此函數作為構造器使用。

      1. 在控制台中輸入:
                      var person3 = new person1.constructor('Karen', 
                          'Stephenson', 26,
                          'female', 
                         ['playing drums', 'mountain climbing']);
        

          

      2. 現在嘗試訪問新建對象的屬性,例如:
                        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.firstname.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/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM