Extension of the ES6 object


May 08, 2021 13:00 ES6


Table of contents


1. The concise notation of the property

ES6 allows variables and functions to be written directly in braces as properties and methods of objects. This kind of writing is more concise.

  1. const foo = 'bar';
  2. const baz = {foo};
  3. baz // {foo: "bar"}
  4. // 等同于
  5. const baz = {foo: foo};

In the code above, the variable foo is written directly in braces. A t this point, the property name is the variable name, and the property value is the variable value. Here's another example.

  1. function f(x, y) {
  2. return {x, y};
  3. }
  4. // 等同于
  5. function f(x, y) {
  6. return {x: x, y: y};
  7. }
  8. f(1, 2) // Object {x: 1, y: 2}

In addition to property short writing, methods can also be short.

  1. const o = {
  2. method() {
  3. return "Hello!";
  4. }
  5. };
  6. // 等同于
  7. const o = {
  8. method: function() {
  9. return "Hello!";
  10. }
  11. };

Here is a practical example.

  1. let birth = '2000/01/01';
  2. const Person = {
  3. name: '张三',
  4. //等同于birth: birth
  5. birth,
  6. // 等同于hello: function ()...
  7. hello() { console.log('我的名字是', this.name); }
  8. };

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

  1. function getPoint() {
  2. const x = 1;
  3. const y = 10;
  4. return {x, y};
  5. }
  6. getPoint()
  7. // {x:1, y:10}

The CommonJS module outputs a set of variables, which is a good fit for simple writing.

  1. let ms = {};
  2. function getItem (key) {
  3. return key in ms ? ms[key] : null;
  4. }
  5. function setItem (key, value) {
  6. ms[key] = value;
  7. }
  8. function clear () {
  9. ms = {};
  10. }
  11. module.exports = { getItem, setItem, clear };
  12. // 等同于
  13. module.exports = {
  14. getItem: getItem,
  15. setItem: setItem,
  16. clear: clear
  17. };

The 赋值器 evaluator (setter) 取值器 (getter) are in fact written in this way.

  1. const cart = {
  2. _wheels: 4,
  3. get wheels () {
  4. return this._wheels;
  5. },
  6. set wheels (value) {
  7. if (value < this._wheels) {
  8. throw new Error('数值太小了!');
  9. }
  10. this._wheels = value;
  11. }
  12. }

Simple writing is also useful when printing objects.

  1. let user = {
  2. name: 'test'
  3. };
  4. let foo = {
  5. bar: 'baz'
  6. };
  7. console.log(user, foo)
  8. // {name: "test"} {bar: "baz"}
  9. console.log({user, foo})
  10. // {user: {name: "test"}, foo: {bar: "baz"}}

In the code above, console .log two sets of key value pairs when you output both user and foo objects directly, which can be confusing. Put them in braces to output, it becomes a simple notation of the object, each set of key values to the front will print the object name, so that it is more clear.

Note that short-write object methods cannot be used as constructors and report errors.

  1. const obj = {
  2. f() {
  3. this.foo = 'bar';
  4. }
  5. };
  6. new obj.f() // 报错

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

2. Property name expression

JavaScript defines the properties of an object in two ways.

  1. // 方法一
  2. obj.foo = true;
  3. // 方法二
  4. obj['a' + 'bc'] = 123;

The method of the above code is to use the identifier directly as the property name, and method two is to use the expression as the property name, at which point the expression is placed in square brackets.

However, if you define an object literally (using braces), you can only define properties using method one (identifier) in ES5.

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

ES6 allows method two (expressions) to be used as the property name of an object when defining objects literally, i.e. to place expressions in square brackets.

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

Here's another example.

  1. let lastWord = 'last word';
  2. const a = {
  3. 'first word': 'hello',
  4. [lastWord]: 'world'
  5. };
  6. a['first word'] // "hello"
  7. a[lastWord] // "world"
  8. a['last word'] // "world"

Expressions can also be used to define method names.

  1. let obj = {
  2. ['h' + 'ello']() {
  3. return 'hi';
  4. }
  5. };
  6. obj.hello() // hi

Note that property name expressions, which cannot be used in the same time as concise notation, report errors.

  1. // 报错
  2. const foo = 'bar';
  3. const bar = 'abc';
  4. const baz = { [foo] };
  5. // 正确
  6. const foo = 'bar';
  7. const baz = { [foo]: 'abc'};

Note that property name expressions, if they are an object, automatically convert the object to a string by default, which is especially careful.

  1. const keyA = {a: 1};
  2. const keyB = {b: 2};
  3. const myObject = {
  4. [keyA]: 'valueA',
  5. [keyB]: 'valueB'
  6. };
  7. myObject // Object {[object Object]: "valueB"}

In the code above, both the keyA and the keyB get the object Object, so the keyB will overwrite the keyA, and myObject ends up with only one object Object property.

3. The name property of the method

The name name the function, which returns 函数名 The object method is also a function, so there is also a name property.

  1. const person = {
  2. sayName() {
  3. console.log('hello!');
  4. },
  5. };
  6. person.sayName.name // "sayName"

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

If the object's method uses a value-taking function (getter) and a memory function (setter), the name property is not above the method, but above the get and set properties of the method's property, with the return value before the method name plus get and set.

  1. const obj = {
  2. get foo() {},
  3. set foo(x) {}
  4. };
  5. obj.foo.name
  6. // TypeError: Cannot read property 'name' of undefined
  7. const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
  8. descriptor.get.name // "get foo"
  9. 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, and the function created by the Function constructor, and the name property returns anonymous.

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

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

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

In the code above, the Symbol value for key1 is described, key2 is not.

4. The enumerability and traversal of the property

Enumerable

Each property of an object has 描述对象 (Descriptor) that controls the behavior of the property. Object.getOwnPropertyDescriptor gets the description object for the property.

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

The enumerable property that describes an object, called enumerability, means that some operations ignore the current property if the property is false.

Currently, there are four operations that ignore properties that enumerable are false.

  • for... in loop: Enumerable properties that traverse only the object itself and inherit.
  • Object.keys(): Returns the key name of all enumerated properties of the object itself.
  • JSON.stringify(): Only enumerated properties of the object itself are serialized.
  • Object.assign(): Ignores the properties of enumerable false and copies only the enumerated properties of the object itself.

Of these four operations, the first three are ES5 and the last Object.assign() is new to ES6. O f these, only for... i n returns inherited properties, and the other three methods ignore inherited properties and deal only with the properties of the object itself. I n fact, “可枚举” the concept of "enumerable" was to allow certain properties to circumvent for... i n operation, otherwise all internal properties and methods will be traversed. F or example, the toString method of the object prototype, as well as the length property of “可枚举性” to avoid being for... in traversal to.

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

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

In addition, ES6 states that all Class prototype methods are non-enumeralable.

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

In general, the introduction of inherited properties into an operation complicates the problem, and most of the time we only care about the properties of the object itself. S o try not to use for... in loop, instead of Object.keys().

The traversal of the property

ES6 has five 5 种 the properties of an object.

(1)for... in

for... The in loop traverses the object itself and inherits enumerable properties (without symbol properties).

(2)Object.keys(obj)

Object.keys returns an array that includes the key names of all enumerated properties (without symbol properties) of the object itself (without inheritance).

(3)Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames returns an array that contains the key names of all properties of the object itself, excluding the Symbol property but including non-enumerated properties.

(4)Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols returns an array that contains the key names of all Symbol properties of the object itself.

(5)Reflect.ownKeys(obj)

Reflect.ownKeys returns an array that contains all key names of the object itself (without inheritance), whether the key name is Symbol or string, or whether or not it can be enumeration.

The key names of the objects are traversed 键名 5 methods, all of which follow the order rules for traversal of the same properties.

  • First traverse all the numeric keys, in ascending order of values.
  • Next, traverse all string keys, in ascending order of join time.
  • Finally, traverse all symbol keys, in ascending order of join time.

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

