New method of object

This chapter introduces the new methods of the Object object.

Object.is()

ES5 compares whether two values ​​are equal, there are only two operators: the equality operator (==) and the strict equality operator (===). Both of them have shortcomings, the former will automatically convert the data type, the latter's NaN is not equal to itself, and +0 is equal to -0. JavaScript lacks an operation. In all environments, as long as two values ​​are the same, they should be equal.

ES6 proposes the "Same-value equality" algorithm to solve this problem. Object.is is a new way to deploy this algorithm. It is used to compare whether two values ​​are strictly equal, which is basically the same as the behavior of the strict comparison operator (===).

Object.is("foo", "foo");
// true
Object.is({}, {});
// false

There are only two differences: one is that +0 is not equal to -0, and the other is that NaN is equal to itself.

+0 === -0; //true
NaN === NaN; // false

Object.is(+0, -0); // false
Object.is(NaN, NaN); // true

ES5 can use the following code to deploy Object.is.

Object.defineProperty(Object, "is", {
  value: function (x, y) {
    if (x === y) {
      // For the case where +0 is not equal to -0
      return x !== 0 || 1 / x === 1 / y;
    }
    // For the case of NaN
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true,
});

Object.assign()

Basic usage

The Object.assign() method is used to merge objects, copy all enumerable properties of the source object (source) to the target object (target).

const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target; // {a:1, b:2, c:3}

The first parameter of the Object.assign() method is the target object, and the following parameters are the source object.

Note that if the target object and the source object have attributes with the same name, or multiple source objects have attributes with the same name, the latter attributes will override the previous attributes.

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}

If there is only one parameter, Object.assign() will return that parameter directly.

const obj = { a: 1 };
Object.assign(obj) === obj; // true

If the parameter 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 into objects, if they are used as parameters, an error will be reported.

Object.assign(undefined); // report an error
Object.assign(null); // error

If the non-object parameter appears in the position of the source object (that is, the non-first parameter), then the processing rules are different. First of all, these parameters will be converted into objects. If they cannot be converted into objects, they will be skipped. This means that if undefined and null are not in the first parameter, no error will be reported.

let obj = { a: 1 };
Object.assign(obj, undefined) === obj; // true
Object.assign(obj, null) === obj; // true

Other types of values ​​(ie, numeric, string, and boolean) are not in the first parameter, and no error will be reported. However, except that the string will be copied into the target object in the form of an array, other values ​​will 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"}

In the above code, v1, v2, and v3 are string, boolean, and numeric value respectively. As a result, only the string is merged into the target object (in the form of a character array), and numeric and boolean values ​​are ignored. This is because there are only strings wrapped objects, which will produce enumerable properties.

Object(true); // {[[PrimitiveValue]]: true}
Object(10); // {[[PrimitiveValue]]: 10}
Object("abc"); // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}

In the above code, the Boolean value, numeric value, and string are respectively converted into the corresponding packaging object. You can see that their original values ​​are all on the internal property [[PrimitiveValue]] of the packaging object. This property will not be Object .assign() copied. Only string-wrapped objects will produce enumerable real attributes, and those attributes will be copied.

The properties copied by Object.assign() are limited. Only the properties of the source object are copied (not inherited), and non-enumerable properties are not copied (enumerable: false).

Object.assign(
  { b: "c" },
  Object.defineProperty({}, "invisible", {
    enumerable: false,
    value: "hello",
  })
);
// {b:'c'}

In the above code, the object to be copied by Object.assign() has only one non-enumerable property invisible, which is not copied in.

The attribute whose attribute name is Symbol value will also be copied by Object.assign().

Object.assign({ a: "b" }, { [Symbol("c")]: "d" });
// {a:'b', Symbol(c):'d'}

be careful

(1) Shallow copy

The Object.assign() method implements a shallow copy, not a deep copy. In other words, if the value of an attribute of the source object is an object, then the target object is copied as a reference to this object.

const obj1 = { a: { b: 1 } };
const obj2 = Object.assign({}, obj1);

obj1.ab = 2;
obj2.ab; // 2

In the above code, the value of the a property of the source object obj1 is an object, and the copy obtained by Object.assign() is a reference to this object. Any changes to this object will be reflected on the target object.

