JavaScript prototype 對象

2018-07-24 11:51 更新

目錄

大部分面向?qū)ο蟮木幊陶Z言,都是以“類”(class)作為對象體系的語法基礎(chǔ)。JavaScript 語言不是如此,它的面向?qū)ο缶幊袒凇霸蛯ο蟆薄?/p>

概述

構(gòu)造函數(shù)的缺點(diǎn)

JavaScript通過構(gòu)造函數(shù)生成新對象,因此構(gòu)造函數(shù)可以視為對象的模板。實(shí)例對象的屬性和方法,可以定義在構(gòu)造函數(shù)內(nèi)部。

function Cat (name, color) {
  this.name = name;
  this.color = color;
}

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

cat1.name // '大毛'
cat1.color // '白色'

上面代碼的Cat函數(shù)是一個(gè)構(gòu)造函數(shù),函數(shù)內(nèi)部定義了name屬性和color屬性,所有實(shí)例對象都會(huì)生成這兩個(gè)屬性。但是,這樣做是對系統(tǒng)資源的浪費(fèi),因?yàn)橥粋€(gè)構(gòu)造函數(shù)的對象實(shí)例之間,無法共享屬性。

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

上面代碼中,cat1cat2是同一個(gè)構(gòu)造函數(shù)的實(shí)例。但是,它們的meow方法是不一樣的,就是說每新建一個(gè)實(shí)例,就會(huì)新建一個(gè)meow方法。這既沒有必要,又浪費(fèi)系統(tǒng)資源,因?yàn)樗?code class="highlighter-rouge">meow方法都是同樣的行為,完全應(yīng)該共享。

prototype屬性的作用

JavaScript的每個(gè)對象都繼承另一個(gè)對象,后者稱為“原型”(prototype)對象。只有null除外,它沒有自己的原型對象。

原型對象上的所有屬性和方法,都能被派生對象共享。這就是JavaScript繼承機(jī)制的基本設(shè)計(jì)。

通過構(gòu)造函數(shù)生成實(shí)例對象時(shí),會(huì)自動(dòng)為實(shí)例對象分配原型對象。每一個(gè)構(gòu)造函數(shù)都有一個(gè)prototype屬性,這個(gè)屬性就是實(shí)例對象的原型對象。

function Animal (name) {
  this.name = name;
}

Animal.prototype.color = 'white';

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

cat1.color // 'white'
cat2.color // 'white'

上面代碼中,構(gòu)造函數(shù)Animalprototype對象,就是實(shí)例對象cat1cat2的原型對象。在原型對象上添加一個(gè)color屬性。結(jié)果,實(shí)例對象都能讀取該屬性。

原型對象的屬性不是實(shí)例對象自身的屬性。只要修改原型對象,變動(dòng)就立刻會(huì)體現(xiàn)在所有實(shí)例對象上。

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

上面代碼中,原型對象的color屬性的值變?yōu)?code class="highlighter-rouge">yellow,兩個(gè)實(shí)例對象的color屬性立刻跟著變了。這是因?yàn)閷?shí)例對象其實(shí)沒有color屬性,都是讀取原型對象的color屬性。也就是說,當(dāng)實(shí)例對象本身沒有某個(gè)屬性或方法的時(shí)候,它會(huì)到構(gòu)造函數(shù)的prototype屬性指向的對象,去尋找該屬性或方法。這就是原型對象的特殊之處。

如果實(shí)例對象自身就有某個(gè)屬性或方法,它就不會(huì)再去原型對象尋找這個(gè)屬性或方法。

cat1.color = 'black';

cat2.color // 'yellow'
Animal.prototype.color // "yellow";

上面代碼中,實(shí)例對象cat1color屬性改為black,就使得它不再去原型對象讀取color屬性,后者的值依然為yellow。

總結(jié)一下,原型對象的作用,就是定義所有實(shí)例對象共享的屬性和方法。這也是它被稱為原型對象的含義,而實(shí)例對象可以視作從原型對象衍生出來的子對象。

Animal.prototype.walk = function () {
  console.log(this.name + ' is walking');
};

上面代碼中,Animal.prototype對象上面定義了一個(gè)walk方法,這個(gè)方法將可以在所有Animal實(shí)例對象上面調(diào)用。

由于JavaScript的所有對象都有構(gòu)造函數(shù),而所有構(gòu)造函數(shù)都有prototype屬性(其實(shí)是所有函數(shù)都有prototype屬性),所以所有對象都有自己的原型對象。

原型鏈

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

如果一層層地上溯,所有對象的原型最終都可以上溯到Object.prototype,即Object構(gòu)造函數(shù)的prototype屬性指向的那個(gè)對象。那么,Object.prototype對象有沒有它的原型呢?回答可以是有的,就是沒有任何屬性和方法的null對象,而null對象沒有自己的原型。

Object.getPrototypeOf(Object.prototype)
// null

上面代碼表示,Object.prototype對象的原型是null,由于null沒有任何屬性,所以原型鏈到此為止。

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

如果對象自身和它的原型,都定義了一個(gè)同名屬性,那么優(yōu)先讀取對象自身的屬性,這叫做“覆蓋”(overriding)。

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

舉例來說,如果讓某個(gè)函數(shù)的prototype屬性指向一個(gè)數(shù)組,就意味著該函數(shù)可以當(dāng)作數(shù)組的構(gòu)造函數(shù),因?yàn)樗傻膶?shí)例對象都可以通過prototype屬性調(diào)用數(shù)組方法。

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是構(gòu)造函數(shù)MyArray的實(shí)例對象,由于MyArrayprototype屬性指向一個(gè)數(shù)組實(shí)例,使得mine可以調(diào)用數(shù)組方法(這些方法定義在數(shù)組實(shí)例的prototype對象上面)。至于最后那行instanceof表達(dá)式,我們知道instanceof運(yùn)算符用來比較一個(gè)對象是否為某個(gè)構(gòu)造函數(shù)的實(shí)例,最后一行就表示mineArray的實(shí)例。

下面的代碼可以找出,某個(gè)屬性到底是原型鏈上哪個(gè)對象自身的屬性。

function getDefiningObject(obj, propKey) {
  while (obj && !{}.hasOwnProperty.call(obj, propKey)) {
    obj = Object.getPrototypeOf(obj);
  }
  return obj;
}

constructor屬性

prototype對象有一個(gè)constructor屬性,默認(rèn)指向prototype對象所在的構(gòu)造函數(shù)。

function P() {}

P.prototype.constructor === P
// true

由于constructor屬性定義在prototype對象上面,意味著可以被所有實(shí)例對象繼承。

function P() {}
var p = new P();

p.constructor
// function P() {}

p.constructor === P.prototype.constructor
// true

p.hasOwnProperty('constructor')
// false

上面代碼中,p是構(gòu)造函數(shù)P的實(shí)例對象,但是p自身沒有contructor屬性,該屬性其實(shí)是讀取原型鏈上面的P.prototype.constructor屬性。

constructor屬性的作用,是分辨原型對象到底屬于哪個(gè)構(gòu)造函數(shù)。

function F() {};
var f = new F();

f.constructor === F // true
f.constructor === RegExp // false

上面代碼表示,使用constructor屬性,確定實(shí)例對象f的構(gòu)造函數(shù)是F,而不是RegExp。

有了constructor屬性,就可以從實(shí)例新建另一個(gè)實(shí)例。

function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true

上面代碼中,x是構(gòu)造函數(shù)Constr的實(shí)例,可以從x.constructor間接調(diào)用構(gòu)造函數(shù)。

這使得在實(shí)例方法中,調(diào)用自身的構(gòu)造函數(shù)成為可能。

Constr.prototype.createCopy = function () {
  return new this.constructor();
};

這也提供了繼承模式的一種實(shí)現(xiàn)。

function Super() {}

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

Sub.superclass = new Super();

上面代碼中,SuperSub都是構(gòu)造函數(shù),在Sub內(nèi)部的this上調(diào)用Super,就會(huì)形成Sub繼承Super的效果。

由于constructor屬性是一種原型對象與構(gòu)造函數(shù)的關(guān)聯(lián)關(guān)系,所以修改原型對象的時(shí)候,務(wù)必要小心。

function A() {}
var a = new A();
a instanceof A // true

function B() {}
A.prototype = B.prototype;
a instanceof A // false

上面代碼中,aA的實(shí)例。修改了A.prototype以后,constructor屬性的指向就變了,導(dǎo)致instanceof運(yùn)算符失真。

所以,修改原型對象時(shí),一般要同時(shí)校正constructor屬性的指向。

// 避免這種寫法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

// 較好的寫法
C.prototype = {
  constructor: C,
  method1: function (...) { ... },
  // ...
};

// 好的寫法
C.prototype.method1 = function (...) { ... };

上面代碼中,避免完全覆蓋掉原來的prototype屬性,要么將constructor屬性重新指向原來的構(gòu)造函數(shù),要么只在原型對象上添加方法,這樣可以保證instanceof運(yùn)算符不會(huì)失真。

此外,通過name屬性,可以從實(shí)例得到構(gòu)造函數(shù)的名稱。

function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"

instanceof運(yùn)算符

instanceof運(yùn)算符返回一個(gè)布爾值,表示指定對象是否為某個(gè)構(gòu)造函數(shù)的實(shí)例。

var v = new Vehicle();
v instanceof Vehicle // true

上面代碼中,對象v是構(gòu)造函數(shù)Vehicle的實(shí)例,所以返回true

instanceof運(yùn)算符的左邊是實(shí)例對象,右邊是構(gòu)造函數(shù)。它的運(yùn)算實(shí)質(zhì)是檢查右邊構(gòu)建函數(shù)的原型對象,是否在左邊對象的原型鏈上。因此,下面兩種寫法是等價(jià)的。

v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)

由于instanceof對整個(gè)原型鏈上的對象都有效,因此同一個(gè)實(shí)例對象,可能會(huì)對多個(gè)構(gòu)造函數(shù)都返回true。

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

上面代碼中,d同時(shí)是DateObject的實(shí)例,因此對這兩個(gè)構(gòu)造函數(shù)都返回true。

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

Object.create(null) instanceof Object // false

上面代碼中,Object.create(null)返回的新對象的原型是null,即不存在原型,因此instanceof就認(rèn)為該對象不是Object的實(shí)例。

除了上面這種繼承null的特殊情況,JavaScript之中,只要是對象,就有對應(yīng)的構(gòu)造函數(shù)。因此,instanceof運(yùn)算符的一個(gè)用處,是判斷值的類型。

var x = [1, 2, 3];
var y = {};
x instanceof Array // true
y instanceof Object // true

上面代碼中,instanceof運(yùn)算符判斷,變量x是數(shù)組,變量y是對象。

注意,instanceof運(yùn)算符只能用于對象,不適用原始類型的值。

var s = 'hello';
s instanceof String // false

上面代碼中,字符串不是String對象的實(shí)例(因?yàn)樽址皇菍ο螅苑祷?code class="highlighter-rouge">false。

此外,undefinednull不是對象,所以instanceOf運(yùn)算符總是返回false。

undefined instanceof Object // false
null instanceof Object // false

利用instanceof運(yùn)算符,還可以巧妙地解決,調(diào)用構(gòu)造函數(shù)時(shí),忘了加new命令的問題。

function Fubar (foo, bar) {
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else {
    return new Fubar(foo, bar);
  }
}

上面代碼使用instanceof運(yùn)算符,在函數(shù)體內(nèi)部判斷this關(guān)鍵字是否為構(gòu)造函數(shù)Fubar的實(shí)例。如果不是,就表明忘了加new命令。

Object.getPrototypeOf()

Object.getPrototypeOf方法返回一個(gè)對象的原型。這是獲取原型對象的標(biāo)準(zhǔn)方法。

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

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

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

Object.setPrototypeOf()

Object.setPrototypeOf方法可以為現(xiàn)有對象設(shè)置原型,返回一個(gè)新對象。

Object.setPrototypeOf方法接受兩個(gè)參數(shù),第一個(gè)是現(xiàn)有對象,第二個(gè)是原型對象。

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

b.x // 1

上面代碼中,b對象是Object.setPrototypeOf方法返回的一個(gè)新對象。該對象本身為空、原型為a對象,所以b對象可以拿到a對象的所有屬性和方法。b對象本身并沒有x屬性,但是JavaScript引擎找到它的原型對象a,然后讀取ax屬性。

new命令通過構(gòu)造函數(shù)新建實(shí)例對象,實(shí)質(zhì)就是將實(shí)例對象的原型,指向構(gòu)造函數(shù)的prototype屬性,然后在實(shí)例對象上執(zhí)行構(gòu)造函數(shù)。

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

var f = new F();

// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);

Object.create()

Object.create方法用于從原型對象生成新的實(shí)例對象,可以替代new命令。

它接受一個(gè)對象作為參數(shù),返回一個(gè)新對象,后者完全繼承前者的屬性,即原有對象成為新對象的原型。

var A = {
 print: function () {
   console.log('hello');
 }
};

var B = Object.create(A);

B.print() // hello
B.print === A.print // true

上面代碼中,Object.create方法在A的基礎(chǔ)上生成了B。此時(shí),A就成了B的原型,B就繼承了A的所有屬性和方法。這段代碼等同于下面的代碼。

var A = function () {};
A.prototype = {
 print: function () {
   console.log('hello');
 }
};

var B = new A();

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

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

if (typeof Object.create !== 'function') {
  Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F();
  };
}

上面代碼表示,Object.create方法實(shí)質(zhì)是新建一個(gè)構(gòu)造函數(shù)F,然后讓Fprototype屬性指向作為原型的對象o,最后返回一個(gè)F的實(shí)例,從而實(shí)現(xiàn)讓實(shí)例繼承o的屬性。

下面三種方式生成的新對象是等價(jià)的。

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

如果想要生成一個(gè)不繼承任何屬性(比如沒有toStringvalueOf方法)的對象,可以將Object.create的參數(shù)設(shè)為null。

var o = Object.create(null);

o.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'

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

使用Object.create方法的時(shí)候,必須提供對象原型,否則會(huì)報(bào)錯(cuò)。

Object.create()
// TypeError: Object prototype may only be an Object or null

object.create方法生成的新對象,動(dòng)態(tài)繼承了原型。在原型上添加或修改任何方法,會(huì)立刻反映在新對象之上。

var o1 = { p: 1 };
var o2 = Object.create(o1);

o1.p = 2;
o2.p
// 2

上面代碼表示,修改對象原型會(huì)影響到新生成的對象。

除了對象的原型,Object.create方法還可以接受第二個(gè)參數(shù)。該參數(shù)是一個(gè)屬性描述對象,它所描述的對象屬性,會(huì)添加到新對象。

var o = Object.create({}, {
  p1: {
    value: 123,
    enumerable: true,
    configurable: true,
    writable: true,
  },
  p2: {
    value: 'abc',
    enumerable: true,
    configurable: true,
    writable: true,
  }
});

// 等同于
var o = Object.create({});
o.p1 = 123;
o.p2 = 'abc';

Object.create方法生成的對象,繼承了它的原型對象的構(gòu)造函數(shù)。

function A() {}
var a = new A();
var b = Object.create(a);

b.constructor === A // true
b instanceof A // true

上面代碼中,b對象的原型是a對象,因此繼承了a對象的構(gòu)造函數(shù)A。

Object.prototype.isPrototypeOf()

對象實(shí)例的isPrototypeOf方法,用來判斷一個(gè)對象是否是另一個(gè)對象的原型。

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

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

上面代碼表明,只要某個(gè)對象處在原型鏈上,isPrototypeOf都返回true。

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

上面代碼中,由于Object.prototype處于原型鏈的最頂端,所以對各種實(shí)例都返回true,只有繼承null的對象除外。

Object.prototype.__proto__

__proto__屬性(前后各兩個(gè)下劃線)可以改寫某個(gè)對象的原型對象。

var obj = {};
var p = {};

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

上面代碼通過__proto__屬性,將p對象設(shè)為obj對象的原型。

根據(jù)語言標(biāo)準(zhǔn),__proto__屬性只有瀏覽器才需要部署,其他環(huán)境可以沒有這個(gè)屬性,而且前后的兩根下劃線,表示它本質(zhì)是一個(gè)內(nèi)部屬性,不應(yīng)該對使用者暴露。因此,應(yīng)該盡量少用這個(gè)屬性,而是用Object.getPrototypeof()(讀?。┖?code class="highlighter-rouge">Object.setPrototypeOf()(設(shè)置),進(jìn)行原型對象的讀寫操作。

原型鏈可以用__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方法。也就是說,ABprint方法,都是在調(diào)用proto對象的print方法。

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

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

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

上面代碼將一個(gè)對象的__proto__屬性設(shè)為null,然后使用Object.getPrototypeOf方法獲取這個(gè)對象的原型,判斷是否等于null。如果當(dāng)前環(huán)境支持__proto__屬性,兩者的比較結(jié)果應(yīng)該是true。

獲取原型對象方法的比較

如前所述,__proto__屬性指向當(dāng)前對象的原型對象,即構(gòu)造函數(shù)的prototype屬性。

var obj = new Object();

obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true

上面代碼首先新建了一個(gè)對象obj,它的__proto__屬性,指向構(gòu)造函數(shù)(Objectobj.constructor)的prototype屬性。所以,兩者比較以后,返回true。

因此,獲取實(shí)例對象obj的原型對象,有三種方法。

  • obj.__proto__
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

上面三種方法之中,前兩種都不是很可靠。最新的ES6標(biāo)準(zhǔn)規(guī)定,__proto__屬性只有瀏覽器才需要部署,其他環(huán)境可以不部署。而obj.constructor.prototype在手動(dòng)改變原型對象時(shí),可能會(huì)失效。

var P = function () {};
var p = new P();

var C = function () {};
C.prototype = p;
var c = new C();

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

上面代碼中,C構(gòu)造函數(shù)的原型對象被改成了p,結(jié)果c.constructor.prototype就失真了。所以,在改變原型對象時(shí),一般要同時(shí)設(shè)置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
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號