Variable deconstruction assignment

Destructuring and Assignment of Array

Basic usage

ES6 allows extracting values ​​from arrays and objects and assigning values ​​to variables according to certain patterns, which is called Destructuring.

Previously, when assigning values ​​to variables, you could only specify the value directly.

let a = 1;
let b = 2;
let c = 3;

ES6 allows it to be written as follows.

let [a, b, c] = [1, 2, 3];

The above code indicates that you can extract values ​​from the array and assign values ​​to variables according to the corresponding positions.

Essentially, this type of writing belongs to "pattern matching", as long as the patterns on both sides of the equal sign are the same, the variable on the left will be assigned the corresponding value. Here are some examples of destructuring using nested arrays.

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo; // 1
bar; // 2
baz; // 3

let [, , third] = ["foo", "bar", "baz"];
third; // "baz"

let [x, , y] = [1, 2, 3];
x; // 1
y; // 3

let [head, ...tail] = [1, 2, 3, 4];
head; // 1
tail; // [2, 3, 4]

let [x, y, ...z] = ["a"];
x; // "a"
y; // undefined
z; // []

If the destructuring is unsuccessful, the value of the variable is equal to undefined.

let [foo] = [];
let [bar, foo] = [1];

In the above two cases, the destructuring is unsuccessful, and the value of foo will be equal to undefined.

Another case is incomplete deconstruction, that is, the pattern on the left side of the equal sign only matches a part of the array on the right side of the equal sign. In this case, deconstruction can still be successful.

let [x, y] = [1, 2, 3];
x; // 1
y; // 2

let [a, [b], d] = [1, [2, 3], 4];
a; // 1
b; // 2
d; // 4

The above two examples are incomplete deconstruction, but they can be successful.

If the right side of the equal sign is not an array (or strictly speaking, not a traversable structure, see Chapter "Iterator"), then an error will be reported.

// report an error
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

The above statement will report an error, because the value on the right side of the equal sign either does not have the Iterator interface (the first five expressions) after being converted to an object, or it does not have the Iterator interface (the last expression) itself.

For the Set structure, you can also use the destructuring assignment of the array.

let [x, y, z] = new Set(["a", "b", "c"]);
x; // "a"

In fact, as long as a certain data structure has an Iterator interface, it can be deconstructed and assigned in the form of an array.

function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

let [first, second, third, fourth, fifth, sixth] = fibs();
sixth; // 5

In the above code, fibs is a Generator function (see the chapter "Generator Function"), which natively has an Iterator interface. Destructuring assignment will in turn obtain values ​​from this interface.

Defaults

Destructuring assignment allows default values ​​to be specified.

let [foo = true] = [];
foo; // true

let [x, y = "b"] = ["a"]; // x='a', y='b'
let [x, y = "b"] = ["a", undefined]; // x='a', y='b'

Note that ES6 uses the strict equality operator (===) internally to determine whether a position has a value. Therefore, the default value will only take effect when an array member is strictly equal to undefined.

let [x = 1] = [undefined];
x; // 1

let [x = 1] = [null];
x; // null

In the above code, if an array member is null, the default value will not take effect, because null is not strictly equal to undefined.

If the default value is an expression, then the expression is evaluated lazily, that is, it will be evaluated only when it is used.

function f() {
  console.log("aaa");
}

let [x = f()] = [1];

In the above code, because x can get a value, the function f will not be executed at all. The above code is actually equivalent to the following code.

let x;
if ([1][0] === undefined) {
  x = f();
} else {
  x = [1][0];
}

The default value can refer to other variables assigned by destructuring, but the variable must be declared.

let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined

The last expression above will report an error because when x uses y as the default value, y has not yet been declared.

Deconstruction and assignment of objects

Introduction

Destructuring can be used not only for arrays, but also for objects.

let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo; // "aaa"
bar; // "bbb"

There is an important difference between object destructuring and arrays. The elements of the array are arranged in order, and the value of the variable is determined by its position; and the properties of the object have no order, and the variable must have the same name as the property in order to get the correct value.

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo; // "aaa"
bar; // "bbb"

