1
2

More than 1 year has passed since last update.

JavaScriptの分割代入について

Posted at

初めに

今回は配列の分割代入とオブジェクトの分割代入、そして使い方に気を付けることをまとめてみました。

Destructuring assignment

Array Destructuring

配列の分割代入は簡潔に言えば要素位置のマッチングです。

// array destructuring
let arr = ['Charlie', 'Brown'];
let [firstName, surName] = arr;
let obj = {
  firstName,
  surName
};
console.log(obj); // { firstName: 'Charlie', surName: 'Brown' }
console.log(firstName, surName); // Charlie Brown

スキップしたい要素があるとしたら、empty slotのように空ければいいです。
一部の要素だけがほしい場合は、残りをrest...で宣言しなくてもいいです。

// using empty slot to jump element
let arr = ['Charlie', 'Brown', 'Snoopy', 'Woodstock'];
let [firstName, , dog, bird] = arr;
console.log(firstName, dog, bird); // Charlie Snoopy Woodstock
// partial matching
let [name1, name2] = ['Charlie', 'Brown', 'Snoopy', 'Woodstock'];
console.log(name1, name2); // Charlie Brown

配列の分割代入は文字列、反復処理可能なオブジェクト、配列だけに使用できる。
これはイテレータと関連しているからです。

// only works with iterables(String, iterable object, array)
// String
let [a, b, c] = 'ABC';
console.log(a, b, c); // A B C
// iterable object
let [one, two, three] = new Set([1, 2, 3]);
console.log(new Set([1, 2, 3])); // Set(3) { 1, 2, 3 }
console.log(one, two, three); // 1 2 3
// iterable object
let [arr1, arr2, arr3] = new Map([[1, '1'], [2, '2'], [3, '3']]);
console.log(arr1, arr2, arr3); // [ 1, '1' ] [ 2, '2' ] [ 3, '3' ]
// iterable object([Symbol.iterator])
let obj = {
  names: ['Charlie', 'Snoopy', 'Woodstock', 'Sally'],
  *[Symbol.iterator]() {
    for (let i = 0; i < this.names.length; i++) {
      yield this.names[i];
    }
  }
}
console.log([...obj]); // [ 'Charlie', 'Snoopy', 'Woodstock', 'Sally' ]
let [character1, character2, character3, character4] = obj;
console.log(character1, character2); // Charlie Snoopy
// generator
function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
let [first, second, third, fourth] = fibs();
console.log(first, second, third, fourth); // 0 1 1 2

右手がイテレータできないならエラーが出てきます。

// error
let [item] = 1; // TypeError: 1 is not iterable
let [item] = false; // TypeError: false is not iterable
let [item] = {}; // TypeError: {} is not iterable
// note: undefined, null, NaN as well

左手がすでに宣言されている状態なら宣言不要です。そしてオブジェクトのプロパティに指定しても代入式が成立します。

// assign to anything at the left-side
let user = {};
[user.firstName, user.surName] = 'Charlie Brown'.split(' ');
console.log(user.firstName, user.surName); // Charlie Brown

Object.entries()で普通のオブジェクトからペア要素の構成した配列やMapなど反復できるオブジェクト、つまりイテレータが持っているならfor...ofが使えるようになる。

// looping obj with entries()
let user = {
  name: 'Charlie Brown',
  age: 10,
};
for (let [key, value] of Object.entries(user)) {
  console.log(`${key}: ${value}`);
}
// name: Charlie Brown
// age: 10
//
let userArr = [['name', 'Charlie Brown'], ['age', 10]];
let map = new Map();
for (let [key, value] of userArr) {
  map.set(key, value);
}
for (let [key, value] of map) {
  console.log(`${key}: ${value}`);
}
// name: Charlie Brown
// age: 10

どんな代入式においてもアサインはいつも右手からです(run-time時期の話)。分割代入もそうです。このスワップは左手の変数へのリアサインに過ぎません。

// swap variable trick
let character1 = 'Sally';
let character2 = 'Linus';
[character1, character2] = [character2, character1];
// note: assignment always start from right-side,
// so it means [character1, character2] = ['Linus', 'Sally'];
console.log(`character1: ${character1}, character2: ${character2}`);
// character1: Linus, character2: Sally

The Rest... & Default value

...は残りのすべての要素を配列にして一つの変数に入れることができます。(残りのすべてだから、真ん中要素だけに...を使うことができません。)

// rest ...
let [name1, name2, ...restName] = ['Charlie', 'Snoopy', 'Woodstock', 'Sally'];
console.log(restName); // [ 'Woodstock', 'Sally' ]
console.log(restName.length); // 2
// error
let [name1, name2, ...restName, name4] = ['Charlie', 'Snoopy', 'Woodstock', 'Sally'];
// SyntaxError: Rest element must be last element

右手に要素がない場合は、undefinedをアサインすることと同じです。...[]の配列を返すしかありません。
上の例で...で残りの要素をまとめたrestName配列内の要素を、もう一度宣言することもできる。また、restName配列をイテレータすることも。

let [item1, item2, ...rest] = ['Charlie'];
console.log(item2); // undefined
console.log(rest); // []
//
let [name3, name4] = restName;
console.log(name3, name4); // Woodstock Sally
//
for (let item of restName) {
  console.log(item);
}
// Woodstock
// Sally

右手の要素がundefinedと厳密等価だったとき、初期値が代わりの値として使われる。empty slotundefinedとして扱われるのでこの場合は初期値も作用する。

// default value
let [firstName = 'Anonymous', surName = 'Anonymous'] = ['Charlie'];
console.log(firstName, surName); // Charlie Anonymous
//
let arr = ['Charlie', 'Snoopy', 'Woodstock', 'Sally']
let [name1, name2, ...restName] = arr;
let listArr = [, , 'Woodstock', 'Sally'];
let [character1 = name1, character2 = name2, ...restCharacter] = listArr;
console.log(`Character list: ${character1}, ${character2}, and rest of them are: ${restCharacter}`);
// Character list: Charlie, Snoopy, and rest of them are: Woodstock,Sally
//
// when assignment was undefined, or empty slot
let [item1 = 'No item', item2 = 'No item'] = [1, undefined];
console.log(item1, item2); // 1 No item
let [item3 = 'No item', item4 = 'No item'] = [3,];
console.log(item3, item4); // 3 No item
//
let [item5 = 'No item'] = [null];
console.log(item5); // null

Object Destructuring

配列の分割代入と違い、オブジェクトの分割代入ではプロパティの指定から値を見つけて、新しい変数に代入することです。たとえば、
let { name1: name1, name2: name2 } = { name1: 'Charlie', name2: 'Snoopy' };は、
{sourceProperty: targetVariable}のように参照するプロパティname1から目標の値Charlieを抽出し、新しい変数name1にアサインする。
でも簡潔化のためによくそのままプロパティ名を利用することになっています。

// Object Destructuring
let { foo1, foo2 } = { name1: 'Charlie', name2: 'Snoopy' };
console.log(foo1, foo2); // undefined undefined
let { name1, name2 } = { name1: 'Charlie', name2: 'Snoopy' };
console.log(name1, name2); // Charlie Snoopy
// actually it means:
let { name1: name1, name2: name2 } = { name1: 'Charlie', name2: 'Snoopy' };
console.log(name1, name2); // Charlie Snoopy
// well, it is just like telling the same story in a different way
let name1 = 'Charlie';
let name2 = 'Snoopy';
let obj = {
  name1,
  name2
};
console.log(obj['name1'], obj['name2']); // Charlie Snoopy

プロパティを指定してから値の再代入というのは、好きのようにプロパティを置き換えたり、一部のプロパティを取り出したりすることができる。

// change order
let characterList = {
  name1: 'Charlie',
  name2: 'Snoopy',
  name3: 'Woodstock'
};
let { name3, name2 } = characterList;
console.log(name3, name2); // Woodstock Snoopy

