ちょっと高度なJavaScriptの話

はじめに

今回は、初心者から中級者になるためのJavaScriptを学んでいきましょう。

JavaScriptを全く勉強したことがないという人には以下の記事がおすすめです。

【JavaScriptの超基本】ファイルのインポートやエクスポートについて簡単に解説

【JavaScriptの超基本】コールバック関数について簡単に解説

【JavaScript】プリミティブ型とオブジェクト型を理解したい

それでは、頑張っていきましょう。

配列やオブジェクトのちょっと便利な構文

それではJavaScriptのちょっと便利に構文について学んでいきましょう。

オブジェクトのkeyとvalueに変数を指定

JavaScriptではオブジェクトのkeyとvalueに変数を指定することができます。以下のコードです。

const keyName = "bar";
const baz = 3
const obj = {foo: 1, [keyName]: 2, baz: baz};
console.log(obj)

{ foo: 1, bar: 2, baz: 3 }

上記の例のように、オブジェクトのkeyに変数を指定するときは[]で囲い、valueに変数を指定するときはそのまま代入します。

keyNameの値であるbarがオブジェクトのkeyに、bazの値である3がオブジェクトのvalueになっていることが分かると思います。

プロパティ名のショートハンド

次は、プロパティ名のショートハンドと呼ばれる書き方です。

変数を{}で囲うことで、変数名をkey・値をvalueに持つオブジェクトを生成することができます。

const organization = 'unreact';
const obj = {organization};
console.log(obj);

{ organization: 'unreact' }

プロパティ名のショートハンドという書き方はES2015から導入された構文で、React開発においても頻繁に使用されます。ぜひともマスターしておいて下さい。

配列の分割代入

他の言語でも頻繁に使われますが、JavaScriptにおいても配列の分割代入は可能です。

配列の分割代入で値を受け取る際は、受け取る側も配列にしておきます。

const [n, m] = [1, 2];
console.log(n, m);

1 2

上記の例では、nに配列の1つ目の要素である1が代入され、mに配列の2つ目の要素である2が代入されます。

JavaScriptの配列には順番が存在することを考えれば、値を受け取る側の順番が、値を与える側の順番に対応するというのは当然の挙動ですよね。

オブジェクトの分割代入

当然ですが、オブジェクトの分割代入も可能です。

const onj = {organization: 'unrect', age: 1};
const {organization, age} = onj;
console.log(organization, age);

unrect 1

オブジェクトの分割代入においては、受け取る側に{}を準備して、その中にオブジェクトのkey名を書きます。

そのkey名に対して、オブジェクトの中で対応するvalueがそれぞれ格納されていきます。

つまり上記の例では、organaizationというオブジェクトのkeyに対応するunreactというvalueが、受け取る側の{}の中のkey名であるorganaizationに代入されることになります。

配列と違いオブジェクトには順番が存在しないため、分割代入においてはkey名で指定することが必要になります。

上記の例では、オブジェクトの分割代入を行った際の変数名が、オブジェクトのkey名になってしまっています。

以下のようにすることで、分割代入で生成する変数名を任意の名前にすることができます。

const onj = {organization: 'unrect', age: 1};
const {organization: group, age} = onj;
console.log(group, age);

unrect 1

上記の例では、organization: groupの部分で、オブジェクトのorganaizationに対応するvalueであるunreactを、groupという変数名で受け取っています。

また以下のようにすれば変数名に初期値を割り当てることができます。

const onj = {age: 1};
const {organization: group = 'unreact', age} = onj;
console.log(group, age);

上記の例では、オブジェクトにはorganaizationが存在しないため何もしなければgroupにはundefinedが格納されます。

それを防ぐために、='unreact'とすることで初期値を設定しています。

オブジェクトの分割代入はReactでpropsを受け取る際などに頻繁に利用されます。ぜひとも覚えておきましょう。

スプレッド構文

それでは次にスプレッド構文をまとめていきます。スプレッド構文とは...をつけることでオブジェクトや配列の中身を展開する構文です。

const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5, 6];

console.log(arr2);

const obj1 = {organaization: 'unreact'};
const obj2 = {...obj1, age: 1};

console.log(obj2);

[ 1, 2, 3, 4, 5, 6 ]
{ organaization: 'unreact', age: 1 }

配列のスプレッド構文はES2015から導入され、オブジェクトのスプレッド構文はES2018で導入されました。

使用例としては、オブジェクトや配列をコピーする際などに用いられます。

具体例を解説する前に、値渡しと参照渡しについて学んでおきましょう。

参照渡し

これは他の言語でも同じですが、配列やオブジェクトをそのまま代入すると値渡しではなく参照渡しが行われます。

どういうことか分からないと思うので、具体例で解説します。以下が参照渡しが行われる例です。

const arr1 = [1, 2, 3];
const arr2 = arr1;
arr2.push(4);

console.log(arr1);

[ 1, 2, 3, 4 ]

上記の例のように、arr2.push(4)を行ってarr2に対して変更を行うと、arr1にも変更が加えられます。

なぜこのような挙動をするのかというと、オブジェクトや配列は参照渡しが行われるからです。

const arr1 = [1, 2, 3]を行うと、配列のためのメモリ領域が確保され、そのポインタ(参照)がarr1に渡されることになります。

この状態でconst arr2 = arr1を行うと、配列のために新たなメモリ領域が確保されるのではなく、arr1に代入されている配列の参照がそのままarr2に渡されることになります。

つまり、arr1arr2は同じメモリ領域の配列を指し示すことになります。

その状態でarr2.push(4);を行うと、そのメモリ領域の配列自体に変更が加えられるため、同じ参照をもつarr1の値も変更することになります。

これを回避するために、スプレッド構文を用いた値渡しが行われます。

値渡し

上記の参照渡しを回避するためには、arr2のために新たに配列のメモリ領域を確保する必要があります。

そのために、下記のようにスプレッド構文を用いることがよくあります。

const arr1 = [1, 2, 3];
const arr2 = [...arr1];
arr2.push(4);

console.log(arr1);

[ 1, 2, 3 ]

このようにすれば、値をコピーして渡すことができます。const arr2 = [...arr1]の部分で新たに配列を生成し、そのメモリ領域を確保した後に、その新たに確保したメモリ領域の参照をarr2に渡しています。

シャローコピーについて

先程配列をスプレッド構文でコピーして値渡しを行う方法について解説しましたが、この方法は少し問題を抱えています。

というのも、スプレッド構文による値渡しは、一段回までの深さしか行えないからです。

つまり、オブジェクトや配列がネストされていた場合は、そのネストされたオブジェクトや配列に対しては参照渡しを行ってしまいます。

以下の具体例で見てみましょう。

const obj1 = {
  organaization: "unreact",
  nest: { group: "react" },
};
const obj2 = {...obj1};
obj2.nest.group = 'React';

console.log(obj1);

{ organaization: 'unreact', nest: { group: 'React' } }

上記の例では、const obj2 = {...obj1}の部分でスプレッド構文による値渡しを行っていますが、ネストされたオブジェクトである{ group: "react" }に関しては参照渡しが行われてしまいます。

そのため、obj2.nest.group = 'React'を行いobj2に対してネストされたオブジェクトの値を変更すると、obj1のネストされたオブジェクトの値まで書き換えられてしまいます。

このシャローコピーを回避するために、いくつかの方法があります。

最も有名なのはJson.parse/stringfyを使う方法でしょうか。

ディープコピーを行う

それでは、このシャローコピーを回避してディープコピーを行う方法について解説していきます。

最も有名なのは、以下のようにJson.parse/stringfyを行う方法です。

const obj1 = {
  organaization: "unreact",
  nest: { group: "react" },
};
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.nest.group = "React";

console.log(obj1);

{ organaization: 'unreact', nest: { group: 'react' } }

JSON.parse(JSON.stringify(obj1))の部分で、オブジェクトを一度JSON文字列に変換した後、再びJavaScriptのオブジェクトに変換しています。

この方法はお手軽で高速にディープコピーを行うことができますが、強引であるがゆえに弊害もあります。

下記の記事に、弊害について分かりやすくまとめてありました。

JavaScriptのDeepCopyでJSON.parse/stringifyを使ってはいけない

かいつまんで説明すると、この方法はオブジェクトのプロパティにDate関数undefinedが存在する場合に上手く動きません。

このような問題があるため、現環境でディープコピーを行う際はLodashというライブラリのcloneDeep()を用いる方法が推奨されています。

Lodashでディープコピー

Lodashを使えば簡単にディープコピーを行うことができます。

import _ from "lodash";

const obj1 = {
  organaization: "unreact",
  nest: { group: "react" },
};
const obj2 = _.cloneDeep(obj1);
obj2.nest.group = "React";

console.log(obj1);

{ organaization: 'unreact', nest: { group: 'react' } }

const obj2 = _.cloneDeep(obj1);の部分でディープコピーを行っています。凄く簡単ですよね。

これからディープコピーを使用する際は積極的に使っていきましょう。

関数型プログラミングっぽく書こう

JavaScriptはかなり関数型プログラミングのパラダイムに対応しています。

関数型プログラミングとは、先行する式の評価を後続の式に適用し、それをつなげていくことで最終的な評価値を得るプログラミング手法です。

ifやfor文による制御構造による手続き書き連ねていく命令型プログラミングのパラダイムとは異なり、関数型プログラミングはy = f(x)のような数学的な式であるべき状態を宣言します。

