JavaScript中prototype用法

hemeinvyiqiluoben發表於2018-02-02

轉自:   http://blog.csdn.net/jasonzds/article/details/53706958


1 概述

大部分物件導向的程式語言,都是以類class作為物件的基礎語法,js語言不是如此,它的物件導向程式設計基於‘原型物件’。

首先說說建構函式的缺點:

js通過建構函式生成新的物件,因此建構函式可以視為獨享的模版。例項物件的屬性和方法,可以定義在建構函式內部

概述

建構函式的缺點

JavaScript通過建構函式生成新物件,因此建構函式可以視為物件的模板。例項物件的屬性和方法,可以定義在建構函式內部。

function Cat (name, color) {

  this.name =name;

  this.color =color;

}

 

var cat1 = new Cat('大毛', '白色');

 

cat1.name // '大毛'

cat1.color // '白色'

上面程式碼的Cat函式是一個建構函式,函式內部定義了name屬性和color屬性,所有例項物件都會生成這兩個屬性。但是,這樣做是對系統資源的浪費,因為同一個建構函式的物件例項之間,無法共享屬性。

function Cat(name, color) {

  this.name =name;

  this.color =color;

  this.meow =function () {

   console.log('mew, mew, mew...');

  };

}

 

var cat1 = new Cat('大毛', '白色');

var cat2 = new Cat('二毛', '黑色');

 

cat1.meow === cat2.meow

// false

上面程式碼中,cat1和cat2是同一個建構函式的例項。但是,它們的meow方法是不一樣的,就是說每新建一個例項,就會新建一個meow方法。這既沒有必要,又浪費系統資源,因為所有meow方法都是同樣的行為,完全應該共享。

prototype屬性的作用

JavaScript的每個物件都繼承另一個物件,後者稱為“原型”(prototype)物件。只有null除外,它沒有自己的原型物件。

原型物件上的所有屬性和方法,都能被派生物件共享。這就是JavaScript繼承機制的基本設計。

通過建構函式生成例項物件時,會自動為例項物件分配原型物件。每一個建構函式都有一個prototype屬性,這個屬性就是例項物件的原型物件。

function Animal (name) {

  this.name =name;

}

 

Animal.prototype.color = 'white';

 

var cat1 = new Animal('大毛');

var cat2 = new Animal('二毛');

 

cat1.color // 'white'

cat2.color // 'white'

上面程式碼中,建構函式Animal的prototype物件,就是例項物件cat1和cat2的原型物件。在原型物件上新增一個color屬性。結果,例項物件都能讀取該屬性。

原型物件的屬性不是例項物件自身的屬性。只要修改原型物件,變動就立刻會體現在所有例項物件上。

Animal.prototype.color = 'yellow';

 

cat1.color // "yellow"

cat2.color // "yellow"

上面程式碼中,原型物件的color屬性的值變為yellow,兩個例項物件的color屬性立刻跟著變了。這是因為例項物件其實沒有color屬性,都是讀取原型物件的color屬性。也就是說,當例項物件本身沒有某個屬性或方法的時候,它會到建構函式的prototype屬性指向的物件,去尋找該屬性或方法。這就是原型物件的特殊之處。

如果例項物件自身就有某個屬性或方法,它就不會再去原型物件尋找這個屬性或方法。

cat1.color = 'black';

 

cat2.color // 'yellow'

Animal.prototype.color // "yellow";

上面程式碼中,例項物件cat1的color屬性改為black,就使得它不再去原型物件讀取color屬性,後者的值依然為yellow。

總結一下,原型物件的作用,就是定義所有例項物件共享的屬性和方法。這也是它被稱為原型物件的含義,而例項物件可以視作從原型物件衍生出來的子物件。

Animal.prototype.walk = function () {

 console.log(this.name + ' is walking');

};

上面程式碼中,Animal.prototype物件上面定義了一個walk方法,這個方法將可以在所有Animal例項物件上面呼叫。

由於JavaScript的所有物件都有建構函式,而所有建構函式都有prototype屬性(其實是所有函式都有prototype屬性),所以所有物件都有自己的原型物件。

原型鏈

物件的屬性和方法,有可能是定義在自身,也有可能是定義在它的原型物件。由於原型本身也是物件,又有自己的原型,所以形成了一條原型鏈(prototype chain)。比如,a物件是b物件的原型,b物件是c物件的原型,以此類推。

如果一層層地上溯,所有物件的原型最終都可以上溯到Object.prototype,即Object建構函式的prototype屬性指向的那個物件。那麼,Object.prototype物件有沒有它的原型呢?回答可以是有的,就是沒有任何屬性和方法的null物件,而null物件沒有自己的原型。