(2) Replacement of attributes with the same name

For such nested objects, once an attribute with the same name is encountered, the processing method of Object.assign() is to replace, not add.

const target = { a: { b: "c", d: "e" } };
const source = { a: { b: "hello" } };
Object.assign(target, source);
// {a: {b:'hello'}}

In the above code, the a attribute of the target object is completely replaced by the a attribute of the source object, instead of getting { a: {b:'hello', d:'e'}} The result. This is usually not what the developer wants and requires special care.

Some libraries provide customized versions of Object.assign() (such as Lodash's _.defaultsDeep() method), which can be combined with deep copies.

(3) Processing of array

Object.assign() can be used to deal with arrays, but will treat arrays as objects.

Object.assign([1, 2, 3], [4, 5]);
// [4, 5, 3]

In the above code, Object.assign() treats the array as an object with attribute names 0, 1, and 2, so the 0th attribute 4 of the source array overrides the 0th attribute 1 of the target array.

(4) Processing of value function

Object.assign() can only copy the value. If the value to be copied is a value function, it will be evaluated and then copied.

const source = {
  get foo() {
    return 1;
  },
};
const target = {};

Object.assign(target, source);
// {foo: 1}

In the above code, the foo property of the source object is a value function, and Object.assign() will not copy this value function. It will only copy the value after the value is obtained.

Common uses

The Object.assign() method has many uses.

(1) Add attributes to the object

class Point {
  constructor(x, y) {
    Object.assign(this, { x, y });
  }
}

The above method uses the Object.assign() method to add the x attribute and the y attribute to the object instance of the Point class.

(2) Add a method to the object

Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// is equivalent to the following writing
SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};

The above code uses the concise notation of object properties, directly put the two functions in curly braces, and then use the assign() method to add to SomeClass.prototype.

(3) Clone the object

function clone(origin) {
  return Object.assign({}, origin);
}

The above code copies the original object to an empty object, and then a clone of the original object is obtained.

However, with this method of cloning, you can only clone the value of the original object itself, not its inherited value. If you want to maintain the inheritance chain, you can use the following code.

function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}

(4) Merge multiple objects

Combine multiple objects into one object.

const merge = (target, ...sources) => Object.assign(target, ...sources);

If you want to return a new object after merging, you can rewrite the above function to merge an empty object.

const merge = (...sources) => Object.assign({}, ...sources);

(5) Specify a default value for the attribute

const DEFAULTS = {
  logLevel: 0,
  outputFormat: "html",
};

function processContent(options) {
  options = Object.assign({}, DEFAULTS, options);
  console.log(options);
  // ...
}

In the above code, the DEFAULTS object is the default value, and the options object is the parameter provided by the user. The Object.assign() method combines DEFAULTS and options into a new object. If both have attributes with the same name, the attribute value of options will overwrite the attribute value of DEFAULTS.

Note that due to the shallow copy problem, the values ​​of all the attributes of the DEFAULTS object and the options object are preferably simple types and do not point to another object. Otherwise, this property of the DEFAULTS object is likely to have no effect.

const DEFAULTS = {
  url: {
    host: "example.com",
    port: 7070,
  },
};

processContent({ url: { port: 8000 } });
// {
// url: {port: 8000}
//}

The original intention of the above code is to change url.port to 8000, and url.host remains unchanged. The actual result is that options.url overwrites DEFAULTS.url, so url.host does not exist.

Object.getOwnPropertyDescriptors()

ES5's Object.getOwnPropertyDescriptor() method returns the descriptor of an object property. ES2017 introduced the Object.getOwnPropertyDescriptors() method, which returns the description object of all its own properties (non-inherited properties) 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}}

In the above code, the Object.getOwnPropertyDescriptors() method returns an object. The property names of all original objects are the property names of the object, and the corresponding property value is the description object of the property.

The implementation of this method is very easy.

function getOwnPropertyDescriptors(obj) {
  const result = {};
  for (let key of Reflect.ownKeys(obj)) {
    result[key] = Object.getOwnPropertyDescriptor(obj, key);
  }
  return result;
}

The purpose of the introduction of this method is mainly to solve the problem that Object.assign() cannot copy the get attributes and the set attributes correctly.

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}

In the above code, the value of the foo property of the source object is an assignment function. The Object.assign method copies this property to the target1 object, and as a result, the value of the property becomes undefined. This is because the Object.assign method always copies the value of an attribute, and does not copy the assignment method or value method behind it.

At this time, the Object.getOwnPropertyDescriptors() method cooperates with the Object.defineProperties() method to realize the correct copy.

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}

In the above code, the logic of merging two objects can be written as a function.

const shallowMerge = (target, source) =>
  Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));

Another use of the Object.getOwnPropertyDescriptors() method is to use the Object.create() method to clone object properties to a new object. 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)
  );

The above code will clone the object obj.

In addition, the Object.getOwnPropertyDescriptors() method allows one object to inherit from another object. In the past, inheriting another object was often written as follows.

const obj = {
  __proto__: prot,
  foo: 123,
};

ES6 stipulates that __proto__ only needs to be deployed by the browser, and other environments do not need to be deployed. If you remove __proto__, the above code will be changed to the following.

const obj = Object.create(prot);
obj.foo = 123;

// or

const obj = Object.assign(Object.create(prot), {
  foo: 123,
});

With Object.getOwnPropertyDescriptors(), we have another way of writing.

const obj = Object.create(
  prot,
  Object.getOwnPropertyDescriptors({
    foo: 123,
  })
);

Object.getOwnPropertyDescriptors() can also be used to implement Mixin mode.

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);

dc; // "c"
db; // "b"
da; // "a"

The above code returns a new object d, which represents the operation of objects a and b being mixed into object c.

For the sake of completeness, after Object.getOwnPropertyDescriptors() enters the standard, the Reflect.getOwnPropertyDescriptors() method will be added in the future.

__proto__ property, Object.setPrototypeOf(), Object.getPrototypeOf()

Object inheritance in JavaScript language is achieved through the prototype chain. ES6 provides more ways to manipulate prototype objects.

__proto__ attribute

The __proto__ property (two underscores before and after) is used to read or set the prototype object (prototype) of the current object. Currently, all browsers (including IE11) have deployed this attribute.

// How to write es5
const obj = {
  method: function() {...}
};
obj.__proto__ = someOtherObj;

// How to write es6
var obj = Object.create(someOtherObj);
obj.method = function() {... };

This attribute is not written into the ES6 body, but is written in the appendix. The reason is the double underscore before and after __proto__, indicating that it is essentially an internal attribute, not a formal external API, just because of the extensive support of browsers , Was added to ES6. The standard clearly stipulates that only the browser must deploy this attribute, other operating environments do not necessarily need to be deployed, and it is better for new code to think that this attribute does not exist. Therefore, no matter from a semantic point of view or from a compatibility point of view, do not use this attribute, but use the following Object.setPrototypeOf() (write operation), Object.getPrototypeOf() (read operation), Object.create() (generate operation) instead.

In terms of implementation, __proto__ calls Object.prototype.__proto__, the specific implementation is 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;
}

If an object itself deploys the __proto__ property, the value of the property is the object's prototype.

Object.getPrototypeOf({ __proto__: null });
// null

Object.setPrototypeOf()

The function of the Object.setPrototypeOf method is the same as that of __proto__, which is used to set the prototype of an object and return the parameter object itself. It is officially recommended by ES6 to set the prototype object.

// format
Object.setPrototypeOf(object, prototype);

// usage
const o = Object.setPrototypeOf({}, null);

This method is equivalent to the following function.

function setPrototypeOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

Below 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

The above code sets the proto object as the prototype of the obj object, so the properties of the proto object can be read from the obj object.

If the first parameter is not an object, it will be automatically converted to an object. But since the first parameter is returned, this operation will not produce any effect.

Object.setPrototypeOf(1, {}) === 1; // true
Object.setPrototypeOf("foo", {}) === "foo"; // true
Object.setPrototypeOf(true, {}) === true; // true

Since undefined and null cannot be converted into objects, if the first parameter is undefined or null, an error will be reported.

Object.setPrototypeOf(undefined, {});
// TypeError: Object.setPrototypeOf called on null or undefined

Object.setPrototypeOf(null, {});
// TypeError: Object.setPrototypeOf called on null or undefined

Object.getPrototypeOf()

This method is matched with the Object.setPrototypeOf method to read the prototype object of an object.

Object.getPrototypeOf(obj);

Below 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

If the parameter is not an object, it will be automatically converted to an object.

// 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

If the parameters are undefined or null, they cannot be converted into objects, so an error will be reported.

Object.getPrototypeOf(null);
// TypeError: Cannot convert undefined or null to object

Object.getPrototypeOf(undefined);
// TypeError: Cannot convert undefined or null to object

Object.keys(), Object.values(), Object.entries()

Object.keys()

ES5 introduced the Object.keys method, which returns an array. The members are the keys of all enumerable properties of the parameter object itself (without inheritance).

var obj = { foo: "bar", baz: 42 };
Object.keys(obj);
// ["foo", "baz"]

ES2017 Introduced added Object.values and Object.entries that accompany Object.keys as a supplement to traversing an object Means, for the use of for...of loops.

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]
}

Object.values()

The Object.values method returns an array. The members are the key values ​​of all enumerable properties of the parameter object itself (without inheritance).

const obj = { foo: "bar", baz: 42 };
Object.values(obj);
// ["bar", 42]

The order of the members of the returned array is consistent with the arrangement rules introduced in the "Attribute Traversal" section of this chapter.

const obj = { 100: "a", 2: "b", 7: "c" };
Object.values(obj);
// ["b", "c", "a"]

In the above code, the attributes whose attribute names are numeric values ​​are traversed from small to large according to the magnitude of the value, so the return order is b, c, and a.

Object.values only returns the traversable properties of the object itself.

const obj = Object.create({}, { p: { value: 42 } });
Object.values(obj); // []

In the above code, the object property (property p) added by the second parameter of the Object.create method is not traversable by default if it is not explicitly declared, because the property of p describes the enumerable of the object The default is false and Object.values will not return this property. Just change enumerable to true, and Object.values will return the value of the property p.

const obj = Object.create(
  {},
  {
    p: {
      value: 42,
      enumerable: true,
    },
  }
);
Object.values(obj); // [42]

Object.values will filter the attributes whose attribute name is Symbol value.

Object.values({ [Symbol()]: 123, foo: "abc" });
// ['abc']

If the parameter of the Object.values method is a string, an array of characters will be returned.

Object.values("foo");
// ['f','o','o']

In the above code, the string will first be converted into an array-like object. Each character of the string is an attribute of the object. Therefore, Object.values returns the key value of each attribute, which is an array of characters.

If the parameter is not an object, Object.values will first convert it to an object. Because of the packaging objects of numeric and boolean values, non-inherited properties are not added to the instance. Therefore, Object.values will return an empty array.

Object.values(42); // []
Object.values(true); // []

Object.entries()

The Object.entries() method returns an array. The members are the key-value pairs array of all enumerable properties of the parameter object itself (without inheritance).

const obj = { foo: "bar", baz: 42 };
Object.entries(obj);
// [["foo", "bar"], ["baz", 42]]

Except that the return value is different, the behavior of this method is basically the same as that of Object.values.

If the attribute name of the original object is a Symbol value, the attribute will be ignored.

Object.entries({ [Symbol()]: 123, foo: "abc" });
// [['foo','abc']]

In the above code, the original object has two attributes, and Object.entries only outputs the attributes whose attribute names are not Symbol values. In the future, there may be a Reflect.ownEntries() method that returns all the attributes of the object itself.

The basic purpose of Object.entries is to traverse the properties of an object.

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

Another use of the Object.entries method is to turn the 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}

It is very simple to implement the Object.entries method by yourself.

// The version of the Generator function
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;
}

Object.fromEntries()

The Object.fromEntries() method is the inverse operation of Object.entries(), used to convert an array of key-value pairs into an object.

Object.fromEntries([
  ["foo", "bar"],
  ["baz", 42],
]);
// {foo: "bar", baz: 42}

The main purpose of this method is to restore the data structure of key-value pairs to objects, so it is 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}

One use of this method is to cooperate with the URLSearchParams object to convert the query string into an object.

Object.fromEntries(new URLSearchParams("foo=bar&baz=qux"));
// {foo: "bar", baz: "qux"}