プロパティ名ではなく使い道によって新しい変数名に作ってもオッケーです。

// {sourceProperty: targetVariable}
let characterList = {
  name1: 'Charlie',
  name2: 'Snoopy',
  name3: 'Woodstock'
};
let { name1: item1, name2: item2, name3: item3 } = characterList;
// console.log(name1); // ReferenceError: name1 is not defined
console.log(item1); // Charlie
// note: assign property value to a variable
// compare
let characterList = {
  name1: 'Charlie',
  name2: 'Snoopy',
  name3: 'Woodstock'
};
let { name1, name2, name3 } = characterList;
console.log(name1); // Charlie
//
let { name1: name1, name2: name2, name3: name3 } = characterList;
console.log(name1); // Charlie
// note: now we can understand {sourceProperty: targetVariable} means that
// we call the corresponding property like name1, and assign its value to variable name1

ネストされたプロパティでも分割代入にできます。

// nested destructuring
let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ['Cake', 'Donut'],
  extra: true
};
let {
  size,
  items,
  extra,
  size: {
    width,
    height
  },
  items: [item1, item2],
  title = 'Menu'
} = options;
console.log(size, items, extra); // { width: 100, height: 200 } [ 'Cake', 'Donut' ] true
console.log(width, height, item1, item2, title); // 100 200 Cake Donut Menu

一部のメソッドを抽出したいときも分割代入ができます。これは参考文章から取った例を少し調整したものです。最初に見たら少し戸惑うかもしれないけど(私も)、これまでconsole.log()メソッドを考えもせずずっと使ってきたけど、なるほど、オブジェクトconsoleのなかのメソッドlogを使用したわけだったんだー、と悟ったらlogのことをメソッドと呼んでもプロパティですね。
(話が長くてすみません、ただオブジェクト内のメソッドも分割代入で抽出できると言いたかっただけです。)

// extract methods from function
const { log, table } = console;
log('hello'); // hello
table({
  name1: 'Charlie',
  name2: 'Snoopy',
  name3: 'Woodstock'
});
// hello
// ┌─────────┬─────────────┐
// │ (index) │   Values    │
// ├─────────┼─────────────┤
// │  name1  │  'Charlie'  │
// │  name2  │  'Snoopy'   │
// │  name3  │ 'Woodstock' │
// └─────────┴─────────────┘

The rest pattern ...

残りのプロパティをまとめる...、使い方が配列とほぼ同じです。(オブジェクトプロパティだから{}括るのです。)
分割代入の前に変数宣言してたら、丸括弧()で代入式全体を括らなければなりません。単に{}なら別のコードブロック、つまりスコープ(scope)の問題が発生してしまうます。

// rest
let options = {
  title: 'Menu',
  width: 100,
  height: 200
};
// let { title, ...rest } = options;
// console.log(rest); // { width: 100, height: 200 }
// error
let title, width, height;
// { title, width, height } = options;
// SyntaxError: Unexpected token '='
// note: if {} has no any declaration, JS engine will assume it a code block
// this way will work
({ title, width, height } = options)
console.log(width, height); // 100 200

Default value

初期値が作用する条件は指定のプロパティの値がundefinedです。

// default value
let characterList = { name1: 'Charlie' };
let { name2 = 'Snoopy', name3 = 'Woodstock', name1 } = characterList;
console.log(name1, name2, name3); // Charlie Snoopy Woodstock
//
let option = {
  title: 'Menu'
};
let { width = console.log('width?'), title = console.log('title?') } = option;
// width?
// note: default value was assigned at run-time
console.log(title); // Menu
console.log(width); // undefined

// combine
let option = {
  title: 'Menu'
};
let { width: w = 100, height: h = 200, title } = option;
console.log(title); // Menu
console.log(w); // 100
console.log(option); // { title: 'Menu' }
// uh, it just like...
let w = 100;
let h = 200;
let option = {
  title: 'Menu',
  innerMethod(key, variable) {
    if (!option[key]) {
      return variable
    }
  }
};
console.log(option.innerMethod('width', w), option.innerMethod('width', h)); // 100 200

Default value in function parameter

関数パラメータの初期値を設置するとき、気をつけるところがあります。

// default value in function parameter
function multiply(a, b = 1) {
  return a * b
}
console.log(multiply(5)); // 5
console.log(multiply(5, 2)); // 10
// pass object to function
let options = {
  title: 'Cake menu',
  items: ['ice cream', 'chocolate']
};
function showMenu({
  title = 'Untitled',
  width = 100,
  height = 50,
  items = [] }) {
  console.log(`There is our ${title}. The size is ${width}cm * ${height}cm.`)
  console.log(`Options include ${items}.`)
}
showMenu(options);
// There is our Cake menu. The size is 100cm * 50cm.
// Options include ice cream,chocolate.
showMenu({});
// There is our Untitled. The size is 100cm * 50cm.
// Options include .
showMenu();
// TypeError: Cannot read properties of undefined
// note: because we have declare those properties in function,JS engin will assume function should accept an object, if we don't, it will throw error

この例では関数パラメータ自身の初期値はオブジェクトです。オブジェクトの中から取り出したいプロパティの初期値を設置したけれど、パラメータ全体を包むオブジェクト{}も初期値を設置しないと、引数が何も与えてない場合はパラメータ中のプロパティの初期値が作用しません。

function showMenu({
  title = 'Untitled',
  width = 100,
  height = 50,
  items = [] } = {}) {
  console.log(`There is our ${title}. The size is ${width}cm * ${height}cm.`)
  console.log(`Options include ${items}.`)
}
showMenu(options);
// There is our Cake menu. The size is 100cm * 50cm.
// Options include ice cream,chocolate.
showMenu({});
// There is our Untitled. The size is 100cm * 50cm.
// Options include.
showMenu();
// There is our Untitled.The size is 100cm * 50cm.
// Options include.
// note: now we assign those default value an default obj, so even we didn't provide any assignment, it will work

Others

配列の要素をオブジェクトに展開すると、インデックスがプロパティになり、要素がプロパティの値になります。ある意味では衝突しない順序付けられたプロパティとして用いられる。

// array destructuring => object
let arr = ['a', 'b', 'c'];
let [item1, item2, item3] = arr;
let obj = {
  item1,
  item2,
  item3
};
console.log(obj); // { item1: 'a', item2: 'b', item3: 'c' }
let [...allItems] = arr;
let obj2 = {
  allItems
};
console.log(obj2);
// { allItems: [ 'a', 'b', 'c' ] }
let obj3 = {
  ...allItems
};
console.log(obj3);
// { '0': 'a', '1': 'b', '2': 'c' }
// 
let arr = ['a', 'b', 'c'];
let { 0: firstItem, [arr.length - 1]: lastItem } = arr;
console.log(firstItem); // a
console.log(lastItem); // c

オブジェクト分割代入では右手が文字列、数値や真偽値の場合は一度オブジェクトにしてから対応のプロパティを抽出するのです。

// string destructuring
let [a, b, c, d, e] = 'Apple';
console.log(a, b, c, d, e); // A p p l e
// length property
let { length: strLength } = 'Apple';
console.log(strLength); // 5
let { length: arrLength } = ['a', 'b'];
console.log(arrLength); // 2
let { size: mapLength } = new Map([
  ['a', 1], ['b', 2],
  ['c', 3], ['d', 4]
])
console.log(mapLength); // 4
// about Number/Boolean 
let { toString: numberMethod } = 123;
console.log(numberMethod); // [Function: toString]
console.log(numberMethod === Number.prototype.toString); // true
let { toString: booleanMethod } = true;
console.log(booleanMethod === Boolean.prototype.toString); // true

console.log((123).toString === numberMethod); // true
console.log((123).toString() === numberMethod.call(123)); // true
console.log((true).toString() === booleanMethod.call(true)); // true

関数のパラメータの初期値は配列の分割代入のように設置することもできる。

// about function parameters
function add([x, y]) {
  return x + y;
}
console.log(add([1, 2])); // 3
//
console.log([[1, 2], [3, 4]].map(([a, b]) => a + b)); // [ 3, 7 ]

初期値の触発条件は指定のプロパティがundefinedです。
二番目の例ではパラメータ(オブジェクト)の中のプロパティに初期値を設置するのではなく、パラメータ全体の初期値への設置をしました。
しかし引数がちゃんと入れられた状態ではundefinedではないので{ x: 0, y: 0 }が作用しない。例えば{ x: 3 }が引数として入れられたのでundefinedではなかった。けれど左手のパラメータのプロパティではxしか見つからないからyundefinedになるしかありません。
move()のように何も与えてないundefinedになるときだけ{ x: 0, y: 0 }が作用する。

// compare and think about following example
function move({ x = 0, y = 0 } = {}) {
  return [x, y];
}
console.log(move({ x: 3, y: 8 })); // [ 3, 8 ]
console.log(move({ x: 3 })); // [ 3, 0 ]
console.log(move({})); // [ 0, 0 ]
console.log(move()); // [ 0, 0 ]
console.log(move({ a: 1, b: 2 })); // [ 0, 0 ]
// note: we declare parameter {x, y} in function, and set default value to x, y
// no matter what arguments we use, x, y always hold its default value
//
function move({ x, y } = { x: 0, y: 0 }) {
  return [x, y];
}
console.log(move({ x: 3, y: 8 })); // [ 3, 8 ]
console.log(move({ x: 3 })); // [ 3, undefined ]
console.log(move({})); // [ undefined, undefined ]
console.log(move()); // [ 0, 0 ]
console.log(move({ a: 1, b: 2 })); // [ undefined, undefined ]
/*note:
We also declare parameter {x, y} in this function, but didn't set any default value to x, y.
Instead, we set another obj { x: 0, y: 0 } as default value to {x, y}.
So, only we didn't use any argument(= undefined), the default value takes effect.
*/

配列の分割代入では初期値のあるとき、undefinedempty slotが分けられるのかな。map()のようにundefinedをスキップしないメソッドならちゃんと反映します。

// only undefined can make default value take effect
console.log([1, 2, 3].map((item = 'lost item') => item)); // [ 1, 2, 3 ]
console.log([1, undefined, 3].map((item = 'lost item') => item)); // [ 1, 'lost item', 3 ]
console.log([1, , 3].map((item = 'lost item') => item)); // [ 1, <1 empty item>, 3 ]

配列にあるundefinedempty slotがどう処理されるのかについて前の文章にまとめてみました。

Practices

function returnArr() {
  return [1, 2, 3];
}
let [a, b, c] = returnArr();
console.log(a, b, c); // 1 2 3
//
function returnObj1() {
  return {
    foo: 1,
    bar: 2
  }
}
let { foo, bar } = returnObj1()
console.log(foo, bar); // 1 2
//
function returnObj2({ x = 0, y = 0 } = {}) {
  return {
    x,
    y
  }
}
let { x, y } = returnObj2();
console.log(x, y); // 0 0
function orderedArr([x, y, z]) {
  return [x, y, z];
};
console.log(orderedArr([1, 2, 3])); // [ 1, 2, 3 ]
//
function unorderedObj({ x, y, z }) {
  return { x, y, z };
}
console.log(unorderedObj({ z: 3, y: 2, x: 1 })); // { x: 1, y: 2, z: 3 }
// JSON
let jsonData = {
  id: 42,
  status: 'OK',
  data: [123, 4567]
}
let { id, status, data: number } = jsonData;
console.log(id, status, number); // 42 OK [ 123, 4567 ]

// Map
const map = new Map([
  ['key1', 'value1'],
  [undefined, 'value2'],
  [, 'value3']
]);
for (let [key = 'no key', value] of map) {
  console.log(key);
}
// key1
// no key
// module
import { properties, methods } from 'module.js';
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2