let { baz } = { foo: "aaa", bar: "bbb" };
baz; // undefined

In the first example of the above code, the order of the two variables on the left side of the equal sign is inconsistent with the order of the two attributes with the same name on the right side of the equal sign, but it has no effect on the value. The variable in the second example does not have a corresponding attribute with the same name, resulting in not getting a value, and finally being equal to undefined.

If the destructuring fails, the value of the variable is equal to undefined.

let { foo } = { bar: "baz" };
foo; // undefined

In the above code, the object on the right side of the equal sign does not have a foo property, so the variable foo cannot get a value, so it is equal to undefined.

Destructuring and assigning an object can easily assign a method of an existing object to a variable.

// Example 1
let { log, sin, cos } = Math;

// Example 2
const { log } = console;
log("hello"); // hello

Example 1 of the above code assigns the logarithm, sine, and cosine methods of the Math object to the corresponding variables, which is much more convenient to use. Example 2: Assign console.log to the log variable.

If the variable name is inconsistent with the attribute name, it must be written as follows.

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz; // "aaa"

let obj = { first: "hello", last: "world" };
let { first: f, last: l } = obj;
f; //'hello'
l; //'world'

This actually shows that the destructuring assignment of an object is a shorthand for the following form (see the chapter "Expansion of Objects").

let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };

In other words, the internal mechanism of object deconstruction assignment is to find the attribute with the same name first, and then assign it to the corresponding variable. The latter is actually assigned, not the former.

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz; // "aaa"
foo; // error: foo is not defined

In the above code, foo is the matching pattern, and baz is the variable. It is the variable baz that is actually assigned, not the pattern foo.

Like arrays, destructuring can also be used for objects with nested structures.

let obj = {
  p: ["Hello", { y: "World" }],
};

let {
  p: [x, { y }],
} = obj;
x; // "Hello"
y; // "World"

Note that at this time, p is a pattern, not a variable, so it will not be assigned. If p is also assigned as a variable, it can be written as follows.

let obj = {
  p: ["Hello", { y: "World" }],
};

let {
  p,
  p: [x, { y }],
} = obj;
x; // "Hello"
y; // "World"
p; // ["Hello", {y: "World"}]

Here is another example.

const node = {
  loc: {
    start: {
      line: 1,
      column: 5,
    },
  },
};

let {
  loc,
  loc: { start },
  loc: {
    start: { line },
  },
} = node;
line; // 1
loc; // Object {start: Object}
start; // Object {line: 1, column: 5}

The above code has three deconstruction assignments, which are deconstruction assignments to the three attributes of loc, start, and line. Note that in the last destructuring assignment to the line attribute, only line is a variable, and loc and start are both patterns, not variables.

The following is an example of nested assignment.

let obj = {};
let arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

obj; // {prop:123}
arr; // [true]

If the deconstruction mode is a nested object, and the parent attribute of the child object does not exist, an error will be reported.

// report an error
let {
  foo: { bar },
} = { baz: "baz" };

In the above code, the foo property of the object on the left of the equal sign corresponds to a child object. The bar property of this sub-object will report an error when it is deconstructed. The reason is very simple, because foo is now equal to undefined, and then an error will be reported if the sub-property is taken.

Note that the destructuring assignment of objects can take inherited properties.

const obj1 = {};
const obj2 = { foo: "bar" };
Object.setPrototypeOf(obj1, obj2);

const { foo } = obj1;
foo; // "bar"

In the above code, the prototype object of the object obj1 is obj2. The foo attribute is not an attribute of obj1 itself, but an attribute inherited from obj2, which can be obtained by destructuring assignment.

Defaults

Deconstruction of objects can also specify default values.

var { x = 3 } = {};
x; // 3

var { x, y = 5 } = { x: 1 };
x; // 1
y; // 5

var { x: y = 3 } = {};
y; // 3

var { x: y = 3 } = { x: 5 };
y; // 5

var { message: msg = "Something went wrong" } = {};
msg; // "Something went wrong"