Object.getPrototypeOf(Object.prototype)

// null

上面程式碼表示,Object.prototype物件的原型是null,由於null沒有任何屬性,所以原型鏈到此為止。

“原型鏈”的作用是,讀取物件的某個屬性時,JavaScript引擎先尋找物件本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。如果直到最頂層的Object.prototype還是找不到,則返回undefined。

如果物件自身和它的原型,都定義了一個同名屬性,那麼優先讀取物件自身的屬性,這叫做“覆蓋”(overiding)。

需要注意的是,一級級向上,在原型鏈尋找某個屬性,對效能是有影響的。所尋找的屬性在越上層的原型物件,對效能的影響越大。如果尋找某個不存在的屬性,將會遍歷整個原型鏈。

舉例來說,如果讓某個函式的prototype屬性指向一個陣列,就意味著該函式可以當作陣列的建構函式,因為它生成的例項物件都可以通過prototype屬性呼叫陣列方法。

var MyArray = function () {};

 

MyArray.prototype = new Array();

MyArray.prototype.constructor = MyArray;

 

var mine = new MyArray();

mine.push(1, 2, 3);

 

mine.length // 3

mine instanceof Array // true

上面程式碼中,mine是建構函式MyArray的例項物件,由於MyArray的prototype屬性指向一個陣列例項,使得mine可以呼叫陣列方法(這些方法定義在陣列例項的prototype物件上面)。至於最後那行instanceof表示式,我們知道instanceof運算子用來比較一個物件是否為某個建構函式的例項,最後一行就表示mine為Array的例項。

下面的程式碼可以找出,某個屬性到底是原型鏈上哪個物件自身的屬性。

function getDefiningObject(obj, propKey) {

  while (obj&& !{}.hasOwnProperty.call(obj, propKey)) {

    obj =Object.getPrototypeOf(obj);

  }

  return obj;

}

constructor屬性

prototype物件有一個constructor屬性,預設指向prototype物件所在的建構函式。

function P() {}

 

P.prototype.constructor === P

// true

由於constructor屬性定義在prototype物件上面,意味著可以被所有例項物件繼承。

function P() {}

var p = new P();

 

p.constructor

// function P() {}

 

p.constructor === P.prototype.constructor

// true

 

p.hasOwnProperty('constructor')

// false

上面程式碼中,p是建構函式P的例項物件,但是p自身沒有contructor屬性,該屬性其實是讀取原型鏈上面的P.prototype.constructor屬性。

constructor屬性的作用,是分辨原型物件到底屬於哪個建構函式。

function F() {};

var f = new F();

 

f.constructor === F // true

f.constructor === RegExp // false

上面程式碼表示,使用constructor屬性,確定例項物件f的建構函式是F,而不是RegExp。

有了constructor屬性,就可以從例項新建另一個例項。

function Constr() {}

var x = new Constr();

 

var y = new x.constructor();

y instanceof Constr // true

上面程式碼中,x是建構函式Constr的例項,可以從x.constructor間接呼叫建構函式。

這使得在例項方法中,呼叫自身的建構函式成為可能。

Constr.prototype.createCopy = function () {

  return newthis.constructor();

};

這也提供了繼承模式的一種實現。

function Super() {}

 

function Sub() {

 Sub.superclass.constructor.call(this);

}

 

Sub.superclass = new Super();

上面程式碼中,Super和Sub都是建構函式,在Sub內部的this上呼叫Super,就會形成Sub繼承Super的效果。

由於constructor屬性是一種原型物件與建構函式的關聯關係,所以修改原型物件的時候,務必要小心。

function A() {}

var a = new A();

a instanceof A // true

 

function B() {}

A.prototype = B.prototype;

a instanceof A // false

上面程式碼中,a是A的例項。修改了A.prototype以後,constructor屬性的指向就變了,導致instanceof運算子失真。

所以,修改原型物件時,一般要同時校正constructor屬性的指向。

// 避免這種寫法

C.prototype = {

  method1:function (...) { ... },

  // ...

};

 

// 較好的寫法

C.prototype = {

  constructor:C,

  method1:function (...) { ... },

  // ...

};

 

// 好的寫法

C.prototype.method1 = function (...) { ... };

上面程式碼中,避免完全覆蓋掉原來的prototype屬性,要麼將constructor屬性重新指向原來的建構函式,要麼只在原型物件上新增方法,這樣可以保證instanceof運算子不會失真。

此外,通過name屬性,可以從例項得到建構函式的名稱。

function Foo() {}

var f = new Foo();

f.constructor.name// "Foo"

2 instanceof運算子

instanceof運算子返回一個布林值,表示指定物件是否為某個建構函式的例項。

var v = new Vehicle();

v instanceof Vehicle // true

上面程式碼中,物件v是建構函式Vehicle的例項,所以返回true。

instanceof運算子的左邊是例項物件,右邊是建構函式。它的運算實質是檢查右邊構建函式的原型物件,是否在左邊物件的原型鏈上。因此,下面兩種寫法是等價的。

v instanceof Vehicle

// 等同於

Vehicle.prototype.isPrototypeOf(v)

由於instanceof對整個原型鏈上的物件都有效,因此同一個例項物件,可能會對多個建構函式都返回true。

var d = new Date();

d instanceof Date // true

d instanceof Object // true

上面程式碼中,d同時是Date和Object的例項,因此對這兩個建構函式都返回true。

instanceof的原理是檢查原型鏈,對於那些不存在原型鏈的物件,就無法判斷。

Object.create(null) instanceof Object // false

上面程式碼中,Object.create(null)返回的新物件的原型是null,即不存在原型,因此instanceof就認為該物件不是Object的例項。

除了上面這種繼承null的特殊情況,JavaScript之中,只要是物件,就有對應的建構函式。因此,instanceof運算子的一個用處,是判斷值的型別。

var x = [1, 2, 3];

var y = {};

x instanceof Array // true

y instanceof Object // true

上面程式碼中,instanceof運算子判斷,變數x是陣列,變數y是物件。

注意,instanceof運算子只能用於物件,不適用原始型別的值。

var s = 'hello';

s instanceof String // false

上面程式碼中,字串不是String物件的例項(因為字串不是物件),所以返回false。

此外,undefined和null不是物件,所以instanceOf運算子總是返回false。

undefined instanceof Object // false

null instanceof Object // false

利用instanceof運算子,還可以巧妙地解決,呼叫建構函式時,忘了加new命令的問題。

function Fubar (foo, bar) {

  if (thisinstanceof Fubar) {

    this._foo =foo;

    this._bar =bar;

  }

  else {

    return newFubar(foo, bar);

  }

}

上面程式碼使用instanceof運算子,在函式體內部判斷this關鍵字是否為建構函式Fubar的例項。如果不是,就表明忘了加new命令。

 

3 Object.getPrototypeOf()

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

4 Object.setPrototypeOf()

Object.setPrototypeOf方法可以為現有物件設定原型,返回一個新物件。

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

5 Object.create()

Object.create方法用於從原型物件生成新的例項物件,可以替代new命令。

它接受一個物件作為引數,返回一個新物件,後者完全繼承前者的屬性,即原有物件成為新物件的原型。

var A = {

 print: function() {

  console.log('hello');

 }

};

 

var B = Object.create(A);

 

B.print() // hello

B.print === A.print // true

上面程式碼中,Object.create方法在A的基礎上生成了B。此時,A就成了B的原型,B就繼承了A的所有屬性和方法。這段程式碼等同於下面的程式碼。

var A = function () {};

A.prototype = {

 print: function() {

  console.log('hello');

 }

};

 

var B = new A();

 

B.print === A.prototype.print // true

實際上,Object.create方法可以用下面的程式碼代替。如果老式瀏覽器不支援Object.create方法,可以就用這段程式碼自己部署。

if (typeof Object.create !== 'function') {

  Object.create= function (o) {

    function F(){}

    F.prototype= o;

    return newF();

  };

}

上面程式碼表示,Object.create方法實質是新建一個建構函式F,然後讓F的prototype屬性指向作為原型的物件o,最後返回一個F的例項,從而實現讓例項繼承o的屬性。

下面三種方式生成的新物件是等價的。

var o1 = Object.create({});

var o2 = Object.create(Object.prototype);

var o3 = new Object();

如果想要生成一個不繼承任何屬性(比如沒有toString和valueOf方法)的物件,可以將Object.create的引數設為null。

var o = Object.create(null);

 

o.valueOf()

// TypeError: Object [object Object] has no method'valueOf'

上面程式碼表示,如果物件o的原型是null,它就不具備一些定義在Object.prototype物件上面的屬性,比如valueOf方法。

使用Object.create方法的時候,必須提供物件原型,否則會報錯。

Object.create()

// TypeError: Object prototype may only be an Objector null

object.create方法生成的新物件,動態繼承了原型。在原型上新增或修改任何方法,會立刻反映在新物件之上。

var o1 = { p: 1 };

var o2 = Object.create(o1);

 

o1.p = 2;

o2.p

// 2

上面程式碼表示,修改物件原型會影響到新生成的物件。