In the above code, the Reflect.ownKeys method returns an array that contains all the properties of the parameter object. The order of properties for this array is like this, starting with numeric properties 2 and 10, followed by string properties b and a, and finally symbol properties.

5. Super keyword

We know that this keyword always points to the current object 当前对象 function is located, and ES6 has added another super to the prototype object of 原型对象

  1. const proto = {
  2. foo: 'hello'
  3. };
  4. const obj = {
  5. foo: 'world',
  6. find() {
  7. return super.foo;
  8. }
  9. };
  10. Object.setPrototypeOf(obj, proto);
  11. obj.find() // "hello"

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

Note that when the super keyword represents a prototype object, it can only be used in the object's method, and errors are reported elsewhere.

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

All three of the above supers are used in error because for the JavaScript engine, supers are not used in the object's methods. T he first type of writing is that super is used in properties, and the second and third is that super is used in a function and then assigned to the foo property. Currently, only the short case of the object method allows the JavaScript engine to confirm that the object's method is defined.

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

  1. const proto = {
  2. x: 'hello',
  3. foo() {
  4. console.log(this.x);
  5. },
  6. };
  7. const obj = {
  8. x: 'world',
  9. foo() {
  10. super.foo();
  11. }
  12. }
  13. Object.setPrototypeOf(obj, proto);
  14. obj.foo() // "world"

In the code above, 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.

6. The extended operator of the object

Extension operators (... ) ES2018 introduces this operator into the object.

Deconstruct the assignment

The 解构赋值 of an object is used to extract 取值 value from an object, which is equivalent to (enumerable) target object itself, but which have not yet been read, to the specified object. All keys and their values are copied to the new object.

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

In the above code, the variable z is the object where the deconstructed assignment is located. It gets all the unread keys (a and b) to the right of the equal sign and copies them along with the values.

Because deconstruction assignments require an object to the right of the equal sign, if the right side of the equal sign is undefined or null, errors are reported because they cannot be converted to objects.

  1. let { ...z } = null; // 运行时错误
  2. let { ...z } = undefined; // 运行时错误

Deconstructing the assignment must be the last argument, otherwise an error will be reported.

  1. let { ...x, y, z } = someObject; // 句法错误
  2. let { x, ...y, ...z } = someObject; // 句法错误

In the above code, deconstructing the assignment is not the last argument, so it will be misaltered.

Note that 解构赋值 assignment is 复合类型 浅拷贝 shallow copy, i.e. if the value of a 引用 key is the value of the compound type (array, object, function), then the deconstructed assignment copy is a reference to that value, not a copy of that value.

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

In the code above, x is the object where the assignment is deconstructed, copying the a property of the object obj. The a property refers to an object, and modifying the value of that object affects deconstructing the assignment's reference to it.

In addition, the deconstruction assignment of the extension operator cannot replicate properties inherited from the prototype object.

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

In the code above, object o3 copies o2, but only o2's own properties, not its prototype object o1.

Here's another example.

  1. const o = Object.create({ x: 1, y: 2 });
  2. o.z = 3;
  3. let { x, ...newObj } = o;
  4. let { y, z } = newObj;
  5. x // 1
  6. y // undefined
  7. z // 3

In the above code, variable x is a simple deconstruction assignment, so you can read the properties inherited by the object o, variables y and z are deconstruction assignments of the extended operator, only the properties of the object o itself can be read, so the variable z can be assigned successfully, the variable y can not get a value. ES6 states that in a variable declaration statement, if a deconstructed assignment is used, the extension operator must be followed by a variable name, not a deconstructed assignment expression, so the above code introduces the intermediate variable newObj, which would be wrong if written below.

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

One use of deconstructing assignments is to extend the parameters of a function and introduce other operations.

  1. function baseFunction({ a, b }) {
  2. // ...
  3. }
  4. function wrapperFunction({ x, y, ...restConfig }) {
  5. // 使用 x 和 y 参数进行操作
  6. // 其余参数传给原始函数
  7. return baseFunction(restConfig);
  8. }

