Skip to content

Commit 8aaf25b

Browse files
authored
feat: Nullish coalescing演算子(??)とOptional chaining(?.) (#1205)
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
1 parent 5b31466 commit 8aaf25b

File tree

10 files changed

+491
-83
lines changed

10 files changed

+491
-83
lines changed

package-lock.json

Lines changed: 3 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

source/basic/condition/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ if文の`条件式`には`true`または`false`といった真偽値以外の値
6161
- `undefined`
6262
- `null`
6363
- `0`
64+
- `0n`
6465
- `NaN`
6566
- `""`(空文字列)
6667

source/basic/function-declaration/OUTLINE.md

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
- 引数の扱い
77
- 関数のシグネチャ
88
- 引数が少ないとき
9-
- デフォルト引数
109
- 引数が多い時
10+
- デフォルト引数
11+
- デフォルト引数と `||` の比較
12+
- `||` と Nullish coalescing
13+
1114
- 可変長引数
1215
- arguments
1316
- Rest Parameters
@@ -20,6 +23,128 @@
2023
- 短縮記法
2124
- まとめ
2225

26+
## 扱っていない。
27+
28+
オプションオブジェクトのデフォルトの値の話はパターンが多すぎて好みの問題になりそう。
29+
30+
- デフォルト引数とオブジェクト
31+
- Object.assign or Spread構文
32+
- デフォルト引数 + Nullish coalescing
33+
- Nullish coalescing演算子(`??`)とOptional chaining(`?.`
34+
35+
36+
関数の引数のデフォルト値を指定する場合にはデフォルト引数を利用することを紹介しました。
37+
38+
しかし、関数の引数にはオブジェクトを渡すこともできます。
39+
デフォルト引数では、仮引数に対応する引数が指定されていなかった場合のデフォルト値です。
40+
そのため、引数として渡されたオブジェクトのプロパティに対するデフォルト値は、デフォルト引数では実現できません。
41+
42+
次のコードの`wrapText`関数では`prefix``suffix`をオプションオブジェクトとして受け取れます。
43+
`options`に対応するオブジェクトを渡さなかった場合のデフォルトオプションをデフォルト引数で指定しています。
44+
`options`を渡さなかった場合は意図した結果となりますが、オプションの一部(`prefix``suffix`の片方)を渡した場合は意図しない結果となります。
45+
これは、デフォルト引数は実際の引数として渡されたオブジェクトをマージをするわけではないためです。
46+
47+
{{book.console}}
48+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
49+
```js
50+
// `options`が指定されなかったときは空のオブジェクトが入る
51+
function wrapText(text, options = { prefix: "接頭辞:", suffix: ":接尾辞" }) {
52+
return options.prefix + text + options.suffix;
53+
}
54+
console.log(wrapText("文字列")); // => "接頭辞:デフォルト:接尾辞"
55+
console.log(wrapText("文字列", {
56+
prefix: "Start:",
57+
suffix: ":End"
58+
})); // => "Start:文字列:End"
59+
// オプションの一部だけを指定した場合に意図しない結果となる
60+
console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:デフォルトundefined"
61+
console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "undefined文字列:カスタム"
62+
```
63+
64+
このときの`prefix``suffix`のそれぞれのデフォルト値は、デフォルト引数とNullish coalescing演算子(`??`)を使うことで実現できます。
65+
次のように、`options`オブジェクトそのものが渡されなかった場合のデフォルト引数として空オブジェクト(`{}`)を指定します。
66+
そして、`options``prefix``suffix`プロパティそれぞれに対してNullish coalescing演算子(`??`)を使いデフォルト値を指定しています。
67+
68+
{{book.console}}
69+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
70+
```js
71+
// `options`が指定されなかったときは空のオブジェクトが入る
72+
function wrapText(text, options = {}) {
73+
const prefix = options.prefix ?? "接頭辞:";
74+
const suffix = options.suffix ?? ":接尾辞";
75+
return prefix + text + suffix;
76+
}
77+
// falsyな値を渡してもデフォルト値は代入されない
78+
console.log(wrapText("文字列")); // => "接頭辞:文字列:接尾辞"
79+
console.log(wrapText("文字列", {
80+
prefix: "Start:",
81+
suffix: ":End"
82+
})); // => "Start:文字列:End"
83+
// オプションの一部だけを指定した場合は、それぞれのデフォルト値が採用される
84+
console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:文字列:接尾辞"
85+
console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "接頭辞:文字列:カスタム"
86+
```
87+
88+
Optional chaining(`?.`)を利用することで、デフォルト引数の指定は次のように書き換えることもできます。
89+
90+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
91+
```js
92+
function wrapText(text, options) {
93+
// `options`がundefinedまたはnullの時点で右辺を評価する
94+
const prefix = options?.prefix ?? "接頭辞:";
95+
const suffix = options?.suffix ?? ":接尾辞";
96+
return prefix + text + suffix;
97+
}
98+
// falsyな値を渡してもデフォルト値は代入されない
99+
console.log(wrapText("文字列")); // => "接頭辞:文字列:接尾辞"
100+
console.log(wrapText("文字列", {
101+
prefix: "Start:",
102+
suffix: ":End"
103+
})); // => "Start:文字列:End"
104+
// オプションの一部だけを指定した場合は、それぞれのデフォルト値が採用される
105+
console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:文字列:接尾辞"
106+
console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "接頭辞:文字列:カスタム"
107+
```
108+
109+
さらにDestructuring + デフォルト引数で次のようにも書けます。
110+
111+
```js
112+
function wrapText(text, { prefix = "接頭辞:", suffix = ":接尾辞" }) {
113+
return prefix + text + suffix;
114+
}
115+
console.log(wrapText("文字列")); // => "接頭辞:デフォルト:接尾辞"
116+
console.log(wrapText("文字列", {
117+
prefix: "Start:",
118+
suffix: ":End"
119+
})); // => "Start:文字列:End"
120+
// オプションの一部だけを指定した場合に意図しない結果となる
121+
console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:デフォルトundefined"
122+
console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "undefined文字列:カスタム"
123+
```
124+
125+
126+
さらにオブジェクトマージを使うと次のような書き方もあります。
127+
128+
```js
129+
const DefaultOptions = { prefix: "接頭辞:", suffix: ":接尾辞" }
130+
function wrapText(text, options) {
131+
const optionsWithDefault = {
132+
...DefaultOptions,
133+
...options
134+
}
135+
return optionsWithDefault.prefix + text + optionsWithDefault.suffix;
136+
}
137+
console.log(wrapText("文字列")); // => "接頭辞:デフォルト:接尾辞"
138+
console.log(wrapText("文字列", {
139+
prefix: "Start:",
140+
suffix: ":End"
141+
})); // => "Start:文字列:End"
142+
// オプションの一部だけを指定した場合に意図しない結果となる
143+
console.log(wrapText("文字列", { prefix: "カスタム:" })); // => "カスタム:デフォルトundefined"
144+
console.log(wrapText("文字列", { suffix: ":カスタム" })); // => "undefined文字列:カスタム"
145+
```
146+
147+
23148
## Issues
24149
25150
* [Destructuring · Issue #113 · asciidwango/js-primer](https://github.com/asciidwango/js-primer/issues/113 "Destructuring · Issue #113 · asciidwango/js-primer")

source/basic/function-declaration/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ falsyな値とは、真偽値へと変換すると`false`となる次のよう
165165
- `undefined`
166166
- `null`
167167
- `0`
168+
- `0n`
168169
- `NaN`
169170
- `""`(空文字列)
170171

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

202+
また、ES2020から導入されたNullish coalescing演算子(`??`)を利用することでも、
203+
OR演算子(`||`)の問題を避けつつデフォルト値を指定できます。
204+
205+
{{book.console}}
206+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
207+
```js
208+
function addPrefix(text, prefix) {
209+
// prefixがnullまたはundefinedの時、デフォルト値を返す
210+
const pre = prefix ?? "デフォルト:";
211+
return pre + text;
212+
}
213+
214+
console.log(addPrefix("文字列")); // => "デフォルト:文字列"
215+
// falsyな値でも意図通りに動作する
216+
console.log(addPrefix("文字列", "")); // => "文字列"
217+
console.log(addPrefix("文字列", "カスタム:")); // => "カスタム:文字列"
218+
```
219+
201220
### 呼び出し時の引数が多いとき {#function-more-arguments}
202221
203222
関数の仮引数に対して引数の個数が多い場合、あふれた引数は単純に無視されます。

source/basic/implicit-coercion/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,13 @@ JavaScriptでは、どの値が`true`でどの値が`false`になるかは、次
180180
- **falsy**な値は`false`になる
181181
- **falsy**でない値は`true`になる
182182

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

185185
- `false`
186186
- `undefined`
187187
- `null`
188188
- `0`
189+
- `0n`
189190
- `NaN`
190191
- `""`(空文字列)
191192

source/basic/map-and-set/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ ES2015では、これらの問題を根本的に解決する`Map`が導入され
193193
たとえばショッピングカートのような仕組みを作るとき、次のように`Map`を使って商品のオブジェクトと注文数をマッピングできます。
194194

195195
{{book.console}}
196+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
196197
```js
197198
// ショッピングカートを表現するクラス
198199
class ShoppingCart {
@@ -202,7 +203,8 @@ class ShoppingCart {
202203
}
203204
// カートに商品を追加する
204205
addItem(item) {
205-
const count = this.items.get(item) || 0;
206+
// `item`がない場合は`undefined`を返すため、Nullish coalescing演算子(`??`)を使いデフォルト値として`0`を設定する
207+
const count = this.items.get(item) ?? 0;
206208
this.items.set(item, count + 1);
207209
}
208210
// カート内の合計金額を返す
@@ -313,14 +315,16 @@ obj = null;
313315
このマップを`Map`で実装してしまうと、明示的に削除されるまでイベントリスナーはメモリ上に残り続けます。
314316
ここで`WeakMap`を使うと、`addListener` メソッドに渡された`listener``EventEmitter` インスタンスが参照されなくなった際、自動的に解放されます。
315317
318+
{{book.console}}
319+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
316320
```js
317321
// イベントリスナーを管理するマップ
318322
const listenersMap = new WeakMap();
319323

320324
class EventEmitter {
321325
addListener(listener) {
322326
// this にひもづいたリスナーの配列を取得する
323-
const listeners = listenersMap.get(this) || [];
327+
const listeners = listenersMap.get(this) ?? [];
324328
const newListeners = listeners.concat(listener);
325329
// this をキーに新しい配列をセットする
326330
listenersMap.set(this, newListeners);

source/basic/object/OUTLINE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
- プロパティの追加
3030
- プロパティの削除
3131
- [コラム] constで定義したオブジェクトは変更可能
32+
- 存在しないプロパティのネストは例外を返す
3233
- プロパティが定義済みかを確認する方法
3334
- undefinedとの比較
3435
- in演算子を使う
3536
- `hasOwnProperty`メソッド(インスタンスメソッド)
37+
- Optional chaining(`?.`)でのアクセス方法
3638
- オブジェクトの静的メソッド
3739
- オブジェクトのプロパティの列挙
3840
- `Object.keys`メソッド

source/basic/object/README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,110 @@ if (obj.hasOwnProperty("key")) {
466466
この動作の違いを知るにはまずプロトタイプオブジェクトという特殊なオブジェクトについて理解する必要があります。
467467
次の章の「[プロトタイプオブジェクト][]」で詳しく解説するため、次の章で`in`演算子と`hasOwnProperty`メソッドの違いを見ていきます。
468468

469+
## [ES2020] Optional chaining演算子(`?.`) {#optional-chaining-operator}
470+
471+
プロパティの存在を確認する方法として`undefined`との比較、`in`演算子、`hasOwnProperty`メソッドを紹介しました。
472+
最終的に取得したいものがプロパティの値であるならば、if文で`undefined`と比較しても問題ありません。
473+
なぜなら、値を取得したい場合には、プロパティが存在するかどうかとプロパティの値が`undefined`かどうかの違いを区別する意味はないためです。
474+
475+
次のコードでは、`widget.window.title`プロパティにアクセスできるなら、そのプロパティの値をコンソールに表示しています。
476+
477+
{{book.console}}
478+
```js
479+
function printWidgetTitle(widget) {
480+
// 例外を避けるために`widget`のプロパティの存在を順場に確認してから、値を表示している
481+
if (widget.window !== undefined && widget.window.title !== undefined) {
482+
console.log(`ウィジェットのタイトルは${widget.window.title}です`);
483+
} else {
484+
console.log("ウィジェットのタイトルは未定義です");
485+
}
486+
}
487+
// タイトルが定義されているwidget
488+
printWidgetTitle({
489+
window: {
490+
title: "Book Viewer"
491+
}
492+
});
493+
// タイトルが未定義のwidget
494+
printWidgetTitle({
495+
// タイトルが定義されてない空のオブジェクト
496+
});
497+
```
498+
499+
この`widget.window.title`のようなネストしたプロパティにアクセスする際には、プロパティの存在を順番に確認してからアクセスする必要があります。
500+
なぜなら、`widget`オブジェクトが`window`プロパティを持っていない場合は`undefined`という値を返すためです。このときに、さらにネストした`widget.window.title`プロパティにアクセスすると、`undefined.title`という参照となり例外が発生してしまいます。
501+
502+
しかし、プロパティへアクセスするたびに`undefined`との比較をAND演算子(`&&`)でつなげて書いていくと冗長です。
503+
504+
この問題を解決するために、ES2020ではネストしたプロパティの存在確認とアクセスを簡単に行う構文としてOptional chaining演算子(`?.`)が導入されました。
505+
Optional chaining演算子(`?.`)は、ドット記法(`.`)の代わりに`?.`をプロパティアクセスに使います。
506+
507+
Optional chaining演算子(`?.`)は左辺のオペランドがnullish(`null`または`undefined`)の場合は、それ以上評価せずに`undefined`を返します。一方で、プロパティが存在する場合は、そのプロパティの評価結果を返します。
508+
509+
つまり、Optional chaining演算子(`?.`)では、存在しないプロパティへアクセスした場合でも例外ではなく、`undefined`という値を返します。
510+
511+
{{book.console}}
512+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
513+
```js
514+
const obj = {
515+
a: {
516+
b: "objのaプロパティのbプロパティ"
517+
}
518+
};
519+
// obj.a.b は存在するので、その評価結果を返す
520+
console.log(obj?.a?.b); // => "objのaプロパティのbプロパティ"
521+
// 存在しないプロパティのネストも`undefined`を返す
522+
// ドット記法の場合は例外が発生してしまう
523+
console.log(obj?.notFound?.notFound); // => undefined
524+
// undefinedやnullはnullishなので、`undefined`を返す
525+
console.log(undefined?.notFound?.notFound); // => undefined
526+
console.log(null?.notFound?.notFound); // => undefined
527+
```
528+
529+
先ほどのウィジェットのタイトルを表示する関数もOptional chaining演算子(`?.`)を使うと、if文を使わずに書けます。
530+
次のコードの`printWidgetTitle`関数では、`widget?.window?.title`にアクセスできる場合はその評価結果が変数`title`に入ります。
531+
プロパティにアクセスできない場合は`undefined`を返すため、Nullish coalescing演算子(`??`)によって右辺の`"未定義"`が変数`title`のデフォルト値となります。
532+
533+
{{book.console}}
534+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
535+
```js
536+
function printWidgetTitle(widget) {
537+
const title = widget?.window?.title ?? "未定義";
538+
console.log(`ウィジェットのタイトルは${title}です`);
539+
}
540+
printWidgetTitle({
541+
window: {
542+
title: "Book Viewer"
543+
}
544+
}); // => "ウィジェットのタイトルはBook Viewerです"
545+
printWidgetTitle({
546+
// タイトルが定義されてない空のオブジェクト
547+
}); // => "ウィジェットのタイトルは未定義です"
548+
```
549+
550+
また、Optional chaining演算子(`?.`)はブラケット記法(`[]`)と組み合わせることもできます。
551+
ブラケット記法の場合も、左辺のオペランドがnullish(`null`または`undefined`)の場合は、それ以上評価せずに`undefined`を返します。一方で、プロパティが存在する場合は、そのプロパティの評価結果を返します。
552+
553+
{{book.console}}
554+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
555+
```js
556+
const languages = {
557+
ja: {
558+
hello: "こんにちは!"
559+
},
560+
en: {
561+
hello: "Hello!"
562+
}
563+
};
564+
const langJapanese = "ja";
565+
const langKorean = "ko";
566+
const messageKey = "hello";
567+
// Optional chaining演算子(`?.`)とブラケット記法を組みわせた書き方
568+
console.log(languages?.[langJapanese]?.[messageKey]); // => "こんにちは!"
569+
// `languages`に`ko`プロパティが定義されていないため、`undefined`を返す
570+
console.log(languages?.[langKorean]?.[messageKey]); // => undefined
571+
```
572+
469573
## `toString`メソッド {#toString-method}
470574
471575
オブジェクトの`toString`メソッドは、オブジェクト自身を文字列化するメソッドです。

0 commit comments

Comments
 (0)