React開発ではこの関数型プログラミングのような書き方が非常に好まれるため、積極的に使用していく必要があります。例えば、ReactのJSXの中ではを使用することができず、全てを用いてプログラムを記述する必要があります。

ifやforのようなではなく、値を返すを組み合わせてあるべき状態を宣言していく、そんな宣言的(Declarative)な書き方を学んでいくことで、少しレベルアップできるはずです。

それでは頑張っていきましょう。

ショートサーキット評価

最初にショートサーキット評価、またの名を短絡評価についてまとめていきます。

短絡評価とは&&||などの論理演算子が左から右に評価される性質を利用して、右辺が評価されるかどうかを左辺に委ねる方法です。

&&を使用した場合、左辺がtruthyのとき右辺を返し、||を使用した場合は左辺がfalsyなときに右辺を返します。

以下で具体例を見ておきましょう。

const organaization = "" || "UnReact";
console.log(organaization);

UnReact

上記の例では、"" || "UnReact"の部分でショートサーキット評価を行っています。

||は左辺がfalsyのときに右辺を返し、左辺がtruthyのときは左辺を返す(短絡する)式です。

空文字はfalsyな値のため、右辺が返ってきてorganaizationに代入されています。

ちなみに、JavaScriptにおいてfalsyな値はfalse0NaNnullundefined''(空文字)のみで、それ以外は全てtruthyな値になります。

以下の例のように、ショートサーキット評価は繋げて書くことができます。

const organaization = 'Uniqlo' && 'Softbank' && 'Toyota' && 'UnReact';
console.log(organaization);

UnReact

&&は値が左辺がtruthyなとき右辺に評価を委ねる記法です。そのため、上記の例では一番右側まで評価されてUnReactが返ってきます。

このショートサーキット評価はif文の代わりに使うことができます。

ifなどのと異なり、値を返すなのでReact開発で頻繁に利用されます。

Nullish Coalescing

次はNullish Coalecing、通称Null合体演算子をやっていきましょう。

Nulllish Coalescingはショットサーキット評価の||と似ています。||は、左辺がfalsyな値のときに右辺を評価するという記法ですが、Nullish Coalescingは??を用いて左辺がnullまたはundefinedのときに右辺を評価するという記法です。

具体例は以下になります。

const organaization = null ?? "UnReact";
const language = undefined ?? "React";
console.log(organaization);
console.log(language);

UnReact
React

このように??を用いて、左辺がnullまたはundefinedのときに右辺を評価するという記法がNullish Coalescingです。

ちなみに、上記のパターンだとNullish Coalescingの??の部分をショートサーキット評価の||に変えても特段違いはありません。

しかし、ショートサーキット評価の||は0や空文字が左辺に入った際も右辺を評価してしまうため、思わぬバグを生む可能性があります。

そのため、nullまたはundefinedのときに右辺を評価するということを明示的に示したい場合にNullish Coalescingを使用することになります。

ちなみにNullsish CoalescingはES2020で追加されたものなので、Node.js 14.0以降で実行することになります。

Optional Chaining

それでは、Optional Chainingについて解説していきます。

こちらもES2020から追加された記法で、端的に書くならオブジェクトに対するプロパティアクセスを用いた際に、各参照が正しいかどうかを明示的に確認せずアクセスすることができるものです。

もしも途中のプロパティが存在していなかったら、式が短絡してundefinedが返ってきます。

この説明では何がなんだか分からないと思うので、きちんと順を追って解説していきます。

まず、前提としてオブジェクトの存在しない要素に対してプロパティアクセスを行うとundefinedが返ってきます。

const obj = { organization: "UnReact" };
console.log(obj.employee);

undefined

そして、この返ってきたundefinedに対してプロパティアクセスを行うとエラーが発生します。

const obj = { organization: "UnReact" };
console.log(obj.employee.age);

at ModuleJob.run (internal/modules/esm/module_job.js:110:37)
at async Loader.import (internal/modules/esm/loader.js:179:24)

この前提を踏まえた上で、次のようなケースを考えます。

const arr = [
  { organaization: "UnReact", employee: { name: "poocomaru", age: 23 } },
  { organaization: "Softbank", employee: null },
];

arr.map((n) => {
  console.log(n.employee.age);
});

at ModuleJob.run (internal/modules/esm/module_job.js:110:37)
at async Loader.import (internal/modules/esm/loader.js:179:24)

上記のコードでは、会社についての情報を格納したオブジェクトの配列を作成して、それをmapで回しながらプロパティにアクセスしています。

このコードは、2つ目のオブジェクトに対してのn.employee.ageを行う部分でエラーが発生してしまいます。

というのも、n.employeeの実行結果がnullであり、そのnullに対してプロパティアクセスを行ってしまったために発生するエラーです。