In the above code, the original function baseFunction accepts a and b as arguments, and the function wrapperFunction is extended from baseFunction to accept extra arguments and preserves the behavior of the original function.

The extension operator

The 扩展运算符 ( ... All traversable properties 可遍历 objects are copied into the current object.

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

Because arrays are special objects, the extended operators of objects can also be used for arrays.

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

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

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

If the extension operator is not followed by an object, it is automatically converted to an object.

  1. // 等同于 {...Object(1)}
  2. {...1} // {}

In the above code, the extension operator is followed by the integer 1, which automatically changes to the value of the wrapper object Number{1}. Because the object does not have its own properties, an empty object is returned.

The following examples are all similar.

  1. // 等同于 {...Object(true)}
  2. {...true} // {}
  3. // 等同于 {...Object(undefined)}
  4. {...undefined} // {}
  5. // 等同于 {...Object(null)}
  6. {...null} // {}

However, if the extension operator is followed by a string, it automatically turns into an array-like object, so it does not return an empty object.

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

The extension operator of an object is equivalent to Object.assign() method.

  1. let aClone = { ...a };
  2. // 等同于
  3. let aClone = Object.assign({}, a);

The above example simply copies the properties of the object instance, and if you want to clone an object completely and copy the properties of the object prototype, you can do so in the following way.

  1. // 写法一
  2. const clone1 = {
  3. __proto__: Object.getPrototypeOf(obj),
  4. ...obj
  5. };
  6. // 写法二
  7. const clone2 = Object.assign(
  8. Object.create(Object.getPrototypeOf(obj)),
  9. obj
  10. );
  11. // 写法三
  12. const clone3 = Object.create(
  13. Object.getPrototypeOf(obj),
  14. Object.getOwnPropertyDescriptors(obj)
  15. )

In the above code, the proto property of Write One is not necessarily deployed in a non-browser environment, so it is recommended to use Write Two and Write Three.

Extension operators can be used to merge two objects.

  1. let ab = { ...a, ...b };
  2. // 等同于
  3. let ab = Object.assign({}, a, b);

If a user-defined property is placed behind an extension operator, the property with the same name inside the extension operator is overwritten.

  1. let aWithOverrides = { ...a, x: 1, y: 2 };
  2. // 等同于
  3. let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
  4. // 等同于
  5. let x = 1, y = 2, aWithOverrides = { ...a, x, y };
  6. // 等同于
  7. let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });

In the code above, the x and y properties of the a object are overwritten when copied to the new object.

This is convenient for modifying the properties of an existing part of an object.

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

In the code above, the newVersion object customizes the name property, and all other properties are copied from the previousVersion object.

If you put a custom property in front of the extension operator, it becomes the default property value for setting the new object.

  1. let aWithDefaults = { x: 1, y: 2, ...a };
  2. // 等同于
  3. let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
  4. // 等同于
  5. let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);

Like an array's extension operator, an object's extended operator can be followed by an expression.

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

Among the argument objects of the extended operator, if there is a value-taking function get, the function is executed.

  1. let a = {
  2. get x() {
  3. throw new Error('not throw yet');
  4. }
  5. }
  6. let aWithXGetter = { ...a }; // 报错

In the example above, the value-taking function get is executed automatically when the a object is extended, resulting in an error.

7. Chain judgment operator

In programming practice, if you read a property 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 like this.

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

Or use the thyme operator ?: to determine the existence of an object.

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

Such layering is cumbersome, so ES2020 introduces the "chain judgment operator"? to simplify the above writing.

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

The above code uses ?. O perator, which determines whether the object on the left is null or undefined when called directly in a chain. If so, instead of going down, return undefined.

Chain judgment operators are used in three ways.

  • Obj?. prop // object properties
  • Obj?. (expr) // Io above
  • func?. ( ... args) // Calls to functions or object methods

The following is an example of determining whether an object method exists and, if so, executes immediately.

  1. iterator.return?.()

