JavaScript 在Object
對(duì)象上面,提供了很多相關(guān)方法,處理面向?qū)ο缶幊痰南嚓P(guān)操作。本章介紹這些方法。
Object.getPrototypeOf
方法返回參數(shù)對(duì)象的原型。這是獲取原型對(duì)象的標(biāo)準(zhǔn)方法。
var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true
上面代碼中,實(shí)例對(duì)象f
的原型是F.prototype
。
下面是幾種特殊對(duì)象的原型。
// 空對(duì)象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true
// 函數(shù)的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
Object.setPrototypeOf
方法為參數(shù)對(duì)象設(shè)置原型,返回該參數(shù)對(duì)象。它接受兩個(gè)參數(shù),第一個(gè)是現(xiàn)有對(duì)象,第二個(gè)是原型對(duì)象。
var a = {};
var b = {x: 1};
Object.setPrototypeOf(a, b);
Object.getPrototypeOf(a) === b // true
a.x // 1
上面代碼中,Object.setPrototypeOf
方法將對(duì)象a
的原型,設(shè)置為對(duì)象b
,因此a
可以共享b
的屬性。
new
命令可以使用Object.setPrototypeOf
方法模擬。
var F = function () {
this.foo = 'bar';
};
var f = new F();
// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
上面代碼中,new
命令新建實(shí)例對(duì)象,其實(shí)可以分成兩步。第一步,將一個(gè)空對(duì)象的原型設(shè)為構(gòu)造函數(shù)的prototype
屬性(上例是F.prototype
);第二步,將構(gòu)造函數(shù)內(nèi)部的this
綁定這個(gè)空對(duì)象,然后執(zhí)行構(gòu)造函數(shù),使得定義在this
上面的方法和屬性(上例是this.foo
),都轉(zhuǎn)移到這個(gè)空對(duì)象上。
生成實(shí)例對(duì)象的常用方法是,使用new
命令讓構(gòu)造函數(shù)返回一個(gè)實(shí)例。但是很多時(shí)候,只能拿到一個(gè)實(shí)例對(duì)象,它可能根本不是由構(gòu)建函數(shù)生成的,那么能不能從一個(gè)實(shí)例對(duì)象,生成另一個(gè)實(shí)例對(duì)象呢?
JavaScript 提供了Object.create()
方法,用來(lái)滿足這種需求。該方法接受一個(gè)對(duì)象作為參數(shù),然后以它為原型,返回一個(gè)實(shí)例對(duì)象。該實(shí)例完全繼承原型對(duì)象的屬性。
// 原型對(duì)象
var A = {
print: function () {
console.log('hello');
}
};
// 實(shí)例對(duì)象
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
上面代碼中,Object.create()
方法以A
對(duì)象為原型,生成了B
對(duì)象。B
繼承了A
的所有屬性和方法。
實(shí)際上,Object.create()
方法可以用下面的代碼代替。
if (typeof Object.create !== 'function') {
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
}
上面代碼表明,Object.create()
方法的實(shí)質(zhì)是新建一個(gè)空的構(gòu)造函數(shù)F
,然后讓F.prototype
屬性指向參數(shù)對(duì)象obj
,最后返回一個(gè)F
的實(shí)例,從而實(shí)現(xiàn)讓該實(shí)例繼承obj
的屬性。
下面三種方式生成的新對(duì)象是等價(jià)的。
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
如果想要生成一個(gè)不繼承任何屬性(比如沒(méi)有toString()
和valueOf()
方法)的對(duì)象,可以將Object.create()
的參數(shù)設(shè)為null
。
var obj = Object.create(null);
obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'
上面代碼中,對(duì)象obj
的原型是null
,它就不具備一些定義在Object.prototype
對(duì)象上面的屬性,比如valueOf()
方法。
使用Object.create()
方法的時(shí)候,必須提供對(duì)象原型,即參數(shù)不能為空,或者不是對(duì)象,否則會(huì)報(bào)錯(cuò)。
Object.create()
// TypeError: Object prototype may only be an Object or null
Object.create(123)
// TypeError: Object prototype may only be an Object or null
Object.create()
方法生成的新對(duì)象,動(dòng)態(tài)繼承了原型。在原型上添加或修改任何方法,會(huì)立刻反映在新對(duì)象之上。
var obj1 = { p: 1 };
var obj2 = Object.create(obj1);
obj1.p = 2;
obj2.p // 2
上面代碼中,修改對(duì)象原型obj1
會(huì)影響到實(shí)例對(duì)象obj2
。
除了對(duì)象的原型,Object.create()
方法還可以接受第二個(gè)參數(shù)。該參數(shù)是一個(gè)屬性描述對(duì)象,它所描述的對(duì)象屬性,會(huì)添加到實(shí)例對(duì)象,作為該對(duì)象自身的屬性。
var obj = Object.create({}, {
p1: {
value: 123,
enumerable: true,
configurable: true,
writable: true,
},
p2: {
value: 'abc',
enumerable: true,
configurable: true,
writable: true,
}
});
// 等同于
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';
Object.create()
方法生成的對(duì)象,繼承了它的原型對(duì)象的構(gòu)造函數(shù)。
function A() {}
var a = new A();
var b = Object.create(a);
b.constructor === A // true
b instanceof A // true
上面代碼中,b
對(duì)象的原型是a
對(duì)象,因此繼承了a
對(duì)象的構(gòu)造函數(shù)A
。
實(shí)例對(duì)象的isPrototypeOf
方法,用來(lái)判斷該對(duì)象是否為參數(shù)對(duì)象的原型。
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);
o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true
上面代碼中,o1
和o2
都是o3
的原型。這表明只要實(shí)例對(duì)象處在參數(shù)對(duì)象的原型鏈上,isPrototypeOf
方法都返回true
。
Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false
上面代碼中,由于Object.prototype
處于原型鏈的最頂端,所以對(duì)各種實(shí)例都返回true
,只有直接繼承自null
的對(duì)象除外。
實(shí)例對(duì)象的__proto__
屬性(前后各兩個(gè)下劃線),返回該對(duì)象的原型。該屬性可讀寫(xiě)。
var obj = {};
var p = {};
obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true
上面代碼通過(guò)__proto__
屬性,將p
對(duì)象設(shè)為obj
對(duì)象的原型。
根據(jù)語(yǔ)言標(biāo)準(zhǔn),__proto__
屬性只有瀏覽器才需要部署,其他環(huán)境可以沒(méi)有這個(gè)屬性。它前后的兩根下劃線,表明它本質(zhì)是一個(gè)內(nèi)部屬性,不應(yīng)該對(duì)使用者暴露。因此,應(yīng)該盡量少用這個(gè)屬性,而是用Object.getPrototypeOf()
和Object.setPrototypeOf()
,進(jìn)行原型對(duì)象的讀寫(xiě)操作。
原型鏈可以用__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.print === B.print // true
A.print === proto.print // true
B.print === proto.print // true
上面代碼中,A
對(duì)象和B
對(duì)象的原型都是proto
對(duì)象,它們都共享proto
對(duì)象的print
方法。也就是說(shuō),A
和B
的print
方法,都是在調(diào)用proto
對(duì)象的print
方法。
如前所述,__proto__
屬性指向當(dāng)前對(duì)象的原型對(duì)象,即構(gòu)造函數(shù)的prototype
屬性。
var obj = new Object();
obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
上面代碼首先新建了一個(gè)對(duì)象obj
,它的__proto__
屬性,指向構(gòu)造函數(shù)(Object
或obj.constructor
)的prototype
屬性。
因此,獲取實(shí)例對(duì)象obj
的原型對(duì)象,有三種方法。
obj.__proto__
obj.constructor.prototype
Object.getPrototypeOf(obj)
上面三種方法之中,前兩種都不是很可靠。__proto__
屬性只有瀏覽器才需要部署,其他環(huán)境可以不部署。而obj.constructor.prototype
在手動(dòng)改變?cè)蛯?duì)象時(shí),可能會(huì)失效。
var P = function () {};
var p = new P();
var C = function () {};
C.prototype = p;
var c = new C();
c.constructor.prototype === p // false
上面代碼中,構(gòu)造函數(shù)C
的原型對(duì)象被改成了p
,但是實(shí)例對(duì)象的c.constructor.prototype
卻沒(méi)有指向p
。所以,在改變?cè)蛯?duì)象時(shí),一般要同時(shí)設(shè)置constructor
屬性。
C.prototype = p;
C.prototype.constructor = C;
var c = new C();
c.constructor.prototype === p // true
因此,推薦使用第三種Object.getPrototypeOf
方法,獲取原型對(duì)象。
Object.getOwnPropertyNames
方法返回一個(gè)數(shù)組,成員是參數(shù)對(duì)象本身的所有屬性的鍵名,不包含繼承的屬性鍵名。
Object.getOwnPropertyNames(Date)
// ["parse", "arguments", "UTC", "caller", "name", "prototype", "now", "length"]
上面代碼中,Object.getOwnPropertyNames
方法返回Date
所有自身的屬性名。
對(duì)象本身的屬性之中,有的是可以遍歷的(enumerable),有的是不可以遍歷的。Object.getOwnPropertyNames
方法返回所有鍵名,不管是否可以遍歷。只獲取那些可以遍歷的屬性,使用Object.keys
方法。
Object.keys(Date) // []
上面代碼表明,Date
對(duì)象所有自身的屬性,都是不可以遍歷的。
對(duì)象實(shí)例的hasOwnProperty
方法返回一個(gè)布爾值,用于判斷某個(gè)屬性定義在對(duì)象自身,還是定義在原型鏈上。
Date.hasOwnProperty('length') // true
Date.hasOwnProperty('toString') // false
上面代碼表明,Date.length
(構(gòu)造函數(shù)Date
可以接受多少個(gè)參數(shù))是Date
自身的屬性,Date.toString
是繼承的屬性。
另外,hasOwnProperty
方法是 JavaScript 之中唯一一個(gè)處理對(duì)象屬性時(shí),不會(huì)遍歷原型鏈的方法。
in
運(yùn)算符返回一個(gè)布爾值,表示一個(gè)對(duì)象是否具有某個(gè)屬性。它不區(qū)分該屬性是對(duì)象自身的屬性,還是繼承的屬性。
'length' in Date // true
'toString' in Date // true
in
運(yùn)算符常用于檢查一個(gè)屬性是否存在。
獲得對(duì)象的所有可遍歷屬性(不管是自身的還是繼承的),可以使用for...in
循環(huán)。
var o1 = { p1: 123 };
var o2 = Object.create(o1, {
p2: { value: "abc", enumerable: true }
});
for (p in o2) {
console.info(p);
}
// p2
// p1
上面代碼中,對(duì)象o2
的p2
屬性是自身的,p1
屬性是繼承的。這兩個(gè)屬性都會(huì)被for...in
循環(huán)遍歷。
為了在for...in
循環(huán)中獲得對(duì)象自身的屬性,可以采用hasOwnProperty
方法判斷一下。
for ( var name in object ) {
if ( object.hasOwnProperty(name) ) {
/* loop code */
}
}
獲得對(duì)象的所有屬性(不管是自身的還是繼承的,也不管是否可枚舉),可以使用下面的函數(shù)。
function inheritedPropertyNames(obj) {
var props = {};
while(obj) {
Object.getOwnPropertyNames(obj).forEach(function(p) {
props[p] = true;
});
obj = Object.getPrototypeOf(obj);
}
return Object.getOwnPropertyNames(props);
}
上面代碼依次獲取obj
對(duì)象的每一級(jí)原型對(duì)象“自身”的屬性,從而獲取obj
對(duì)象的“所有”屬性,不管是否可遍歷。
下面是一個(gè)例子,列出Date
對(duì)象的所有屬性。
inheritedPropertyNames(Date)
// [
// "caller",
// "constructor",
// "toString",
// "UTC",
// ...
// ]
如果要拷貝一個(gè)對(duì)象,需要做到下面兩件事情。
下面就是根據(jù)上面兩點(diǎn),實(shí)現(xiàn)的對(duì)象拷貝函數(shù)。
function copyObject(orig) {
var copy = Object.create(Object.getPrototypeOf(orig));
copyOwnPropertiesFrom(copy, orig);
return copy;
}
function copyOwnPropertiesFrom(target, source) {
Object
.getOwnPropertyNames(source)
.forEach(function (propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
}
另一種更簡(jiǎn)單的寫(xiě)法,是利用 ES2017 才引入標(biāo)準(zhǔn)的Object.getOwnPropertyDescriptors
方法。
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
更多建議: