Object extension

Object is the most important data structure of JavaScript. ES6 has made a major upgrade to it. This chapter introduces the changes in the data structure itself, and the next chapter introduces the new methods of the Object object.

Concise notation of attributes

ES6 allows you to write variables and functions directly inside the curly braces as properties and methods of the object. This writing is more concise.

const foo = "bar";
const baz = { foo };
baz; // {foo: "bar"}

// Equivalent to
const baz = { foo: foo };

In the above code, the variable foo is written directly inside the braces. At this time, the attribute name is the variable name, and the attribute value is the variable value. Here is another example.

function f(x, y) {
  return { x, y };
}

// Equivalent to

function f(x, y) {
  return { x: x, y: y };
}

f(1, 2); // Object {x: 1, y: 2}

In addition to attribute abbreviations, methods can also be abbreviated.

const o = {
  method() {
    return "Hello!";
  },
};

// Equivalent to

const o = {
  method: function () {
    return "Hello!";
  },
};

The following is a practical example.

let birth = "2000/01/01";

const Person = {
  name: "Zhang San",

  //Same as birth: birth
  birth,

  // Equivalent to hello: function ()...
  hello() {
    console.log("My name is", this.name);
  },
};

This way of writing is very convenient for the return value of a function.

function getPoint() {
  const x = 1;
  const y = 10;
  return { x, y };
}

getPoint();
// {x:1, y:10}

The CommonJS module outputs a set of variables, which is very suitable for concise writing.

let ms = {};

function getItem(key) {
  return key in ms ? ms[key] : null;
}

function setItem(key, value) {
  ms[key] = value;
}

function clear() {
  ms = {};
}

module.exports = { getItem, setItem, clear };
// Equivalent to
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear,
};

Property setters and getters are actually written in this way.

const cart = {
  _wheels: 4,

  get wheels() {
    return this._wheels;
  },

  set wheels(value) {
    if (value < this._wheels) {
      throw new Error("The value is too small!");
    }
    this._wheels = value;
  },
};

Concise writing is also useful when printing objects.

let user = {
  name: "test",
};

let foo = {
  bar: "baz",
};

console.log(user, foo);
// {name: "test"} {bar: "baz"}
console.log({ user, foo });
// {user: {name: "test"}, foo: {bar: "baz"}}

In the above code, when console.log directly outputs the two objects of user and foo, there are two sets of key-value pairs, which may be confused. Put them in curly braces and output them, and it becomes a concise representation of the object. The name of the object will be printed in front of each group of key-value pairs, which makes it clearer.

Note that the abbreviated object method cannot be used as a constructor, and an error will be reported.

const obj = {
  f() {
    this.foo = "bar";
  },
};

new obj.f(); // report an error

In the above code, f is an abbreviated object method, so obj.f cannot be used as a constructor.

Attribute name expression

JavaScript defines the properties of an object, there are two ways.

// method one
obj.foo = true;

// Method Two
obj["a" + "bc"] = 123;

The first method of the above code is to directly use the identifier as the attribute name, and the second method is to use the expression as the attribute name. In this case, put the expression in square brackets.

However, if an object is defined literally (using braces), only method one (identifier) ​​can be used to define attributes in ES5.

var obj = {
  foo: true,
  abc: 123,
};

When ES6 allows literal definition of an object, use method two (expression) as the attribute name of the object, that is, put the expression in square brackets.

let propKey = "foo";

let obj = {
  [propKey]: true,
  ["a" + "bc"]: 123,
};

Here is another example.

let lastWord = "last word";

const a = {
  "first word": "hello",
  [lastWord]: "world",
};

a["first word"]; // "hello"
a[lastWord]; // "world"
a["last word"]; // "world"

Expressions can also be used to define method names.

let obj = {
  ["h" + "ello"]() {
    return "hi";
  },
};

obj.hello(); // hi

Note that the attribute name expression and concise notation cannot be used at the same time, and an error will be reported.

// report an error
const foo ='bar';
const bar ='abc';
const baz = {[foo] };

// correct
const foo ='bar';
const baz = {[foo]:'abc'};

Note that if the attribute name expression is an object, it will automatically convert the object to the string [object Object] by default. Be careful about this.

const keyA = { a: 1 };
const keyB = { b: 2 };

const myObject = {
  [keyA]: "valueA",
  [keyB]: "valueB",
};

myObject; // Object {[object Object]: "valueB"}

In the above code, both [keyA] and [keyB] get [object Object], so [keyB] will overwrite [keyA], and myObject has only one at the end [object Object]Properties.

Method's name attribute

The name attribute of the function returns the name of the function. Object methods are also functions, so they also have the name attribute.

const person = {
  sayName() {
    console.log("hello!");
  },
};

person.sayName.name; // "sayName"

In the above code, the name property of the method returns the function name (namely the method name).

If the method of the object uses the value function (getter) and the stored value function (setter), the name property is not on the method, but the property of the method describes the object's get and Above the set property, the return value is the method name with get and set.

const obj = {
  get foo() {},
  set foo(x) {},
};

obj.foo.name;
// TypeError: Cannot read property'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, "foo");

descriptor.get.name; // "get foo"
descriptor.set.name; // "set foo"

There are two special cases: the function created by the bind method, the name property returns bound plus the name of the original function; the function created by the Function constructor, the name property returns anonymous.

new Function().name; // "anonymous"

var doSomething = function () {
  // ...
};
doSomething.bind().name; // "bound doSomething"

If the method of the object is a Symbol value, then the name property returns a description of the Symbol value.

const key1 = Symbol("description");
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name; // "[description]"
obj[key2].name; // ""

In the above code, the Symbol value corresponding to key1 is described, and key2 does not.

Enumerability and traversal of attributes

Enumerability

Each property of the object has a description object (Descriptor), which is used to control the behavior of the property. The Object.getOwnPropertyDescriptor method can get the description object of the property.

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, "foo");
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
//}

The enumerable property that describes the object is called "enumerability". If the property is false, it means that some operations will ignore the current property.

Currently, there are four operations that ignore attributes where the enumerable is false.

-for...in loop: only traverse the object's own and inherited enumerable properties. -Object.keys(): Returns the keys of all enumerable properties of the object itself. -JSON.stringify(): Only serialize the enumerable properties of the object itself. -Object.assign(): Ignore the attributes where enumerable is false, and only copy the enumerable attributes of the object itself.

Among these four operations, the first three are available in ES5, and the last Object.assign() is a new addition to ES6. Among them, only for...in will return inherited properties, and the other three methods will ignore inherited properties and only process the properties of the object itself. In fact, the original purpose of introducing the concept of "enumerable" (enumerable) is to make certain properties circumvent the for...in operation, otherwise all internal properties and methods will be traversed. For example, the toString method of object prototypes and the length property of arrays pass "enumerability" to avoid being traversed by for...in.

Object.getOwnPropertyDescriptor(Object.prototype, "toString").enumerable;
// false

Object.getOwnPropertyDescriptor([], "length").enumerable;
// false

In the above code, the enumerable of the toString and length properties are both false, so for...in will not traverse these two properties inherited from the prototype.

In addition, ES6 stipulates that all methods of Class prototypes are not enumerable.

Object.getOwnPropertyDescriptor(
  class {
    foo() {}
  }.prototype,
  "foo"
).enumerable;
// false

In general, the introduction of inherited properties in operations will complicate the problem. Most of the time, we only care about the properties of the object itself. So, try not to use for...in loops, and use Object.keys() instead.

Traversal of attributes

ES6 has 5 methods to traverse the properties of an object.

(1) for...in

for...in loops through the object's own and inherited enumerable properties (excluding Symbol properties).

(2) Object.keys(obj)

Object.keys returns an array, including the key names of all enumerable properties (excluding Symbol property) of the object itself (excluding inherited).

(3) Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames returns an array containing the key names of all the properties of the object itself (excluding Symbol properties, but including non-enumerable properties).

(4) Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols returns an array containing the keys of all Symbol properties of the object itself.

(5) Reflect.ownKeys(obj)

Reflect.ownKeys returns an array containing all keys of the object itself (without inheritance), regardless of whether the key is a Symbol or a string, or whether it is enumerable or not.

The above five methods to traverse the key names of objects all obey the same order rule of attribute traversal.

-First traverse all numeric keys and arrange them in ascending order of numeric value. -Secondly, all string keys are traversed and arranged in ascending order according to the time they were added. -Finally traverse all Symbol keys and arrange them in ascending order according to the time they were added.

Reflect.ownKeys({ [Symbol()]: 0, b: 0, 10: 0, 2: 0, a: 0 });
// ['2', '10','b','a', Symbol()]

In the above code, the Reflect.ownKeys method returns an array containing all the attributes of the parameter object. The order of the attributes of this array is like this, first is the numeric attributes 2 and 10, second is the string attributes b and a, and finally is the Symbol attribute.

super keyword

