O'Reilly JavaScript第7版を読んだので、自分の学習メモを主目的として気づきをまとめます。
前置き
本投稿内容は第14章までの内容アウトプットになります。
お仕事柄、第15章 「Webブラウザ上の JavaScript」 のメモが多くなりそうなのでその手前までで分けることにしました。
実務では TypeScirpt を使うエンジニアリングマネージャーをしていますが、体系立てて JavaScript を学んだことがほぼ無かったため、本書を手に取って、スキマ時間で読み進めてきました。今回を機に、新たに勉強になったこと・解釈間違いなどを整理してメモとしておきます。
Null合体演算子(??)
左辺のオペランドが
null
でもundefined
でもない場合は、右辺の値を返します。
とのこと。なんとなく理解していたことを今回ちゃんと理解。
||
に似ているが、0
や ''
など、 Falsy な値での挙動が異なる。
let options = { timeout: 0, title: '', verbose: false, n: null }
console.log(options.timeout ?? 1000) // => 0
console.log(options.title ?? 'Undefined') // => ''
console.log(options.verbose ?? true) // => false
console.log(options.quiet ?? false) // => false
console.log(options.n ?? 10) // => 10
for/of文
Ruby の #each
, #map
に慣れてしまっているので、JavaScript でも forEach
や map
を使うことが多く、 for
を使う意識が希薄になっていた。
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9],
sum = 0
for (let element of data) {
sum += element
}
console.log(sum) // => 45
Object.keys()
, Object.values()
, Object.entries()
を使ってあげればforEach などで書くよりシンプルに書けるので便利そう。
let o = { x: 1, y: 2, z: 3 }
let keys = ''
for (let k of Object.keys(o)) {
keys += k
}
console.log(keys) // => 'xyz'
let values = 0
for (let v of Object.values(o)) {
values += v
}
console.log(values) // => 123
let pairs = ''
for (let [k, v] of Object.entries(o)) {
pairs += k + v
}
console.log(pairs) // => "x1y2z3"
Set
標準ライブラリを網羅的・体系的にインプットした経験が無かったので、効率的に学習して定着できた気がする。
Set は配列と同じ、値の集合。配列とは違って順序やインデックスは存在しない。Set()コンストラクタの引数は、反復可能なオブジェクトであれば使える。また重複は許さない↓
let unique = new Set('Mississippi')
console.log(unique) // => Set(4) { 'M', 'i', 's', 'p' }
add()
, delete()
, clear()
を使っていつでも要素の増減が可能だが、Set で最も重要なのは、要素を追加したり削除することではない。重要なのは、ある値が Set のメンバーであるかどうかを調べること。has()
メソッドで調べることが出来、配列の includes()
よりも高速らしい。
let oneDigitPrimes = new Set([2, 3, 5, 7])
console.log(oneDigitPrimes.has(2)) // => true
console.log(oneDigitPrimes.has(3)) // => true
console.log(oneDigitPrimes.has(4)) // => false
console.log(oneDigitPrimes.has('5')) // => false
console.log(oneDigitPrimes.has(9)) // => false
oneDigitPrimes.add(9)
console.log(oneDigitPrimes.has(9)) // => true
反復可能なオブジェクトであれば使えるので、先程出てきた for/of ループで巡回できる。
let sum = 0
for (let p of oneDigitPrimes) {
sum += p
}
console.log(sum) // => 26
Map
Map は他の値に関連付けられた値の集合配列と同じ値の集合。Map()コンストラクタの引数は、反復可能なオブジェクトであれば使えるが、[key, value]
という2つの要素を持つ配列が生成されるようにする。
let m = new Map([
['one', 1],
['two', 2],
])
console.log(m) // => Map(2) { 'one' => 1, 'two' => 2 }
set() を使って新たなキー/バリューのペアを追加できる。ただし、すでに存在するキーを渡すと上書きする。わお
指定したキーに関連付けられたバリューは、get() を使って読み出せる。
let m = new Map()
m.set('one', 1)
m.set('two', 2)
console.log(m) // => Map(2) { 'one' => 1, 'two' => 2 }
console.log(m.get('two')) // => 2
console.log(m.get('three')) // => undefined
m.set('one', true)
console.log(m.get('one')) // => true
Map クラスも反復可能なので、for/ofループで巡回可能。
let m = new Map([
['x', 1],
['y', 2],
])
for (let [k, v] of m) {
console.log(`key: ${k}, value: ${v}`)
}
// => key: x, value: 1
// => key: y, value: 2
また、スプレッド演算子で展開することで、キーだけ・バリューだけ・キー/バリューのペアで取り出せる。知識が繋がっていって楽しい。
let m = new Map([
['one', 1],
['two', 2],
])
console.log([...m.keys()]) // => ['one', 'two']
console.log([...m.values()]) // => [1, 2]
console.log([...m.entries()]) // => [['one', 1], ['two', 2]]
正規表現
不勉強だったために存在すら知らなかった便利な機構が複数あった。業務では正規表現を扱うシーンが多く、モダンブラウザがお相手なので使いこなしていきたい。
サブパターン
() で囲まれた箇所はグループにマッチする文字が記憶され、後続で \1
, \2
・・・の形式で再利用可能。下の例だと、 \1
は fun.+
の文字列と記憶される、ということか。
let r = /JavaScript\sis\s(fun.+)\1/g
console.log('JavaScript is fun!fun!'.match(r)) // => ['JavaScript is fun!fun!']
console.log('JavaScript is fun!'.match(r)) // => null
インデックスのインクリメント管理は煩わしいし、表意文字である日本語が母語である自分にとっては、コードも読み手に意味を持たせたいと思っていたら・・↓
名前付きキャプチャグループ
ES2018 から導入。上記を数字のインクリメントではなく任意の名称を振れるようになった!(?<xxx>)
の形式。以下はアメリカの郵送住所を表す正規表現。
let r = /(?<city>\w+) (?<state>[A-Z]{2}) (?<zipcode>\d{5})(?<zip9>-\d{4})?/g
console.log('aaa AA 12345-1234'.match(r)) // => ['aaa AA 12345-1234']
後述でこのグループ呼び出しについて言及するが、グループ呼び出ししないケースにおいても、名前をつけておくだけでだいぶ読みやすい。
先読み言明
x(?=y)
の形式で、x に y が続く場合のみ x にマッチすることを判定できる。相当便利だ。もっと早く知りたかった。
let r = /JavaScript?(?=\:)/g
console.log('JavaScript: The Definitive Guide'.match(r)) // => ['JavaScript']
console.log('Java in a Nutshell'.match(r)) // => null
否定先読み言明
x(?!y)
の形式で、x に y が続かない場合のみ x にマッチしすることを判定できる。
let r = /Java(?!Script)([A-Z]\w*)/g
console.log('JavaBeans'.match(r)) // => ['JavaBeans']
console.log('Javanese'.match(r)) // => null
console.log('JavaScript'.match(r)) // => null
後読み言明、否定後読み言明
ES2018 では、 (?<=y)x
, (?<!y)x
でそれぞれ後読み言明、否定後読み言明もできるようになっている。後は繰り返しなので詳しくは下記を参照
match()
String オブジェクトの正規表現メソッドで有用なメソッド。g フラグがあればマッチしたすべての文字列を配列で返す。
console.log('7 plus 8 equals 15'.match(/\d+/g)) // => ['7', '8', '15']
- g フラグがない場合も配列となる
- この場合、1番目の要素はマッチした文字列
- 2番目以降は、丸括弧で囲まれた正規表現にマッチした文字列が格納される
let url = /(\w+):\/\/([\w.]+)\/(\S*)/
let text = 'Visit my blog at http://www.example.com/~david'
let match = text.match(url)
let fullUrl, protocol, host, path
if (match !== null) {
fullUrl = match[0]
protocol = match[1]
host = match[2]
path = match[3]
}
console.log(fullUrl) // => 'http://www.example.com/~david'
console.log(protocol) // => 'http'
console.log(host) // => 'www.example.com'
console.log(path) // => '~david'
g フラグがない場合にmatch() メソッドが返す配列にはいくつかのプロパティがある。
プロパティ | 説明 |
---|---|
input | match() が呼び出された文字列 |
index | マッチした箇所の始点 |
groups | 名前付きキャプチャと同じ名前のプロパティ |
let text = 'Visit my blog at http://www.example.com/~david'
let url = /(?<protocol>\w+):\/\/(?<host>[\w.]+)\/(?<path>\S*)/
let match = text.match(url)
console.log(match)
// =>
// [
// 'http://www.example.com/~david',
// 'http',
// 'www.example.com',
// '~david',
// index: 17,
// input: 'Visit my blog at http://www.example.com/~david',
// groups: [Object: null prototype] {
// protocol: 'http',
// host: 'www.example.com',
// path: '~david'
// }
// ]
console.log(match2.groups.protocol) // => 'http'
やはり名前付きキャプチャが個人的に落ち着く体験だ。グループ呼び出しできるのは素敵。
Promise - then() メソッドの戻り値 -
then() メソッドにコールバック関数を渡すとき、新たな Promise(p1) オブジェクトを返す。この Promise(p1) は then() に渡した処理が終わると fulfilled になるのだが、then() に渡す処理2パターンに応じて挙動が変わる。
then() に渡すコールバック関数が Promise 以外の値を返すとき
then() の返した Promise はコールバック関数の戻り値がその値となって、fulfilled になる。
そのため、then 内部で setTimeout
等の待機処理を入れてしまうと、意図した Promise チェーンにならない。
Promise.resolve()
.then(() => {
setTimeout(() => {
console.log('Promise 返さない then1')
}, 2000)
})
.then(() => {
setTimeout(() => {
console.log('Promise 返さない then2')
}, 1000)
})
// => Promise 返さない then2(1秒後に)
// => Promise 返さない then1(その更に1秒後に)
then() に渡したコールバック関数が Promise を返すとき
その Promise を 便宜上、p2 とすると、then() の返した Promise(p1) は resolved にはなるが fulfilled にはならない。
Promise(p2) が fulfilled になると、p2 と同じ値で p1 も fulfilled になる。p2 が rejected になると、p2 と同じ値で p1 も rejected になる。
Promise.resolve(2)
.then((value) => {
return new Promise((resolve) => {
setTimeout(() => {
const v = value * 2
console.log('Promise 返す then1: ', v)
resolve(v)
}, 2000)
})
})
.then((value) => {
return new Promise((resolve) => {
setTimeout(() => {
const v = value * 2
console.log('Promise 返す then2: ', v)
resolve(v)
}, 3000)
})
})
// => Promise 返す then1: 4(2秒後に)
// => Promise 返す then2: 8(その更に3秒後に)
過去何度かハマったことがあったのだが、そういう仕様だったのか。