Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Nullish coalescing演算子(??)とOptional chaining(?. #1205

Merged
merged 52 commits into from
Aug 29, 2020
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
4f5505e
feat(operator): Nullish coalescing演算子を追加
azu Jul 5, 2020
e4a223c
feat(function-declaration): デフォルト引数とNullish coalescing演算子
azu Jul 5, 2020
09b7bc1
feat(object): Optional chaining(`?.`)演算子についてを追加
azu Jul 5, 2020
f4884bd
fix(map-and-set): Nullish coalescing演算子(`??`)を利用
azu Jul 5, 2020
a6f35e4
chore: add <!-- doctest:meta:{ "ECMAScript": 2020 } -->
azu Jul 5, 2020
b99f806
chore: fixup!
azu Jul 5, 2020
b3cccc7
update
azu Jul 19, 2020
01e1b87
refactor(operator): falsyについてを追加
azu Jul 19, 2020
9f1d5bf
fixup
azu Jul 19, 2020
83718cf
test: fix test
azu Jul 19, 2020
499a558
fixup
azu Jul 19, 2020
57b6207
fixup!
azu Jul 19, 2020
c832a6f
fixup
azu Jul 19, 2020
612be1c
fixup!
azu Jul 19, 2020
ec07d2e
fix(function-declaration): ??を追加
azu Jul 19, 2020
b49556d
fixup!
azu Jul 19, 2020
260f12f
fix
azu Jul 19, 2020
e5522d4
fix
azu Jul 19, 2020
afdc240
fixup!
azu Jul 19, 2020
0341e8a
fixup!
azu Jul 19, 2020
324009a
fix
azu Jul 19, 2020
120b613
fixup!
azu Jul 19, 2020
bd955c0
fixup!
azu Jul 19, 2020
f5af3b5
fixup!
azu Jul 19, 2020
c8b09c3
fix
azu Jul 19, 2020
8661302
fix
azu Jul 19, 2020
0140286
fix: Optional Chainigの解釈を修正
azu Jul 19, 2020
488c982
fix
azu Jul 19, 2020
aed465e
Update source/basic/operator/README.md
azu Aug 22, 2020
14849ab
Update source/basic/operator/README.md
azu Aug 22, 2020
fcfeb3a
fix
azu Jul 19, 2020
bd0a91e
fix
azu Aug 22, 2020
f5e17d2
fix
azu Aug 22, 2020
3b6f577
fix
azu Aug 22, 2020
2e92b10
fix
azu Aug 22, 2020
1ade65f
feat: falsyは0nも含める
azu Aug 22, 2020
07cd52b
fix
azu Aug 22, 2020
f6b1b5c
fix(function): remove デフォルト引数とオブジェクト
azu Aug 22, 2020
fb400f6
fix
azu Aug 29, 2020
bc163d2
udpate codemirror
azu Aug 29, 2020
9d27c84
Update source/basic/object/README.md
azu Aug 29, 2020
6401107
feat: ブラケット記法についてを追加
azu Aug 29, 2020
ea20ff0
fix
azu Aug 29, 2020
381c1f8
fix
azu Aug 29, 2020
42d97fc
fix
azu Aug 29, 2020
b255172
fix
azu Aug 29, 2020
ee54eb1
fix
azu Aug 29, 2020
63bacf5
fix
azu Aug 29, 2020
a97b7e6
fix
azu Aug 29, 2020
18767e8
fix
azu Aug 29, 2020
59566ff
fix
azu Aug 29, 2020
82e7c9b
fix
azu Aug 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
73 changes: 73 additions & 0 deletions source/basic/function-declaration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,79 @@ 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("文字列", "カスタム:")); // => "カスタム:文字列"
```

### デフォルト引数とオブジェクト {#default-parameters-nullish-coalescing}
azu marked this conversation as resolved.
Show resolved Hide resolved

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

しかし、関数の引数にオブジェクトを指定したい場合には、デフォルト引数だけでは問題が発生します。
そのオブジェクトのプロパティに対するデフォルト値はデフォルト引数では指定できません。

次のコードの`wrapText`関数は`prefix`と`suffix`プロパティをもつ`options`オブジェクトを引数として受け取ります。
`options`オブジェクトに対応する引数を渡さなかった場合は、デフォルト引数で指定したデフォルトのオブジェクトが利用されます。
また、明示的に`prefix`と`suffix`プロパティをもつオブジェクトを引数に渡せば、その値を利用します。

しかし、オプションの一部のプロパティ(`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: ":カスタム" })); // => "接頭辞:文字列:カスタム"
```

### 呼び出し時の引数が多いとき {#function-more-arguments}

関数の仮引数に対して引数の個数が多い場合、あふれた引数は単純に無視されます。
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
79 changes: 79 additions & 0 deletions source/basic/object/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,85 @@ if (obj.hasOwnProperty("key")) {
この動作の違いを知るにはまずプロトタイプオブジェクトという特殊なオブジェクトについて理解する必要があります。
次の章の「[プロトタイプオブジェクト][]」で詳しく解説するため、次の章で`in`演算子と`hasOwnProperty`メソッドの違いを見ていきます。

## [ES2020] Optional chaining演算子(`?.`) {#optional-chaining-operator}

プロパティの存在を確認する方法として`undefined`との比較や`in`演算子、`hasOwnProperty`メソッドについて紹介しました。

存在を確認して真偽値を取得することが目的ではなく、そのプロパティの評価結果を得る目的ならば、if文で`undefined`と比較しても問題ありません。

次のコードでは、`widget.window.title`プロパティにアクセスできるなら、そのプロパティの値を表示します。

{{book.console}}
```js
function printWidgetTitle(widget) {
azu marked this conversation as resolved.
Show resolved Hide resolved
// 例外を避けるために`widget.window.title`の存在を確認してから、値を表示している
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`プロパティにアクセスして、例外が発生してしまうのを避けるためです。

しかし、プロパティへアクセスするたびにif文で`undefined`と比較してAND演算子(`&&`)でつなげて書いていくと冗長です。
azu marked this conversation as resolved.
Show resolved Hide resolved

ES2020ではネストしたプロパティの存在確認とアクセスを簡単に行う構文としてOptional chaining演算子(`?.`)が導入されました。
Optional chaining演算子(`?.`)は、ドット記法(`.`)の代わりに`?.`をプロパティアクセスに使うだけです。

Optional chaining演算子(`?.`)は左辺のオペランドがnullish(`null`または`undefined`)の場合は、それ以上評価せずに`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`を返す
// Optional chaining(`?.`)ではない場合は例外が発生する
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 ?? "未定義";
azu marked this conversation as resolved.
Show resolved Hide resolved
console.log(`ウィジェットのタイトルは${wtitle}です`);
}
printWidgetTitle({
window: {
title: "Book Viewer"
}
}); // => "ウィジェットのタイトルはBook Viewerです"
printWidgetTitle({
// タイトルが定義されてない空のオブジェクト
}); // => "ウィジェットのタイトルは未定義です"
```

## `toString`メソッド {#toString-method}

オブジェクトの`toString`メソッドは、オブジェクト自身を文字列化するメソッドです。
Expand Down
Loading