はじめに
JavaScript を久しぶりに触ったところ、全然覚えられていなかったのでメモ書き程度に残しておく。
ただ、このブログは JavaScript を体系的に学ぶものではなく、勉強になったことや落とし穴だと思ったものを列挙する。
データ
オブジェクトとプリミティブ
内部的に別のデータやそのデータに対する処理を持つことができるのはオブジェクト
それ以外はプリミティブ (非オブジェクト) と呼ばれる。
プリミティブは 7 種類: undefined、null、boolean、number、string、symbol、bigint
なお typeof null を実行すると object になるのは後方互換性のために残されたバグ
console.assert(typeof null === "object")
また、“abc”.toUpperCase() のようにプリミティブがメソッドを持っているように見えるのは、オートボクシングのため。
エンジンが一時的にプリミティブを String オブジェクトでラップし、メソッドを呼び、そのラッパーを破棄する。プリミティブ自体はプロパティを保持しないため、書き込みは黙って失敗する:
const s = "abc";
s.foo = 1; // ラッパーが作られ、変更され、捨てられる
console.log(s.foo); // undefined
ラッパーオブジェクトはプリミティブではなく Object。
new String(“abc”) は Object、String(“abc”) はプリミティブ。
typeof new String("abc") // "object"
new String("a") === "a" // false
Boolean(new Boolean(false)) // true ← あらゆるオブジェクトは truthy
リテラル
リテラル(literal) とは、値をソースコードに「そのまま直接書いた表記」のこと。変数や関数呼び出しの結果として得るのではなく、コードに書かれた文字列がそのまま値になる。
const a = 42; // 42 は数値リテラル
const b = "hello"; // "hello" は文字列リテラル
const c = a; // a はリテラルではない(変数の参照)
const d = a + 1; // これもリテラルではない(式の評価結果)
リテラルとプリミティブは別の軸の概念。
- プリミティブ = 値の 種類(型が Object でない)
- リテラル = 値の 書き方(ソースコード上の記法)
なので組み合わせは 4 通りある:
┌──────────────┬──────────────┬─────────────────────────────┐ │ │ リテラル記法 │ 非リテラル │ ├──────────────┼──────────────┼─────────────────────────────┤ │ プリミティブ │ 42、“abc” │ Number(“42”)、x + y │ ├──────────────┼──────────────┼─────────────────────────────┤ │ Object │ {}、[1,2] │ new Object()、Array.of(1,2) │ └──────────────┴──────────────┴─────────────────────────────┘
なお、undefined はリテラルではなくグローバルオブジェクトのプロパティ名。システムに初めから用意されている定数だと考えることができる。
null 合体演算子
左辺が null または undefined のときだけ右辺を返す演算子
|| は falsy (false、0、""、NaN、null、undefined) すべてで右辺に切り替わるが、?? は nullish (null or undefined) のみ。
function setVolume(vol) {
this.volume = vol || 50; // vol=0 (ミュート) が 50 になる
this.volume = vol ?? 50; // 0 はそのまま、未指定なら 50
}
?. (オプショナルチェーン) と組み合わせるのが最も一般的な用法:
const city = user?.address?.city ?? "不明";
// user が null でも、address がなくても、city が null でも "不明"
オプショナルチェーン演算子 (?.)
オブジェクトの null/undefined チェックを行い、オブジェクトが存在する時だけプロパティアクセスを行う。
iggy?.params?.potential // iggy && iggy.params && iggy.params.potential と同義
厳密な等価演算子、抽象的な等価演算子
厳密な等価演算子 (===) はオペランドの型と値両方が等しい場合に true を返し、抽象的な等価演算子 (==) は可能であれば数値に変換し、そのあとで比較をする。
1 === 1 //true
1 === "1" // false
null === undefined // false
1 == "1" // true
1 == true // true
null == undefined //true
基本的には厳密な等価演算子を使うことが推奨。
ただ val == null (null or undefined) とした方が簡潔に記述できる場合がある。
分割代入
オブジェクト (または配列) の要素をまとめて取得し、変数に代入する場合に便利。 分割代入と認識させるためには () で囲む必要がある。
また、プロパティの後に ”:” を続けて書くことで変数名を記述できる。さらに undefined の場合のデフォルト値も書ける。
> params = {power: "B", speed: "C", range: "D", durabiliity: "C", precision: "D"}
{
power: 'B',
speed: 'C',
range: 'D',
durabiliity: 'C',
precision: 'D'
}
> ({power:p, development:d="F"} = params)
{
power: 'B',
speed: 'C',
range: 'D',
durabiliity: 'C',
precision: 'D'
}
> p
'B'
> d
'F'
処理
ラベル付き break
ラベルのない break は一番近くの繰り返しブロックから抜け出すが、ラベルを指定するとそのラベルが付与された処理から抜けることができる。
> outer: for (let i=0; i<3; i++) {
... innter: for (let j=0; j<3; j++) {
... console.log(`${i} ${j}`)
... if (j == 2) break outer
... }
... }
0 0
0 1
0 2
for-of と for-in
配列の各要素に対して処理を行う場合、for-of, オブジェクトの各要素に対して処理を行う場合、for-in。
> days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
[
31, 28, 31, 30, 31,
30, 31, 31, 30, 31,
30, 31
]
> for (const day of days) {
... console.log(day)
... }
31
28
31
30
31
30
31
31
30
31
30
31
> params = {power: "B", speed: "C", range: "D", durabiliity: "C", precision: "D"}
{
power: 'B',
speed: 'C',
range: 'D',
durabiliity: 'C',
precision: 'D'
}
> for (const i in params) {
... console.log(`${i}`)
... }
power
speed
range
durabiliity
precision
なお、配列はオブジェクトの一種なので for-in でも処理できる。ただその場合はループ変数に代入される値はインデックス値になる。
関数
関数もオブジェクトの一種であり、関数式やアロー関数のように変数に代入できる。
関数の種類
関数宣言
function FN(args) {
}
関数式
FN = function(args){
}
関数名を省略して無名関数 (=匿名関数) として定義できる。
関数を引数として受け取るような高階関数に使える。
> [1,4,5].map(function(n) {return n*n})
[ 1, 16, 25 ]
関数定義本文の評価は関数が呼び出された時に行われるので以下のコードも動く
> let fact = function(num) {
... return num === 1 ? 1 : num * fact(num-1)
... }
undefined
> fact(3)
6
アロー関数
FN = (args) => {
}
引数が 1 つの場合は () を省略でき、本文が単一の式になる場合は {} や return を省略できる。
> let fact = num => num === 1 ? 1 : num * fact(num-1)
残余引数
任意の数の引数を処理する場合に使用できる。
> function getAverage(n1, n2, ...ns) {
... console.log(n1, n2, ns)
... }
undefined
> getAverage(22, 42, 42, 52, 62)
22 42 [ 42, 52, 62 ]
クラス
簡単なクラス
特に指定しない場合は、暗黙的に Object クラスがスーパークラスになる。
class Vector2 {
constructor(x, y) {
this.x = x
this.y = y
}
lengthSq() {
return this.x * this.x + this.y * this.y
}
}
インスタンスを作成するには new 演算子を使う
let v = new Vector2(3, 4)
console.log(v.lengthSq()) // 25
クラスも関数のように式で定義できる
const Vector2 = class {
constructor(x, y) {
this.x = x
this.y = y
}
}
静的プロパティ、静的メソッド
静的プロパティ、静的メソッドはインスタンスによらない、クラス自身のためのプロパティ、メソッド。インスタンスではなくクラス経由でアクセスする。
class Vector2 {
static instanceCount = 0
static incrementCount() {
this.instanceCount++
}
constructor(x, y) {
this.x = x
this.y = y
Vector2.incrementCount()
}
lengthSq() {
return this.x * this.x + this.y * this.y
}
}
let v = new Vector2(3, 4)
console.log(v.instanceCount) // undefined
console.log(Vector2.instanceCount) // 1
console.log(Vector2.incrementCount()) // undefined
console.log(Vector2.instanceCount) // 2
オブジェクトとしてのクラス
オブジェクトリテラルをクラスのように使える
let tusk1 = {
cry: "chumimin",
act1() {
console.log(this.cry)
},
act2() {
this.act1()
console.log("Tail")
}
}
継承もできる。継承する場合は親にあたるオブジェクトを proto プロパティに渡す。
let tusk2 = {
__proto__: tusk1,
act3() {
super.act2()
console.log("Head")
}
}
オブジェクト
ボックス化
プリミティブに対して、メソッド、プロパティへのアクセスを行うと内部的にプリミティブに対応するオブジェクトに変換し、処理を続行する。
> (42).toString()
'42'
> true.toString()
'true'
> 42.toString() // . が小数点なのか判断ができないのでエラー
42.toString()
^^^
Uncaught SyntaxError: Invalid or unexpected token
> 42..toString() // 代わりに .. を使用すると小数点と区別できるので成功する
'42'
Infinity
JavaScript だと 0 で割り算するとエラーにならず Infinity になる
> 1/0
Infinity
文字列→数値
parseFloat(), parseInt() を使う
> Number.parseInt("100")
100
文字列
JavaScript は内部的に UTF-16 を採用している。
16 bit を一符号単位として、一符号単位で表せるものは “基本多言語面 (BMP)” とよび、二符号単位で表されるものは “サロゲートペア” と呼ぶ。
Symbol オブジェクト
シンボルは唯一リテラルを持たないプリミティブ
シンボルは等価演算子が真になる値を作り出すことができないプリミティブであり、新しく作成するシンボルは全て一意の値を持つ。
> const s1 = Symbol()
undefined
> const s2 = Symbol()
undefined
> s1 == s2
false
プロパティのキーとしてシンボルを使用することで、意図しない上書きから守ることができる。(この時、キーを変数に格納しておかないと、後で取り出せなくなる)
> power = Symbol("power")
Symbol(power)
> theFool = {[power]: "B"}
{ [Symbol(power)]: 'B' }
> theFool[power]
'B'
globalThis オブジェクト
JavaScript のどこからでも利用できるオブジェクト。
globalThis のプロパティアクセスには globalThis. を省略でき、Number や Boolean もこのオブジェクトのプロパティ。
配列
配列の初期化は主に以下の 4 つ
> days = Array.of(31, 28, 31, 30)
[ 31, 28, 31, 30 ]
> day2 = Array.from(days)
[ 31, 28, 31, 30 ]
> days == day2
false
> empty = new Array(4)
[ <4 empty items> ]
> day = [30, 31, 28]
[ 30, 31, 28 ]
some, every メソッド
some は条件式を渡して、条件が成り立つが要素があれば true
every は条件式を渡して、全ての要素が条件を満たせば true
> days.some(n => n < 30)
true
> days.every(n => n < 30)
false
sort
sort() は辞書順で並び替えされるので数値として比較されるわけではない
> [34, 31, 13, 2, 6, 2, 5, 11].sort()
[
11, 13, 2, 2,
31, 34, 5, 6
]
数値として比較したい場合は比較する関数を渡す必要がある。
> [34, 31, 13, 2, 6, 2, 5, 11].sort((a,b) => a-b)
[
2, 2, 5, 6,
11, 13, 31, 34
]
reduce
配列の各要素を順に処理して値を集約するような状況で使う。
コールバック関数と初期値を渡す。(初期値を省略できるが、その場合は 1 番目の要素が初期値になる)
コールバック関数の引数は、前回のコールバック関数の実行結果 (sum)、および各要素 (n)。
> [1,3,5,7,9].reduce((sum, n) => sum += n, 0)
25
Map
任意のオブジェクトをキーにできるコレクション。
Object オブジェクトは文字列とシンボルだけキーに取れる。
> map = new Map()
Map(0) {}
> map.set(1, "number")
Map(1) { 1 => 'number' }
> map.set("1", "string")
Map(2) { 1 => 'number', '1' => 'string' }
ジェネレーター関数
イテレーターを作る関数
ジェネレーター関数を呼び出すと、定義された処理を実行するのではなく、処理を実行するジェネレーターオブジェクトを返す。
function の代わりに function* と書く。
また、繰り返し処理を行うために yeild 式を使える。
> function* generator() {
... console.log("処理1")
... yield 1
... console.log("処理2")
... yield 2
... console.log("処理3")
... yield 3
... return 4
... }
undefined
> generator = generator()
Object [Generator] {}
> generator.next()
処理1
{ value: 1, done: false }
> generator.next()
処理2
{ value: 2, done: false }
> generator.next()
処理3
{ value: 3, done: false }
> generator.next()
{ value: 4, done: true }
バイナリデータ
バイナリデータを扱うには Arraybuffer オブジェクトが使える。
ただ、データを保持するのみであり、読み書きする手段はない。
> ab = new ArrayBuffer(8)
ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00>,
byteLength: 8
}
> ab.byteLength
8
読み書きするには、ビューと呼ばれるオブジェクトにする必要がある。
ビューは大きく分けて、型付き配列と DataView オブジェクトがある。
// 型付き配列
> i8 = new Int8Array(ab)
Int8Array(8) [
0, 0, 0, 0,
0, 0, 0, 0
]
> i8[0] = 8
8
> ab
ArrayBuffer {
[Uint8Contents]: <08 00 00 00 00 00 00 00>,
byteLength: 8
}
// DataView
> data = new DataView(ab)
DataView {
byteLength: 8,
byteOffset: 0,
buffer: ArrayBuffer {
[Uint8Contents]: <08 00 00 00 00 00 00 00>,
byteLength: 8
}
}
> data.getInt8(0)
8
> data.getInt16(0)
2048
> data.setInt16(0, 1024)
undefined
> data.getInt8(0)
4
> data.getInt16(0).toString(2).padStart(16, "0")
'0000010000000000'
DataView は自由度が高く汎用性が高いが、記述が煩雑になりやすい
JSON
JSON 形式の文字列と JSON オブジェクトを相互に変換するには以下。(一般的にオブジェクトを復元可能な文字列やバイト列にすることを直列化、シリアライズと呼ぶ)
> arry = JSON.parse('{"id": 1, "name":"Echoes", "power":["E", "C", "B"]}')
{ id: 1, name: 'Echoes', power: [ 'E', 'C', 'B' ] }
> JSON.stringify(arry)
'{"id":1,"name":"Echoes","power":["E","C","B"]}'
JSON オブジェクトに toJSON メソッドがあると stringify 実行時に toJSON メソッドの結果が返される。
> arry = {"id": 1, "name":"Echoes", "power":["E", "C", "B"], toJSON() {return "TEST"}}
{
id: 1,
name: 'Echoes',
power: [ 'E', 'C', 'B' ],
toJSON: [Function: toJSON]
}
> JSON.stringify(arry)
'"TEST"'
JSON オブジェクトに限らずコピーはシャロ―コピーになる。
> arry = {"id": 1, "name":"Echoes", "power":["E", "C", "B"]}
{ id: 1, name: 'Echoes', power: [ 'E', 'C', 'B' ] }
> arry2 = arry
{ id: 1, name: 'Echoes', power: [ 'E', 'C', 'B' ] }
> arry2["id"] = 2
2
> arry["id"]
2
JSON オブジェクトの場合、 JSON.stringify と JSON.parse を順に実行すればディープコピーができる。
> arry = {"id": 1, "name":"Echoes", "power":["E", "C", "B"]}
{ id: 1, name: 'Echoes', power: [ 'E', 'C', 'B' ] }
> arry2 = JSON.parse(JSON.stringify(arry))
{ id: 1, name: 'Echoes', power: [ 'E', 'C', 'B' ] }
> arry2["id"] = 2
2
> arry["id"]
1
非同期処理
Web ブラウザはシングルスレッドで動作し、概念的には以下のようなループを実行しているものと考えられる。(イベントループと呼ばれる)
while(true) {
schedule(taskQueue)
const task = taskQueue.pop()
process(task)
}
非同期処理を行わないと特定のタスクがブロックしてしまい、ほかのタスクが実行できなくなってしまう。
コールバック処理
ボタンを押したときなどのイベントを処理するためによく使用される。
イベントを処理するためのコールバック関数はイベントハンドラやイベントリスナーと呼ばれる。
以下はコールバック関数を定義し、10 秒後に実行させる処理
> let callbackFunc = () => console.log("10秒")
undefined
> setTimeout(callbackFunc, 10000)
Timeout {
_idleTimeout: 10000,
_idlePrev: [TimersList],
_idleNext: [TimersList],
_idleStart: 3952282,
_onTimeout: [Function: callbackFunc],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(refed)]: true,
[Symbol(kHasPrimitive)]: false,
[Symbol(asyncId)]: 1151,
[Symbol(triggerId)]: 6,
[Symbol(kAsyncContextFrame)]: undefined
}
> 10秒
何度も発生するようなイベントに対して都度処理するという用途には向いているが、一般的な非同期処理全般には向いてない。
その理由として callback hell がある。
コールバック関数の結果をもとにさらにコールバック関数を呼び… といったことが繰り返されるとコードの可読性が低下する。(例えば API を呼び出すときとか)
Promise
まだ完了していない(かもしれない)結果を渡すオブジェクト
その処理が完了していなくても即座に Promise オブジェクトを返し、完了後の処理を then() メソッドの引数で定義できる。
> function waitFor(msec) {
return new Promise(resolve => {
setTimeout(resolve, msec)
})
}
> waitFor(5000).then(() => console.log("success"))
Promise {
<pending>,
[Symbol(async_id_symbol)]: 1268,
[Symbol(trigger_async_id_symbol)]: 1266
}
> success
Promise の状態は「pending」「fullfilled」「rejected」があり fullfilled と rejected はまとめて settled (完了) と呼ばれる。
また、then メソッドの第二引数に失敗時の処理が書ける。
function waitForAMoment(msec) {
return new Promise((resolve, reject) => {
// 成功時に resolve を呼び出し、失敗時に reject を呼び出す
if (msec < 2000) {
setTimeout(() => resolve(msec), msec)
} else {
setTimeout(() => reject(new Error("Timeout")), 2000)
}
})
}
> waitForAMoment(1000).then(value => console.log(`success: ${value}`), error => console.log(`fail: ${error}`))
Promise {
<pending>,
[Symbol(async_id_symbol)]: 337,
[Symbol(trigger_async_id_symbol)]: 335
}
> success: 1000
> waitForAMoment(5000).then(value => console.log(`success: ${value}`), error => console.log(`fail: ${error}`))
Promise {
<pending>,
[Symbol(async_id_symbol)]: 367,
[Symbol(trigger_async_id_symbol)]: 365
}
> fail: Error: Timeout
then() メソッドの戻り値は、コールバック関数の戻り値を Promise.resolve() 関数に渡したものと考えるとわかりやすい。
また、失敗時の処理は catch() メソッドで書くことで可読性があがり、finally() メソッドで戻り値に影響を与えずに最終的な処理を実行できる。
Promise.resolve(42)
.then(() => console.log("1"))
.then(() => {
console.log("2")
return Promise.reject()
})
.then(() => console.log("3"))
.catch(() => console.log("エラー"))
.finally(() => console.log("終了"))
//1
//2
//エラー
//終了
async, await
async によって修飾された関数は実行結果が Promise になる
async 関数では return で値を返せば fullfilled, throw で例外を発生させれば rejected の Promise が返る。
> async function answer() {
... return 42
... }
undefined
> promise = answer()
Promise {
42,
[Symbol(async_id_symbol)]: 476,
[Symbol(trigger_async_id_symbol)]: 6
}
> promise.then(result => console.log(result))
Promise {
<pending>,
[Symbol(async_id_symbol)]: 529,
[Symbol(trigger_async_id_symbol)]: 476
}
> 42
await 式は Promise をパラメータとして受け取り、その Promise の解決を待ってから解決された値を返す。
function waitFor(msec) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("Tick"), msec)
})
}
async function count(num) {
for (let i=num; 0<=i; i--) {
const tick = waitFor(1000)
console.log(tick, i)
}
}
> count(3)
Promise {
<pending>,
[Symbol(async_id_symbol)]: 638,
[Symbol(trigger_async_id_symbol)]: 6
}
> Tick 3 // 1秒置きに表示される。await がないと一気に出る
Tick 2
Tick 1
Tick 0
非同期イテレータ
連続的な非同期データを処理できる。
非同期イテレータでは next() メソッドを持つが、その戻り値は Promise
一般的なイテレータと同じように、非同期イテレータにも非同期イテラブルと for-await-of 文がある。
さらにジェネレータ関数に async 修飾子を追加すると非同期ジェネレータ関数になる。
これらを使用して 1 秒ごとに FizzBuzz を表示する関数を定義するとこうなる
async function* makeFizzBuzzAsyncGenerator(max) {
for (let count=1; count<=max; count++) {
await new Promise(resolve => setTimeout(resolve, 1000))
if (count % 6 == 0) {
yield "FizzBuzz"
} else if (count % 2 == 0) {
yield "Fizz"
} else if (count % 3 == 0) {
yield "Buzz"
} else {
yield count
}
}
}
非同期ジェネレータ関数の戻り値は非同期ジェネレータであり、非同期イテレータ、非同期イテラブルである。
for await (let value of makeFizzBuzzAsyncGenerator(7)) {
console.log(value)
}
1
Fizz
Buzz
Fizz
5
FizzBuzz
7
undefined
Proxy
その名の通り、オブジェクトへのプロキシをするようなオブジェクト
Proxy オブジェクトのコンストラクタの引数は、プロキシ対象のオブジェクトと、ハンドラオブジェクト (オブジェクトへのアクセスを仲介する際の追加処理)。
以下のような状況に適している
- バリデーション / 型チェックの自動化
- ロギング / デバッグ用のインターセプト
- リアクティブシステムの構築
- API のラッパー / 抽象化レイヤー
- デフォルト値の自動提供
get, set でプロパティアクセスに割り込む例:
const user = { name: "Takashi", age: 30 };
const proxy = new Proxy(user, {
get(target, prop) {
console.log(`📖 "${prop}" を読み取り`);
return target[prop]; // 直接アクセス
},
set(target, prop, value) {
console.log(`✏️ "${prop}" を "${value}" に変更`);
target[prop] = value; // 直接代入
return true; // set トラップは true を返す必要がある
}
});
> proxy.name
📖 "name" を読み取り
'Takashi'
> proxy.age = 31
✏️ "age" を "31" に変更
31
バリデーションの例:
const person = new Proxy({}, {
set(target, prop, value) {
if (prop === "age") {
if (typeof value !== "number") throw new TypeError("age は数値のみ");
if (value < 0 || value > 150) throw new RangeError("age は 0〜150");
}
target[prop] = value;
return true;
}
});
> person.name = "Takashi"
'Takashi'
> person.age = 30
30
> person.age = 200
Uncaught RangeError: age は 0〜150
at Object.set (REPL34:5:43)
関数呼び出しへの割り込み例 (apply):
function sum(a, b) {
return a + b;
}
const proxiedSum = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`🔧 sum(${args.join(", ")}) が呼ばれました`);
const result = target(...args); // 元の関数を実行
console.log(`→ 結果: ${result}`);
return result;
}
});
> proxiedSum(3,5)
🔧 sum(3, 5) が呼ばれました
→ 結果: 8
8
Reflect
オブジェクトへの基本操作(読み取り、書き込み、削除など)をメソッドとして提供するビルトインオブジェクト
主に Proxy オブジェクトの中で使われ、基本的にはトラップメソッドの引数をそのまま Reflect のメソッドに渡すだけなので統一的な書き方ができる。
また、結果が boolean で返るので try-catch で例外処理をする必要がない。
let array = {0:3.14, 1:9.8, 2:2.718, length:3}
arrayProxy = new Proxy(array, {
get(target, prop) {
if (prop in target) {
return Reflect.get(target, prop)
} else {
return Reflect.get(Array.prototype, prop)
}
},
set(target, prop, value) {
return Reflect.set(target, prop, value)
}
})
> arrayProxy[1]
9.8
> arrayProxy[3]
undefined
> arrayProxy[2] = 8.31
8.31
> array
{ '0': 3.14, '1': 9.8, '2': 8.31, length: 3 }