JavaScript 原型鏈

大部分面向對象的編程語言,都是以“類”( class )作為對象體系的語法基礎。 JavaScript 語言中是沒有 class 的概念的( ES6之前 ,ES6中雖然提供了 class 的寫法,但實現原理并不是傳統的“類” class 概念,僅僅是一種寫法), 但是它依舊可以實現面向對象的編程,這就是通過 JavaScript 中的“ 原型對象 ”( prototype )來實現的。

prototype 屬性

請看這樣一個例子:

 
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);
    };
}

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

 
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.sayHell 和 xh.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.sayHell 和 xh.sayHello 是相同的內容,這樣就很貼近面向對象的思想了。那么 zs 和 xh 這兩個對象,是怎么訪問到這個 sayHello 方法的呢?

在瀏覽器控制臺中打印出 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 這個對象只有兩個自身的屬性 gender 和 name ,這和其構造函數 Person 的模板相同,并且可以在 Person 對象的 __proto__ 屬性下找到 sayHello 方法。那么這個 __proto__ 是什么呢?它是 瀏覽器環境下 部署的一個對象,它指的是當前對象的原型對象,也就是構造函數的 prototype 屬性。

現在就可以明白了,我們給構造函數 Person 對象的 prototype 屬性添加了 sayHello 方法, zs 和 xh 這兩個通過 Person 構造函數產生的對象,是可訪問到 Person 對象的 prototype 屬性的,所以我們定義在 prototype 下的 sayHello 方法, Person 的實例對象都可以訪問到。

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

  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();

上面代碼中, Super 和 Sub 都是構造函數,在 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();

基本原理就是在子類中使用父類的構造函數。在 Person 和 Dog 中均沒有對 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.superclass 是 Animal 的一個實例,是有 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.prototype 和 Animal.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 對象,它的原型對象是 Person 的 prototype 屬性,而 Person 的 prototype 本身也是一個對象,它的原型對象是 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()

ES5 Object.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()

ES5 Object.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 ,然后讀取 a 的 x 屬性。

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

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

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

Object.create()

ES5 Object.create 方法用于從原型對象生成新的實例對象,它接收兩個參數:第一個為一個對象,新生成的對象完全繼承前者的屬性(即新生成的對象的原型此對象);第二個參數為一個屬性描述對象,此對象的屬性將會被添加到新對象。

上面代碼舉例:

 
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"}

參考鏈接

 

來自:http://blog.cdswyda.com/post/javascript/2016-11-21-javascript-prototype

 

 本文由用戶 MarVroland 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!