除了物件的原型,Object.create方法還可以接受第二個引數。該引數是一個屬性描述物件,它所描述的物件屬性,會新增到新物件。

var o = Object.create({}, {

  p1: { value:123, enumerable: true },

  p2: { value:'abc', enumerable: true }

});

 

// 等同於

var o = Object.create({});

o.p1 = 123;

o.p2 = 'abc';

Object.create方法生成的物件,繼承了它的原型物件的建構函式。

function A() {}

var a = new A();

var b = Object.create(a);

 

b.constructor === A // true

b instanceof A // true

上面程式碼中,b物件的原型是a物件,因此繼承了a物件的建構函式A。

6 Object.prototype.isPrototypeOf()

物件例項的isPrototypeOf方法,用來判斷一個物件是否是另一個物件的原型。

var o1 = {};

var o2 = Object.create(o1);

var o3 = Object.create(o2);

 

o2.isPrototypeOf(o3) // true

o1.isPrototypeOf(o3) // true

上面程式碼表明,只要某個物件處在原型鏈上,isPrototypeOf都返回true。

Object.prototype.isPrototypeOf({}) // true

Object.prototype.isPrototypeOf([]) // true

Object.prototype.isPrototypeOf(/xyz/) // true

Object.prototype.isPrototypeOf(Object.create(null)) //false

上面程式碼中,由於Object.prototype處於原型鏈的最頂端,所以對各種例項都返回true,只有繼承null的物件除外。

 

7 Object.prototype.__prop__

__proto__屬性(前後各兩個下劃線)可以改寫某個物件的原型物件。

var obj = {};

var p = {};

 

obj.__proto__ = p;

Object.getPrototypeOf(obj) === p // true

上面程式碼通過__proto__屬性,將p物件設為obj物件的原型。

根據語言標準,__proto__屬性只有瀏覽器才需要部署,其他環境可以沒有這個屬性,而且前後的兩根下劃線,表示它本質是一個內部屬性,不應該對使用者暴露。因此,應該儘量少用這個屬性,而是用Object.getPrototypeof()(讀取)和Object.setPrototypeOf()(設定),進行原型物件的讀寫操作。

原型鏈可以用__proto__很直觀地表示。

var A = {

  name: '張三'

};

var B = {

  name: '李四'

};

 

var proto = {

  print: function() {

    console.log(this.name);

  }

};

 

A.__proto__ = proto;

B.__proto__ = proto;

 

A.print() // 張三

B.print() // 李四

上面程式碼中,A物件和B物件的原型都是proto物件,它們都共享proto物件的print方法。也就是說,A和B的print方法,都是在呼叫proto物件的print方法。

A.print === B.print // true

A.print === proto.print // true

B.print === proto.print // true

可以使用Object.getPrototypeOf方法,檢查瀏覽器是否支援__proto__屬性,老式瀏覽器不支援這個屬性。

Object.getPrototypeOf({ __proto__: null }) === null

上面程式碼將一個物件的__proto__屬性設為null,然後使用Object.getPrototypeOf方法獲取這個物件的原型,判斷是否等於null。如果當前環境支援__proto__屬性,兩者的比較結果應該是true。

8 獲取原型物件方法的比較

如前所述,__proto__屬性指向當前物件的原型物件,即建構函式的prototype屬性。

var obj = new Object();

 

obj.__proto__ === Object.prototype

// true

obj.__proto__ === obj.constructor.prototype

// true

上面程式碼首先新建了一個物件obj,它的__proto__屬性,指向建構函式(Object或obj.constructor)的prototype屬性。所以,兩者比較以後,返回true。

因此,獲取例項物件obj的原型物件,有三種方法。

•   obj.__proto__

•   obj.constructor.prototype

•   Object.getPrototypeOf(obj)

上面三種方法之中,前兩種都不是很可靠。最新的ES6標準規定,__proto__屬性只有瀏覽器才需要部署,其他環境可以不部署。而obj.constructor.prototype在手動改變原型物件時,可能會失效。

var P = function () {};

var p = new P();

 

var C = function () {};

C.prototype = p;

var c = new C();

 

c.constructor.prototype === p // false

上面程式碼中,C建構函式的原型物件被改成了p,結果c.constructor.prototype就失真了。所以,在改變原型物件時,一般要同時設定constructor屬性。

C.prototype = p;

C.prototype.constructor = C;

 

c.constructor.prototype === p // true

所以,推薦使用第三種Object.getPrototypeOf方法,獲取原型物件。

var o = new Object();

Object.getPrototypeOf(o) === Object.prototype

// true


相關文章