We know that the this keyword always points to the current object where the function is located. ES6 has added another similar keyword super, which points to the prototype object of the current object.

const proto = {
  foo: "hello",
};

const obj = {
  foo: "world",
  find() {
    return super.foo;
  },
};

Object.setPrototypeOf(obj, proto);
obj.find(); // "hello"

In the above code, in the object obj.find() method, the foo property of the prototype object proto is referenced through super.foo.

Note that when the super keyword represents a prototype object, it can only be used in object methods, and an error will be reported when used in other places.

// report an error
const obj = {
  foo: super.foo,
};

// report an error
const obj = {
  foo: () => super.foo,
};

// report an error
const obj = {
  foo: function () {
    return super.foo;
  },
};

The above three usages of super will report errors, because for the JavaScript engine, the super here is not used in object methods. The first way of writing is that super is used in attributes. The second and third way of writing is that super is used in a function and then assigned to the foo attribute. Currently, only the shorthand of object method allows the JavaScript engine to confirm that it defines the object method.

Inside the JavaScript engine, super.foo is equivalent to Object.getPrototypeOf(this).foo (property) or Object.getPrototypeOf(this).foo.call(this) (method).

const proto = {
  x: "hello",
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: "world",
  foo() {
    super.foo();
  },
};

Object.setPrototypeOf(obj, proto);

obj.foo(); // "world"

In the above code, super.foo points to the foo method of the prototype object proto, but the bound this is still the current object obj, so the output is world.

Object expansion operator

In the chapter "Expansion of Arrays", the spread operator (...) has been introduced. ES2018 introduced this operator into the object.

Destructuring assignment

The destructuring assignment of an object is used to obtain a value from an object, which is equivalent to assigning all enumerable but not yet read properties of the target object to the specified object. All keys and their values ​​will be copied to the new object.

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // {a: 3, b: 4}

In the above code, the variable z is the object where the destructuring assignment is located. It takes all the unread keys (a and b) on the right side of the equal sign and copies them along with the value.

Since destructuring assignment requires an object on the right side of the equal sign, if the right side of the equal sign is undefined or null, an error will be reported because they cannot be converted into objects.

let { ...z } = null; // runtime error
let { ...z } = undefined; // runtime error

The destructuring assignment must be the last parameter, otherwise an error will be reported.

let {...x, y, z} = someObject; // syntax error
let {x, ...y, ...z} = someObject; // syntax error

In the above code, the destructuring assignment is not the last parameter, so an error will be reported.

Note that the copy of the destructuring assignment is a shallow copy, that is, if the value of a key is a value of a composite type (array, object, function), then the destructuring assignment copy is a reference to this value, not a copy of the value.

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.ab = 2;
xab; // 2

In the above code, x is the object where the destructuring assignment is located, and the a property of the object obj is copied. The a attribute refers to an object. Modifying the value of this object will affect the reference to it by the destructuring assignment.

In addition, the destructuring assignment of the spread operator cannot copy the properties inherited from the prototype object.

let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3; // {b: 2}
o3.a; // undefined

In the above code, the object o3 copied o2, but only the properties of o2 itself were copied, not the properties of its prototype object o1.

Here is another example.

const o = Object.create({ x: 1, y: 2 });
oz = 3;

let { x, ...newObj } = o;
let { y, z } = newObj;
x; // 1
y; // undefined
z; // 3

In the above code, the variable x is a purely deconstructed assignment, so the properties inherited by the object o can be read; the variables y and z are deconstructed assignments of the spread operator and can only read the object o Own property, so the variable z can be successfully assigned, but the variable y cannot get a value. ES6 stipulates that in the variable declaration statement, if destructuring assignment is used, the spread operator must be followed by a variable name, not a destructuring assignment expression, so the above code introduces the intermediate variable newObj, if it is written as follows, an error will be reported .

let {x, ...{ y, z}} = o;
// SyntaxError: ... must be followed by an identifier in declaration contexts

One use of destructuring assignment is to extend the parameters of a function and introduce other operations.

function baseFunction({ a, b }) {
  // ...
}
function wrapperFunction({ x, y, ...restConfig }) {
  // Use x and y parameters to operate
  // The remaining parameters are passed to the original function
  return baseFunction(restConfig);
}

In the above code, the original function baseFunction accepts a and b as parameters, and the function wrapperFunction is extended on the basis of baseFunction to accept redundant parameters and retain the behavior of the original function.

Spread operator

The expansion operator (...) of the object is used to retrieve all the traversable properties of the parameter object and copy them to the current object.

let z = { a: 3, b: 4 };
let n = { ...z };
n; // {a: 3, b: 4}

Since arrays are special objects, the spread operator of objects can also be used for arrays.

let foo = { ...["a", "b", "c"] };
foo;
// {0: "a", 1: "b", 2: "c"}

If the spread operator is followed by an empty object, it has no effect.

{...{}, a: 1}
// {a: 1}

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

// equivalent to {...Object(1)}
{...1} // {}

In the above code, the spread operator is followed by the integer 1, which will be automatically converted to the numeric packaging object Number{1}. Since the object has no properties of its own, an empty object is returned.

The following examples are similar in reason.

// equivalent to {...Object(true)}
{...true} // {}

// equivalent to {...Object(undefined)}
{...undefined} // {}

// equivalent to {...Object(null)}
{...null} // {}

However, if the spread operator is followed by a string, it will automatically be converted into an array-like object, so the returned object is not an empty object.

{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

The spread operator of an object is equivalent to using the Object.assign() method.

let aClone = { ...a };
// Equivalent to
let aClone = Object.assign({}, a);

The above example only copies the properties of the object instance. If you want to clone an object completely and also copy the properties of the object prototype, you can use the following writing method.

// Writing method one
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj,
};

// Writing method two
const clone2 = Object.assign(Object.create(Object.getPrototypeOf(obj)), obj);

// Writing method three
const clone3 = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

In the above code, the __proto__ attribute of writing method one may not be deployed in a non-browser environment, so writing method two and writing method three are recommended.

The spread operator can be used to merge two objects.

let ab = { ...a, ...b };
// Equivalent to
let ab = Object.assign({}, a, b);

If the user-defined attribute is placed after the spread operator, the attribute with the same name inside the spread operator will be overwritten.

let aWithOverrides = { ...a, x: 1, y: 2 };
// Equivalent to
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// Equivalent to
let x = 1,
  y = 2,
  aWithOverrides = { ...a, x, y };
// Equivalent to
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });

In the above code, the x property and y property of the a object will be overwritten after being copied to the new object.

This is very convenient for modifying the properties of existing object parts.

let newVersion = {
  ...previousVersion,
  name: "New Name", // Override the name property
};

In the above code, the newVersion object has a custom name attribute, and all other attributes are copied from the previousVersion object.

If you put the custom attribute before the spread operator, it becomes the default attribute value of the new object.

let aWithDefaults = { x: 1, y: 2, ...a };
// Equivalent to
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// Equivalent to
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);

Like the spread operator of an array, the spread operator of an object can be followed by an expression.

const obj = {
  ...(x > 1 ? { a: 1 } : {}),
  b: 2,
};

Among the parameter objects of the spread operator, if there is a value function get, this function will be executed.

let a = {
  get x() {
    throw new Error("not throw yet");
  },
};

let aWithXGetter = { ...a }; // report an error

In the above example, the value function get will be automatically executed when the a object is expanded, resulting in an error.

Chain judgment operator

In programming practice, if you read an attribute inside an object, you often need to determine whether the object exists. For example, to read message.body.user.firstName, the safe way to write it is as follows.

// wrong way
const firstName = message.body.user.firstName;

// correct writing
const firstName =
  (message &&
    message.body &&
    message.body.user &&
    message.body.user.firstName) ||
  "default";

In the above example, the firstName attribute is on the fourth layer of the object, so it needs to be judged four times to see if each layer has a value.

The ternary operator ?: is also often used to determine whether an object exists.

const fooInput = myForm.querySelector("input[name=foo]");
const fooValue = fooInput ? fooInput.value : undefined;

In the above example, you must first determine whether fooInput exists before reading fooInput.value.

Such layer-by-layer judgment is very troublesome, so ES2020 introduces the "optional chaining operator" (optional chaining operator) ?. to simplify the above Writing.

const firstName = message?.body?.user?.firstName || "default";
const fooValue = myForm.querySelector("input[name=foo]")?.value;

The above code uses the ?. operator to directly determine whether the object on the left is null or undefined during the chain call. If it is, it will not go down and return to undefined.

The following is an example of judging whether the object method exists and executing it immediately if it exists.

iterator.return?.();

In the above code, if iterator.return is defined, this method will be called, otherwise iterator.return will directly return to undefined, and the following part of ?. will not be executed.

This operator is especially useful for methods that may not be implemented.

if (myForm.checkValidity?.() === false) {
  // Form validation failed
  return;
}

