VDone Demo VDone Demo
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Nikolay Tuzov

Backend Developer
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • Introduction to ECMAScript 6
  • let and const Commands
  • Destructuring Assignment of Variables
  • String Extensions
  • New String Methods
  • Regular Expression Extensions
  • Number Extensions
  • Function Extensions
  • Array Extensions
  • Object Extensions
    • Shorthand Property Notation
    • Computed Property Names
    • The name Property of Methods
    • Property Enumerability and Traversal
      • Enumerability
      • Property Traversal
    • The super Keyword
    • Object Spread Operator
      • Destructuring Assignment
      • Spread Operator
    • Optional Chaining Operator
    • Nullish Coalescing Operator
  • 对象的新增方法
  • Symbol
  • Set 和 Map 数据结构
  • Proxy
  • Reflect
  • Promise 对象
  • Iterator 和 for-of 循环
  • Generator Function Syntax
  • Asynchronous Applications of Generator Functions
  • async Functions
  • Class 的基本语法
  • Class 的继承
  • Module 的语法
  • Module 的加载实现
  • 编程风格
  • 读懂 ECMAScript 规格
  • Async Iterator
  • ArrayBuffer
  • 最新提案
  • 装饰器
  • 函数式编程
  • Mixin
  • SIMD
  • 参考链接
  • 《ES6 教程》笔记
阮一峰
2020-02-09
Contents

Object Extensions

# Object Extensions

Objects are the most important data structure in JavaScript. ES6 made significant upgrades to them. This chapter covers changes to the data structure itself; the next chapter covers new methods added to the Object object.

# Shorthand Property Notation

ES6 allows variables and functions to be written directly inside curly braces as object properties and methods. This results in more concise code.

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

// equivalent to
const baz = {foo: foo};
1
2
3
4
5
6

In the code above, the variable foo is written directly inside curly braces. The property name is the variable name, and the property 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}
1
2
3
4
5
6
7
8
9
10
11

In addition to property shorthand, methods can also be shortened.

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

// equivalent to