Optional Chainingの実装以前はifによる条件分岐でnullやundefinedチェックを行い、エラーを回避していました。

しかし、Optional Chainingを使用すれば、以下のコードのように簡単にエラーを回避することができます。

const arr = [
  { organaization: "UnReact", employee: { name: "poocomaru", age: 23 } },
  { organaization: "Softbank", employee: null },
];

arr.map((n) => {
  console.log(n?.employee?.age);
});

23

undefined

変更が加わったのはn?.employee?.ageの部分です。通常のプロパティアクセス.から、Optional Chainingである?.に変更しています。

この変化により、nullに対してのプロパティアクセスが短絡されてundefinedが返ってくるようになり、nullに対してプロパティアクセスを行ったが故のエラーが発生しなくなります。

終わりに

今回はちょっと高度なJavaScriptの文法について取り扱いました。

少しでも皆様のお役に立てれば幸いです。

ここまで読んで頂きありがとうございました。

renesisu727
株式会社UnReactのCEOです。 北九州でECサイト作ってます。 React + Firebaseでアプリ制作中。 TwitterではJavaScriptについてつぶやいています。
https://unreact.jp/
unreact
北九州市でShopifyを用いたECサイトの制作を行っています。 また、React + Firebaseを用いた自社アプリの開発も行っています。
https://unreact.jp/
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
この記事は以下の記事からリンクされています
過去の0件を表示する
コメント

ディープコピーは簡単なものならこんなコードで実現できます。

const objectToString = value => {
  return Object.prototype.toString.call(value);
};

const isObject = (value) => {
  if (objectToString(value) !== '[object Object]') {
    return false;
  }
  return true;
};

const isArray = Array.isArray;

const cloneDeep = (
  source,
) => {
  const cloneDeep_ = (value) => {
    if (isObject(value)) {
      const cloneValue = {};
      for (const key in value) {
        cloneValue[key] = cloneDeep_(value[key]);
      }
      return cloneValue;
    } else if (isArray(value)) {
      const cloneValue = Array(value.length);
      for (let i = 0, l = value.length; i < l; i += 1) {
        cloneValue[i] = cloneDeep_(value[i]);
      }
      return cloneValue;
    }
    return value;
  };
  return cloneDeep_(source);
};

const a = {
  a1: {
    a1_1: 'A1_1',
    a1_2: 'A1_2',
  }, 
  a2: [
    'A2_1',
    'A2_2',
  ]
};

const b = cloneDeep(a);

b.b1 = 'B1';
b.b2 = 'B2';


console.log(b);
// [object Object] {
//   a1: [object Object] {
//     a1_1: "B1_1",
//     a1_2: "A1_2"
//   },
//   a2: ["A2_1", "A2_2"],
//   b1: "B1",
//   b2: "B2"
// }

console.log(a);
// [object Object] {
//   a1: [object Object] {
//     a1_1: "A1_1",
//     a1_2: "A1_2"
//   },
//   a2: ["A2_1", "A2_2"]
// }

https://jsbin.com/puhejovope/1/edit?html,js,console

このコードではDate型などには対応できていないので、なんでもちゃんとディープコピーできる cloneDeep も作っています。

JavaScript さまざまな型に対応できる拡張機能つき clone と cloneDeep を実装しました。 - Qiita
https://qiita.com/standard-software/items/54bf2284ae1833a786d7

あと、Null合体とか、Optional Chainingなんかは便利でよいですが

短絡評価は、
コードがすごく読みにくくなり可読性が落ちる上に、False属性の関係で不具合の温床になっていて、顕在化しないバグに非常に苦しめられた経験が多いので、なるべく使わない人が増えたらいいなと思っています。
極端にいうとORとANDの短絡評価を使うプログラマは下手だなと認識してます。

自分は使わないのですが誰かの引き継ぎプロジェクトなどで苦しめられるのは減らしたい。

より詳細はQuoraで書いたことがありますので、
なんで短絡評価が不具合の温床なのかを詳しく知りたい人はご参考にでもどうぞです。

プログラミングでは、より短いコードで同じ目的を達成した方が、優れているのですか?に対する山本 聡 (Satoshi Yamamoto)さんの回答 - Quora
https://qr.ae/Tm2RNb

短絡評価はJSXの中で使うのは許してください

3項演算子だとコードを追うとき
条件不一致であった場合に
後ろの値を確認しなくちゃいけません

JSXだと条件一致の場合に返したい
コンポーネントのコードが割と長いことがあって
後ろの値の確認するのが手間になるんです

&&だったら条件不一致であれば
その{ }の部分を丸ごと読み飛ばせます

JSX Reactでよく使われるので、そこを詳しくQuoraに書いてみています。

うまくやれば、短絡評価はなくしても比較的読みやすく安全にかけますよ。

あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした