JavaScript 原型鏈

大部分面向對象的編程語言,都是以「類」(class)做爲對象體系的語法基礎。JavaScript語言中是沒有class的概念的(ES6以前,ES6中雖然提供了class的寫法,但實現原理並非傳統的「類」class概念,僅僅是一種寫法), 可是它依舊能夠實現面向對象的編程,這就是經過JavaScript中的「原型對象」(prototype)來實現的。javascript

prototype 屬性

請看這樣一個例子:html

function Person(name, gender) {
    this.name = name;
    this.gender = gender;
    this.sayHello = function() {
        console.log('Hello,I am', this.name, '. I\'m a', this.gender);
    };
}

這樣定義了一個構造函數,咱們建立對象就可使用這個構造函數做爲模板來生成。不過以面向對象的思想來看,不難發現其中的一點問題:namegender屬性是每一個實例都各不相同,做爲一個自身的屬性沒有問題,而sayHello方法,每一個實例對象應該都有,並且都同樣,給每一個實例對象一個全新的、徹底不一樣(雖然代碼內容同樣,但JavaScript中每一個sayHello的值都在內存中單獨存在)的sayHello方法是沒有必要的。java

var zs = new Person('zhang san', 'male'),
    xh = new Person('xiao hong', 'female');

zs.sayHello(); // Hello,I am zhang san . I'm a male
xh.sayHello(); // Hello,I am xiao hong . I'm a female

zs.sayHello === xh.sayHello;  // false

上面代碼中展現了zs.sayHellxh.sayHello這兩個做用相同,並且看起來代碼內容也是徹底同樣的對象,實際是兩個獨立的,互不相關的對象。編程

面向對象思想中,是將公共的、抽象的屬性和方法提取出來,做爲一個基類,子類繼承這個基類,從而繼承到這些屬性和方法。而JavaScript中則能夠經過prototype屬性來實現相似的做用。如下是上面代碼的改進示例:瀏覽器

function Person(name, gender) {
    this.name = name;
    this.gender = gender;
}
Person.prototype.sayHello = function() {
    console.log('Hello,I am', this.name, '. I\'m a', this.gender);
};

var zs = new Person('zhang san', 'male'),
    xh = new Person('xiao hong', 'female');

zs.sayHello(); // Hello,I am zhang san . I'm a male
xh.sayHello(); // Hello,I am xiao hong . I'm a female

zs.sayHello === xh.sayHello;  // true

這時將sayHello方法定義到Person對象上的prototype屬性上,取代了在構造函數中給每一個實例對象添加sayHello方法。能夠看到,其還能實現和以前相同的做用,並且zs.sayHellxh.sayHello是相同的內容,這樣就很貼近面向對象的思想了。那麼zsxh這兩個對象,是怎麼訪問到這個sayHello方法的呢?app

在瀏覽器控制檯中打印出zs,將其展開,能夠看到下面的結果:編程語言

zs;
/**
 * 
Person
    gender: "male"
    name: "zhang san"
    __proto__: Object
        constructor: function Person(name, gender) 
            arguments: null
            caller: null
            length: 2 
            name: "Person"
            prototype: Object
        sayHello:function()
            arguments:null
            caller:null
            length:0
            name:""
            prototype:Object
*/

zs這個對象只有兩個自身的屬性gendername,這和其構造函數Person的模板相同,而且能夠在Person對象的__proto__屬性下找到sayHello方法。那麼這個__proto__是什麼呢?它是瀏覽器環境下部署的一個對象,它指的是當前對象的原型對象,也就是構造函數的prototype屬性。函數

如今就能夠明白了,咱們給構造函數Person對象的prototype屬性添加了sayHello方法,zsxh這兩個經過Person構造函數產生的對象,是可訪問到Person對象的prototype屬性的,因此咱們定義在prototype下的sayHello方法,Person的實例對象均可以訪問到。oop

關於構造函數的new命令原理是這樣的:post

  1. 建立一個空對象,做爲將要返回的對象實例

  2. 將這個空對象的原型,指向構造函數的prototype屬性

  3. 將這個空對象賦值給函數內部的this關鍵字

  4. 開始執行構造函數內部的代碼

constructor 屬性

prototype下有一個屬性constructor,默認指向此prototype對象所在的構造函數。

如上例中的zs__proto__constructor值爲function Person(name, gender)

因爲此屬性定義在prototype屬性上,因此它能夠在全部的實例對象中獲取到。

zs.constructor;
// function Person(name, gender) {
//     this.name = name;
//     this.gender = gender;
// }

zs.hasOwnProperty('constructor'); // false
zs.constructor === Person; // true

zs.constructor === Function; // false
zs.constructor === Object; // false

constructor屬性放在prototype屬性中的一個做用是,能夠經過這個屬性來判斷這個對象是由哪一個構造函數產生的,上面代碼中,zs是由Person構造函數產生的,而不是Function或者Object構造函數產生。

constructor屬性的另外一個做用就是:提供了一種繼承的實現模式。

function Super() {
    // ...
}

function Sub() {
    Sub.superclass.constructor.call(this);
    // ...
}

Sub.superclass = new Super();

上面代碼中,SuperSub都是構造函數,在Sub內部的this上調用Super,就會造成Sub繼承Super的效果,miniui中是這樣實現繼承的:

mini.Control = function(el) {    
    mini.Control.superclass.constructor.apply(this, arguments);
    // ...
}
// 其中的superclass指代父類的prototype屬性

咱們本身寫一個例子:

// 父類
function Animal(name) {
    this.name = name;
    this.introduce = function() {
        console.log('Hello , My name is', this.name);
    }
}
Animal.prototype.sayHello = function() {
    console.log('Hello, I am:', this.name);
}

// 子類
function Person(name, gender) {
    Person.superclass.constructor.apply(this, arguments);
    this.gender = gender;
}
Person.superclass = new Animal();

// 子類
function Dog(name) {
    Dog.superclass.constructor.apply(this, arguments);    
}
Dog.superclass = new Animal();

基本原理就是在子類中使用父類的構造函數。在PersonDog中均沒有對name屬性和introduce方法進行操做,只是使用了父類Animal的構造函數,就能夠將name屬性和introduce方法繼承來,請看下面例子:

var zs = new Person('zhang san', 'male');

zs; // Person {name: "zhang san", gender: "male"}
zs.sayHello(); // Uncaught TypeError: zs.sayHello is not a function(…)
zs.introduce(); // Hello , My name is zhang san

var wangCai = new Dog("旺財");

wangCai; // Dog {name: "旺財"}
wangCai.introduce(); // Hello , My name is 旺財

確實實現了咱們須要的效果。但是咱們發如今調用zs.sayHello()時報錯了。爲何呢?

其實不難發現問題,咱們的Person.superclassAnimal的一個實例,是有sayHello方法的,可是咱們在Perosn構造函數的內部,只是使用了Person.superclass.constructor。而Person.superclass.constructor指的僅僅是Animal構造函數自己,並無包括Animal.prototype,因此沒有sayHello方法。

一種改進方法是:將自定義的superclass換爲prototype,即:

function Person(name, gender) {
    Person.prototype.constructor.apply(this, arguments);
    this.gender = gender;
}
Person.prototype = Animal.prototype;

var zs = new Person('zhang san', 'male');
zs.sayHello(); // Hello, I am: zhang san
zs.introduce() // Hello , My name is zhang san

這樣就所有繼承到了Animal.prototype下的方法。

可是通常不要這樣作,上面寫法中Person.prototype = Animal.prototype; 等號兩端都是一個完整的對象,進行賦值時,Person.prototype的原對象徹底被Animal.prototype替換,切斷了和以前原型鏈的聯繫,並且此時Person.prototypeAnimal.prototype是相同的引用,給Person.prototype 添加的屬性方法也將添加到Animal.prototype,反之亦然,這將引發邏輯混亂。

所以咱們在原型上進行擴展是,一般是添加屬性,而不是替換爲一個新對象。

// 好的寫法
Person.prototype.sayHello = function() {
    console.log('Hello,I am', this.name, '. I\'m a', this.gender);
};
Person.prototype. // .. 其餘屬性 

// 很差的寫法
Person.prototype = {
    sayHello:function(){
        console.log('Hello,I am', this.name, '. I\'m a', this.gender);
    },
    // 其餘屬性方法 ...
}

JavaScript 原型鏈

JavaScript的全部對象都有構造函數,而全部構造函數都有prototype屬性(實際上是全部函數都有prototype屬性),因此全部對象都有本身的原型對象。

對象的屬性和方法,有多是定義在自身,也有多是定義在它的原型對象。因爲原型自己也是對象,又有本身的原型,因此造成了一條原型鏈(prototype chain)。

zs.sayHello(); // Hello,I am zhang san . I'm a male

zs.toString(); // "[object Object]"

例如上面的zs對象,它的原型對象是Personprototype屬性,而Personprototype自己也是一個對象,它的原型對象是Object.prototype

zs自己沒有sayHello方法,JavaScript經過原型鏈向上繼續尋找,在Person.prototype上找到了sayHello方法。toString方法在zs對象自己上沒有,Person.prototype上也沒有,所以繼續沿原型鏈查找,最終能夠在Object.prototype上找到了toString方法。

Object.prototype的原型指向null,因爲null沒有任何屬性,所以原型鏈到Object.prototype終止,因此Object.prototype是原型鏈的最頂端。

「原型鏈」的做用是,讀取對象的某個屬性時,JavaScript引擎先尋找對象自己的屬性,若是找不到,就到它的原型去找,若是仍是找不到,就到原型的原型去找。若是直到最頂層的Object.prototype仍是找不到,則返回undefined

若是對象自身和它的原型,都定義了一個同名屬性,那麼優先讀取對象自身的屬性,這叫作「覆蓋」(overiding)。