const o = {
  method: function() {
    return "Hello!";
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13

Here is a practical example.

let birth = '2000/01/01';

const Person = {

  name: '张三',

  // equivalent to birth: birth
  birth,

  // equivalent to hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};
1
2
3
4
5
6
7
8
9
10
11
12
13

This notation is very convenient for function return values.

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

getPoint()
// {x:1, y:10}
1
2
3
4
5
6
7
8

CommonJS module exports of a set of variables are well-suited for shorthand notation.

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
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Property setters and getters actually also use this notation.

const cart = {
  _wheels: 4,

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

  set wheels (value) {
    if (value < this._wheels) {
      throw new Error('Value is too small!');
    }
    this._wheels = value;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Shorthand notation 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"}}
1
2
3
4
5
6
7
8
9
10
11
12

In the code above, when console.log outputs user and foo directly, the result is two sets of key-value pairs that may be confusing. Placing them inside curly braces uses shorthand notation, printing the object name before each set of key-value pairs for clarity.

Note that shorthand object methods cannot be used as constructors — this will throw an error.

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

new obj.f() // Error
1
2
3
4
5
6
7

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

# Computed Property Names

JavaScript provides two ways to define object properties.

// Method 1
obj.foo = true;

// Method 2
obj['a' + 'bc'] = 123;
1
2
3
4
5

Method 1 uses an identifier directly as the property name. Method 2 uses an expression as the property name, which must be placed inside square brackets.

However, when defining objects using literal syntax (curly braces), ES5 only allows method 1 (identifiers) for defining properties.

var obj = {
  foo: true,
  abc: 123
};
1
2
3
4

ES6 allows method 2 (expressions) to be used as property names when defining objects with literals — placing the expression inside square brackets.

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};
1
2
3
4
5
6

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"
1
2
3
4
5
6
7
8
9
10

Expressions can also be used for defining method names.

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

obj.hello() // hi
1
2
3
4
5
6
7

Note that computed property names and shorthand notation cannot be used simultaneously — this will throw an error.

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

// Correct
const foo = 'bar';
const baz = { [foo]: 'abc'};
1
2
3
4
5
6
7
8

Note that if a computed property name is an object, it is automatically converted to the string [object Object] by default. This requires special attention.

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

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

myObject // Object {[object Object]: "valueB"}
1
2
3
4
5
6
7
8
9

In the code above, both [keyA] and [keyB] resolve to [object Object], so [keyB] overwrites [keyA], and myObject ends up with only one [object Object] property.

# The name Property of Methods

The name property of a function returns the function name. Object methods are also functions and therefore also have a name property.

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

person.sayName.name   // "sayName"
1
2
3
4
5
6
7

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

If an object method uses a getter or setter, the name property is not on the method itself but on the get and set properties of the method's property descriptor object. The return value is the method name prefixed with get or 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"
1
2
3
4
5
6
7
8
9
10
11
12

There are two special cases: functions created by bind have a name property that returns bound plus the original function name; functions created by the Function constructor have a name property that returns anonymous.

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

var doSomething = function() {
  // ...
};
doSomething.bind().name // "bound doSomething"
1
2
3
4
5
6

If an object method is a Symbol value, the name property returns the Symbol's description.

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
1
2
3
4
5
6
7
8

In the code above, key1 has a description for its Symbol value, while key2 does not.

# Property Enumerability and Traversal

# Enumerability

Each property of an object has a descriptor object that controls the property's behavior. The Object.getOwnPropertyDescriptor method retrieves a property's descriptor.

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }
1
2
3
4
5
6
7
8

The enumerable property of the descriptor is called "enumerability." If it is false, certain operations will ignore that property.

Currently, four operations ignore properties where enumerable is false.

  • for...in loop: Iterates over the object's own and inherited enumerable properties.
  • Object.keys(): Returns an array of the object's own enumerable property key names.
  • JSON.stringify(): Only serializes the object's own enumerable properties.
  • Object.assign(): Ignores properties where enumerable is false; only copies the object's own enumerable properties.

Of these four operations, the first three existed in ES5, and Object.assign() is new in ES6. Only for...in returns inherited properties; the other three ignore inherited properties and only deal with the object's own properties. The original purpose of introducing "enumerability" was to allow certain properties to be excluded from for...in iteration — otherwise all internal properties and methods would be iterated. For example, the toString method on Object's prototype and the length property of arrays use "enumerability" to avoid being iterated by for...in.

Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false

Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
1
2
3
4
5

In the code above, toString and length both have enumerable set to false, so for...in does not iterate these two inherited properties from the prototype.

Additionally, ES6 specifies that all methods on Class prototypes are non-enumerable.

Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
1
2

In general, introducing inherited properties into operations complicates things. Most of the time, we only care about the object's own properties. Therefore, try to avoid for...in loops and use Object.keys() instead.

# Property Traversal

ES6 provides 5 methods to traverse an object's properties.

(1) for...in

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

(2) Object.keys(obj)

Object.keys returns an array of all the object's own (non-inherited) enumerable property key names (excluding Symbol properties).

(3) Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames returns an array of all the object's own property key names (excluding Symbol properties but including non-enumerable properties).

(4) Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols returns an array of all the object's own Symbol property key names.

(5) Reflect.ownKeys(obj)

Reflect.ownKeys returns an array of all the object's own key names, regardless of whether they are Symbols or strings, or whether they are enumerable.

All 5 methods for traversing object keys follow the same property traversal order rules.

  • First, all numeric keys are traversed, sorted in ascending numerical order.
  • Then, all string keys are traversed, sorted by their addition time.
  • Finally, all Symbol keys are traversed, sorted by their addition time.
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
1
2

In the code above, Reflect.ownKeys returns an array containing all properties of the argument object. The property order is: first numeric properties 2 and 10, then string properties b and a, and finally the Symbol property.

# The super Keyword

We know that the this keyword always refers to the current object where the function resides. ES6 introduced another similar keyword, super, which refers to the current object's prototype.

const proto = {
  foo: 'hello'
};

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

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
1
2
3
4
5
6
7
8
9
10
11
12
13

In the code above, within the obj.find() method, super.foo references the foo property of the prototype object proto.

Note that the super keyword can only be used inside object methods when referring to the prototype object. Using it elsewhere will throw an error.

// Error
const obj = {
  foo: super.foo
}

// Error
const obj = {
  foo: () => super.foo
}

// Error
const obj = {
  foo: function () {
    return super.foo
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

All three uses of super above throw errors because, as far as the JavaScript engine is concerned, super is not used inside an object method. In the first case, super is used in a property. In the second and third cases, super is used inside a function that is then assigned to the foo property. Currently, only the method shorthand syntax lets the JavaScript engine confirm that what is being defined is an object method.

Internally in the JavaScript engine, super.foo is equivalent to Object.getPrototypeOf(this).foo (for properties) or Object.getPrototypeOf(this).foo.call(this) (for methods).

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

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

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

# Object Spread Operator

The "Array Extensions" chapter already covered the spread operator (...). ES2018 introduced (opens new window) this operator for objects.

# Destructuring Assignment

Object destructuring assignment is used to extract values from an object. It assigns all of the target object's own enumerable but not-yet-read properties to the specified object. All keys and their values are 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 }
1
2
3
4

In the code above, variable z is the object where destructuring assignment takes place. It receives all keys not yet read from the right side of the equals sign (a and b), along with their values.

Since destructuring assignment requires the right side to be an object, if the right side is undefined or null, an error is thrown because they cannot be converted to objects.

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

The destructuring assignment must be the last parameter; otherwise, an error is thrown.

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

In the code above, the destructuring assignment is not the last parameter, so it throws an error.

Note that destructuring assignment performs a shallow copy: if a key's value is a composite type (array, object, function), the destructuring assignment copies the reference to this value, not a deep copy of the value.

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
1
2
3
4

In the code above, x is the object where destructuring assignment takes place, copying the a property from object obj. The a property references an object. Modifying that object's value affects the destructuring assignment's reference to it.

Additionally, the destructuring assignment with the spread operator cannot copy properties inherited from the prototype.

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

In the code above, object o3 copies o2, but only its own properties — not those of its prototype o1.

Here is another example.

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

let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined
z // 3
1
2
3
4
5
6
7
8

In the code above, variable x uses simple destructuring assignment, so it can read inherited properties of object o. Variables y and z use destructuring assignment with the spread operator, which can only read object o's own properties. So z is successfully assigned, while y gets no value. The ES6 specification states that in variable declaration statements, if destructuring assignment is used, the spread operator must be followed by a variable name and not a destructuring expression. This is why the intermediate variable newObj is introduced above. Writing it as follows would throw an error.

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

One use of destructuring assignment is extending function parameters to introduce additional operations.

function baseFunction({ a, b }) {
  // ...
}
function wrapperFunction({ x, y, ...restConfig }) {
  // Use x and y parameters for operations
  // Pass remaining parameters to the original function
  return baseFunction(restConfig);
}
1
2
3
4
5
6
7
8

In the code above, the original function baseFunction accepts a and b as parameters. The wrapperFunction extends baseFunction, accepting additional parameters while preserving the original function's behavior.

# Spread Operator

The object spread operator (...) extracts all enumerable properties of the argument object and copies them into the current object.

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

Since arrays are special objects, the object spread operator can also be used with arrays.

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

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

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

If the spread operator is followed by something other than an object, it is automatically converted to an object.

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

In the code above, the integer 1 follows the spread operator and is automatically converted to the numeric wrapper object Number{1}. Since this object has no own properties, an empty object is returned.

The following examples follow the same logic.

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

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

// equivalent to {...Object(null)}
{...null} // {}
1
2
3
4
5
6
7
8

However, if the spread operator is followed by a string, it is automatically converted to an array-like object, so the result is not an empty object.

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

The object spread operator is equivalent to using Object.assign().

let aClone = { ...a };
// equivalent to
let aClone = Object.assign({}, a);
1
2
3

The example above only copies the object instance's properties. To completely clone an object, including its prototype properties, use the following approach.

// Approach 1
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj
};

// Approach 2
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
);

// Approach 3
const clone3 = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

In the code above, approach 1's __proto__ property may not be available in non-browser environments, so approaches 2 and 3 are recommended.

The spread operator can be used to merge two objects.

let ab = { ...a, ...b };
// equivalent to
let ab = Object.assign({}, a, b);
1
2
3

If user-defined properties are placed after the spread operator, properties with the same name inside the spread operator are overridden.

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 });
1
2
3
4
5
6
7

In the code above, the x and y properties of object a are overridden after being copied to the new object.

This is very convenient for modifying some properties of existing objects.

let newVersion = {
  ...previousVersion,
  name: 'New Name' // Override the name property
};
1
2
3
4

In the code above, newVersion customizes the name property while copying all other properties from previousVersion.

If custom properties are placed before the spread operator, they become default values for 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);
1
2
3
4
5

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

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