In the code above, iterator.return calls the method, otherwise it returns directly to the undefined.

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

  1. if (myForm.checkValidity?.() === false) {
  2. // 表单校验失败
  3. return;
  4. }

In the code above, the old browser form may not checkValidity method, at this point ? The operator returns the undefined, and the judgment statement becomes undefined, so the following code is skipped.

Here's how this operator is commonly used, and the equivalent form when it's not used.

  1. a?.b
  2. // 等同于
  3. a == null ? undefined : a.b
  4. a?.[x]
  5. // 等同于
  6. a == null ? undefined : a[x]
  7. a?.b()
  8. // 等同于
  9. a == null ? undefined : a.b()
  10. a?.()
  11. // 等同于
  12. a == null ? undefined : a()

In the above code, pay special attention to the 3d forms, if a? b () The a-.b is not a function and cannot be called, so a?. b () Errors are reported. A ?. ( ) The same is true, if a is not null or undefined, but it is not a function, then a? () Errors will be reported.

With this operator, there are a few notations.

(1) Short-circuit mechanism

  1. a?.[++x]
  2. // 等同于
  3. a == null ? undefined : a[++x]

In the above code, if a is undefined or null, x does not increment. That is, once the chain judgment operator is true, the expression on the right is no longer evaluated.

(2) Delete operator

  1. delete a?.b
  2. // 等同于
  3. a == null ? undefined : delete a.b

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

(3) The effect of parentheses

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

  1. (a?.b).c
  2. // 等价于
  3. (a == null ? undefined : a.b).c

In the code above, ? There is no effect on the outside of the parenthesis, regardless of whether the a object exists, and the .c after the parenthesis is always executed.

Generally speaking, the use of ?. In the event of an operator, parentheses should not be used.

(4) The occasion of error reporting

The following writing is prohibited and will report errors.

  1. // 构造函数
  2. new a?.()
  3. new a?.b()
  4. // 链判断运算符的右侧有模板字符串
  5. a?.`{b}`
  6. a?.b`{c}`
  7. // 链判断运算符的左侧是 super
  8. super?.()
  9. super?.foo
  10. // 链运算符用于赋值运算符左侧
  11. a?.b = c

(5) The right side must not be a hedding value

In order to ensure compatibility with previous code, allow foo?. 3 :0 is resolved to foo ? .3 : 0 , so it is stipulated that if . F ollowed by a hedding number, then ? It is no longer considered a complete operator, but is processed as a thynon operator, that is, the decimal point belongs to the decimal number that follows, forming a decimal number.

8. Null judgment operator

When you read object properties, if the value of a property is null or undefined, you sometimes need to specify a default value for them. A common practice is to || The operator specifies the default value.

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

All three lines of code above are || T he operator specifies the default value, but this is wrong. The developer's original idea was that the default value would take effect as long as the value of the property was null or undefined, but the default value would also take effect if the value of the property was an empty string or false or 0.

To avoid this, ES2020 introduces a new Null judgment operator. 。 I t behaves like || , but only if the value on the left side of the operator is null or undefined.

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

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

One of the purposes of this operator is to judge the operator with the chain. Use together to set default values for null or undefined values.

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

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

This operator is ideal for determining whether function parameters are assigned.

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

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

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

?? T here is an computational priority issue, which is related to the || t he priority of the high and low. The rule now is that if multiple logical operators are used together, the priority must be indicated in parentheses or an error will be reported.

  1. // 报错
  2. lhs && middle ?? rhs
  3. lhs ?? middle && rhs
  4. lhs || middle ?? rhs
  5. lhs ?? middle || rhs

All four expressions above are error-positive and must be preceded by parentheses indicating priority.

  1. (lhs && middle) ?? rhs;
  2. lhs && (middle ?? rhs);
  3. (lhs ?? middle) && rhs;
  4. lhs ?? (middle && rhs);
  5. (lhs || middle) ?? rhs;
  6. lhs || (middle ?? rhs);
  7. (lhs ?? middle) || rhs;
  8. lhs ?? (middle || rhs);