JavaScript中經過原型鏈實現了相似面向對象編程語言中的繼承,咱們在複製一個對象時,只用複製其自身的屬性便可,無需將整個原型鏈進行一次複製,Object.prototype下的hasOwnProperty方法能夠判斷一個屬性是不是該對象自身的屬性。

實例對象構造函數prototype之間的關係可用下圖表示:

instranceof 運算符

instanceof運算符返回一個布爾值,表示指定對象是否爲某個構造函數的實例。因爲原型鏈的關係,所謂的實例並不必定是某個構造函數的直接實例,更準確的描述,應該是:返回一個後者的原型對象是否在前者的原型鏈上

zs instanceof Person; // true
zs instanceof Object ;// true 

var d = new Date();
d instanceof Date; // true
d instanceof Object; // true

原型鏈相關屬性和方法

Object.prototype.hasOwnProperty()

hasOwnProperty()方法用來判斷某個對象是否含有指定的自身屬性。這個方法能夠用來檢測一個對象是否含有特定的自身屬性,和 in 運算符不一樣,該方法會忽略掉那些從原型鏈上繼承到的屬性。

zs.hasOwnProperty('name'); // true
zs.hasOwnProperty('gender'); // true

zs.hasOwnProperty('sayHello'); // fasle
Person.prototype.hasOwnProperty('sayHello'); // true 

zs.hasOwnProperty('toString'); // fasle
Object.prototype.hasOwnProperty('toString'); // true

Object.prototype.isPrototypeOf()

對象實例的isPrototypeOf方法,用來判斷一個對象是不是另外一個對象的原型。

var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);

o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true

上面代碼代表,只要某個對象處在原型鏈上,isProtypeOf都返回true

Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false

看起來這個方法和instanceof運算符做用相似,但實際使用是不同的

例如:

zs instanceof Person ; // true;

Person.isPrototypeOf(zs);// false
Person.prototype.isPrototypeOf(zs); // true

zs instanceof Person可理解爲判斷Person.prototype在不在zs的原型鏈上。 而Person.isPrototypeOf(zs)指的就是Person自己在不在zs的原型鏈上,因此返回false,只有Person.prototype.isPrototypeOf(zs)才爲 true

Object.getPrototypeOf()

ES5Object.getPrototypeOf方法返回一個對象的原型。這是獲取原型對象的標準方法。

// 空對象的原型是Object.prototype
Object.getPrototypeOf({}) === Object.prototype
// true

// 函數的原型是Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype
// true

// f 爲 F 的實例對象,則 f 的原型是 F.prototype
var f = new F();
Object.getPrototypeOf(f) === F.prototype
// true

Object.getPrototypeOf("foo");
// TypeError: "foo" is not an object (ES5 code)
Object.getPrototypeOf("foo");
// String.prototype                  (ES6 code)

此方法是ES5方法,須要IE9+。在ES5中,參數只能是對象,不然將拋出異常,而在ES6中,此方法可正確識別原始類型。

Object.setPrototypeOf()

ES5Object.setPrototypeOf方法能夠爲現有對象設置原型,返回一個新對象。接受兩個參數,第一個是現有對象,第二個是原型對象。

var a = {x: 1};
var b = Object.setPrototypeOf({}, a);
// 等同於
// var b = {__proto__: a};

b.x // 1

上面代碼中,b對象是Object.setPrototypeOf方法返回的一個新對象。該對象自己爲空、原型爲a對象,因此b對象能夠拿到a對象的全部屬性和方法。b對象自己並無x屬性,可是JavaScript引擎找到它的原型對象a,而後讀取ax屬性。

new命令經過構造函數新建實例對象,實質就是將實例對象的原型,指向構造函數的prototype屬性,而後在實例對象上執行構造函數。

var F = function () {
  this.foo = 'bar';
};

// var f = new F();等同於下面代碼
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);

Object.create()

ES5Object.create方法用於從原型對象生成新的實例對象,它接收兩個參數:第一個爲一個對象,新生成的對象徹底繼承前者的屬性(即新生成的對象的原型此對象);第二個參數爲一個屬性描述對象,此對象的屬性將會被添加到新對象。(關於屬性描述對象可參考:MDN - Object.defineProperty())

上面代碼舉例:

var zs = new Person('zhang san', 'male');

var zs_clone = Object.create(zs);

zs_clone; // {}
zs_clone.sayHello(); // Hello,I am zhang san . I'm a male
zs_clone.__proto__ === zs; // true
// Person
//     __proto__: Person
//         gender: "male"
//         name: "zhang san"
//         __proto__: Object

能夠 看出 建立的新對象zs_clone的原型爲zs,從而得到了zs的所有屬性和方法。可是其自身屬性爲空,若須要爲新對象添加自身屬性,則使用第二個參數便可。

var zs_clone = Object.create(zs, {
    name: { value: 'zhangsan\'s clone' },
    gender: { value: 'male' },
    age: { value: '25' }
});
zs_clone; // Person {name: "zhangsan's clone", gender: "male", age: "25"}

參考連接

更多可見JavaScript 原型鏈

相關文章
相關標籤/搜索