diff --git a/package-lock.json b/package-lock.json index 7c91a36413..ff139502ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4377,10 +4377,9 @@ "dev": true }, "codemirror": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.51.0.tgz", - "integrity": "sha512-vyuYYRv3eXL0SCuZA4spRFlKNzQAewHcipRQCOKgRy7VNAvZxTKzbItdbCl4S5AgPZ5g3WkHp+ibWQwv9TLG7Q==", - "dev": true + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz", + "integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg==" }, "codemirror-console": { "version": "2.0.1", diff --git a/source/basic/condition/README.md b/source/basic/condition/README.md index 56dfce3fae..2c4edfe27c 100644 --- a/source/basic/condition/README.md +++ b/source/basic/condition/README.md @@ -61,6 +61,7 @@ if文の`条件式`には`true`または`false`といった真偽値以外の値 - `undefined` - `null` - `0` +- `0n` - `NaN` - `""`(空文字列) diff --git a/source/basic/function-declaration/OUTLINE.md b/source/basic/function-declaration/OUTLINE.md index af34995710..c8c9c0c89b 100644 --- a/source/basic/function-declaration/OUTLINE.md +++ b/source/basic/function-declaration/OUTLINE.md @@ -6,8 +6,11 @@ - 引数の扱い - 関数のシグネチャ - 引数が少ないとき - - デフォルト引数 - 引数が多い時 +- デフォルト引数 + - デフォルト引数と `||` の比較 + - `||` と Nullish coalescing + - 可変長引数 - arguments - Rest Parameters @@ -20,6 +23,128 @@ - 短縮記法 - まとめ +## 扱っていない。 + +オプションオブジェクトのデフォルトの値の話はパターンが多すぎて好みの問題になりそう。 + +- デフォルト引数とオブジェクト + - Object.assign or Spread構文 + - デフォルト引数 + Nullish coalescing + - Nullish coalescing演算子(`??`)とOptional chaining(`?.`) + + +関数の引数のデフォルト値を指定する場合にはデフォルト引数を利用することを紹介しました。 + +しかし、関数の引数にはオブジェクトを渡すこともできます。 +デフォルト引数では、仮引数に対応する引数が指定されていなかった場合のデフォルト値です。 +そのため、引数として渡されたオブジェクトのプロパティに対するデフォルト値は、デフォルト引数では実現できません。 + +次のコードの`wrapText`関数では`prefix`と`suffix`をオプションオブジェクトとして受け取れます。 +`options`に対応するオブジェクトを渡さなかった場合のデフォルトオプションをデフォルト引数で指定しています。 +`options`を渡さなかった場合は意図した結果となりますが、オプションの一部(`prefix`や`suffix`の片方)を渡した場合は意図しない結果となります。 +これは、デフォルト引数は実際の引数として渡されたオブジェクトをマージをするわけではないためです。 + +{{book.console}} + +```js +// `options`が指定されなかったときは空のオブジェクトが入る +function wrapText(text, options = { prefix: "接頭辞:", suffix: ":接尾辞" }) { + return options.prefix + text + options.suffix; +} +console.log(wrapText("文字列")); // => "接頭辞:デフォルト:接尾辞" +console.log(wrapText("文字列", { + prefix: "Start:", + suffix: ":End" +})); // => "Start:文字列:End" +// オプションの一部だけを指定した場合に意図しない結果となる +console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:デフォルトundefined" +console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "undefined文字列:カスタム" +``` + +このときの`prefix`と`suffix`のそれぞれのデフォルト値は、デフォルト引数とNullish coalescing演算子(`??`)を使うことで実現できます。 +次のように、`options`オブジェクトそのものが渡されなかった場合のデフォルト引数として空オブジェクト(`{}`)を指定します。 +そして、`options`の`prefix`と`suffix`プロパティそれぞれに対してNullish coalescing演算子(`??`)を使いデフォルト値を指定しています。 + +{{book.console}} + +```js +// `options`が指定されなかったときは空のオブジェクトが入る +function wrapText(text, options = {}) { + const prefix = options.prefix ?? "接頭辞:"; + const suffix = options.suffix ?? ":接尾辞"; + return prefix + text + suffix; +} +// falsyな値を渡してもデフォルト値は代入されない +console.log(wrapText("文字列")); // => "接頭辞:文字列:接尾辞" +console.log(wrapText("文字列", { + prefix: "Start:", + suffix: ":End" +})); // => "Start:文字列:End" +// オプションの一部だけを指定した場合は、それぞれのデフォルト値が採用される +console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:文字列:接尾辞" +console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "接頭辞:文字列:カスタム" +``` + +Optional chaining(`?.`)を利用することで、デフォルト引数の指定は次のように書き換えることもできます。 + + +```js +function wrapText(text, options) { + // `options`がundefinedまたはnullの時点で右辺を評価する + const prefix = options?.prefix ?? "接頭辞:"; + const suffix = options?.suffix ?? ":接尾辞"; + return prefix + text + suffix; +} +// falsyな値を渡してもデフォルト値は代入されない +console.log(wrapText("文字列")); // => "接頭辞:文字列:接尾辞" +console.log(wrapText("文字列", { + prefix: "Start:", + suffix: ":End" +})); // => "Start:文字列:End" +// オプションの一部だけを指定した場合は、それぞれのデフォルト値が採用される +console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:文字列:接尾辞" +console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "接頭辞:文字列:カスタム" +``` + +さらにDestructuring + デフォルト引数で次のようにも書けます。 + +```js +function wrapText(text, { prefix = "接頭辞:", suffix = ":接尾辞" }) { + return prefix + text + suffix; +} +console.log(wrapText("文字列")); // => "接頭辞:デフォルト:接尾辞" +console.log(wrapText("文字列", { + prefix: "Start:", + suffix: ":End" +})); // => "Start:文字列:End" +// オプションの一部だけを指定した場合に意図しない結果となる +console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:デフォルトundefined" +console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "undefined文字列:カスタム" +``` + + +さらにオブジェクトマージを使うと次のような書き方もあります。 + +```js +const DefaultOptions = { prefix: "接頭辞:", suffix: ":接尾辞" } +function wrapText(text, options) { + const optionsWithDefault = { + ...DefaultOptions, + ...options + } + return optionsWithDefault.prefix + text + optionsWithDefault.suffix; +} +console.log(wrapText("文字列")); // => "接頭辞:デフォルト:接尾辞" +console.log(wrapText("文字列", { + prefix: "Start:", + suffix: ":End" +})); // => "Start:文字列:End" +// オプションの一部だけを指定した場合に意図しない結果となる +console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:デフォルトundefined" +console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "undefined文字列:カスタム" +``` + + ## Issues * [Destructuring · Issue #113 · asciidwango/js-primer](https://github.com/asciidwango/js-primer/issues/113 "Destructuring · Issue #113 · asciidwango/js-primer") diff --git a/source/basic/function-declaration/README.md b/source/basic/function-declaration/README.md index 54fe78eae7..6b3c8f5bf6 100644 --- a/source/basic/function-declaration/README.md +++ b/source/basic/function-declaration/README.md @@ -165,6 +165,7 @@ falsyな値とは、真偽値へと変換すると`false`となる次のよう - `undefined` - `null` - `0` +- `0n` - `NaN` - `""`(空文字列) @@ -198,6 +199,24 @@ console.log(addPrefix("文字列", "")); // => "文字列" console.log(addPrefix("文字列", "カスタム:")); // => "カスタム:文字列" ``` +また、ES2020から導入されたNullish coalescing演算子(`??`)を利用することでも、 +OR演算子(`||`)の問題を避けつつデフォルト値を指定できます。 + +{{book.console}} + +```js +function addPrefix(text, prefix) { + // prefixがnullまたはundefinedの時、デフォルト値を返す + const pre = prefix ?? "デフォルト:"; + return pre + text; +} + +console.log(addPrefix("文字列")); // => "デフォルト:文字列" +// falsyな値でも意図通りに動作する +console.log(addPrefix("文字列", "")); // => "文字列" +console.log(addPrefix("文字列", "カスタム:")); // => "カスタム:文字列" +``` + ### 呼び出し時の引数が多いとき {#function-more-arguments} 関数の仮引数に対して引数の個数が多い場合、あふれた引数は単純に無視されます。 diff --git a/source/basic/implicit-coercion/README.md b/source/basic/implicit-coercion/README.md index 9832fdf466..68c85f4fd5 100644 --- a/source/basic/implicit-coercion/README.md +++ b/source/basic/implicit-coercion/README.md @@ -180,12 +180,13 @@ JavaScriptでは、どの値が`true`でどの値が`false`になるかは、次 - **falsy**な値は`false`になる - **falsy**でない値は`true`になる -**falsy**な値とは次の6種類の値のことを言います。 +**falsy**な値とは次の7種類の値のことを言います。 - `false` - `undefined` - `null` - `0` +- `0n` - `NaN` - `""`(空文字列) diff --git a/source/basic/map-and-set/README.md b/source/basic/map-and-set/README.md index e8059fb15e..8ff7ab5fc4 100644 --- a/source/basic/map-and-set/README.md +++ b/source/basic/map-and-set/README.md @@ -193,6 +193,7 @@ ES2015では、これらの問題を根本的に解決する`Map`が導入され たとえばショッピングカートのような仕組みを作るとき、次のように`Map`を使って商品のオブジェクトと注文数をマッピングできます。 {{book.console}} + ```js // ショッピングカートを表現するクラス class ShoppingCart { @@ -202,7 +203,8 @@ class ShoppingCart { } // カートに商品を追加する addItem(item) { - const count = this.items.get(item) || 0; + // `item`がない場合は`undefined`を返すため、Nullish coalescing演算子(`??`)を使いデフォルト値として`0`を設定する + const count = this.items.get(item) ?? 0; this.items.set(item, count + 1); } // カート内の合計金額を返す @@ -313,6 +315,8 @@ obj = null; このマップを`Map`で実装してしまうと、明示的に削除されるまでイベントリスナーはメモリ上に残り続けます。 ここで`WeakMap`を使うと、`addListener` メソッドに渡された`listener`は `EventEmitter` インスタンスが参照されなくなった際、自動的に解放されます。 +{{book.console}} + ```js // イベントリスナーを管理するマップ const listenersMap = new WeakMap(); @@ -320,7 +324,7 @@ const listenersMap = new WeakMap(); class EventEmitter { addListener(listener) { // this にひもづいたリスナーの配列を取得する - const listeners = listenersMap.get(this) || []; + const listeners = listenersMap.get(this) ?? []; const newListeners = listeners.concat(listener); // this をキーに新しい配列をセットする listenersMap.set(this, newListeners); diff --git a/source/basic/object/OUTLINE.md b/source/basic/object/OUTLINE.md index 407398eea6..eac8e82a72 100644 --- a/source/basic/object/OUTLINE.md +++ b/source/basic/object/OUTLINE.md @@ -29,10 +29,12 @@ - プロパティの追加 - プロパティの削除 - [コラム] constで定義したオブジェクトは変更可能 +- 存在しないプロパティのネストは例外を返す - プロパティが定義済みかを確認する方法 - undefinedとの比較 - in演算子を使う - `hasOwnProperty`メソッド(インスタンスメソッド) +- Optional chaining(`?.`)でのアクセス方法 - オブジェクトの静的メソッド - オブジェクトのプロパティの列挙 - `Object.keys`メソッド diff --git a/source/basic/object/README.md b/source/basic/object/README.md index 17dcb4dd19..13ef3b5252 100644 --- a/source/basic/object/README.md +++ b/source/basic/object/README.md @@ -466,6 +466,110 @@ if (obj.hasOwnProperty("key")) { この動作の違いを知るにはまずプロトタイプオブジェクトという特殊なオブジェクトについて理解する必要があります。 次の章の「[プロトタイプオブジェクト][]」で詳しく解説するため、次の章で`in`演算子と`hasOwnProperty`メソッドの違いを見ていきます。 +## [ES2020] Optional chaining演算子(`?.`) {#optional-chaining-operator} + +プロパティの存在を確認する方法として`undefined`との比較、`in`演算子、`hasOwnProperty`メソッドを紹介しました。 +最終的に取得したいものがプロパティの値であるならば、if文で`undefined`と比較しても問題ありません。 +なぜなら、値を取得したい場合には、プロパティが存在するかどうかとプロパティの値が`undefined`かどうかの違いを区別する意味はないためです。 + +次のコードでは、`widget.window.title`プロパティにアクセスできるなら、そのプロパティの値をコンソールに表示しています。 + +{{book.console}} +```js +function printWidgetTitle(widget) { + // 例外を避けるために`widget`のプロパティの存在を順場に確認してから、値を表示している + if (widget.window !== undefined && widget.window.title !== undefined) { + console.log(`ウィジェットのタイトルは${widget.window.title}です`); + } else { + console.log("ウィジェットのタイトルは未定義です"); + } +} +// タイトルが定義されているwidget +printWidgetTitle({ + window: { + title: "Book Viewer" + } +}); +// タイトルが未定義のwidget +printWidgetTitle({ + // タイトルが定義されてない空のオブジェクト +}); +``` + +この`widget.window.title`のようなネストしたプロパティにアクセスする際には、プロパティの存在を順番に確認してからアクセスする必要があります。 +なぜなら、`widget`オブジェクトが`window`プロパティを持っていない場合は`undefined`という値を返すためです。このときに、さらにネストした`widget.window.title`プロパティにアクセスすると、`undefined.title`という参照となり例外が発生してしまいます。 + +しかし、プロパティへアクセスするたびに`undefined`との比較をAND演算子(`&&`)でつなげて書いていくと冗長です。 + +この問題を解決するために、ES2020ではネストしたプロパティの存在確認とアクセスを簡単に行う構文としてOptional chaining演算子(`?.`)が導入されました。 +Optional chaining演算子(`?.`)は、ドット記法(`.`)の代わりに`?.`をプロパティアクセスに使います。 + +Optional chaining演算子(`?.`)は左辺のオペランドがnullish(`null`または`undefined`)の場合は、それ以上評価せずに`undefined`を返します。一方で、プロパティが存在する場合は、そのプロパティの評価結果を返します。 + +つまり、Optional chaining演算子(`?.`)では、存在しないプロパティへアクセスした場合でも例外ではなく、`undefined`という値を返します。 + +{{book.console}} + +```js +const obj = { + a: { + b: "objのaプロパティのbプロパティ" + } +}; +// obj.a.b は存在するので、その評価結果を返す +console.log(obj?.a?.b); // => "objのaプロパティのbプロパティ" +// 存在しないプロパティのネストも`undefined`を返す +// ドット記法の場合は例外が発生してしまう +console.log(obj?.notFound?.notFound); // => undefined +// undefinedやnullはnullishなので、`undefined`を返す +console.log(undefined?.notFound?.notFound); // => undefined +console.log(null?.notFound?.notFound); // => undefined +``` + +先ほどのウィジェットのタイトルを表示する関数もOptional chaining演算子(`?.`)を使うと、if文を使わずに書けます。 +次のコードの`printWidgetTitle`関数では、`widget?.window?.title`にアクセスできる場合はその評価結果が変数`title`に入ります。 +プロパティにアクセスできない場合は`undefined`を返すため、Nullish coalescing演算子(`??`)によって右辺の`"未定義"`が変数`title`のデフォルト値となります。 + +{{book.console}} + +```js +function printWidgetTitle(widget) { + const title = widget?.window?.title ?? "未定義"; + console.log(`ウィジェットのタイトルは${title}です`); +} +printWidgetTitle({ + window: { + title: "Book Viewer" + } +}); // => "ウィジェットのタイトルはBook Viewerです" +printWidgetTitle({ + // タイトルが定義されてない空のオブジェクト +}); // => "ウィジェットのタイトルは未定義です" +``` + +また、Optional chaining演算子(`?.`)はブラケット記法(`[]`)と組み合わせることもできます。 +ブラケット記法の場合も、左辺のオペランドがnullish(`null`または`undefined`)の場合は、それ以上評価せずに`undefined`を返します。一方で、プロパティが存在する場合は、そのプロパティの評価結果を返します。 + +{{book.console}} + +```js +const languages = { + ja: { + hello: "こんにちは!" + }, + en: { + hello: "Hello!" + } +}; +const langJapanese = "ja"; +const langKorean = "ko"; +const messageKey = "hello"; +// Optional chaining演算子(`?.`)とブラケット記法を組みわせた書き方 +console.log(languages?.[langJapanese]?.[messageKey]); // => "こんにちは!" +// `languages`に`ko`プロパティが定義されていないため、`undefined`を返す +console.log(languages?.[langKorean]?.[messageKey]); // => undefined +``` + ## `toString`メソッド {#toString-method} オブジェクトの`toString`メソッドは、オブジェクト自身を文字列化するメソッドです。 diff --git a/source/basic/operator/README.md b/source/basic/operator/README.md index e27904480e..d9e6eec2ed 100644 --- a/source/basic/operator/README.md +++ b/source/basic/operator/README.md @@ -727,88 +727,72 @@ const obj = { const key = obj.key; ``` -## 条件(三項)演算子(`?`と`:`) {#ternary-operator} +## 論理演算子 {#logical-operator} -条件演算子(`?`と`:`)は三項をとる演算子であるため、三項演算子とも呼ばれます。 +論理演算子は基本的に真偽値を扱う演算子でAND(かつ)、OR(または)、NOT(否定)を表現できます。 -条件演算子は`条件式`を評価した結果が`true`ならば、`Trueのとき処理する式`の評価結果を返します。 -`条件式`が`false`である場合は、`Falseのとき処理する式`の評価結果を返します。 - - -```js -条件式 ? Trueのとき処理する式 : Falseのとき処理する式; -``` +### AND演算子(`&&`) {#and-operator} -if文との違いは、条件演算子は式として書くことができるため値を返します。 -次のように、`条件式`の評価結果により`"A"` または `"B"` どちらかを返します。 +AND演算子(`&&`)は、左辺の値の評価結果が`true`ならば、右辺の評価結果を返します。 +一方で、左辺の値の評価結果が`false`ならば、そのまま左辺の値を返します。 {{book.console}} ```js -const valueA = true ? "A" : "B"; -console.log(valueA); // => "A" -const valueB = false ? "A" : "B"; -console.log(valueB); // => "B" +// 左辺はtrueであるため、右辺の評価結果を返す +console.log(true && "右辺の値"); // => "右辺の値" +// 左辺がfalseであるなら、その時点でfalseを返す +// 右辺は評価されない +console.log(false && "右辺の値"); // => false ``` -条件分岐による値を返せるため、条件によって変数の初期値が違う場合などに使われます。 - -次の例では、`text`文字列に`prefix`となる文字列を先頭につける関数を書いています。 -`prefix`の第二引数を省略したり文字列ではないものが指定された場合に、デフォルトの`prefix`を使います。 -第二引数が省略された場合には、`prefix`に`undefined`が入ります。 - -条件演算子の評価結果は値を返すので、`const`を使って宣言と同時に代入できます。 +AND演算子(`&&`)は、左辺の評価が`false`の場合、オペランドの右辺は評価されません。 +次のように、左辺が`false`の場合は、右辺に書いた`console.log`関数自体が実行されません。 {{book.console}} ```js -function addPrefix(text, prefix) { - // `prefix`が指定されていない場合は"デフォルト:"を付ける - const pre = typeof prefix === "string" ? prefix : "デフォルト:"; - return pre + text; -} - -console.log(addPrefix("文字列")); // => "デフォルト:文字列" -console.log(addPrefix("文字列", "カスタム")); // => "カスタム文字列" +// 左辺がtrueなので、右辺は評価される +true && console.log("このコンソールログは実行されます"); +// 左辺がfalseなので、右辺は評価されない +false && console.log("このコンソールログは実行されません"); ``` -if文を使った場合は、宣言と代入を分ける必要があるため、`const`を使うことができません。 - -{{book.console}} -```js -function addPrefix(text, prefix) { - let pre = "デフォルト:"; - if (typeof prefix === "string") { - pre = prefix; - } - return pre + text; -} +このような値が決まった時点でそれ以上評価しないことを**短絡評価**と呼びます。 -console.log(addPrefix("文字列")); // => "デフォルト:文字列" -console.log(addPrefix("文字列", "カスタム")); // => "カスタム文字列" -``` +また、AND演算子は左辺を評価する際に、左辺を真偽値へと[暗黙的な型変換][]をしてから判定します。 +真偽値への暗黙的な型変換ではどの値が`true`でどの値が`false`になるかは、次のルールによって決まります。 -## 論理演算子 {#logical-operator} +- **falsy**な値は`false`になる +- **falsy**でない値は`true`になる -論理演算子は基本的に真偽値を扱う演算子で、AND、OR、NOTを表現できます。 +**falsy**な値とは次の7種類の値のことを言います。 -### AND演算子(`&&`) {#and-operator} +- `false` +- `undefined` +- `null` +- `0` +- `0n` +- `NaN` +- `""`(空文字列) -AND演算子(`&&`)は、左辺の値の評価結果が`true`であるならば、右辺の評価結果を返します。 -左辺の評価が`true`ではない場合、右辺は評価されません。 +`true`へと変換される値の種類は多いため、`false`へと変換されない値は`true`となることは覚えておくとよいです。 +このオペランドを真偽値に変換してから評価するのはAND、OR、NOT演算子で共通の動作です。 -このような値が決まった時点でそれ以上評価しないことを**短絡評価**(ショートサーキット)と呼びます。 +次のように、AND演算子(`&&`)は左辺を真偽値へと変換した結果が`true`の場合に、右辺の評価結果を返します。 +つまり、左辺がfalsyの場合は、右辺は評価されません。 {{book.console}} ```js -const x = true; -const y = false; -// x -> y の順に評価される -console.log(x && y); // => false -// 左辺がfalseであるなら、その時点でfalseを返す -// xは評価されない -console.log(y && x); // => false +// 左辺はfalsyではないため、評価結果として右辺を返す +console.log("文字列" && "右辺の値"); // => "右辺の値" +console.log(42 && "右辺の値"); // => "右辺の値" +// 左辺がfalsyであるため、評価結果として左辺を返す +console.log("" && "右辺の値"); // => "" +console.log(0 && "右辺の値"); // => 0 +console.log(null && "右辺の値"); // => null ``` AND演算子は、if文と組み合わせて利用することが多い演算子です。 + 次のように、`value`がString型で **かつ** 値が`"str"`である場合という条件をひとつの式として書くことができます。 {{book.console}} @@ -825,31 +809,56 @@ if (typeof value === "string") { } ``` -このときに、`value`がString型でない場合は、その時点で`false`となります。 +このときに、`value`がString型でない場合は、その時点でif文の条件式は`false`となります。 +そのため、`value`がString型ではない場合は、AND演算子(`&&`)の右辺は評価されずに、if文の中身も実行されません。 -短絡評価はif文のネストに比べて短く書くことができます。 +AND演算子(`&&`)を使うと、if文のネストに比べて短く書くことができます。 -しかし、if文が3重4重にネストしているのは不自然なのと同様に、 -AND演算子やOR演算子が3つ4つ連続する場合は複雑で読みにくいコードです。 +しかし、if文が3重4重にネストしているのは複雑なのと同様に、 +AND演算子やOR演算子が3つ4つ連続すると複雑で読みにくいコードとなります。 その場合は抽象化ができないかを検討するべきサインとなります。 ### OR演算子(`||`) {#or-operator} -OR演算子(`||`)は、左辺の値の評価結果が`false`であるならば、右辺の評価結果を返します。 -AND演算子(`&&`)とは逆に、左辺が`true`である場合は、右辺を評価せず`true`を返します。 +OR演算子(`||`)は、左辺の値の評価結果が`true`ならば、そのまま左辺の値を返します。 +一方で、左辺の値の評価結果が`false`であるならば、右辺の評価結果を返します。 + +{{book.console}} +```js +// 左辺がtrueなので、左辺の値が返される +console.log(true || "右辺の値"); // => true +// 左辺がfalseなので、右辺の値が返される +console.log(false || "右辺の値"); // => "右辺の値" +``` + +OR演算子(`||`)は、左辺の評価が`true`の場合、オペランドの右辺を評価しません。 +これは、AND演算子(`&&`)と同様の短絡評価となるためです。 {{book.console}} ```js -const x = true; -const y = false; -// xがtrueなのでyは評価されない -console.log(x || y); // => true -// yはfalseなのでxを評価した結果を返す -console.log(y || x); // => true +// 左辺がtrueなので、右辺は評価されない +true || console.log("このコンソールログは実行されません"); +// 左辺がfalseなので、右辺は評価される +false || console.log("このコンソールログは実行されます"); +``` + +また、OR演算子は左辺を評価する際に、左辺を真偽値へと暗黙的な型変換します。 +次のように、OR演算子は左辺がfalsyの場合には右辺の値を返します。 + +{{book.console}} +```js +// 左辺がfalsyなので、右辺の値が返される +console.log(0 || "左辺はfalsy"); // => "左辺はfalsy" +console.log("" || "左辺はfalsy"); // => "左辺はfalsy" +console.log(null || "左辺はfalsy"); // => "左辺はfalsy" +// 左辺はfalsyではないため、左辺の値が返される +console.log(42 || "右辺の値"); // => 42 +console.log("文字列" || "右辺の値"); // => "文字列" ``` OR演算子は、if文と組み合わせて利用することが多い演算子です。 -次のように、`value`が`0`または`1`の場合にif文の中身が実行されます。 + +次のように、`value`が`0`**または**`1`の場合にif文の中身が実行されます。 {{book.console}} ```js @@ -861,7 +870,9 @@ if (value === 0 || value === 1) { ### NOT演算子(`!`) {#not-operator} -NOT演算子(`!`)は、`オペランド`の評価結果が`true`であるならば、`false`を返します。 +NOT演算子(`!`)は、`オペランド`の評価結果が`true`ならば、`false`を返します。 +一方で、`オペランド`の評価結果が`false`ならば、`true`を返します。 +つまり、オペランドの評価結果を反転した真偽値を返します。 {{book.console}} ```js @@ -869,12 +880,27 @@ console.log(!false); // => true console.log(!true); // => false ``` +NOT演算子(`!`)もAND演算子(`&&`)とOR演算子(`||`)と同様に真偽値へと[暗黙的な型変換][]します。 +falsyである値は`true`へ変換され、falsyではない値は`false`へと変換されます。 + +{{book.console}} +```js +// falsyな値は`true``となる +console.log(!0); // => true +console.log(!""); // => true +console.log(!null); // => true +// falsyではない値は`false`となる +console.log(!42); // => false +console.log(!"文字列"); // => false +``` + NOT演算子は必ず真偽値を返すため、次のように2つNOT演算子を重ねて真偽値へ変換するという使い方も見かけます。 +たとえば、`!!falsyな値`のように2度反転すれば`false`になります。 {{book.console}} ```js const str = ""; -// 空文字列はfalseへと変換される +// 空文字列はfalsyであるため、true -> falseへと変換される console.log(!!str); // => false ``` @@ -884,11 +910,119 @@ console.log(!!str); // => false {{book.console}} ```js const str = ""; -// 空文字列でないことを判定 +// 空文字列(長さが0より大きな文字列)でないことを判定 console.log(str.length > 0); // => false ``` -### グループ化演算子(`(`と`)`) {#group-operator} +## [ES2020] Nullish coalescing演算子(`??`) {#nullish-coalescing-operator} + +Nullish coalescing演算子(`??`)は、左辺の値が**nulish**であるならば、右辺の評価結果を返します。 +**nulish**とは、評価結果が`null`または`undefined`となる値のことです。 + +{{book.console}} + +```js +// 左辺がnullishであるため、右辺の値の評価結果を返す +console.log(null ?? "右辺の値"); // => "右辺の値" +console.log(undefiend ?? "右辺の値"); // => "右辺の値" +// 左辺がnullishではないため、右辺の値の評価結果を返す +console.log(true ?? "右辺の値"); // => true +console.log(false ?? "右辺の値"); // => false +console.log(0 ?? "右辺の値"); // => 0 +console.log("文字列" ?? "右辺の値"); // => "左辺の値" +``` + +Nullish coalescing演算子(`??`)とOR演算子(`||`)は、値のデフォルト値を指定する場合によく利用されています。 +OR演算子(`||`)左辺がfalsyの場合に右辺を評価するため、意図しない結果となる場合が知られています。 + +次のコードは、`inputValue`が未定義だった場合に、`value`に対するデフォルト値をOR演算子(`||`)で指定しています。 +`inputValue`が未定義(`undefined`)の場合は、意図したようにOR演算子(`||`)の右辺で指定した`42`が入ります。 +しかし、`inputValue`が`0`という値であった場合は、`0`はfalsyであるため`value`には右辺の`42`が入ります。 +これでは`0`という値が扱えないため、意図しない動作となっています。 + + +```js +const inputValue = 任意の値または未定義; +// `inputValue`がfalsyの場合は、`value`には`42`が入る +// `inputValue`が`0`の場合は、`value`に`42`が入ってしまう +const value = inputValue || 42; +console.log(value); +``` + +この問題を解決するためにES2020でNullish coalescing演算子(`??`)が導入されています。 + +Nullish coalescing演算子(`??`)では、左辺がnullishの場合のみ、`value`に右辺で指定した`42`が入ります。 +そのため、`inputValue`が`0`という値が入った場合は、`value`にはそのまま`inputValue`の値である`0`が入ります。 + + +```js +const inputValue = 任意の値または未定義; +// `inputValue`がnullishの場合は、`value`には42が入る +// `inputValue`が`0`の場合は、`value`に`0`が入る +const value = inputValue ?? 42; +console.log(value); +``` + +## 条件(三項)演算子(`?`と`:`) {#ternary-operator} + +条件演算子(`?`と`:`)は三項をとる演算子であるため、三項演算子とも呼ばれます。 + +条件演算子は`条件式`を評価した結果が`true`ならば、`Trueのとき処理する式`の評価結果を返します。 +`条件式`が`false`である場合は、`Falseのとき処理する式`の評価結果を返します。 + + +```js +条件式 ? Trueのとき処理する式 : Falseのとき処理する式; +``` + +if文との違いは、条件演算子は式として書くことができるため値を返します。 +次のように、`条件式`の評価結果により`"A"` または `"B"` どちらかを返します。 + +{{book.console}} +```js +const valueA = true ? "A" : "B"; +console.log(valueA); // => "A" +const valueB = false ? "A" : "B"; +console.log(valueB); // => "B" +``` + +条件分岐による値を返せるため、条件によって変数の初期値が違う場合などに使われます。 + +次の例では、`text`文字列に`prefix`となる文字列を先頭につける関数を書いています。 +`prefix`の第二引数を省略したり文字列ではないものが指定された場合に、デフォルトの`prefix`を使います。 +第二引数が省略された場合には、`prefix`に`undefined`が入ります。 + +条件演算子の評価結果は値を返すので、`const`を使って宣言と同時に代入できます。 + +{{book.console}} +```js +function addPrefix(text, prefix) { + // `prefix`が指定されていない場合は"デフォルト:"を付ける + const pre = typeof prefix === "string" ? prefix : "デフォルト:"; + return pre + text; +} + +console.log(addPrefix("文字列")); // => "デフォルト:文字列" +console.log(addPrefix("文字列", "カスタム")); // => "カスタム文字列" +``` + +if文を使った場合は、宣言と代入を分ける必要があるため、`const`を使うことができません。 + +{{book.console}} +```js +function addPrefix(text, prefix) { + let pre = "デフォルト:"; + if (typeof prefix === "string") { + pre = prefix; + } + return pre + text; +} + +console.log(addPrefix("文字列")); // => "デフォルト:文字列" +console.log(addPrefix("文字列", "カスタム")); // => "カスタム文字列" +``` + +## グループ化演算子(`(`と`)`) {#group-operator} グループ化演算子は複数の二項演算子が組み合わさった場合に、演算子の優先順位を明示できる演算子です。 diff --git a/test/summary-test.js b/test/summary-test.js index 1a5292d565..6450bed287 100644 --- a/test/summary-test.js +++ b/test/summary-test.js @@ -40,7 +40,7 @@ describe("SUMMARY", function() { // 許可リスト(読み方の解説など) const allowFilePathList = []; const searchPatterns = ["/falsy/gi"]; - const falsyChapter = path.join(sourceDir, "basic/implicit-coercion/README.md"); + const falsyChapter = path.join(sourceDir, "basic/operator/README.md"); return findUsage(falsyChapter, searchPatterns, allowFilePathList).then(results => { if (results.length === 0) { return; @@ -54,6 +54,25 @@ ${result.matchedTexts.join("\n")} ${message}`); }); }); + + it("nullishの説明をする前にnullishの表記を利用してはいけない", () => { + // 許可リスト(読み方の解説など) + const allowFilePathList = []; + const searchPatterns = ["/nullish/gi"]; + const nullishChapter = path.join(sourceDir, "basic/operator/README.md"); + return findUsage(nullishChapter, searchPatterns, allowFilePathList).then(results => { + if (results.length === 0) { + return; + } + const message = results.map(result => { + return `${result.normalizedFilePath} が利用しているので、確認してください。 +${result.matchedTexts.join("\n")} +`; + }); + throw new Error(`${results.length}件のドキュメントがnullishを説明前に利用しています。 +${message}`); + }); + }); it("prototypeメソッドの説明をする前にObject#methodの表記を利用してはいけない", () => { // 許可リスト(読み方の解説など) const allowFilePathList = [];