# Object 對象的相關方法
JavaScript 在`Object`對象上面,提供了很多相關方法,處理面向對象編程的相關操作。本章介紹這些方法。
## Object.getPrototypeOf()
`Object.getPrototypeOf`方法返回參數對象的原型。這是獲取原型對象的標準方法。
```javascript
var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true
```
上面代碼中,實例對象`f`的原型是`F.prototype`。
下面是幾種特殊對象的原型。
```javascript
// 空對象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true
// 函數的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
```
## Object.setPrototypeOf()
`Object.setPrototypeOf`方法為參數對象設置原型,返回該參數對象。它接受兩個參數,第一個是現有對象,第二個是原型對象。
```javascript
var a = {};
var b = {x: 1};
Object.setPrototypeOf(a, b);
Object.getPrototypeOf(a) === b // true
a.x // 1
```
上面代碼中,`Object.setPrototypeOf`方法將對象`a`的原型,設置為對象`b`,因此`a`可以共享`b`的屬性。
`new`命令可以使用`Object.setPrototypeOf`方法模擬。
```javascript
var F = function () {
this.foo = 'bar';
};
var f = new F();
// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
```
上面代碼中,`new`命令新建實例對象,其實可以分成兩步。第一步,將一個空對象的原型設為構造函數的`prototype`屬性(上例是`F.prototype`);第二步,將構造函數內部的`this`綁定這個空對象,然后執行構造函數,使得定義在`this`上面的方法和屬性(上例是`this.foo`),都轉移到這個空對象上。
## Object.create()
生成實例對象的常用方法是,使用`new`命令讓構造函數返回一個實例。但是很多時候,只能拿到一個實例對象,它可能根本不是由構建函數生成的,那么能不能從一個實例對象,生成另一個實例對象呢?
JavaScript 提供了`Object.create()`方法,用來滿足這種需求。該方法接受一個對象作為參數,然后以它為原型,返回一個實例對象。該實例完全繼承原型對象的屬性。
```javascript
// 原型對象
var A = {
print: function () {
console.log('hello');
}
};
// 實例對象
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
```
上面代碼中,`Object.create()`方法以`A`對象為原型,生成了`B`對象。`B`繼承了`A`的所有屬性和方法。
實際上,`Object.create()`方法可以用下面的代碼代替。
```javascript
if (typeof Object.create !== 'function') {
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
}
```
上面代碼表明,`Object.create()`方法的實質是新建一個空的構造函數`F`,然后讓`F.prototype`屬性指向參數對象`obj`,最后返回一個`F`的實例,從而實現讓該實例繼承`obj`的屬性。
下面三種方式生成的新對象是等價的。
```javascript
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
```
如果想要生成一個不繼承任何屬性(比如沒有`toString()`和`valueOf()`方法)的對象,可以將`Object.create()`的參數設為`null`。
```javascript
var obj = Object.create(null);
obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'
```
上面代碼中,對象`obj`的原型是`null`,它就不具備一些定義在`Object.prototype`對象上面的屬性,比如`valueOf()`方法。
使用`Object.create()`方法的時候,必須提供對象原型,即參數不能為空,或者不是對象,否則會報錯。
```javascript
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()`方法生成的新對象,動態繼承了原型。在原型上添加或修改任何方法,會立刻反映在新對象之上。
```javascript
var obj1 = { p: 1 };
var obj2 = Object.create(obj1);
obj1.p = 2;
obj2.p // 2
```
上面代碼中,修改對象原型`obj1`會影響到實例對象`obj2`。
除了對象的原型,`Object.create()`方法還可以接受第二個參數。該參數是一個屬性描述對象,它所描述的對象屬性,會添加到實例對象,作為該對象自身的屬性。
```javascript
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()`方法生成的對象,繼承了它的原型對象的構造函數。
```javascript
function A() {}
var a = new A();
var b = Object.create(a);
b.constructor === A // true
b instanceof A // true
```
上面代碼中,`b`對象的原型是`a`對象,因此繼承了`a`對象的構造函數`A`。
## Object.prototype.isPrototypeOf()
實例對象的`isPrototypeOf`方法,用來判斷該對象是否為參數對象的原型。
```javascript
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);
o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true
```
上面代碼中,`o1`和`o2`都是`o3`的原型。這表明只要實例對象處在參數對象的原型鏈上,`isPrototypeOf`方法都返回`true`。
```javascript
Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false
```
上面代碼中,由于`Object.prototype`處于原型鏈的最頂端,所以對各種實例都返回`true`,只有直接繼承自`null`的對象除外。
## Object.prototype.\_\_proto\_\_
實例對象的`__proto__`屬性(前后各兩個下劃線),返回該對象的原型。該屬性可讀寫。
```javascript
var obj = {};
var p = {};
obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true
```
上面代碼通過`__proto__`屬性,將`p`對象設為`obj`對象的原型。
根據語言標準,`__proto__`屬性只有瀏覽器才需要部署,其他環境可以沒有這個屬性。它前后的兩根下劃線,表明它本質是一個內部屬性,不應該對使用者暴露。因此,應該盡量少用這個屬性,而是用`Object.getPrototypeOf()`和`Object.setPrototypeOf()`,進行原型對象的讀寫操作。
原型鏈可以用`__proto__`很直觀地表示。
```javascript
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`對象和`B`對象的原型都是`proto`對象,它們都共享`proto`對象的`print`方法。也就是說,`A`和`B`的`print`方法,都是在調用`proto`對象的`print`方法。
## 獲取原型對象方法的比較
如前所述,`__proto__`屬性指向當前對象的原型對象,即構造函數的`prototype`屬性。
```javascript
var obj = new Object();
obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
```
上面代碼首先新建了一個對象`obj`,它的`__proto__`屬性,指向構造函數(`Object`或`obj.constructor`)的`prototype`屬性。
因此,獲取實例對象`obj`的原型對象,有三種方法。
- `obj.__proto__`
- `obj.constructor.prototype`
- `Object.getPrototypeOf(obj)`
上面三種方法之中,前兩種都不是很可靠。`__proto__`屬性只有瀏覽器才需要部署,其他環境可以不部署。而`obj.constructor.prototype`在手動改變原型對象時,可能會失效。
```javascript
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`卻沒有指向`p`。所以,在改變原型對象時,一般要同時設置`constructor`屬性。
```javascript
C.prototype = p;
C.prototype.constructor = C;
var c = new C();
c.constructor.prototype === p // true
```
因此,推薦使用第三種`Object.getPrototypeOf`方法,獲取原型對象。
## Object.getOwnPropertyNames()
`Object.getOwnPropertyNames`方法返回一個數組,成員是參數對象本身的所有屬性的鍵名,不包含繼承的屬性鍵名。
```javascript
Object.getOwnPropertyNames(Date)
// ["parse", "arguments", "UTC", "caller", "name", "prototype", "now", "length"]
```
上面代碼中,`Object.getOwnPropertyNames`方法返回`Date`所有自身的屬性名。
對象本身的屬性之中,有的是可以遍歷的(enumerable),有的是不可以遍歷的。`Object.getOwnPropertyNames`方法返回所有鍵名,不管是否可以遍歷。只獲取那些可以遍歷的屬性,使用`Object.keys`方法。
```javascript
Object.keys(Date) // []
```
上面代碼表明,`Date`對象所有自身的屬性,都是不可以遍歷的。
## Object.prototype.hasOwnProperty()
對象實例的`hasOwnProperty`方法返回一個布爾值,用于判斷某個屬性定義在對象自身,還是定義在原型鏈上。
```javascript
Date.hasOwnProperty('length') // true
Date.hasOwnProperty('toString') // false
```
上面代碼表明,`Date.length`(構造函數`Date`可以接受多少個參數)是`Date`自身的屬性,`Date.toString`是繼承的屬性。
另外,`hasOwnProperty`方法是 JavaScript 之中唯一一個處理對象屬性時,不會遍歷原型鏈的方法。
## in 運算符和 for...in 循環
`in`運算符返回一個布爾值,表示一個對象是否具有某個屬性。它不區分該屬性是對象自身的屬性,還是繼承的屬性。
```javascript
'length' in Date // true
'toString' in Date // true
```
`in`運算符常用于檢查一個屬性是否存在。
獲得對象的所有可遍歷屬性(不管是自身的還是繼承的),可以使用`for...in`循環。
```javascript
var o1 = { p1: 123 };
var o2 = Object.create(o1, {
p2: { value: "abc", enumerable: true }
});
for (p in o2) {
console.info(p);
}
// p2
// p1
```
上面代碼中,對象`o2`的`p2`屬性是自身的,`p1`屬性是繼承的。這兩個屬性都會被`for...in`循環遍歷。
為了在`for...in`循環中獲得對象自身的屬性,可以采用`hasOwnProperty`方法判斷一下。
```javascript
for ( var name in object ) {
if ( object.hasOwnProperty(name) ) {
/* loop code */
}
}
```
獲得對象的所有屬性(不管是自身的還是繼承的,也不管是否可枚舉),可以使用下面的函數。
```javascript
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`對象的每一級原型對象“自身”的屬性,從而獲取`obj`對象的“所有”屬性,不管是否可遍歷。
下面是一個例子,列出`Date`對象的所有屬性。
```javascript
inheritedPropertyNames(Date)
// [
// "caller",
// "constructor",
// "toString",
// "UTC",
// ...
// ]
```
## 對象的拷貝
如果要拷貝一個對象,需要做到下面兩件事情。
- 確保拷貝后的對象,與原對象具有同樣的原型。
- 確保拷貝后的對象,與原對象具有同樣的實例屬性。
下面就是根據上面兩點,實現的對象拷貝函數。
```javascript
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;
}
```
另一種更簡單的寫法,是利用 ES2017 才引入標準的`Object.getOwnPropertyDescriptors`方法。
```javascript
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
```
## 參考鏈接
- Dr. Axel Rauschmayer, [JavaScript properties: inheritance and enumerability](http://www.2ality.com/2011/07/js-properties.html)
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio