对象(object)是 JavaScript 最重要的数据结构。ES6中的重大升级,更使其如虎添翼,关于数据结构本身的改变,详见《纵深精耕--ES6对JS对象的极大提升》。这次先把JS中的对象方法做一次最详全的展示。
这篇的结构与拙文《拘神遣将--最详全的JS数组方法总结》相仿,在此省略赘述。
开始之前,劣者依旧先将本文谈到的方法(21个)按讲解次序阵列下方,供读者参考:
ES5:
Object.create()、Object.defineProperty()、Object.defineProperties()、
Object.getPrototypeOf()、Object.getOwnPropertyDescriptor()、Object.keys()、
Object.getOwnPropertyNames()、Object.preventExtensions()、Object.isExtensible()、
Object.seal()、Object.isSealed()、Object.freeze()、Object.isFrozen()
ES6:
Object.is()、Object.assign()、Object.getOwnPropertyDescriptors()、
Object.setPrototypeOf()、Object.getPrototypeOf()、Object.keys()、
Object.values()、Object.entries()、Object.fromEntries()
1、Object.create(obj):方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
Object.create方法的实质是新建一个空的构造函数F,然后让F.prototype属性指向参数对象obj,最后返回一个F的实例,从而实现让该实例继承obj的属性。
实际上,Object.create也可以用下面的代码代替:
Object.create = function (obj) {
function F() {}
F.prototype = obj
return new F()
}
// 或者可以更直观些
Object.create = function (obj) {
let F = {}
F.__proto__=obj
return F
}
Object.create方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,都会立刻反映在新对象之上,可以将其视作是一种浅拷贝。下面的代码是一个例子:
let obj1 = { p: 1 };
let obj2 = Object.create(obj1);
obj1.p = 2;
obj2.p // 2
//在obj1上添加的属性,立刻反应到了新的对象obj2上
除了对象原型,Object.create方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到目标对象,作为目标对象自身的属性。
let obj = Object.create({}, {
p1: {
value: 123,
enumerable: true,
configurable: true,
writable: true,
},
p2: {
value: 'abc',
enumerable: true,
configurable: true,
writable: true,
}
});
// 等同于
let obj = Object.create({})
obj.p1 = 123
obj.p2 = 'abc'
如果理解了Object.create,就可以轻松去理解ES6转ES5的继承代码了。
2、Object.defineProperty(obj, prop, descript): 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
本方法接收三个参数,分别如下:
obj:要在其上定义属性的对象。
prop:要定义或修改的属性的名称。
Descriptor:将被定义或修改的属性描述符,返回被传递给函数的对象。
下面的代码是一个例子:
let obj = {};
Object.defineProperty(obj, 'name', {
value: '斛律光'
});
console.log(obj); // '斛律光'
// 成功地给目标对象定义了name属性
使用Object.defineProperty可以实现vue中的双向绑定,详见拙文《浅析Vue的两项原理》。
3、Object.defineProperties(obj, props):方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。
该方法有两个参数:
obj: 在其上定义或修改属性的对象。
props: 要定义其可枚举属性或修改的属性描述符的对象。
在这里稍微提一下对象中存在的属性描述符,主要有两种:数据描述符和访问器描述符,如下:
configurable
、enumerable
、value
、writable
、get
、set
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
// Object
property1: true
property2: "Hello"
__proto__: Object
});
4、Object.getPrototypeOf(obj):方法返回指定对象的原型(内部[[Prototype]]属性的值)
该方法只有一个参数obj,表示要返回其原型的对象,返回值是给定对象的原型。
如果没有继承属性,则返回 null。下面的代码是一个例子:
const prototype1 = {};
const object1 = Object.create(prototype1);
console.log(Object.getPrototypeOf(object1) === prototype1);
// true
// object1是Object.create()以prototype1为蓝本创造的对象,它的原型自然指向prototype1
5、Object.getOwnPropertyDescriptor(obj, prop):方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
该方法接收两个参数,分别如下:
obj:需要查找的目标对象
prop:目标对象内属性名称
关于其返回值,如果指定的属性存在于对象上,则返回其属性描述符对象,否则返回 undefind。下面的代码是一个例子:
let o, d
o = { get foo() { return 17 } }
d = Object.getOwnPropertyDescriptor(o, "foo")
// d {
// configurable: true,
// enumerable: true,
// get: /*the getter function*/,
// set: undefined
// }
// 拿到了foo()的自有属性描述对象
o = { bar: 66 }
d = Object.getOwnPropertyDescriptor(o, "bar")
// d {
// configurable: true,
// enumerable: true,
// value: 66,
// writable: true
// }
//
o = {}
Object.defineProperty(o, "baz", {
value: 76341,
writable: false,
enumerable: false
})
d = Object.getOwnPropertyDescriptor(o, "baz")
// d {
// value: 76341,
// writable: false,
// enumerable: false,
// configurable: false
// }
/* Object.defineProperty()方法给o定义了baz属性,d通过
Object.getOwnPropertyDescriptor()获得了o的自有属性描述符对象。*/
6、Object.keys(obj):方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for...in 循环遍历该对象时返回的顺序一致 。如果对象的键-值都不可枚举,那么将返回由键组成的数组。
参数对象是 被要求返回其自身属性的对象,返回值是一个可表示给定对象所有可枚举属性的字符串数组。
下面的代码从四个不同的角度去举例:
// 数组
let arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']
// 类似数组的对象
let obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
// 随机索引的类数组对象
let anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.keys(anObj)); // console: ['2', '7', '100']
// 不可枚举属性:getFoo
let myObj = Object.create({}, {
getFoo: {
value: function () { return this.foo; }
}
});
myObj.foo = 1;
console.log(Object.keys(myObj)); // console: ['foo']
7、Object.getOwnPropertyNames(obj):方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
该方法的返回值是一个字符串数组。
// 数组
var arr = ["a", "b", "c"];
console.log(Object.getOwnPropertyNames(arr).sort()); // ["0", "1", "2", "length"]
// 类数组对象
var obj = { 0: "a", 1: "b", 2: "c"};
console.log(Object.getOwnPropertyNames(obj).sort()); // ["0", "1", "2"]
// 使用Array.forEach输出属性名和属性值
Object.getOwnPropertyNames(obj).forEach(function(val, idx, array) {
console.log(val + " -> " + obj[val]);
});
// 输出
// 0 -> a
// 1 -> b
// 2 -> c
//不可枚举属性
var my_obj = Object.create({}, {
getFoo: {
value: function() { return this.foo; },
enumerable: false
}
});
my_obj.foo = 1;
console.log(Object.getOwnPropertyNames(my_obj).sort()); // ["foo", "getFoo"]
8、Object.preventExtensions(obj): 方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
该方法会返回一个已经不可拓展的对象。比较直观,简单。下面代码是一个例子:
// Object.preventExtensions将原对象变的不可扩展,并且返回原对象.
let obj = {};
let obj2 = Object.preventExtensions(obj);
obj === obj2; // true
// 使用Object.defineProperty方法为一个不可扩展的对象添加新属性会抛出异常.
let nonExtensible = { removable: true };
Object.preventExtensions(nonExtensible);
Object.defineProperty(nonExtensible, "new", { value: 8675309 }); // 抛出TypeError异常
9、Object.isExtensible(obj): 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
很简单的方法,返回值是Boolean。
// 新对象默认是可扩展的.
let empty = {};
Object.isExtensible(empty); // === true
// ...可以变的不可扩展.
Object.preventExtensions(empty);
Object.isExtensible(empty); // === false
10、Object.seal(obj):方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
参数是将要被密封的对象,返回值是被密封的对象。
Object.seal() 比 Object.isExtensible() 严格的一点是,它将不在允许配置当前所有属性。
let arr = ["a", "b", "c"];
console.log(Object.getOwnPropertyNames(arr).sort()); // ["0", "1", "2", "length"]
// 类数组对象
let obj = { 0: "a", 1: "b", 2: "c"};
console.log(Object.getOwnPropertyNames(obj).sort()); // ["0", "1", "2"]
// 使用Array.forEach输出属性名和属性值
Object.getOwnPropertyNames(obj).forEach(function(val, idx, array) {
console.log(val + " -> " + obj[val]);
});
// 输出
// 0 -> a
// 1 -> b
// 2 -> c
//不可枚举属性
let my_obj = Object.create({}, {
getFoo: {
value: function() { return this.foo; },
enumerable: false
}
});
my_obj.foo = 1;
console.log(Object.getOwnPropertyNames(my_obj).sort()); // ["foo", "getFoo"]
11、Object.isSealed(obj):方法判断一个对象是否被密封。
很简单的方法,返回值是Boolean。
// 新建的对象默认不是密封的.
var empty = {};
Object.isSealed(empty); // === false
// 如果你把一个空对象变的不可扩展,则它同时也会变成个密封对象.
Object.preventExtensions(empty);
Object.isSealed(empty); // === true
12、Object.freeze(obj):方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。
冻结可以视作是对一个对象最严格的限制。
该方法返回传递的对象,而不是创建一个被冻结的副本。下面的例子将展示一些对冻结对象操作的结果:
var obj = {
prop: function() {},
foo: 'bar'
};
// 新的属性会被添加, 已存在的属性可能
// 会被修改或移除
obj.foo = 'baz';
obj.lumpy = 'woof';
delete obj.prop;
// 作为参数传递的对象与返回的对象都被冻结
// 所以不必保存返回的对象(因为两个对象全等)
var o = Object.freeze(obj);
o === obj; // true
Object.isFrozen(obj); // === true
// 现在任何改变都会失效
obj.foo = 'quux'; // 静默地不做任何事
// 静默地不添加此属性
obj.quaxxor = 'the friendly duck';
// 在严格模式,如此行为将抛出 TypeErrors
function fail(){
'use strict';
obj.foo = 'sparky'; // throws a TypeError
delete obj.quaxxor; // 返回true,因为quaxxor属性从来未被添加
obj.sparky = 'arf'; // throws a TypeError
}
fail();
// 试图通过 Object.defineProperty 更改属性
// 下面两个语句都会抛出 TypeError.
Object.defineProperty(obj, 'ohai', { value: 17 });
Object.defineProperty(obj, 'foo', { value: 'eit' });
// 也不能更改原型
// 下面两个语句都会抛出 TypeError.
Object.setPrototypeOf(obj, { x: 20 })
obj.__proto__ = { x: 20 }
13、Object.isFrozen(obj):方法判断一个对象是否被冻结。
很简单的方法,返回值是Boolean。
// 一个对象默认是可扩展的,所以它也是非冻结的.
Object.isFrozen({}); // === false
// 一个不可扩展的空对象同时也是一个冻结对象.
let vacuouslyFrozen = Object.preventExtensions({});
Object.isFrozen(vacuouslyFrozen) //=== true;
// 一个非空对象默认也是非冻结的.
let oneProp = { p: 42 };
Object.isFrozen(oneProp) //=== false
// 让这个对象变的不可扩展,并不意味着这个对象变成了冻结对象,
// 因为p属性仍然是可以配置的(而且可写的).
Object.preventExtensions(oneProp);
Object.isFrozen(oneProp) //=== false
14、Object.is(obj, obj):它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
Object.is()的参数是要比较的两个值,返回值是Boolean。
看下面的代码:
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
// +0不等于-0
+0 === -0 //true
NaN === NaN // false
// NaN等于自身
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
15、Object.assign():方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。
该方法有不少值得推敲和注意的点,下面的代码举出基本用法,其他部分详见拙文《Object.assign详解》
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
// 如果目标对象与源对象有同名属性,或多个源对象有同名属性
// 则后面的属性会覆盖前面的属性。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
// source2 中的 c属性覆盖了source1 中的 c属性
16、Object.getOwnPropertyDescriptors():返回指定对象所有自身属性(非继承属性)的描述对象。
ES5的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。ES2017引入了
Object.getOwnPropertyDescriptor()方法,返回指定对象所有的自身属性(非继承属性)的描述对象。
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
上面代码中,Object.getOwnPropertyDescriptors()
方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。
17、Object.setPrototypeOf():方法的作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
const o = Object.setPrototypeOf({}, null);
该方法等同于下面的函数。
function setPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
下面是一个例子。
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
18、Object.getPrototypeOf():该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。
该方法与Object.setPrototypeOf
方法配套,用于读取一个对象的原型对象。
Object.getPrototypeOf(obj);
下面是一个例子。
function Rectangle() {
// ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
20、Object.values():方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值
Object.values
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
返回数组的成员顺序,与本章的《属性的遍历》部分介绍的排列规则一致。
const obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]
上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b
、c
、a
。
Object.values
只返回对象自身的可遍历属性。
const obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []
上面代码中,Object.create
方法的第二个参数添加的对象属性(属性p
),如果不显式声明,默认是不可遍历的,因为p
的属性描述对象的enumerable
默认是false
,Object.values
不会返回这个属性。只要把enumerable
改成true
,Object.values
就会返回属性p
的值。
const obj = Object.create({}, {p:
{
value: 42,
enumerable: true
}
});
Object.values(obj) // [42]
Object.values
会过滤属性名为 Symbol 值的属性。
Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']
如果Object.values
方法的参数是一个字符串,会返回各个字符组成的一个数组。
Object.values('foo')
// ['f', 'o', 'o']
上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,Object.values
返回每个属性的键值,就是各个字符组成的一个数组。
如果参数不是对象,Object.values
会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values
会返回空数组。
Object.values(42) // []
Object.values(true) // []
20、Object.entries():方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。
Object.entries()
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
除了返回值不一样,该方法的行为与Object.values
基本一致。
如果原对象的属性名是一个 Symbol 值,该属性会被忽略。
Object.entries({ [Symbol()]: 123, foo: 'abc' });
// [ [ 'foo', 'abc' ] ]
上面代码中,原对象有两个属性,Object.entries
只输出属性名非 Symbol 值的属性。将来可能会有Reflect.ownEntries()
方法,返回对象自身的所有属性。
Object.entries
的基本用途是遍历对象的属性。
let obj = { one: 1, two: 2 };
for (let [k, v] of Object.entries(obj)) {
console.log(
`${JSON.stringify(k)}: ${JSON.stringify(v)}`
);
}
// "one": 1
// "two": 2
Object.entries
方法的另一个用处是,将对象转为真正的Map
结构。
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
自己实现Object.entries
方法,非常简单。
// Generator函数的版本
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
// 非Generator函数的版本
function entries(obj) {
let arr = [];
for (let key of Object.keys(obj)) {
arr.push([key, obj[key]]);
}
return arr;
}
21、Object.fromEntries():方法是Object.entries()的逆操作,用于将一个键值对数组转为对象。
Object.fromEntries()
方法是Object.entries()
的逆操作,用于将一个键值对数组转为对象。
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。
// 例一
const entries = new Map([
['foo', 'bar'],
['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }
该方法的一个用处是配合URLSearchParams
对象,将查询字符串转为对象。
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }