对象的新增方法
# New Methods of Objects
This chapter introduces the new methods added to the Object object.
# Object.is()
In ES5, there are only two operators for comparing whether two values are equal: the equality operator (==) and the strict equality operator (===). Both have drawbacks -- the former automatically converts data types, while with the latter, NaN is not equal to itself, and +0 is equal to -0. JavaScript lacked an operation that, in all environments, would consider two values equal as long as they are the same.
ES6 introduced the "Same-value equality" algorithm to solve this problem. Object.is is the new method that implements this algorithm. It compares whether two values are strictly equal, and its behavior is essentially the same as the strict equality operator (===).
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
2
3
4
There are only two differences: first, +0 is not equal to -0; second, NaN is equal to itself.
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
2
3
4
5
In ES5, you can implement Object.is with the following code.
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// Handle the case where +0 is not equal to -0
return x !== 0 || 1 / x === 1 / y;
}
// Handle the case of NaN
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});
2
3
4
5
6
7
8
9
10
11
12
13
# Object.assign()
# Basic Usage
The Object.assign method is used for merging objects, copying all enumerable properties from one or more source objects to a target object.
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
2
3
4
5
6
7
The first argument to Object.assign is the target object, and all subsequent arguments are source objects.
Note that if the target object and source objects share properties with the same name, or if multiple source objects have same-named properties, later properties will override earlier ones.
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}
2
3
4
5
6
7
If there is only one argument, Object.assign returns that argument directly.
const obj = {a: 1};
Object.assign(obj) === obj // true
2
If the argument is not an object, it will be converted to an object first and then returned.
typeof Object.assign(2) // "object"
Since undefined and null cannot be converted to objects, passing them as arguments will throw an error.
Object.assign(undefined) // Error
Object.assign(null) // Error
2
If non-object arguments appear in the source object positions (i.e., not the first argument), the handling rules differ. First, these arguments are converted to objects; if they cannot be converted, they are skipped. This means that undefined and null won't cause errors if they are not the first argument.
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
2
3
Other value types (numbers, strings, and booleans) in non-first-argument positions also won't cause errors. However, only strings will be copied into the target object as an array of characters; other values have no effect.
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
2
3
4
5
6
In the code above, v1, v2, and v3 are a string, a boolean, and a number, respectively. Only the string is merged into the target object (as a character array); the number and boolean are ignored. This is because only the wrapper object for strings produces enumerable properties.
Object(true) // {[[PrimitiveValue]]: true}
Object(10) // {[[PrimitiveValue]]: 10}
Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
2
3
In the code above, booleans, numbers, and strings are converted to their corresponding wrapper objects. You can see that their primitive values are stored in the internal property [[PrimitiveValue]] of the wrapper objects, and this property is not copied by Object.assign. Only the wrapper object for strings produces enumerable meaningful properties, and those properties are copied.
Object.assign has limitations on which properties it copies -- it only copies the source object's own properties (not inherited properties), and it does not copy non-enumerable properties (enumerable: false).
Object.assign({b: 'c'},
Object.defineProperty({}, 'invisible', {
enumerable: false,
value: 'hello'
})
)
// { b: 'c' }
2
3
4
5
6
7
In the code above, the object that Object.assign is copying from has only one non-enumerable property invisible, and this property was not copied.
Properties with Symbol values as property names are also copied by Object.assign.
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }
2
# Important Notes
(1) Shallow Copy
The Object.assign method performs a shallow copy, not a deep copy. This means that if the value of a source object property is an object, the target object gets a reference to that object.
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2
2
3
4
5
In the code above, the value of obj1's a property is an object, and Object.assign copies only the reference to that object. Any changes to that object will be reflected in the target object.
(2) Replacement of Same-Named Properties
For nested objects, when same-named properties are encountered, Object.assign replaces the entire property rather than adding to it.
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
2
3
4
In the code above, target's a property is entirely replaced by source's a property, rather than producing { a: { b: 'hello', d: 'e' } }. This is usually not what developers want, so be particularly careful.
Some libraries provide customized versions of Object.assign (such as Lodash's _.defaultsDeep method) that can achieve deep-copy merging.
(3) Handling Arrays
Object.assign can be used to process arrays, but it treats arrays as objects.
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
2
In the code above, Object.assign treats the array as an object with property names 0, 1, and 2, so property 4 at index 0 of the source array overrides property 1 at index 0 of the target array.
(4) Handling Getters
Object.assign can only copy values. If the value to be copied is a getter, it will be evaluated before copying.
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
2
3
4
5
6
7
In the code above, the foo property of the source object is a getter. Object.assign does not copy this getter; instead, it gets the value and copies that value over.
# Common Uses
The Object.assign method has many uses.
(1) Adding Properties to an Object
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
2
3
4
5
The method above uses Object.assign to add the x and y properties to instances of the Point class.
(2) Adding Methods to an Object
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// Equivalent to the following
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
The code above uses the concise method syntax for object properties, placing two functions directly inside curly braces and then using the assign method to add them to SomeClass.prototype.
(3) Cloning an Object
function clone(origin) {
return Object.assign({}, origin);
}
2
3
The code above copies the original object to an empty object, resulting in a clone of the original object.
However, this method of cloning can only clone the original object's own values, not its inherited values. To preserve the prototype chain, you can use the following code.
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
2
3
4
(4) Merging Multiple Objects
Merge multiple objects into a single object.
const merge =
(target, ...sources) => Object.assign(target, ...sources);
2
If you want to return a new object from the merge, you can modify the function above to merge into an empty object.
const merge =
(...sources) => Object.assign({}, ...sources);
2
(5) Specifying Default Values for Properties
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
2
3
4
5
6
7
8
9
10
In the code above, DEFAULTS is the default values object, and options is the user-provided parameters. Object.assign merges DEFAULTS and options into a new object. If both have same-named properties, the property values from options will override those from DEFAULTS.
Note that due to shallow copy behavior, all property values of both DEFAULTS and options should ideally be simple types and not point to other objects. Otherwise, the corresponding property of DEFAULTS may not take effect.
const DEFAULTS = {
url: {
host: 'example.com',
port: 7070
},
};
processContent({ url: {port: 8000} })
// {
// url: {port: 8000}
// }
2
3
4
5
6
7
8
9
10
11
The original intent of the code above is to change url.port to 8000 while keeping url.host unchanged. However, the actual result is that options.url overrides DEFAULTS.url, so url.host no longer exists.
# Object.getOwnPropertyDescriptors()
The ES5 Object.getOwnPropertyDescriptor() method returns the descriptor of a given property on an object. ES2017 introduced the Object.getOwnPropertyDescriptors() method, which returns the descriptors of all own properties (non-inherited) of the specified object.
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 } }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In the code above, Object.getOwnPropertyDescriptors() returns an object where every property name of the original object becomes a property name of the returned object, and the corresponding values are the descriptors of those properties.
The implementation of this method is straightforward.
function getOwnPropertyDescriptors(obj) {
const result = {};
for (let key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
}
2
3
4
5
6
7
The main purpose of introducing this method is to solve the problem that Object.assign() cannot correctly copy get and set properties.
const source = {
set foo(value) {
console.log(value);
}
};
const target1 = {};
Object.assign(target1, source);
Object.getOwnPropertyDescriptor(target1, 'foo')
// { value: undefined,
// writable: true,
// enumerable: true,
// configurable: true }
2
3
4
5
6
7
8
9
10
11
12
13
14
In the code above, the value of source's foo property is a setter. When Object.assign copies this property to target1, the property's value becomes undefined. This is because Object.assign always copies the value of a property, not its underlying setter or getter methods.
Using Object.getOwnPropertyDescriptors() together with Object.defineProperties() enables correct copying.
const source = {
set foo(value) {
console.log(value);
}
};
const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo')
// { get: undefined,
// set: [Function: set foo],
// enumerable: true,
// configurable: true }
2
3
4
5
6
7
8
9
10
11
12
13
In the code above, the logic for merging two objects can be written as a function.
const shallowMerge = (target, source) => Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);
2
3
4
Another use of Object.getOwnPropertyDescriptors() is to clone object properties to a new object using Object.create(). This is a shallow copy.
const clone = Object.create(Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj));
// Or
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
2
3
4
5
6
7
8
9
The code above clones the object obj.
Additionally, Object.getOwnPropertyDescriptors() can be used to implement one object inheriting from another. Previously, inheriting from another object was commonly written as follows.
const obj = {
__proto__: prot,
foo: 123,
};
2
3
4
ES6 specifies that __proto__ only needs to be implemented in browsers, not in other environments. If you remove __proto__, the code above would need to be rewritten as follows.
const obj = Object.create(prot);
obj.foo = 123;
// Or
const obj = Object.assign(
Object.create(prot),
{
foo: 123,
}
);
2
3
4
5
6
7
8
9
10
11
With Object.getOwnPropertyDescriptors(), we have another way to write it.
const obj = Object.create(
prot,
Object.getOwnPropertyDescriptors({
foo: 123,
})
);
2
3
4
5
6
Object.getOwnPropertyDescriptors() can also be used to implement the Mixin pattern.
let mix = (object) => ({
with: (...mixins) => mixins.reduce(
(c, mixin) => Object.create(
c, Object.getOwnPropertyDescriptors(mixin)
), object)
});
// multiple mixins example
let a = {a: 'a'};
let b = {b: 'b'};
let c = {c: 'c'};
let d = mix(c).with(a, b);
d.c // "c"
d.b // "b"
d.a // "a"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
The code above returns a new object d, representing the result of mixing objects a and b into object c.
For the sake of completeness, after Object.getOwnPropertyDescriptors() entered the standard, a Reflect.getOwnPropertyDescriptors() method will also be added in the future.
# The __proto__ Property, Object.setPrototypeOf(), Object.getPrototypeOf()
Object inheritance in JavaScript is implemented through the prototype chain. ES6 provides more methods for working with prototype objects.
# The __proto__ Property
The __proto__ property (with two underscores on each side) is used to read or set the prototype object of the current object. Currently, all browsers (including IE11) implement this property.
// ES5 approach
const obj = {
method: function() { ... }
};
obj.__proto__ = someOtherObj;
// ES6 approach
var obj = Object.create(someOtherObj);
obj.method = function() { ... };
2
3
4
5
6
7
8
9
This property was not included in the main body of ES6, but rather in an appendix. The reason is that the double underscores before and after __proto__ indicate that it is fundamentally an internal property, not an official public API. It was added to ES6 only because of widespread browser support. The standard clearly specifies that only browsers are required to implement this property; other runtime environments may not need to. Furthermore, new code should assume this property does not exist. Therefore, whether from a semantic or compatibility perspective, do not use this property. Instead, use Object.setPrototypeOf() (for writing), Object.getPrototypeOf() (for reading), and Object.create() (for creating).
Under the hood, __proto__ calls Object.prototype.__proto__, implemented as follows.
Object.defineProperty(Object.prototype, '__proto__', {
get() {
let _thisObj = Object(this);
return Object.getPrototypeOf(_thisObj);
},
set(proto) {
if (this === undefined || this === null) {
throw new TypeError();
}
if (!isObject(this)) {
return undefined;
}
if (!isObject(proto)) {
return undefined;
}
let status = Reflect.setPrototypeOf(this, proto);
if (!status) {
throw new TypeError();
}
},
});
function isObject(value) {
return Object(value) === value;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
If an object has its own __proto__ property, the value of that property is the object's prototype.
Object.getPrototypeOf({ __proto__: null })
// null
2
# Object.setPrototypeOf()
The Object.setPrototypeOf method serves the same purpose as __proto__ -- it sets the prototype object of an object and returns the object itself. It is the officially recommended ES6 method for setting the prototype object.
// Syntax
Object.setPrototypeOf(object, prototype)
// Usage
const o = Object.setPrototypeOf({}, null);
2
3
4
5
This method is equivalent to the following function.
function setPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
2
3
4
Here is an example.
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
2
3
4
5
6
7
8
9
10
The code above sets the proto object as the prototype of obj, so properties from proto can be read from obj.
If the first argument is not an object, it will be automatically converted to an object. However, since the first argument is still returned, this operation has no effect.
Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf('foo', {}) === 'foo' // true
Object.setPrototypeOf(true, {}) === true // true
2
3
Since undefined and null cannot be converted to objects, if the first argument is undefined or null, an error will be thrown.
Object.setPrototypeOf(undefined, {})
// TypeError: Object.setPrototypeOf called on null or undefined
Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined
2
3
4
5
# Object.getPrototypeOf()
This method complements Object.setPrototypeOf and is used to read the prototype object of an object.
Object.getPrototypeOf(obj);
Here is an example.
function Rectangle() {
// ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false
2
3
4
5
6
7
8
9
10
11
12
If the argument is not an object, it will be automatically converted to one.
// Equivalent to Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1)
// Number {[[PrimitiveValue]]: 0}
// Equivalent to Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo')
// String {length: 0, [[PrimitiveValue]]: ""}
// Equivalent to Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true)
// Boolean {[[PrimitiveValue]]: false}
Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
If the argument is undefined or null, they cannot be converted to objects, so an error will be thrown.
Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object
Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object
2
3
4
5
# Object.keys(), Object.values(), Object.entries()
# Object.keys()
ES5 introduced the Object.keys method, which returns an array of the names of all enumerable (enumerable) own (non-inherited) properties of the given object.
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]
2
3
ES2017 introduced (opens new window) Object.values and Object.entries as companion methods to Object.keys, providing supplementary means for iterating over an object, designed for use with the for...of loop.
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# Object.values()
The Object.values method returns an array of the values of all enumerable (enumerable) own (non-inherited) properties of the given object.
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
2
3
The order of elements in the returned array is consistent with the sorting rules described in the "Property Traversal" section of this chapter.
const obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]
2
3
In the code above, properties with numeric names are traversed in ascending numeric order, so the returned order is b, c, a.
Object.values only returns the object's own enumerable properties.
const obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []
2
In the code above, the property (p) added as the second argument to Object.create is non-enumerable by default because p's property descriptor has enumerable set to false by default. Therefore, Object.values does not return this property. If you change enumerable to true, Object.values will return the value of property p.
const obj = Object.create({}, {p:
{
value: 42,
enumerable: true
}
});
Object.values(obj) // [42]
2
3
4
5
6
7
Object.values filters out properties whose names are Symbol values.
Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']
2
If the argument to Object.values is a string, it returns an array of individual characters.
Object.values('foo')
// ['f', 'o', 'o']
2
In the code above, the string is first converted to an array-like object. Each character of the string becomes a property of that object. Therefore, Object.values returns the value of each property, which is an array of individual characters.
If the argument is not an object, Object.values will first convert it to an object. Since wrapper objects for numbers and booleans do not add non-inherited properties to instances, Object.values returns an empty array for them.
Object.values(42) // []
Object.values(true) // []
2
# Object.entries()
The Object.entries() method returns an array of the key-value pairs of all enumerable (enumerable) own (non-inherited) properties of the given object.
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
2
3
Aside from the return value being different, this method's behavior is essentially the same as Object.values.
If the original object's property name is a Symbol value, that property will be ignored.
Object.entries({ [Symbol()]: 123, foo: 'abc' });
// [ [ 'foo', 'abc' ] ]
2
In the code above, the original object has two properties. Object.entries only outputs properties whose names are not Symbol values. In the future, there may be a Reflect.ownEntries() method that returns all properties of the object itself.
A basic use of Object.entries is to iterate over an object's properties.
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
2
3
4
5
6
7
8
Another use of the Object.entries method is to convert an object into a real Map structure.
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
2
3
Implementing Object.entries yourself is very simple.
// Generator function version
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
// Non-Generator function version
function entries(obj) {
let arr = [];
for (let key of Object.keys(obj)) {
arr.push([key, obj[key]]);
}
return arr;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Object.fromEntries()
The Object.fromEntries() method is the inverse operation of Object.entries(), used for converting an array of key-value pairs into an object.
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
2
3
4
5
The main purpose of this method is to convert key-value pair data structures back into objects, making it particularly suitable for converting Map structures to objects.
// Example 1
const entries = new Map([
['foo', 'bar'],
['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
// Example 2
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }
2
3
4
5
6
7
8
9
10
11
12
13
One use of this method is to combine it with the URLSearchParams object to convert a query string into an object.
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
2