1. 程式人生 > >淺談javascript的原型及原型鏈

淺談javascript的原型及原型鏈

枚舉 實例對象 ole 技術分享 num code 更改 actions 機制

淺談javascript的原型及原型鏈

這裏,我們列出原型的幾個概念,如下:

  • prototype屬性
  • [[prototype]]
  • __proto__

prototype屬性

只要創建了一個函數,就會為該函數創建一個prototype屬性,指向該函數的原型對象。實例對象是不會擁有該屬性的。
默認情況下,該原型對象也會獲得一個constructor屬性,該屬性包含一個指針,指向prototype屬性所在的函數。

Person.prototype.constructor===Person

[[prototype]]和__proto__

javascript中,不可以訪問的內部屬性都是用[[propertyName]]

這種形式來表示的,比如還有枚舉屬性[[Enumberable]]。

[[prototype]]屬性只能是對象可以擁有的屬性。比如實例化的對象以及原型對象,而不是構造函數。這個屬性指向擁有其屬性的對象的構造函數的原型對象。註意,此處的構造函數指的是使用new方式的構造函數。並不因為更改了原型對象上的constructor屬性而改變。

__proto__是個啥呢,就是對[[propertyName]]的實現。也就是說,你可以在支持該實現的瀏覽器下(FF,chrome,safari),去訪問對象的構造函數的原型對象。比如:


var Person=function(name){ this.name=name; }; var p1=new Person(); p1.__proto__===Person.prototype;//true Person.prototype={}; var p2=new Person(); p2.__proto__===Object.prototype;//false

當然,__proto__只是瀏覽器的私有實現,目前ECMAScript標準實現方法是Object.getPrototypeOf(object)


var Person=function(name){ this.name=name; }; var p1=new Person(); Object.getPrototypeOf(p1)===Person.prototype;//true Person.prototype={}; var p2=new Person(); Object.getPrototypeOf(p2)===Object.prototype;//false

另外一種判斷實例對象和其原型對象存在指向關系(由實例的[[prototype]]指向其構造函數的原型對象)的方法是:isPrototypeOf

。比如:

Person.prototype.isPrototypeOf(p1);//true

由於原型對象也是一個對象,所以,它自然而然也擁有[[prototype]]屬性。

Javascript 並沒有類繼承模型,而是使用原型對象 prototype 進行原型式繼承。
盡管人們經常將此看做是 Javascript 的一個缺點,然而事實上,原型式繼承比傳統的類繼承模型要更加強大。舉個例子,在原型式繼承頂端構建一個類模型很簡單,然而反過來則是個困難得多的任務。
Javascript 是唯一一個被廣泛運用的原型式繼承的語言,所以理解兩種繼承方式的差異是需要時間的。

第一個主要差異就是 Javascript 使用原型鏈來繼承:

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};
function Bar() {}

設置 BarprototypeFoo 的對象實例:

Bar.prototype = new Foo();
Bar.prototype.foo = ‘Hello World‘;

確保 Bar 的構造函數為本身,並新建一個 Bar 對象實例:

Bar.prototype.constructor = Bar;
var test = new Bar();

下面我們來看下整個原型鏈的組成:

test [instance of Bar]
    Bar.prototype [instance of Foo]
        { foo: ‘Hello World‘ }
        Foo.prototype
            { method: ... }
            Object.prototype
                { toString: ... /* etc. */ }

在上面的例子中,對象 test 將會同時繼承 Bar.prototypeFoo.prototype。因此它可以訪問定義在 Foo 中的函數 method。當然,它也可以訪問屬性 value。需要提到的是,當 new Bar() 時並不會創建一個新的 Foo 實例,而是重用它原型對象自帶的 Foo 實例。同樣,所有的 Bar 實例都共享同一個 value 屬性。我們舉例說明:

 test1 = new Bar();
 test2 = new Bar();
 Bar.prototype.value = 41;
 test1.value //41
 test2.value//41

原型鏈查找機制

當訪問一個對象的屬性時,Javascript 會從對象本身開始往上遍歷整個原型鏈,直到找到對應屬性為止。如果此時到達了原型鏈的頂部,也就是上例中的 Object.prototype,仍然未發現需要查找的屬性,那麽 Javascript 就會返回 undefined 值。

原型對象的屬性

盡管原型對象的屬性被 Javascript 用來構建原型鏈,我們仍然可以值賦給它。但是原始值復制給 prototype 是無效的,如:

function Foo() {}
Foo.prototype = 1; // no effect

這裏講個本篇的題外話,介紹下什麽是原始值:
Javascript 中,變量可以存放兩種類型的值,分別是原始值和引用值。

