Skip to content

Commit

Permalink
feat: Nullish coalescing演算子(??)とOptional chaining(?.) (#1205)
Browse files Browse the repository at this point in the history
Nullish coalescing演算子(`??`)とOptional chaining(`?.`)に関する変更

## 変更点

- [x] **falsy**の説明を演算子の章に移動
  - falsyの対応としてnullishを演算子の章で解説するため
  - BigIntの `0n` も falsy に追加 #445 
- [x] Optional chaining演算子の(`?.`)の解説を"オブジェクト"の章に追加
- [x] Nullish coalescing演算子(`??`)とOptional chaining(`?.`)の組み合わせを説明
- [x] Nullish coalescing演算子(`??`)の解説を"演算子"の章に追加
- [x] 一部のコードを `||` を `??` に置き換え
  - 例としては問題ないけど、`??` 推奨気味に変更

## 追加しなかったこと

- デフォルト値に対するNullish coalescing演算子(`??`)とOptional chaining(`?.`)の組み合わせのパターン
  - 他にもいろいろな書き方があるため含めないようにした
- 関数呼び出しとOptional chaining演算子(`?.`)
  - `window.fn?.()` みたいなケース
  - ユースケースがイマイチ。別の解決方法でも良いと思える気がする

fix #1178 
fix #1179
  • Loading branch information
azu authored Aug 29, 2020
1 parent 5b31466 commit 8aaf25b
Show file tree
Hide file tree
Showing 10 changed files with 491 additions and 83 deletions.
7 changes: 3 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions source/basic/condition/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ if文の`条件式`には`true`または`false`といった真偽値以外の値
- `undefined`
- `null`
- `0`
- `0n`
- `NaN`
- `""`(空文字列)

Expand Down
127 changes: 126 additions & 1 deletion source/basic/function-declaration/OUTLINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
- 引数の扱い
- 関数のシグネチャ
- 引数が少ないとき
- デフォルト引数
- 引数が多い時
- デフォルト引数
- デフォルト引数と `||` の比較
- `||` と Nullish coalescing

- 可変長引数
- arguments
- Rest Parameters
Expand All @@ -20,6 +23,128 @@
- 短縮記法
- まとめ

## 扱っていない。

オプションオブジェクトのデフォルトの値の話はパターンが多すぎて好みの問題になりそう。

- デフォルト引数とオブジェクト
- Object.assign or Spread構文
- デフォルト引数 + Nullish coalescing
- Nullish coalescing演算子(`??`)とOptional chaining(`?.`


関数の引数のデフォルト値を指定する場合にはデフォルト引数を利用することを紹介しました。

しかし、関数の引数にはオブジェクトを渡すこともできます。
デフォルト引数では、仮引数に対応する引数が指定されていなかった場合のデフォルト値です。
そのため、引数として渡されたオブジェクトのプロパティに対するデフォルト値は、デフォルト引数では実現できません。

次のコードの`wrapText`関数では`prefix``suffix`をオプションオブジェクトとして受け取れます。
`options`に対応するオブジェクトを渡さなかった場合のデフォルトオプションをデフォルト引数で指定しています。
`options`を渡さなかった場合は意図した結果となりますが、オプションの一部(`prefix``suffix`の片方)を渡した場合は意図しない結果となります。
これは、デフォルト引数は実際の引数として渡されたオブジェクトをマージをするわけではないためです。

{{book.console}}
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```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}}
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```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(`?.`)を利用することで、デフォルト引数の指定は次のように書き換えることもできます。
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```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")
Expand Down
19 changes: 19 additions & 0 deletions source/basic/function-declaration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ falsyな値とは、真偽値へと変換すると`false`となる次のよう
- `undefined`
- `null`
- `0`
- `0n`
- `NaN`
- `""`(空文字列)

Expand Down Expand Up @@ -198,6 +199,24 @@ console.log(addPrefix("文字列", "")); // => "文字列"
console.log(addPrefix("文字列", "カスタム:")); // => "カスタム:文字列"
```

また、ES2020から導入されたNullish coalescing演算子(`??`)を利用することでも、
OR演算子(`||`)の問題を避けつつデフォルト値を指定できます。

{{book.console}}
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```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}
関数の仮引数に対して引数の個数が多い場合、あふれた引数は単純に無視されます。
Expand Down
3 changes: 2 additions & 1 deletion source/basic/implicit-coercion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,13 @@ JavaScriptでは、どの値が`true`でどの値が`false`になるかは、次
- **falsy**な値は`false`になる
- **falsy**でない値は`true`になる

**falsy**な値とは次の6種類の値のことを言います
**falsy**な値とは次の7種類の値のことを言います

- `false`
- `undefined`
- `null`
- `0`
- `0n`
- `NaN`
- `""`(空文字列)

Expand Down
8 changes: 6 additions & 2 deletions source/basic/map-and-set/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ ES2015では、これらの問題を根本的に解決する`Map`が導入され
たとえばショッピングカートのような仕組みを作るとき、次のように`Map`を使って商品のオブジェクトと注文数をマッピングできます。

{{book.console}}
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```js
// ショッピングカートを表現するクラス
class ShoppingCart {
Expand All @@ -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);
}
// カート内の合計金額を返す
Expand Down Expand Up @@ -313,14 +315,16 @@ obj = null;
このマップを`Map`で実装してしまうと、明示的に削除されるまでイベントリスナーはメモリ上に残り続けます。
ここで`WeakMap`を使うと、`addListener` メソッドに渡された`listener``EventEmitter` インスタンスが参照されなくなった際、自動的に解放されます。
{{book.console}}
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```js
// イベントリスナーを管理するマップ
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);
Expand Down
2 changes: 2 additions & 0 deletions source/basic/object/OUTLINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@
- プロパティの追加
- プロパティの削除
- [コラム] constで定義したオブジェクトは変更可能
- 存在しないプロパティのネストは例外を返す
- プロパティが定義済みかを確認する方法
- undefinedとの比較
- in演算子を使う
- `hasOwnProperty`メソッド(インスタンスメソッド)
- Optional chaining(`?.`)でのアクセス方法
- オブジェクトの静的メソッド
- オブジェクトのプロパティの列挙
- `Object.keys`メソッド
Expand Down
104 changes: 104 additions & 0 deletions source/basic/object/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```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}}
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```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}}
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
```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`メソッドは、オブジェクト自身を文字列化するメソッドです。
Expand Down
Loading

0 comments on commit 8aaf25b

Please sign in to comment.