The condition for the default value to take effect is that the property value of the object is strictly equal to undefined.

var { x = 3 } = { x: undefined };
x; // 3

var { x = 3 } = { x: null };
x; // null

In the above code, the property x is equal to null, because null and undefined are not strictly equal, so it is a valid assignment, causing the default value of 3 to not take effect.

be careful

(1) If you want to use a declared variable for destructuring assignment, you must be very careful.

// wrong way
let x;
{x} = {x: 1};
// SyntaxError: syntax error

The writing of the above code will report an error, because the JavaScript engine will interpret {x} as a code block, resulting in a grammatical error. This problem can only be solved by not putting the braces at the beginning of the line and avoiding JavaScript interpreting them as code blocks.

// correct writing
let x;
({x} = (x: 1});

The above code puts the entire destructuring assignment statement inside a parenthesis, and it can be executed correctly. For the relationship between parentheses and destructuring assignment, see below.

(2) Destructuring assignment allows no variable names to be placed in the pattern on the left side of the equal sign. Therefore, very weird assignment expressions can be written.

({} = [true, false]);
({} = "abc");
({} = []);

Although the above expression is meaningless, the syntax is legal and can be executed.

(3) Since the nature of the array is a special object, the object property of the array can be deconstructed.

let arr = [1, 2, 3];
let { 0: first, [arr.length - 1]: last } = arr;
first; // 1
last; // 3

The above code deconstructs the array of objects. The 0 key of the array arr corresponds to the value 1, and [arr.length-1] is the 2 key, and the corresponding value is 3. The wording of square brackets belongs to "attribute name expressions" (see the chapter "Expanding Objects").

String destructuring assignment

Strings can also be deconstructed and assigned. This is because at this time, the string is converted into an array-like object.

const [a, b, c, d, e] = "hello";
a; // "h"
b; // "e"
c; // "l"
d; // "l"
e; // "o"

Objects like arrays have a length property, so this property can also be deconstructed and assigned.

let { length: len } = "hello";
len; // 5

Destructuring and Assignment of Numerical and Boolean Values

When destructuring assignment, if the right side of the equal sign is a numeric value and a boolean value, it will be converted to an object first.

let { toString: s } = 123;
s === Number.prototype.toString; // true

let { toString: s } = true;
s === Boolean.prototype.toString; // true

In the above code, the packaging objects of both numeric and boolean values ​​have the toString attribute, so the variable s can take the value.

The rule of destructuring assignment is that as long as the value on the right side of the equal sign is not an object or an array, it is converted to an object first. Since undefined and null cannot be converted into objects, destructuring and assigning them will cause errors.

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

Deconstruction and assignment of function parameters

The parameters of the function can also be assigned using destructuring.

function add([x, y]) {
  return x + y;
}

add([1, 2]); // 3

In the above code, the parameter of the function add is an array on the surface, but at the moment when the parameter is passed in, the array parameter is deconstructed into the variables x and y. For the code inside the function, the parameters they can feel are x and y.

Here is another example.

[
  [1, 2],
  [3, 4],
].map(([a, b]) => a + b);
// [3, 7]

The destructuring of function parameters can also use default values.

function move({ x = 0, y = 0 } = {}) {
  return [x, y];
}

move({ x: 3, y: 8 }); // [3, 8]
move({ x: 3 }); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

In the above code, the parameter of the function move is an object. By destructuring this object, the values ​​of the variables x and y are obtained. If the destructuring fails, x and y are equal to the default values.

Note that the following writing method will get different results.

function move({ x, y } = { x: 0, y: 0 }) {
  return [x, y];
}

move({ x: 3, y: 8 }); // [3, 8]
move({ x: 3 }); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

The above code is to specify default values ​​for the parameters of the function move, instead of specifying default values ​​for the variables x and y, so it will get a different result from the previous way of writing.

undefined will trigger the default value of the function parameter.

[1, undefined, 3].map((x = "yes") => x);
// [1,'yes', 3]

Parenthesis problem