1.原始值(primitive value):
原始值是固定而簡單的值,是存放在棧 stack 中的簡單數據段,也就是說,它們的值直接存儲在變量訪問的位置。
原始類型有以下五種型: Undefined, Null, Boolean, Number, String

2.引用值(reference value):
引用值則是比較大的對象,存放在堆 heap 中的對象,也就是說,存儲在變量處的值是一個指針 pointer,指向存儲對象的內存處。所有引用類型都集成自 Object

原型鏈性能問題

如果需要查找的屬性位於原型鏈的上端,那麽查找過程對於性能而言無疑會帶來負面影響。當在性能要求必要嚴格的場景中這將是需要重點考慮得因素。此外,試圖查找一個不存在屬性時將會遍歷整個原型鏈。
同樣,當遍歷一個對象的屬性時,所有在原型鏈上的屬性都將被訪問。

總結

理解原型式繼承是寫較為復雜的 Javascript 代碼的前提,同時要註意代碼中原型鏈的高度。當面臨性能瓶頸時要學會將原型鏈拆分開來。此外,要將原型對象 prototype 和原型 __proto__ 區分開來,這裏主要討論原型對象 prototype 就不闡述關於原型 __proto__ 的問題了。

技術分享圖片

圖片來自基友 kzloser

圖片說明

1.總共三類對象(藍色大框)

2.實例對象(通過new XX() 所得到的實例),跟原型鏈相關的只有 __proto__ 屬性,指向其對應的原型對象 *.prototype

3.構造函數對象分原生和自定義兩類。跟原型鏈相關的有 __proto__ 屬性,除此之外還有 prototype 屬性。它們的 __proto__ 屬性都是指向 Function.prototype 這個原型對象的。prototype 也是指向對應的原型對象。

4.原型對象除了一樣擁有 __proto__ 外,也擁有獨有的屬性 constructor 。它的__proto__ 指向的都是 Object.prototype ,除了 Object.prototype 本身,它自己是指向 null 。而 constructor 屬性指向它們對應的構造函數對象。

5.原型鏈是基於 __proto__ 的。實例只能通過其對應原型對象的 constructor 才能訪問到對應的構造函數對象。構造函數只能通過其對應的 prototype 來訪問相應的原型對象。

原型與原型鏈是javascript裏面最最核心的內容,如果不能理解它們之間的存在關系的話,那麽我們是不能理解這門語言的。

在JS中,主要有兩種創建對象的方法, 分別是對象字面量以及調用構造函數

//對象字面量
var obj1 = {}

//調用構造函數
var obj2 = new Object()

其實上述兩種創建對象的方法,本質上是一樣的,都是JS引擎調用對象的構造函數來新建出一個對象。構造函數本身也是一個普通的JS函數

下面我們來看一個例子

//創建構造函數
function Person(name){
    this.name = name
}

//每個構造函數JS引擎都會自動添加一個prototype屬性,我們稱之為原型,這是一個對象
//每個由構造函數創建的對象都會共享prototype上面的屬性與方法
console.log(typeof Person.prototype) // ‘object‘


//我們為Person.prototype添加sayName方法
Person.prototype.sayName = function(){
    console.log(this.name)
}

//創建實例
var person1 = new Person(‘Messi‘)
var person2 = new Person(‘Suarez‘)

person1.sayName() // ‘Messi‘
person2.sayName() // ‘Suarez‘

person1.sayName === person2.sayName //true

我們借助上面的例子來理解構造函數-原型-實例,三者之間的關系,主要有幾個基本概念

  • 構造函數默認會有一個protoype屬性指向它的原型

  • 構造函數的原型會有一個consctructor的屬性指向構造函數本身, 即

    Person.prototype.constructor === Person
  • 每一個new出來的實例都有一個隱式的__proto__屬性,指向它們的構造函數的原型,即

    person1.__proto__ === Person.prototype
    person1.__proto__.constructor === Person

了解了這些基本概念之後,我們再來看看javascript的一些原生構造函數的關系網,看下列的圖

技術分享圖片
引自stackoverflow

按照我們上面的理解, Oject本身是一個構造函數,它也是一個對象,那麽

Object.__proto__ === Function.prototype

為了方便我們記住上圖,還有幾個需要我們知道的特殊概念:

  • Function的原型屬性與Function的原型指向同一個對象. 即

    Function.__proto__ == Function.prototype
  • Object.prototype.__proto__ === null

  • typeof Function.prototype === ‘function‘

淺談javascript的原型及原型鏈