In the above code, the form of the old browser may not have the checkValidity method. At this time, the ?. operator will return undefined, and the judgment statement will become undefined === false, so it will jump. Go through the code below.

There are three uses of chain judgment operators.

-obj?.prop // object properties -obj?.[expr] // Same as above -func?.(...args) // function or object method call

The following is an example of the usage of obj?.[expr].

let hex = "#C0FFEE".match(/#([AZ]+)/i)?.[1];

In the above example, the match() method of the string will return null if no match is found, and an array if a match is found. The ?. operator plays a judgment role.

The following are the common forms of the ?. operator and the equivalent form when the operator is not used.

a?.b;
// Equivalent to
a == null ? undefined : ab;

a?.[x];
// Equivalent to
a == null ? undefined : a[x];

a?.b();
// Equivalent to
a == null ? undefined : ab();

a?.();
// Equivalent to
a == null ? undefined : a();

In the above code, pay special attention to the latter two forms. If the ab in a?.b() is not a function and cannot be called, then a?.b() will report an error. The same is true for a?.(), if a is not null or undefined, but not a function, then a?.() will report an error.

There are several points to note when using this operator.

(1) Short-circuit mechanism

The ?. operator is equivalent to a short-circuit mechanism, as long as the condition is not met, no further execution will be performed.

a?.[++x];
// Equivalent to
a == null ? undefined : a[++x];

In the above code, if a is undefined or null, then x will not be incremented. In other words, once the chain judgment operator is true, the expression on the right is no longer evaluated.

(2) delete operator

delete a?.b
// Equivalent to
a == null? undefined: delete ab

In the above code, if a is undefined or null, it will directly return undefined without performing the delete operation.

(3) The influence of brackets

If the attribute chain has parentheses, the chain judgment operator has no effect on the outside of the parentheses, but only on the inside of the parentheses.

(a?.b).c(
  // Equivalent to
  a == null ? undefined : ab
).c;

In the above code, ?. has no effect on the outside of the parentheses. No matter whether the a object exists or not, the .c after the parentheses will always be executed.

Generally speaking, when using the ?. operator, parentheses should not be used.

(4) Where the error is reported

The following wording is prohibited, and an error will be reported.

// Constructor
new a?.()
new a?.b()

// There is a template string on the right side of the chain judgment operator
a?.`{b}`
a?.b`{c}`

// The left side of the chain judgment operator is super
super?.()
super?.foo

// The chain operator is used on the left side of the assignment operator
a?.b = c

(5) The right side cannot be a decimal value

In order to ensure compatibility with the previous code, foo?.3:0 is allowed to be parsed into foo? .3: 0, so it is stipulated that if ?. is followed by a decimal number, then ?. will no longer be It is regarded as a complete operator, and will be processed according to the ternary operator, that is, the decimal point will be attributed to the following decimal number to form a decimal.

Null judgment operator

When reading object properties, if the value of a certain property is null or undefined, sometimes it is necessary to specify a default value for them. A common practice is to specify default values ​​through the || operator.

const headerText = response.settings.headerText || "Hello, world!";
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;

The above three lines of code use the || operator to specify the default value, but this is wrong. The original intention of the developer was that as long as the value of the property is null or undefined, the default value will take effect, but if the value of the property is an empty string or false or 0, the default value will also take effect.

To avoid this situation, ES2020 introduced a new null judgment operator ??. It behaves like ||, but only if the value on the left side of the operator is null or undefined, it will return the value on the right side.

const headerText = response.settings.headerText ?? "Hello, world!";
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;

In the above code, the default value will only take effect when the left attribute value is null or undefined.

One purpose of this operator is to use it with the chain judgment operator ?. to set a default value for the value of null or undefined.

const animationDuration = response.settings?.animationDuration ?? 300;

In the above code, if response.settings is null or undefined, or response.settings.animationDuration is null or undefined, the default value of 300 will be returned. In other words, this line of code includes two levels of attribute judgment.

This operator is very suitable for judging whether a function parameter is assigned a value.

function Component(props) {
  const enable = props.enabled ?? true;
  //…
}

The above code determines whether the enabled property of the props parameter is assigned a value, which is basically equivalent to the following writing.

function Component(props) {
  const { enabled: enable = true } = props;
  //…
}

?? has an operation priority problem, which is higher or lower than && and ||. The current rule is that if multiple logical operators are used together, parentheses must be used to indicate the priority, otherwise an error will be reported.

// report an error
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs

The above four expressions will report errors, and parentheses indicating the priority must be added.

(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);

(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);

(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);

(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);