Although destructuring assignment is very convenient, it is not easy to parse. For the compiler, there is no way to know whether a formula is a pattern or an expression from the beginning. It must be resolved (or cannot be resolved) to know the equal sign.

The question that arises is what to do if there are parentheses in the pattern. The rule of ES6 is that parentheses should not be used as long as there is an ambiguity that may lead to deconstruction.

However, this rule is actually not so easy to discern and it is quite troublesome to deal with. Therefore, it is recommended that you do not put parentheses in the pattern whenever possible.

When parentheses cannot be used

The following three types of destructuring assignments must not use parentheses.

(1) Variable declaration statement

// All errors
let [(a)] = [1];

let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};

let {o: ({ p: p })} = {o: {p: 2} };

The above 6 statements will all report errors, because they are all variable declaration statements, and parentheses cannot be used in the pattern.

(2) Function parameters

Function parameters are also variable declarations, so they cannot be enclosed in parentheses.

// report an error
function f([(z)]) {return z;}
// report an error
function f([z,(x)]) {return x;}

(3) The mode of assignment statement

// All errors
({ p: a }) = {p: 42 };
([a]) = [5];

The above code puts the entire pattern in parentheses, resulting in an error.

// report an error
[({ p: a }), {x: c }] = [{}, {}];

The above code puts part of the pattern in parentheses, causing an error.

When parentheses can be used

There is only one situation where you can use parentheses: you can use parentheses in the non-pattern part of an assignment statement.

[(b)] = [3]; // correct
({ p: (d)} = ()); // correct
[(parseInt.prop)] = [3]; // correct

The above three lines of statements can be executed correctly, because firstly they are assignment statements, not declaration statements; secondly, their parentheses are not part of the pattern. In the first statement, the pattern is to take the first member of the array, which has nothing to do with parentheses; in the second statement, the pattern is p instead of d; the nature of the third and first statements Unanimous.

Purpose

There are many uses for variable destructuring assignment.

(1) Exchange the value of the variable

let x = 1;
let y = 2;

[x, y] = [y, x];

The above code exchanges the values ​​of the variables x and y, which is not only concise, but also easy to read, with very clear semantics.

(2) Return multiple values ​​from the function

Functions can only return one value. If you want to return multiple values, you can only return them in an array or object. With destructive assignment, it is very convenient to take out these values.

// return an array

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// return an object

function example() {
  return {
    foo: 1,
    bar: 2,
  };
}
let { foo, bar } = example();

(3) Definition of function parameters

Destructuring assignment can easily associate a set of parameters with variable names.

// The parameter is an ordered set of values
function f([x, y, z]) {...}
f([1, 2, 3]);

// parameter is a set of unordered values
function f({x, y, z}) {...}
f({z: 3, y: 2, x: 1});

(4) Extract JSON data

Destructuring assignment is especially useful for extracting data in JSON objects.

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309],
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

The above code can quickly extract the value of JSON data.

(5) Default value of function parameters

jQuery.ajax = function (
  url,
  {
    async = true,
    beforeSend = function () {},
    cache = true,
    complete = function () {},
    crossDomain = false,
    global = true,
    // ... more config
  } = {}
) {
  // ... do stuff
};

Specifying the default value of the parameter avoids writing var foo = config.foo ||'default foo'; inside the function body.

(6) Traverse the Map structure

Any object deployed with the Iterator interface can be traversed using for...of loop. The Map structure natively supports the Iterator interface. With the destructuring and assignment of variables, it is very convenient to get the key name and key value.

const map = new Map();
map.set("first", "hello");
map.set("second", "world");

for (let [key, value] of map) {
  console.log(key + "is" + value);
}
// first is hello
// second is world

If you only want to get the key name, or just want to get the key value, you can write it as follows.

// Get the key name
for (let [key] of map) {
  // ...
}

// Get key value
for (let [, value] of map) {
  // ...
}

(7) Specifying method of input module

When loading a module, you often need to specify which methods to enter. Destructuring assignment makes the input sentence very clear.

const { SourceMapConsumer, SourceNode } = require("source-map");