If the argument object of the spread operator contains a getter function get, that function will be executed.

// Does not throw an error because the x property is only defined, not executed
let aWithXGetter = {
  ...a,
  get x() {
    throw new Error('not throw yet');
  }
};

// Throws an error because the x property is executed
let runtimeError = {
  ...a,
  ...{
    get x() {
      throw new Error('throw now');
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Optional Chaining Operator

In practice, reading an internal property of an object often requires checking whether the object exists. For example, to read message.body.user.firstName, the safe approach is to write it like this.

const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';
1
2
3
4

Or use the ternary operator ?: to check whether an object exists.

const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined
1
2

This layer-by-layer checking is very cumbersome, so ES2020 (opens new window) introduced the "optional chaining operator" ?. to simplify the above.

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

The code above uses the ?. operator to check directly during chain calls whether the left-side object is null or undefined. If so, evaluation stops and undefined is returned.

The optional chaining operator has three forms of usage.

  • obj?.prop // object property
  • obj?.[expr] // same as above
  • func?.(...args) // function or method invocation

Below is an example that checks whether an object method exists and calls it immediately if it does.

iterator.return?.()
1

In the code above, if iterator.return is defined, it will be called; otherwise, undefined is returned directly.

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

if (myForm.checkValidity?.() === false) {
  // form validation failed
  return;
}
1
2
3
4

In the code above, older browsers may not have the checkValidity method on forms. The ?. operator returns undefined, and the conditional becomes undefined === false, so the subsequent code is skipped.

Below are common usage forms of this operator and their equivalent forms without the operator.

a?.b
// equivalent to
a == null ? undefined : a.b

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

a?.b()
// equivalent to
a == null ? undefined : a.b()

a?.()
// equivalent to
a == null ? undefined : a()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

In the code above, pay special attention to the last two forms. If a?.b() is used and a.b is not a function and cannot be called, a?.b() will throw an error. The same applies to a?.(): if a is not null or undefined but also not a function, a?.() will throw an error.

There are several points to note when using this operator.

(1) Short-circuit mechanism

a?.[++x]
// equivalent to
a == null ? undefined : a[++x]
1
2
3

In the code above, if a is undefined or null, x will not be incremented. In other words, once the optional chaining operator evaluates to true, the right-side expression is not evaluated.

(2) The delete operator

delete a?.b
// equivalent to
a == null ? undefined : delete a.b
1
2
3

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

(3) Effect of parentheses

If the property chain has parentheses, the optional chaining operator has no effect outside the parentheses — only inside.

(a?.b).c
// equivalent to
(a == null ? undefined : a.b).c
1
2
3

In the code above, ?. has no effect outside the parentheses. Regardless of whether object a exists, .c after the parentheses is always executed.

Generally, when using ?., parentheses should not be used.

(4) Error scenarios

The following usages are forbidden and will throw errors.

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

// Template string on the right side of optional chaining
a?.`{b}`
a?.b`{c}`

// super on the left side of optional chaining
super?.()
super?.foo

// Optional chaining used on the left side of assignment
a?.b = c
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(5) Right side must not be a decimal number

To maintain backward compatibility, foo?.3:0 is parsed as foo ? .3 : 0. Therefore, if ?. is immediately followed by a decimal digit, ?. is no longer treated as a complete operator. Instead, it is processed as a ternary operator — the decimal point belongs to the subsequent decimal number.

# Nullish Coalescing Operator

When reading object properties, if a property's value is null or undefined, you sometimes need to specify a default value. The common approach is to use the || operator.

const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
1
2
3

The three lines of code above all use || to specify default values, but this is incorrect. The developer's intent is for the default value to take effect only when the property value is null or undefined. However, when the property value is an empty string, false, or 0, the default value also takes effect.

To avoid this, ES2020 (opens new window) introduced the nullish coalescing operator ??. It behaves like ||, but only returns the right-side value when the left-side value is null or undefined.

const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
1
2
3

In the code above, the default value only takes effect when the property value is null or undefined.

One purpose of this operator is to be used in conjunction with the optional chaining operator ?. to set default values for null or undefined.

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

In the code above, if response.settings is null or undefined, the default value 300 is returned.

This operator is well-suited for checking whether function parameters have been assigned values.

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

The code above checks whether the enabled property of the props parameter has been assigned. This is equivalent to the following.

function Component(props) {
  const {
    enabled: enable = true,
  } = props;
  // …
}
1
2
3
4
5
6

?? has an operator precedence concern regarding its relationship with && and ||. The current rule is that when multiple logical operators are used together, parentheses must be used to indicate precedence; otherwise, an error is thrown.

// Error
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
1
2
3
4
5

All four expressions above throw errors. Parentheses must be added to indicate precedence.

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

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

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

(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
1
2
3
4
5
6
7
8
9
10
11
Edit (opens new window)
#ES6
Last Updated: 2026/03/21, 12:14:36
Array Extensions
对象的新增方法

← Array Extensions 对象的新增方法→

Recent Updates
01
How I Discovered Disposable Email — A True Story
06-12
02
Animations in Grid Layout
09-15
03
Renaming a Git Branch
08-11
More Articles >
Theme by VDone | Copyright © 2026-2026 Nikolay Tuzov | MIT License | Telegram
  • Auto
  • Light Mode
  • Dark Mode
  • Reading Mode