💨

個人JavaScriptコードレシピ

2024/02/14に公開

文字列を配列に分解

const str = 'string'
console.log([...str])
console.log(str.split(''))
実行結果
[ 's', 't', 'r', 'i', 'n', 'g' ]
[ 's', 't', 'r', 'i', 'n', 'g' ]

文字列のfor...of

for (str of 'string') {
  console.log(str)
}
実行結果
s
t
r
i
n
g

先頭大文字、残りを小文字に変換

str = 'hELLO'
console.log(str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase())
実行結果
Hello

indexOfによる文字列の出現回数カウント

const str = 'Hello World'
let count = 0
const keyword = 'o'
let position = str.indexOf(keyword)

while (position !== -1) {
  count++
  position = str.indexOf(keyword, position + keyword.length)
}
console.log(`${count}件ヒット`)
実行結果
2件ヒット

文字列をUnicode正規化

Unicode正規化することにより全角半角を跨いだ検索が可能となる。

normalizeメソッドの構文
/*
  str: 元の文字列
  form: 正規化の形式 (NFD, NFC, NFKD, NFKCのいずれか)
              引数省略の場合は NFCがデフォルトで設定される
*/
str.normalize(form)
const list = ['アイウエオ']
const type = ['NFD', 'NFC', 'NFKD', 'NFKC']

for (let t of type) {
  for (let l of list) {
    console.log(`${t}: ${l} => ${l.normalize(t)}`)
  }
}
実行結果
---------- NFD ----------
アイウエオ => アイウエオ
abcABC => abcABC
123 => 123
---------- NFC ----------
アイウエオ => アイウエオ
abcABC => abcABC
123 => 123
---------- NFKD ----------
アイウエオ => アイウエオ
abcABC => abcABC
123 => 123
---------- NFKC ----------
アイウエオ => アイウエオ
abcABC => abcABC
123 => 123

NaNの比較

「==」、「===」演算子でNaNを比較しても正しい結果は得られないため、NumberオブジェクトのisNanメソッドを使用する。

// 必ずfalseになる
console.log(NaN === NaN)
// NaNを正しく比較するにはisNaNメソッドを使用する
console.log(Number.isNaN(NaN))
実行結果
false
true

Number型のフォーマット

const num = 1234.567
const fmt = new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY',
  currencyDisplay: 'symbol'
})
console.log(fmt.format(num))
実行結果
¥1,235

min 〜 maxの乱数

乱数範囲が50 〜 100

const min = 50
const max = 100
console.log(Math.floor(Math.random() * (max - min + 1)) + min)
実行結果
55

乱数範囲が0 〜 100

console.log(Math.floor(Math.random() * 101))
実行結果
66

配列から任意の要素を取得

const list = [1, 2, 3, 4, 5]
console.log(list[Math.floor(Math.random() * list.length)])
実行結果
5

月の末日

console.log(new Date(2024, 2, 0).toLocaleDateString())
実行結果
2024/2/29

日付差

const dt1 = new Date(2000, 0, 1)
const dt2 = new Date(2000, 0, 15)
const diff = (dt2.getTime() - dt1.getTime()) / (1000 * 60 * 60 * 24)
console.log(`${diff}日の差がある`)
実行結果
14日の差がある

日付/時刻値を文字列に変換

toLocalexxxString()メソッドは現在の環境での地域情報に応じて最適な形式で、日付/時刻を文字列化する。

const dt = new Date(2000, 0, 1, 12, 15, 30, 45)

console.log(dt)

console.log(dt.toLocaleString())

console.log(dt.toLocaleDateString())

console.log(dt.toLocaleTimeString())

console.log(dt.toISOString())

console.log(dt.toDateString())

console.log(dt.toJSON())
実行結果
// 変換なし
Sat Jan 01 2000 12:15:30 GMT+0900 (日本標準時)

// toLocaleString
2000/1/1 12:15:30

// toLocaleDateString
2000/1/1

// toLocaleTimeString
12:15:30

// toISOString
2000-01-01T03:15:30.045Z

// toDateString
Sat Jan 01 2000

// toJSON
2000-01-01T03:15:30.045Z

Date型のフォーマット

const dt = new Date(2000, 0, 1, 12, 15, 30, 45)
const fmt = new Intl.DateTimeFormat('ja-JP', {
  year: 'numeric',
  month: 'short',
  day: '2-digit',
  weekday: 'long',
  hour12: true,
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  dayPeriod: 'short'
})
console.log(fmt.format(dt))
実行結果
2000年1月01日土曜日 昼00:15:30

ラベル構文のbreak

loop:
for (let i = 0; i < 10; i++) {
  if (i === 1) {
    break loop
  }
  console.log(i)
}
実行結果
0
label2: {
  label1: {
    console.log('label1 block')
    break label2
  }
  console.log('label2 block')
}
実行結果
label1 block

配列のインデックス

先頭から見た場合のインデックスは0始まり: 0, 1, 2, ...
末尾から見た場合のインデックスは-1始まり: ..., -2, -1

末尾から要素を取得
const list = [0, 1, 2, 3, 4, 5]

for(let i = -1; i > -list.length; i--) {
  console.log(list.at(i))
} 
5
4
3
2
1

配列の要素取得

ブラケット構文とatメソッド2パターンがある。
atメソッドは負のインデックスに対応している。

let list = [1, 2, 3, 4, 5]
// ブラケット構文
console.log(list[0])
// atメソッド
console.log(list.at(-1))
実行結果
1
5

配列に複数要素を追加/置換/削除

spliceメソッドの構文
/*
  list: 元の配列
  start: 開始位置
  count: 要素数
  items: 書き換え後の要素 (可変長引数)
*/ 
list.splice(start, conut, items)
要素の追加
const list = [0, 1, 2, 3, 4, 5]
console.log(list.splice(3, 0, 'x', 'y', 'z'))
console.log(list)
実行結果

0,1,2,x,y,z,3,4,5
要素の置換
const list = [0, 1, 2, 3, 4, 5]
console.log(list.splice(3, 2, 'x', 'y', 'z'))
console.log(list)
実行結果
3,4
0,1,2,x,y,z,5
要素の削除
const list = [0, 1, 2, 3, 4, 5]
console.log(list.splice(3, 2))
console.log(list)

const list2 = [0, 1, 2, 3, 4, 5]
console.log(list2.splice(3))
console.log(list2)
実行結果
// list
3,4
0,1,2,5

// list2
3,4,5
0,1,2

配列のすべて要素位置を検索

const list = [0, 1, 2, 3, 4, 5, 1]
const keyword = 1
const result = []

list.forEach((value, index) => {
  if (value === keyword) {
    result.push(index)
  }
})
console.log(result)
実行結果
[ 1, 6 ]

入れ子配列をフラット化

flatメソッドの構文
/*
  list: 元の配列
  depth: 平坦化する階層 (規定は1), 不特定数の階層の場合はInfinityを指定
*/
list.flat(depth)
const list = [0, 1, 2, 3, 4, 5, [6, 7, [8, 9]], [10]]
console.log(list.flat())
console.log(list.flat(Infinity))
実行結果
[ 0, 1, 2, 3, 4, 5, 6, 7, [ 8, 9 ], 10 ]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

多次元配列の要素数取得

const list = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]
console.log(list.flat().length)
実行結果
9

配列内の要素を移動

配列内の指定された要素を、同じ配列の別の場所に複製する

copyWithinメソッドの構文
/*
  list: 元の配列
  tartget: 移動先の位置
  start: コピー開始位置
  end: コピー終了位置
*/
copyWithin(target, start, end)
console.log([0, 1, 2, 3, 4, 5].copyWithin(3, 4, 6))
console.log([0, 1, 2, 3, 4, 5].copyWithin(1, 2))
console.log([0, 1, 2, 3, 4, 5].copyWithin(2))
console.log([0, 1, 2, 3, 4, 5].copyWithin(3, -6, -3))
実行結果
[ 0, 1, 2, 4, 5, 5 ]
[ 0, 2, 3, 4, 5, 5 ]
[ 0, 1, 0, 1, 2, 3 ]
[ 0, 1, 2, 0, 1, 2 ]

配列ライクなオブジェクトを配列化

配列ライクなオブジェクトとは、配列ような見た目を持つが、配列ではないオブジェクトのこと。
Map、Set、HTMLCollection/NoList、arguments、Stringが相当する。

fromメソッドの構文
/*
  obj: 配列ライクなオブジェクト
  mapFn: 値変換に利用する関数
  thisArg: 引数mapFnでthisが表す値
*/
Array.from(obj, mapFn, thisArg)
DOMから要素を取得
<form>
  <select id="select-box">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
  </select>
</form>
const opts = Array.from(document.querySelector('#select-box').options)
opts.forEach((opt) => {
  console.log(opt.value)
})
実行結果
1
2
3

初期値を持った配列を生成

インデックスで初期化
const list = Array.from(
  { length: 5 },
  (value, index) => {
    return index
  }
)
console.log(list)
実行結果
[ 0, 1, 2, 3, 4 ]
全要素を固定値で初期化
const data = new Array(5)
data.fill('-', 0, data.length)
console.log(data)
実行結果
[ '-', '-', '-', '-', '-' ]
特定範囲の連番を生成
const begin = 10
const end = 20
const step = 3

const list = Array.from(
  { length: (end - begin) / step + 1 },
  (valuue, index) => {
    return begin + (index * step)
  }
)
console.log(list)
実行結果
[ 10, 13, 16, 19 ]

配列をシャローコピー

fromメソッド
const list = [0, 1, [0, 1, 2]]
const copy = Array.from(list)
copy[2][0] = 100
console.log(list)
実行結果
[ 0, 1, [ 100, 1, 2 ] ]
スプレッド構文
const list = [0, 1, [0, 1, 2]]
const copy = [...list]
copy[2][0] = 100
console.log(list)
実行結果
[ 0, 1, [ 100, 1, 2 ] ]
scliceメソッド
const list = [0, 1, [0, 1, 2]]
const copy = list.slice()
copy[2][0] = 100
console.log(list)
実行結果
[ 0, 1, [ 100, 1, 2 ] ]
concatメソッド
const list = [0, 1, [0, 1, 2]]
const copy = list.concat()
copy[2][0] = 100
console.log(list)
実行結果
[ 0, 1, [ 100, 1, 2 ] ]

配列を数値順で並べ替える

sortメソッドの構文
/*
  list: 元の配列
  m, n: 比較する要素
  statements: 比較ルール
*/
list.sort(fuction(m, n) {
   ...statements...
})
昇順
const list = [5, 25, 10].sort((m, n) => {
  return m - n
})
console.log(list)
実行結果
[ 5, 10, 25 ]
降順
const list = [5, 25, 10].sort((m, n) => {
  return n -m
})
console.log(list)
実行結果
[ 25, 10, 5 ]

配列を任意のルールで並べ替える

const levels = ['one', 'two', 'three']
const list = [
  { name: 'name0', level: 'three' },
  { name: 'name1', level: 'two' },
  { name: 'name2', level: 'one' },
  { name: 'name3', level: 'one' },
  { name: 'name4', level: 'three' },
  { name: 'name5', level: 'two' },
]
const result = list.sort((m, n) => {
  return levels.indexOf(m.level) - levels.indexOf(n.level)
})
console.log(result)
実行結果
[
  { name: 'name2', level: 'one' },
  { name: 'name3', level: 'one' },
  { name: 'name1', level: 'two' },
  { name: 'name5', level: 'two' },
  { name: 'name0', level: 'three' },
  { name: 'name4', level: 'three' }
]

配列をランダムに並べ替える

const list = [0, 1, 2, 3, 4].sort(() => {
  // -0.5 〜 0.5の範囲の乱数
  return Math.random() - 0.5
})
console.log(list)
実行結果
[ 2, 1, 3, 0, 4 ]

配列の内容を順に処理

forEachメソッドの構文
/*
  list: 元の配列
  value: インデックス値
  index: 元の配列
  array: 元の配列
  statements: 要素に対する処理, fuctionとアロー関数ではthisArgの扱いが異なる
  thisArg: コールバック関数でthisが表す値
*/
list.forEach(fuction(value, index, array) {
   ...statements...
}, thisArg)
const obj = { x: 0, y: 1, z: 2 }
const list = ['x', 'y']

list.forEach(function(value) {
  console.log(this[value])
}, obj)
実行結果
0
1

配列を指定されたルールで加工

mapメソッドの構文
/*
  list: 元の配列
  value: 要素値
  index: インデックス値
  array: 元の配列
  statements: 要素に対する処理 (戻り値は加工後の値)
  thisArg: コールバック関数でthisが表す値
*/
list.map(fuction(value, index, array) {
   ...statements...
}, thisArg)
const list = [1, 2, 3]
const result = list.map((value) => {
  return value * 10
})
console.log(result)
実行結果
[ 10, 20, 30 ]
flatMap (map + flat) メソッド

mapとflatメソッドの組み合わせ処理よりもわずかならがら効率的。

const list = [1, 2, ,3, null]
const result = list.flatMap((value) => {
  // 平坦化による空白列は取り除かれるので取り除きたい要素は空配列を返却
  if (value === null) {
    return []
  }
  return [value * 10, value * 100]
})
console.log(result)
実行結果
[ 10, 100, 20, 200, 30, 300 ]

任意の条件式によって配列を検索

findメソッドの構文
/*
  list: 元の配列
  value: 要素値
  index: インデックス値
  array: 元の配列
  statements: 要素値を判定するための処理 (戻り値はtrue/false)
  thisArg: コールバック関数でthisが表す値
*/
list.find(fuction(value, index, array) {
   ...statements...
}, thisArg)
const list = [
  { name: 'name0', level: 0 },
  { name: 'name1', level: 1 },
  { name: 'name2', level: 2 },
]

let result = list.find((value) => {
  // 判定結果がtrueとなった要素を取得する
  return value.name.includes('0')
})
console.log(result)

// アロー関数の省略形
result = list.find(value => value.name.includes('0'))
console.log(result)
実行結果
{ name: 'name0', level: 0 }
{ name: 'name0', level: 0 }
findIndexメソッド

インデックス値を取得したい場合に使用する。
構文はfindメソッドと同じ。

const list = [
  { name: 'name0', level: 0 },
  { name: 'name1', level: 1 },
  { name: 'name2', level: 2 },
]
const index = list.findIndex(value => value.name.includes('0'))
console.log(index)
実行結果
0

条件式に合致する要素が存在するかを判定

someメソッドの構文

合致する要素が1つでも存在する場合に使用する。

/*
  list: 元の配列
  value: 要素値
  index: インデックス値
  array: 元の配列
  statements: 要素値を判定するための処理 (戻り値はtrue/false)
  thisArg: コールバック関数でthisが表す値
*/
list.some(fuction(value, index, array) {
   ...statements...
}, thisArg)
const list = [
  { name: 'name0', level: 0 },
  { name: null, level: null },
  { name: 'name1', level: 1 },
  { name: 'name2', level: 2 },
]
let result = list.some(value => value.level > 1)
console.log(result)

// 任意の条件で中断
result = list.some((value) => {
  if (value.name === null) {
    return true
  }
  console.log(value)
})
console.log(result)
実行結果
true
{ name: 'name0', level: 0 }
true
everyメソッドの構文

すべての要素が合致する場合に使用する。
構文はsomeメソッドと同じ。

const list = [
  { name: 'name0', level: 0 },
  { name: 'name1', level: 1 },
  { name: 'name2', level: 2 },
]
const result = list.every(value => value.level >= 0)
console.log(result)
実行結果
true

配列から条件に合致した要素だけを取得

filterメソッドの構文
/*
  list: 元の配列
  value: 要素値
  index: インデックス値
  array: 元の配列
  statements: 要素値を判定するための処理 (戻り値はtrue/false)
  thisArg: コールバック関数でthisが表す値
*/
list.filter(fuction(value, index, array) {
   ...statements...
}, thisArg)
const list = [
  { name: 'name0', level: 0 },
  { name: 'name1', level: 1 },
  { name: 'name2', level: 2 },
]
const result = list.filter(value => value.level > 0)
console.log(result)
実行結果
[ { name: 'name1', level: 1 }, { name: 'name2', level: 2 } ]

配列の要素を順に処理して1つにまとめる

reduceメソッドの構文
/*
  list: 元の配列
  result: 前回までの結果
  value: 要素値
  index: インデックス値
  array: 元の配列
  statements: 要素値への処理
  initial: 初期値, 最初のループでresultに渡す値
*/
list.reduce(fuction(result, value, index, array) {
   ...statements...
}, initial)
const list = [0, 1, 2]
// result引数に初期値をセットする場合: 初期値として100をセット
let result = list.reduce((result, value) => {
  return result + value
}, 100)
console.log(result)

// result引数に初期値をセットしない場合: 初期値として配列の0番目の値がセットされる
result = list.reduce((result, value) => result + value)
console.log(result)
実行結果
103
3
reduceRightの構文

reduceメソッドが左から右方法に演算するのに対して、reduceRightメソッドは右から左に演算する。
構文はreduceメソッドと同じ。

const list = [
  [0, 1],
  [2, 3],
  [4, 5],
]
let result = list.reduce((result, value) => result.concat(value))
console.log(result)

result = list.reduceRight((result, value) => result.concat(value))
console.log(result)
実行結果
// reduce
[ 0, 1, 2, 3, 4, 5 ]

// reduceRight
[ 4, 5, 2, 3, 0, 1 ]

for文での分割代入

リスト
const list = [['x', 0], ['y', 1], ['z', 2]]

for (const [value1, value2] of list) {
  console.log(value1, value2)
}
実行結果
x 0
y 1
z 2
オブジェクト
const map = new Map([
  ['x', 0],
  ['y', 1],
  ['z', 2]
])

// for...ofのmapはmap.entries()の省略
for (const [key, value] of map) {
  console.log(key, value)
}
実行結果
x 0
y 1
z 2
マップ
const obj = { x: 0, y: 1, z: 2 }

for (const [key, value] of Object.entries(obj)) {
  console.log(key, value)
}
実行結果
x 0
y 1
z 2

Mapのメソッドチェーン

const map = new Map()
console.log(map.set('x', 0).set('y', 1).set('z', 2))
実行結果
Map(3) { 'x' => 0, 'y' => 1, 'z' => 2 }

Mapの取得キーが存在しない場合の規定値処理

const map = new Map([
  ['x', 0],
  ['y', 1],
  ['z', 2],
])
console.log(map.has('xxx') ? map.get('xxx') : 'xxx')
実行結果
xxx

Mapのアンチパターン

mapに対してブラケット構文で値を格納しない。
ブラケット構文で値をセットした場合は、getメソッドで値を取得できない。

const map = new Map()
map['x'] = 0
console.log(map['x'])
// ブラケット構文で値を格納するとgetメソッドを使用して値を取得できない
console.log(map.get('x'))
実行結果
0
undefined

Mapの配列化

const map = new Map([
  ['x', 0],
  ['y', 1],
  ['z', 2],
])
// キーを配列化
console.log(Array.from(map.keys()))
// 値を配列化
console.log(Array.from(map.values()))
// キー/値を配列化
console.log(Array.from(map.entries()))
実行結果
[ 'x', 'y', 'z' ]
[ 0, 1, 2 ]
[ [ 'x', 0 ], [ 'y', 1 ], [ 'z', 2 ] ]

Mapの内容を順に処理

forEachメソッドの構文

/*
  dic: 元のマップ
  value: 要素値
  key: キー値
  map: 元のマップ
  statements: 要素値に対する処理
  thisArg: コールバック関数でthisが表す値
*/
dic.forEach(fuction(value, key, map)) {
   ...statements...
}, thisArag)
const map = new Map([
  ['x', 0],
  ['y', 1],
  ['z', 2],
])
map.forEach((value, key) => {
  console.log(`${key}: ${value}`)
})
実行結果
x: 0
y: 1
z: 2

ObjectとMapの相互変換

ObjectをMapに変換
const map = new Map([
  ['x', 0],
  ['y', 1],
  ['z', 2],
])
const obj = Object.fromEntries(map)
console.log(obj)
実行結果
{ x: 0, y: 1, z: 2 }
Mapをオブジェクトに変換
const obj = {
  x: 0,
  y: 1,
  z: 2,
}
const map = new Map(Object.entries(obj))
console.log(map)
実行結果
Map(3) { 'x' => 0, 'y' => 1, 'z' => 2 }

WeakMap - 弱い参照キーのMap

弱参照とは、Map以外でキーが参照されなくなると、ガベージコレクションの対象になるということ。
WeakMapではキー(オブジェクト)が破棄されるとともに値も破棄されるので、メモリリークを解消できる。

WeakMapの制限
  • キーは参照型である
  • get、set、has、deletのメソッドのみを使用できる
使い道

主な用途として、オブジェクトの付随的なデータを管理するために使用する。たとえば、オブジェクトに対してのアクセス数を監視するなど。

Mapでキー(オブジェクト)を削除
let obj = {}
const map = new Map()
map.set(obj, 'object')
// オブジェクトを破棄
obj = null
// Mapに格納したオブジェクトは破棄されず、生き続ける
// 消えるべきオブジェクトが消えないのでメモリーリークの原因となる
console.log(map.size)
実行結果
1
WeakMapでキー(オブジェクト)を削除
let obj = {}
const map = new WeakMap()
map.set(obj, 'object')
// オブジェクトを破棄
obj = null
// WeakMapからもキーが破棄される
console.log(map.size)
実行結果
undefined
WeakMapを利用したクラス
class WeakMapClass {
  // 初期化
  constructor(init) {
    this._weakMap = new WeakMap(init)
  }
   // 存在チェック
  has(key) {
    return this._weakMap.has(key)
  }
  // 格納
  set(key, value) {
    this._weakMap.set(key, value)
    return this
  }
  // 取得
  get(key) {
    return this._weakMap.get(k)
  }
  // 削除
  delete(key) {
    return this._weakMap.delete(key)
  }
  // 全削除
  clear() {
    this._weakMap = new WeakMap()
  }
}

配列の重複削除

indexOfとfilterメソッド

配列内で一番最初に一致するかどうかで判定して、重複を取り除く。

const list = [0, 1, 2, 0, 1, 2, 3]
const result = list.filter((value, index) => {
  return list.indexOf(value) === index
})
console.log(result)
実行結果
[ 0, 1, 2, 3 ]
Set
const set = new Set([0, 1, 2, 0, 1, 2, 3])
// fromメソッドで配列化
console.log(Array.from(set))
// スプレッド構文で配列化
console.log([...set])
実行結果
[ 0, 1, 2, 3 ]
[ 0, 1, 2, 3 ]

Setのメソッドチェーン

const set = new Set()
set.add(0).add(1).add(2).add(0)
console.log(set)
実行結果
Set(3) { 0, 1, 2 }

Objectリテラルを文字列に変換

console.log(`{"x":0,"y":1,"z":2}`)
実行結果
{"x":0,"y":1,"z":2}

ObjectとJSON文字列の相互変換

オブジェクトをJSON文字列に変換
const obj = {
  x: 0,
  y: 1,
  z: 2,
}
console.log(JSON.stringify(obj))
実行結果
{"x":0,"y":1,"z":2}
JSON文字列をオブジェクトに変換
const json = `{"x":0,"y":1,"z":2}`
console.log(JSON.parse(json))
実行結果
{ x: 0, y: 1, z: 2 }
JSON文字列をオブジェクトに変換 - Date型復元
const obj = {
  str: 'string',
  date: new Date()
}
// obj.dateは文字列として復元される
const jsonString = JSON.stringify(obj)
console.log(jsonString)

// jsonString.dateをDate型に変換
const parsedObj = JSON.parse(jsonString, (key, value) => {
  if (typeof(value) === 'string' &&
      value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)){
    return new Date(value)
  }
  return value
})
console.log(parsedObj)
実行結果
{"str":"string","date":"2024-02-05T14:41:29.981Z"}
{ str: 'string', date: 2024-02-05T14:41:29.981Z }

eval関数

eval関数は与えられた文字列をコードとして評価し実行する。
eval関数を利用するよりもほかの方法を利用できないを検討した方が良い。たいがい、代替策は用意されている。
以下の理由から濫用は避けるべき。

  • ユーザーの入力内容をevalに与えると、任意のスクリプトを自由に実行できてしまう可能性がある
  • 通常のコードよりもパフォーマンスが悪く、処理速度が遅い
eval('console.log("eval関数")')

let data = 'data'
// { var obj = data } と同じ意味
eval(`var obj = ${data}`)
console.log(obj)

// オブジェクトのアクセスプロパティの切り替え
const obj = { x: 0, y: 1, z: 2 }
const prop = 'x'
eval(`console.log(obj.${prop})`)
実行結果
eval関数
data
0

RegExpオブジェクトを生成

RegExpオブジェクトの生成は以下の2パターンある。

  • RegExpオブジェクトのコンストラクターを利用する
  • 正規表現リテラルを利用する
/*
  patter: 正規表現パターン
  opts: 動作オプション
*/
new RegExp(pattern, opts) // コンストラクター
/pattern/opts // リテラル
オプション 概要
d マッチした位置を記録する
g グローバル(全体)に対してマッチする
i 大文字/小文字を区別する
m 複数行に対応する
s 「.」が改行文字に一致するようにする
u Unicodeに対応する, サロゲートペア対策等で使う
y lastIndexプロパティで指定した位置からマッチする
RegExpコンストラクターで正規表現オブジェクトを受け取る

RegExpのコンストラクターは正規表現リテラルを受け取ることができる。

console.log(new RegExp(/[0-9]/, 'i'))
/[0-9]/i

文字列が正規表現パターンにマッチしたか判定

const pattern = /^[0-9]{3}-[0-9]{4}$/

const str1 = '012-3456'
console.log(pattern.test(str1))

const str2 = '0123-4567'
console.log(pattern.test(str2))
実行結果
true
false

正規表現パターンにマッチした文字列を取得

matchメソッドはグローバル(全体)マッチの有効/無効によって結果が変わってくる。

グローバル(全体)マッチ 結果 サブマッチ文字列
有効 マッチしたすべての文字列 含まない
無効 マッチした最初の文字列 含む
const str = `zip code1: 012-3456
             zip code2: 123-4567`
const pattern1 = /[0-9]{3}-[0-9]{4}/g
for (const result of str.match(pattern1)) {
  console.log(result)
}

const pattern2 = /([0-9]{3})-([0-9]{4})/
console.log(str.match(pattern2))
実行結果
// グローバル(全体)マッチ有効、サブマッチなし
012-3456
123-4567

// グローバル(全体)マッチ無効、サブマッチあり
[
  '012-3456',
  '012',
  '3456',
  index: 11,
  input: 'zip code1: 012-3456\n             zip code2: 123-4567',
  groups: undefined
]
サブマッチのグループに名前をつける
const str = `zip code1: 012-3456
             zip code2: 123-4567`
const pattern = /(?<city>[0-9]{3})-(?<local>[0-9]{4})/
const result = str.match(pattern)
console.log(result.groups.city, result.groups.local)
実行結果
012 3456

正規表現のマッチ結果をまとめて取得する

matchAllメソッドはグローバル(全体)マッチするすべての文字列とサブマッチ文字列などを結果として返却する。

const str = `zip code1: 012-3456
             zip code2: 123-4567`
const pattern = /([a-z].*)([0-9]{3}-[0-9]{4})/g

for (const result of str.matchAll(pattern)) {
  console.log(result)
}
実行結果
[
  'zip code1: 012-3456',
  'zip code1: ',
  '012-3456',
  index: 0,
  input: 'zip code1: 012-3456\n             zip code2: 123-4567',
  groups: undefined
]
[
  'zip code2: 123-4567',
  'zip code2: ',
  '123-4567',
  index: 33,
  input: 'zip code1: 012-3456\n             zip code2: 123-4567',
  groups: undefined
]
レガシー環境

matchAllメソッドはES2020で追加されたメソッドなため、IEのようなレガシーなブラウザでは利用できない。
レガシーな環境でmatchAllメソッドのような挙動を実装した場合はexecメソッドを利用する。
execメソッドは、以下の特徴を持つ。

  • グローバル(全体)検索か関係なく、実行結果は常に1つ
  • サブマッチ文字列、拡張プロパティなどの情報を含む
  • 最後にマッチした文字列の位置を記録する (次回は前回の文字列位置から検索を再開する)
  • マッチング文字列が存在しない場合は戻り値をnull
const str = `zip code1: 012-3456
             zip code2: 123-4567`
const pattern = /[0-9]{3}-[0-9]{4}/g

while ((result = pattern.exec(str)) !== null) {
  console.log(result)
}
実行結果
[
  '012-3456',
  index: 11,
  input: 'zip code1: 012-3456\n             zip code2: 123-4567',
  groups: undefined
]
[
  '123-4567',
  index: 44,
  input: 'zip code1: 012-3456\n             zip code2: 123-4567',
  groups: undefined
]

正規表現で文字列を置き換える

replaceメソッドの構文
/*
  str: 置き換え対象文字列
  pattern: 正規表現
  rep: 置き換え後の文字列
*/
str.replace(pattern, rep)
replaceメソッドの構文 - コールバック関数
```javascript
/*
  str: 置き換え対象文字列
  pattern: 正規表現
  fuction: コールバック関数
  match: マッチした文字列
  p1, p2, p3...: サブマッチ文字列 (グループの数に応じて変動)
  offset: マッチした文字列の位置
  string: 検索対象の文字列
  statements: マッチした文字列に対する処理
*/
str.replace(pattern, fuction(match, p1, p2, p3..., offset, string)) {
   ...statements...
}
変数 概要
$& マッチした部分文字列
$` マッチした部分文字列の直前の文字列
$' マッチした部分文字列の直後の文字列
$1 〜 100 サブマッチ文字列
$$ ドル記号
const str = 'tel number: 00-111-2222'
const pattern = /(\d{1,2})-(\d{2,4})-(\d{3,4})/g
console.log(str.replace(pattern, '$1($2)$3'))
// 正規表現でgオプションを指定しない場合はマッチした最初の文字列のみを置き換える
console.log(str.replace(/\d/, 'x'))
実行結果
tel number: 00(111)2222
tel number: x0-111-2222
replcaeAllメソッド

すべてのマッチング文字列を置換するならば、replcaeメソッドの代わりにreplaceAllメソッドを利用することもできる。
replaceAllメソッドを利用することで以下のメリットがある。

  • すべてのマッチング文字列を置換するという意図が明確となる
  • グローバル(全体)マッチするためのgオプションが指定されてない場合は、エラーとなる
const str = 'tel number: 00-111-2222'
const pattern = /(\d{1,2})-(\d{2,4})-(\d{3,4})/g
console.log(str.replaceAll(pattern, '$1($2)$3'))
console.log(str.replaceAll(/\d/g, 'x'))
実行結果
tel number: 00(111)2222
tel number: xx-xxx-xxxx
コールバック関数を利用した置換
const str = 'tel number: 00-111-2222'
const pattern = /\D/g
const result = str.replace(pattern, (match) => {
  return match.toUpperCase()
})
console.log(result)
実行結果
TEL NUMBER: 00-111-2222

正規表現で文字列を分割

const pattern = /[\/\./-]/g
console.log('1970-1-1'.split(pattern))
console.log('1970/1/1'.split(pattern))
console.log('1970.1.1'.split(pattern))
実行結果
[ '1970', '1', '1' ]
[ '1970', '1', '1' ]
[ '1970', '1', '1' ]

正規表現 - 後方参照

後方参照は、同じ正規表現で以前にキャプチャしたものを参照する。

const str = 'tel number: 00-00-00'
// \1: キャプチャグループの番号
const pattern1 = /(\d{1,2})-\1-\1/g
// 名前付きキャプチャグループ
const pattern2 = /(?<num>\d{1,2})-\k<num>-\k<num>/g
console.log(str.match(pattern1))
console.log(str.match(pattern2))
実行結果
[ '00-00-00' ]
[ '00-00-00' ]

正規表現 - キャプチャグループを無効化

(?:...) でマッチパターンを囲むことで、グリープをサブマッチの対象から除外できる。

const str = 'xyz0123'
// キャプチャありのグルーピング
const pattern1 = /^([a-z0-9]{3})(\d{1,4})$/
// キャプチャなしのグルーピング
const pattern2 = /^(?:[a-z0-9]{3})(\d{1,4})$/
console.log(str.match(pattern1))
console.log(str.match(pattern2))
実行結果
// キャプチャありのグルーピング
[
  'xyz0123',
  'xyz',
  '0123',
  index: 0,
  input: 'xyz0123',
  groups: undefined
]

// キャプチャなしのグルーピング
[ 
  'xyz0123',
  '0123',
  index: 0,
  input: 'xyz0123',
  groups: undefined
]

正規表現 - 前後の文字列の有無によるマッチ判定

正規表現の先読み、後読みは前後の文字列が存在するかどうかで文字列をマッチさせるか判定するための表現。
先読み、後読みは以下の4種類がある。

正規表現 概要
X(?=Y) 肯定先読み: Xの直後にYが続く場合はXにマッチする
X(?!Y) 否定先読み: Xの直後にYが続かない場合はXにマッチする
(?<=Y)X 肯定後読み: Xの直前にYがある場合はXにマッチする
(?<!Y)X 否定後読み: Xの直前にYがない場合はXにマッチする
// 肯定先読み
console.log(str.match(/xyz(?=0)/g))
// 否定先読み
console.log(str.match(/xyz(?!3)/g))
// 否定先読み
console.log(str.match(/(?<=z)0123/g))
// 否定後読み
console.log(str.match(/(?<!x)0123/g))
実行結果
// 肯定先読み
[ 'xyz' ]

// 否定先読み
[ 'xyz' ]

// 否定先読み
[ '0123' ]

// 否定後読み
[ '0123' ]

Unicodeプロパティで特定の文字列を取得

UnicodeプロパティとはUnicodeで定義された文字に対して割り当てられた属性(プロパティ)のこと。
正規表現パターンの中でUnicodeプロパティを利用できるようにしたものがUnicodeプロパティエスケープという構文。
正規表現パターンで以下のように使用する。

// 「sc=」はプロパティの分類を意味する
// 「gc=」を省略できるので省略した場合は次のようになる「gc=Letter」 -> 「Letter」
/[\p{sc=Han}]+/gu

以下のようなプロパティがある。

プロパティ 概要
\p{Letter}, \p{L} 文字
\p{Punctuation}, \p{P} 句読点
\p{Uppercase_Letter}, \p{Lu} 英大文字 (全角, 半角)
\p{Lowercase_Letter}, \p{Ll} 英小文字 (全角, 半角)
\p{Number}, \p{N} 半角 / 全角数字 (ローマ数字も含む)
\p{Nd} 半角 / 全角数字 (10進数)
\p{Space_Separator}, \p{Zs} 空白
\p{sc=Hiragana}, \p{sc=Hira} ひらがな
\p{sc=Katakana}, \p{sc=Kana} カタカナ
\p{sc=Han} 漢字
const str = 'STRINGもじれつ文字列'
// 英大文字
console.log(str.match(/[\p{Lu}]+/gu))
// ひらがな
console.log(str.match(/[\p{sc=Hira}]+/gu))
// 漢字
console.log(str.match(/[\p{sc=Han}]+/gu))
実行結果
[ 'STRING' ]
[ 'もじれつ' ]
[ '文字列' ]

関数定義

関数を定義するための構文は4つ存在する。

  • Functionコンストラクター
  • function命令
  • 関数リテラル
  • アロー関数

Functionコンストラクターで定義

Functionコンストラクターの構文
/**
  args: 関数の引数 (可変長引数)
  body: 関数の本体
*/
new Function(args, ..., body)
const func = new Function(
  'x',
  'y',
  'return x * y'
)
console.log(func(1, 2))
実行結果
2

new演算子を省略、仮引数の部分を1つにまとめる、関数本体に複数の文を含めることもできる。

// new演算子を省略
const func = Function(
  // 仮引数をまとめる
  'x, y',
   // 複数の文
  `
  const result = x * y
  return result
  `
)
console.log(func(1, 2))
2

スクリプト上で文字列を加工して、引数 / 関数本体を動的に生成することもできる。

const args = 'x, y'
const body = 'return x * y'
const func = new Function(args, body)
console.log(func(1, 2))
2
利用は要注意

Functionコンストラクターの引数 / 関数本体は、あくまで文字列なので外部からの入力をもとに動的に生成する場合は、悪意のある危険なコードが実装されてしまう可能性がある。
また、実行時にコード解析から関数の生成まで行うため、実行パフォーマンス低下の一因となる可能性がある。
特別な理由がない限り、Fuctionコンストラクターを利用するメリットはなく、利用すべきではない。

function命令で定義

/**
  func: 関数名
  args: 関数の引数
  statements: 関数の本体
*/
function func(args, ...) {
   ...statements...
}
function func(x, y) {
  return x * y
}
console.log(func(1, 2))
実行結果
2

fuction命令では関数名が変数として機能するので、定義した関数本体を変数に代入していることになる。そのため、以下のように関数名(変数)に対して再代入をすることができる。

function func() {
  console.log('do function')
}
func = null
console.log(func)
実行結果
null

function命令で宣言された関数はスコープの先頭に巻き上げられるため、関数定義よりも上の行で関数を呼び出すことができる。

func()

function func() {
  console.log('do function')
}
実行結果
do function

関数リテラルで定義

関数リテラルの構文
/**
  func: 関数名
  args: 関数の引数
  statements: 関数の本体
*/
func = function(args, ...) {
   ...statements...
}

関数リテラルは宣言した時点では名前を持たないことから匿名関数、または無名関数と呼ばれることもある。

const func = function(x, y) {
  return x * y
}
console.log(func(1, 2))
実行結果
2

アロー関数で定義

アロー関数の構文
/**
  func: 関数名
  args: 関数の引数
  statements: 関数の本体
*/
func = (args, ...) => {
   ...statements...
}
const func = (x, y) => {
  return x * y
}
console.log(func(1, 2))
実行結果
2
関数本体が1文の場合の省略

{...}を省略することができる。また、関数自体が戻り値として見なさるのでreturnも省略可能。

const func = (x, y) => x * y // (x, y) => { return x * y } と同じ
console.log(func(1, 2))
実行結果
2

ただし、returnを省略してオブジェクトを返却することはできない。

// { x: console.log('label block') } は関数ブロックとして見なされる
let func = () => { x: console.log('label block') } // { ラベル: 処理 } と同じ
console.log(func())

// オブジェクトをreturnする場合は以下のようにオブジェクトを{...}で囲んでreturnしないといけない
func = () => { return { x: 0 } }
console.log(func())
実行結果
// ラベル構文の実行結果
label block
// ラベル構文として実行されるのでundefinedが返却される
undefined

// オブジェクトの返却
{ x: 0 }
引数が1個の場合の省略

引数をくくるカッコを省略することができる。

const func = x => x * 100 // (x) => { return x * 100 } と同じ
console.log(func(1))
実行結果
100
利用制限
  • this、super、argumentsへの紐づきを持たないので利用できない
  • yield命令を利用できない
  • コンストラクターとして利用できない

実引数と仮引数の数チェック

argumentsは引数の情報を管理するオブジェクト。

function func (arg1, atg2) {
  if (arguments.length != 2) {
   throw new Error(`実引数と仮引数の数が異なる -> 実引数の数: ${arguments.length}`) 
  }
}

try {
  func(0)
} catch(e) {
  console.log(e.message)
}
実行結果
実引数と仮引数の数が異なる -> 実引数の数: 1

引数の規定値を設定

規定値が適応されるのは、引数を明示的に渡されなかった場合のみ。
ただし、undefinedを渡す場合は、渡されたなかったとみなし、規定値が適用される。

const func = (arg1 = 1, arg2 = 2) => {
  console.log(arg1 + arg2)
}

func()
func(10)
func(10, 11)
func(10, undefined)
実行結果
3
12
30
12

規定値で前の引数を参照

const func = (arg1 = 1, arg2 = arg1) => {
  console.log(arg1 + arg2)
}

func()
func(10)
func(10, 20)
func(10, undefined)
実行結果
2
20
30
20

規定値付き引数は引数リストの末尾に置く

const func = (x, pi = Math.PI) => {
  console.log(x ** 2 * pi)
}

func(1)
func(1, 3.1415)
func(1, undefined)
実行結果
3.141592653589793
3.14
3.141592653589793

引数の必須チェック

const func = (arg1, arg2 = 2) => {
  if (arg1 === undefined) {
    throw new Error('第1引数は必須')
  }
}

try {
  func()
} catch(e) {
  console.log(e.message)
}

try {
  // undefinedは引数を渡してないとみなされる。
  func(undefined)
} catch(e) {
  console.log(e.message)
}

可変長引数による合計値計算

可変長引数は引数をまとめて配列として取得できる。
すべての引数を可変長引数にしてしまうと可読性が低下するため、関数定義時に引数の個数をあらかじめ特定できないものだけをまとめるのが原則。

const func = (...args) => {
  if (args.length === 0) {
    return
  }

  return args.reduce((result, value) => {
    return result + value
  })
}

console.log(func(0, 1, 2))
実行結果
3

可変長引数によるプレースホルダー文字列埋め込み

const format = 'Hello {0} {1}'
const func = (format, ...args) => {
  if (typeof format !== 'string') {
    return
  }
  if (args.length === 0) {
    return
  }

  for (let i = 0; i < args.length; i++) {
    format = format.replaceAll(new RegExp(`\\{${i}\\}`, 'g'), args[i])
  }
  return format
}

console.log(func(format, 'JavaScript', '!!!'))
実行結果
Hello JavaScript !!!

最大値取得

// Math.maxメソッドは可変長引数を受け取るのでスプレッド構文で配列を展開
console.log(Math.max(...[0, 1, 2]))
console.log(Math.max(0, 1, 2))
実行結果
2
2

最小値取得

// Math.minメソッドは可変長引数を受け取るのでスプレッド構文で配列を展開
console.log(Math.min(...[0, 1, 2]))
console.log(Math.min(0, 1, 2))
実行結果
0
0

名前付き引数

名前付き引数は分割代入構文を使用しているので呼ばれる関数側で受け取る引数と規定値を指定できる。
以下のようなメリットがある。

  • 引数の意味を把握しやすい
  • 必要な引数だけを指定できる
  • 引数の順序を自由に変更できる
const func = ({
  x = 0,
  y = 1,
  z = 2
}) => {
  console.log(`x: ${x}`)
  console.log(`y: ${y}`)
  console.log(`z: ${z}`)
}

func({ z: 3, y: 2, })
実行結果
x:  0
y:  2
z:  3

複数の戻り値から値を取得

const func = (...args) => {
  // 可変長引数はリストにまとめられるのでスレッド構文で展開
  return [Math.max(...args), Math.min(...args)]
}

// 分割代入を用いて結果を取得
const [max1, min1] = func(0, 1, 2)
console.log(`max1: ${max1}`)
console.log(`min1: ${min1}`)

// 分割代入を用いて必要な結果だけを取得
const [, min2] = func(0, 1, 2)
console.log(`min2: ${min2}`)
実行結果
max1: 2
min1: 0
min2: 0
const func = (...args) => {
  // 可変長引数はリストにまとめられるのでスレッド構文で展開
  return { max: Math.max(...args), min: Math.min(...args) }
}

// 分割代入を用いて結果を取得
const { max, min } = func(0, 1, 2)
console.log(`max1: ${max}`)
console.log(`min1: ${min}`)

// 分割代入を用いて必要な結果だけを取得
// { min: minのエリアス名 }
const { min: min2 } = func(0, 1, 2)
console.log(`min2: ${min2}`)
実行結果
max1: 2
min1: 0
min2: 0

高階関数

関数を引数、戻り値として扱う関数のことを高階関数と呼ぶ。
高階関数を利用することでベースの機能を再利用し、具体的な処理内容を差し替えることができる。

const func = (obj, callback) => {
  for (const [key, value] of Object.entries(obj)) {
    callback(key, value)
  }
}
const callback = (key, value) => {
  console.log(`${key}: ${value}`)
}

func({ x: 0, y: 1, Z: 2 }, callback)
実行結果
x: 0
y: 1
Z: 2

HTMLエスケープ

const func = (str) => {
  if (!str) {
    return ''
  }

  const map = new Map()
  map.set(/&/g, '&amp;')
  .set(/</g, '&lt;')
  .set(/>/g, '&gt;')
  .set(/'/g, '&#39;')
  .set(/"/g, '&quot;')

  Array.from(map.entries()).forEach((list) => {
    let [ pattern, replaceStr ] = list
    str = str.replaceAll(pattern, replaceStr)
  })

  return str
}

console.log(func('<html>str</html>'))
実行結果
&lt;html&gt;str&lt;/html&gt;

タグ付きテンプレート文字列

タグ付きテンプレート文字列についての説明はMDNより引用。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Template_literals

タグ付きテンプレートは、テンプレートリテラルのより高度な形式です。

タグを使用すると、テンプレートリテラルを関数で解析できます。タグ関数の最初の引数には、文字列リテラルの配列を含みます。残りの引数は式に関連付けられます。

タグ関数は、これらの引数に対して何でも望み通りの操作を実行することができ、加工された文字列を返します。(または、以下の例の一つで示しているように、まったく異なるものを返すこともできます。)

タグに使用される関数の名前は、自由に指定できます。

タグ付き文字列テンプレートの構文
/*
  func: 関数名
  str: 任意の文字列
*/
func`str`
タグ付きテンプレート文字列の関数定義
/**
 * 
 * @param {Array} templates テンプレート文字列
 * @param {Array} values 埋め込み変数
 */
const func = (templates, ...values) => {
  console.log('templates: ', templates)
  console.log('values: ', values)
  
  return 'result'
}

const str1 = 'str1'
const str2 = 'str2'
console.log(func`this is tag template ${str1} ${str2} !!!`)
実行結果
templates:  [ 'this is tag template ', ' ', ' !!!' ]
values:  [ 'str1', 'str2' ]
result

タグ付き文字列テンプレート - HTMLエスケープ

// HTMLエスケープ
const escapeHtml = (str) => {
  if (!str) {
    return ''
  }

  const map = new Map()
  map.set(/&/g, '&amp;')
  .set(/</g, '&lt;')
  .set(/>/g, '&gt;')
  .set(/'/g, '&#39;')
  .set(/"/g, '&quot;')

  Array.from(map.entries()).forEach((list) => {
    let [ pattern, replaceStr ] = list
    str = str.replaceAll(pattern, replaceStr)
  })

  return str
}

// タグ付きテンプレート関数
const tagFunc = (templates, ...values) => {
  let result = ''

  for (const [i, temp] of templates.entries()) {
    result += temp + escapeHtml(values[i])
  }

  return result
}

const escapeTarget = '<html>str</html>'
console.log(tagFunc`this is escapeTarget: ${escapeTarget} !!!`)
実行結果
this is escapeResult: &lt;html&gt;str&lt;/html&gt; !!!

エスケープシーケンスをせず生の文字列とみなす

標準のタグ関数 - String.rawメソッド

String.rawメソッドを使用することで、エスケープシーケンスを処理せずに生の文字列として扱うことができる。

const filePath = String.raw`\home\usr\index.html`

console.log(`File path: ${filePath}`)
// String.rawを使わない場合は、エスケープ処理を行う必要がある
console.log(`File path: \\home\\usr\\index.html`)
実行結果
File path: \home\usr\index.html
File path: \home\usr\index.html

クロージャ - カウンター関数

クロージャとは、関数内のローカル変数を参照する関数内関数のこと。

const func = (init = 0) => {
  let counter = init

  return () => {
    return ++counter
  }
}

// 返却された匿名関数が関数内のローカル変数を参照し続けているので、関数実行後もローカル変数は値を保持し続ける
const closureFunc1 = func()
console.log(closureFunc1())
console.log(closureFunc1())
console.log(closureFunc1())

const closureFunc2 = func(2)
console.log(closureFunc2())
console.log(closureFunc2())
console.log(closureFunc2())
実行結果
// closureFunc1
1
2
3

// closureFunc2
3
4
5

オブジェクトにプロパティ名と同じ変数を割り当てる

プロパティ名と格納する値の変数名が同じ場合は、値の指定を省略できる。

const x = 0
const y = 1
const obj = { x , y }
console.log(obj)
実行結果
{ p1: 'x', p2: 'y' }

オブジェクトのプロパティを動的に生成

式から動的に生成されるプロパティを算出プロパティという。

let i = 0
const obj = {
  x: 0,
  [`dynamic${++i}`]: 1,
  [`dynamic{++i}`]: 2,
}
console.log(obj)
実行結果
{ x: 0, dynamic1: 1, dynamic2: 2 }

設定付きオブジェクト

createメソッドの構文
/*
  prototype: プロトタイプ
  props: プロパティの属性情報
*/
Object.create(prototype, props)

プロトタイプでnullを指定することで空のオブジェクトを生成することもできる。

const obj = Object.create(null)
console.log(obj)
実行結果
{}

プロパティの属性値一覧

属性 概要 既定値
value false
writable 書き換え可能か false
configurable 属性の変更やプロパティの削除が可能か -
enumerable for .. in, Object.keys() による列挙が可能か false
get ゲッター -
set セッター -
// Strictモードを有効にすることでエラーを明示的に発生させることができる
// 非Strictモードの場合は、無視されるだけでエラーは発生しない
'use strict'

const obj = Object.create(Object.prototype, {
  x: {
    value: 0,
    writable: true,
    configurable: true,
    enumerable: true,
  },
  y: {
    value: 1,
  },
})
console.log(obj)

// プロパティ名の列挙
for (const prop in obj) {
  console.log(prop)
}

// 試す時は以下の①、②どっちかをコメントアウトする

// プロパティへの書き込み
obj.y = 100 // ・・・ ①

// プロタティの削除
delete obj.y // ・・・ ②
実行結果
{ x: 0 }

// プロパティ名の列挙
x

// プロパティへの書き込み
Uncaught TypeError: Cannot assign to read only property 'y' of object '#<Object>'

// プロタティの削除
Uncaught TypeError: Cannot delete property 'y' of #<Object>

オブジェクトのGetter / Setter

Getter / Setterの構文 - オブジェクトリテラル

/*
  _prop: プライベート変数のプロパティ
  value: 設定する値
  prop: Getter / Setter の名前
*/
{
  _prop: 規定値, 
  get prop() {
    return _prop
  },
  set prop(value) {
    this._prop = value
  },
}
const obj = {
  _x: 0,
  set x(value) {
    this._x = value
  },
  get x() {
    return this._x
  },
  set y(value) {
    this._y = value
  },
  get z() {
    return this._x + this._y
  },
}

console.log(obj.x)
obj.x = 1
console.log(obj.x)

obj.y = 2
console.log(obj.z)
実行結果
0
1
3

Getter / Setterの構文 - createメソッド

オブジェクトリテラルと違ってvalueオプションで規定値を指定することはできない。

/*
  prototype: プロトタイプ
  prop: プロパティ
  _prop: プライベート変数のプロパティ
  value: 設定する値
*/
Object.create(prototype, {
  prop: {
    set(value) {
      this._prop = value
    },
    get () {
      return this._prop
    },
  },
})
const obj = Object.create(Object.prototype, {
  x: {
    set(value) {
      this._x = value
    },
    get () {
      return this._x
    },
  },
  y : {
    set(value) {
      this._y = value
    },
  },
  z: {
    get() {
      return this._x + this._y
    },
  },
})

console.log(obj.x)
obj.x = 1
console.log(obj.x)

obj.y = 2
console.log(obj.z)
実行結果
undefined
1
3

プロトタイプを持たないオブジェクト

createメソッドでプロトタイプにnullを指定することでプロトタイプを持たないオブジェクトを定義できる。

const obj = Object.create(null, {
  x: {
    value: 0,
  }
})
// プロトタイプ’取得
console.log(Object.getPrototypeOf(obj))
実行結果
null

オブジェクトのプロトタイプ参照

オブジェクトのプロトタイプを含むプロパティの参照は以下を使用することでできる。

  • for ... in
  • Object.getPrototypeOf
  • in演算子

オブジェクトのプロトタイプを含まない、自身のプロパティの参照は以下を使用することでできる。

  • Object.keys
  • hasOwn (ES2022以降)
  • hasOwnProperty (ES2021以前)
const protoType = {
  x: 0,
  y: 1,
}

const obj = Object.create(protoType, {
  z: {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true,
  },
})

// 参照元のプロトタイプ
console.log(protoType)
// オブジェクトの参照元プロトタイプを取得
console.log(Object.getPrototypeOf(obj))
console.log(obj)

// for ... inでプロトタイプを含めた列挙可能なプロパティを参照できる
for (const prop in obj) {
  console.log(`${prop}: ${obj[prop]}`)
}

// Object.keysでオブジェクト自身のプロパティのみを参照できる
for (const prop of Object.keys(obj)) {
  console.log(`${prop}: ${obj[prop]}`)
}

// hasOwnメソッドを使用することでオブジェクト自身が持つプロパティかどうかを判定して参照できる
for (const prop in obj) {
  if (Object.hasOwn(obj, prop)) {
    console.log(`${prop}: ${obj[prop]}`)
  }
}

// in演算子を使用することでプロトタイプまで遡ってプロパティが存在するか確認できる
console.log('x' in obj)
console.log('y' in obj)
console.log('z' in obj)
実行結果
// プロトタイプの参照
{ x: 0, y: 1 }
{ x: 0, y: 1 }

// オブジェクト自身が持つプロパティの参照
{ z: 2 }

// for ... in でプロトタイプを含めた列挙可能なプロパティの参照
z: 2
x: 0
y: 1

// Object.keysでオブジェクト自身のプロパティのみを参照
z: 2

// hasOwnメソッドでオブジェクト自身が持つプロパティかどうかを判定して参照
z: 2

// in演算子でプロトタイプまで遡って存在を確認
true
true
true

オブジェクトのプロトタイプのプロパティ追加

const protoType = {
  x: 0,
  y: 1,
}

const obj = Object.create(protoType, {
  z: {
    value: 2,
  },
})

console.log(protoType)
console.log(obj)

// プロトタイプのオブジェクトにプロパティを追加
protoType.v = 100

console.log(protoType)
console.log(obj)
実行結果
// プロトタイプのオブジェクトにプロパティを追加する前
{ x: 0, y: 1}
{ z: 2}

// プロトタイプのオブジェクトにプロパティを追加する後
{ x: 0, y: 1, v: 100}
{ z: 2}

オブジェクトのプロトタイプのプロパティ隠蔽

オブジェクトに参照元のプロトタイプと同じプロパティ前を定義することでプロトタイプのプロパティへの参照を隠蔽できる

const protoType = {
  x: 0,
  y: 1,
}

const obj = Object.create(protoType, {
  z: {
    value: 2,
  },
})

console.log(protoType)
console.log(obj)

// プロトタイプに存在するプロパティと同じ名前を指定することで、プロトタイプのプロパティを隠蔽することができる
obj.x = 100

console.log(protoType)
console.log(obj)
// プロトタプのプロパティにアクセスしない
console.log(obj.x)
実行結果
// プロトタイプのプロパティに対して隠蔽する前
{ z: 2}
{ x: 0, y: 1}

// プロトタイプのプロパティに対して隠蔽する後
{ x: 100, z: 2}
100

プロトタイプにSetterが定義されている場合は、プロトタイプのSetter経由でオブジェクトに対してプロパティを追加する

const protoType = Object.create(Object.prototype, {
  x: {
    set(value) {
      this._x = value
    },
  },
  y: {
    value: 1,
  },
})

const obj = Object.create(protoType, {
  z: {
    value: 2,
  },
})

console.log(protoType)
console.log(obj)

obj.x = 100

console.log(protoType)
console.log(obj)
console.log(obj._x)
実行結果
// プロトタイプのプロパティに対して隠蔽する前
{ y: 1}
{ z: 2}

// プロトタイプのプロパティに対して隠蔽する後
{ y: 1}
{_x: 0, z: 2}
100

オブジェクトのプロトタイプのプロパティ削除

プロトタイプの削除は以下のようにできる。

  • Object.getPrototypeOfを使用してプロトタイプにアクセスして削除する
  • オブジェクトで参照元のプロトタイプのプロパティを隠蔽する
    • オブジェクトにプロトタイプと同名プロパティを定義し、undefinedやnullで初期化する
const protoType = {
  x: 0,
  y: 1,
}

const obj = Object.create(protoType, {
  z: {
    value: 2,
  },
})

// プロトタイプのプロパティを隠蔽することで削除に見せかける
obj.x = undefined
console.log(protoType)
console.log(obj)

// オブジェクトプロトタイプにアクセスしてプロパティを削除
delete Object.getPrototypeOf(obj).x
console.log(protoType)
console.log(obj)
実行結果
// プロトタイプのプロパティを隠蔽することで削除に見せかける
{ x: 0, y: 1}
{ x: undefined, z: 2}

// オブジェクトプロトタイプにアクセスしてプロパティを削除
{ y: 1}
{ x: undefined, z: 2}

プロトタイプの入れ替え

const protoType1 = {
  x: 0,
}

const protoType2 = {
  y: 1,
}

const obj = Object.create(protoType1, {
  z: {
    value: 2,
  },
})

console.log(Object.getPrototypeOf(obj))

// オブジェクトのプロトタイプに定義したprotoType2を設定
Object.setPrototypeOf(obj, protoType2)
console.log(Object.getPrototypeOf(obj))

// オブジェクトのプロトタイプにnullを設定
Object.setPrototypeOf(obj, null)
console.log(Object.getPrototypeOf(obj))
実行結果
{ x: 0 }
{ y: 1 }
null

オブジェクトのマージ

assignメソッド、スプレッド構文を使用することでオブジェクトのマージを行うことができる。ただし、どちらもシャローコピーなので注意が必要。

assignメソッドの構文
/*
  target: マージ先のオブジェクト
  source: コピー元 (可変長引数)
*/
Object.assign(target, source, ...)
const obj1 = {
  x: 0,
}
const obj2 = {
  x: 100,
  y: 1
}
const obj3 = {
  z: 2,
}

// 同じ名前のプロパティは、後のもので上書きされる
// マージ先のオブジェクトに影響を及ぼしたくない場合は、空オブジェクトに対してマージする
const mergedObj = Object.assign({}, obj1, obj2, obj3)
console.log(mergedObj)

console.log(Object.assign(obj1, obj2, obj3))
console.log(obj1)
実行結果
{ x: 100, y: 1, z: 2 }
{ x: 100, y: 1, z: 2 }
{ x: 100, y: 1, z: 2 }
const obj1 = {
  x: 0,
}
const obj2 = {
  x: 100,
  y: 1
}
const obj3 = {
  z: 2,
}

// 同じ名前のプロパティは、後のもので上書きされる
const mergedObj = { ...obj1, ...obj2, ...obj3 }
console.log(mergedObj)
実行結果
{ x: 100, y: 1, z: 2 }

オブジェクトのディープコピー

オブジェクトのディープコピーは以下のようにできる。

  • JSON.parseとJSON.stringifyメソッドを組み合わせることでオブジェクトを文字列化し、再構築する
    • ただし、JSONで対応していないundefined / NaN / 関数/ Symbol など型はスキップされるか、自動的にnullに変換されてしまう
  • structuredCloneメソッドを使用する
  • ライブラリーを使用する
const obj = {
  x: 0,
  y: 1, 
  z: {
    z1: 2,
  },
}

let deepCopiedObj = JSON.parse(JSON.stringify(obj))
deepCopiedObj.z.z1 = 100
console.log(deepCopiedObj)
console.log(obj)

deepCopiedObj = structuredClone(obj)
deepCopiedObj.z.z1 = 100
console.log(deepCopiedObj)
console.log(obj)

// Lodashライブラー、ライブラリーのインポートは公式サイト参照
deepCopiedObj = _.cloneDeep(obj)
deepCopiedObj.z.z1 = 100
console.log(deepCopiedObj)
console.log(obj)
実行結果
// JSON.parseとJSON.stringifyメソッド
{ x: 0, y: 1, z: { z1: 100 } }
{ x: 0, y: 1, z: { z1: 2 } }

// structuredCloneメソッド
{ x: 0, y: 1, z: { z1: 100 } }
{ x: 0, y: 1, z: { z1: 2 } }

// Lodashライブラリー
{ x: 0, y: 1, z: { z1: 100 } }
{ x: 0, y: 1, z: { z1: 2 } }

オブジェクトのプロパティ情報を追加 / 更新

definePropertyメソッドの構文
/*
  obj: 対象オブジェクト
  prop: プロパティ名
  desc: プロパティの詳細情報
*/
Object.defineProperty(obj, prop, desc)
const obj = {
  x: 0,
  y: 1,
}

// プロパティの設定を更新
Object.defineProperty(obj, 'x', {
  value: 100,
  writable: false,
  configurable: false,
  enumerable: false,
})
console.log(obj)

// プロパティを追加
Object.defineProperty(obj, 'z', {
  value: 2,
  writable: true,
  configurable: true,
  enumerable: true,
})
console.log(obj)
実行結果
{ y: 1 }
{ y: 1, z: 2 }
definePropertiesメソッドの構文 - まとめて定義
/*
  obj: 対象オブジェクト
  props: プロパティ (1個以上)
*/
Object.defineProperties(obj, props)
const obj = {
  x: 0,
  y: 1,
}

Object.defineProperties(obj, {
  // プロパティの設定を変更
  x: {
    value: 100,
    writable: false,
    configurable: false,
    enumerable: false,
  },
  // プロパティを追加
  z: {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true,
  },
})
console.log(obj)
実行結果
{ y: 1, z: 2 }

オブジェクトの詳細情報取得

getOwnPropertyDescriptorメソッドの構文
/*
  obj: 対象オブジェクト
  prop: プロパティ
*/
Object.getOwnPropertyDescriptor(obj, prop)
getOwnPropertyDescriptorsメソッドの構文 - まとめて取得
/*
  obj: 対象オブジェクト
*/
Object.getOwnPropertyDescriptors(obj)
const obj = {
  x: 0,
  y: 1,
  z: 2,
}

// オブジェクトのプロパティごとの詳細情報を取得
for (const prop of Object.keys(obj)) {
  console.log(Object.getOwnPropertyDescriptor(obj, prop))
}
// オブジェクトのプロパティの詳細情報をまとめて取得
console.log(Object.getOwnPropertyDescriptors(obj))
実行結果
// getOwnPropertyDescriptor
{ value: 0, writable: true, enumerable: true, configurable: true }
{ value: 1, writable: true, enumerable: true, configurable: true }
{ value: 2, writable: true, enumerable: true, configurable: true }

// getOwnPropertyDescriptors
{
  x: { value: 0, writable: true, enumerable: true, configurable: true },
  y: { value: 1, writable: true, enumerable: true, configurable: true },
  z: { value: 2, writable: true, enumerable: true, configurable: true }
}

オブジェクトのプロパティを列挙 - プロトタイプ

オブジェクトのプロパティの列挙を以下の方法で行うことができる。

  • for ... in
    • プロトタイプを遡って列挙可能なプロパティにアクセスできる
  • Object.keys
    • プロトタイプを遡らず列挙可能なプロパティにをアクセスできる
  • Object.getOwnPropertyNames
    • プロトタイプを遡らず列挙の可否を問わず、すべてのプロパティにアクセスできる
  • getOwnPropertySymbols
    • Symbol型の列挙可能なプロパティにアクセスできる
const protoType = Object.create(Object.prototype, {
  x: {
    value: 0,
    enumerable: true,
  },  
})

const obj = Object.create(protoType, {
  y: {
    value: 1,
    enumerable: true,
  },
  z: {
    value: 0,
    enumerable: false,
  },
})

// for ... inでプロトタイプを遡って列挙可能なプロパティを参照
for (const prop in obj) {
  console.log(`${prop}: ${obj[prop]}`)
}

// Object.keysでプロトタイプを遡らず列挙可能なプロパティをを参照
for (const prop of Object.keys(obj)) {
  console.log(`${prop}: ${obj[prop]}`)
}

// Object.getOwnPropertyNamesではプロトタイプを遡らず列挙の可否を問わず、すべてのプロパティを参照
for (const prop of Object.getOwnPropertyNames(obj)) {
  console.log(`${prop}: ${obj[prop]}`)
}
実行結果
// for ... in
y: 1
x: 0

// Object.keys
y: 1

// Object.getOwnPropertyNames
y: 1
z: 0
const symbol = Symbol('symbol')
const obj = {
  x: 0,
  y: 1,
}
obj[symbol] = 'symbol'

// 値がSymbol型のプロパティのみが列挙対象となる
for (const prop of Object.getOwnPropertySymbols(obj)) {
  console.log(prop)
}
実行結果
Symbol(symbol)

propertyIsEnumerableメソッドで列挙可能なプロパティかどうかを判定できる。

const protoType = Object.create(Object.prototype, {
  x: {
    value: 0,
    enumerable: true,
  },  
})

const obj = Object.create(protoType, {
  y: {
    value: 1,
    enumerable: true,
  },
  z: {
    value: 0,
    enumerable: false,
  },
})

console.log(obj.propertyIsEnumerable('x'))
console.log(obj.propertyIsEnumerable('y'))
console.log(obj.propertyIsEnumerable('z'))
実行結果
false
true
false

不変オブジェクト

不変オブジェクトを定義することでオブジェクトに対して意図しない変更を制御できるため、バグの発生を防ぐことができる。

プロパティに設定可能な制限一覧

メソッド プロパティ値の変更 プロパティの削除 プロパティの追加
preventExtensions 不可
seal 不可 不可
freeze 不可 不可 不可
// Strictモードを有効にすることでエラーを明示的に発生させることができる
// 非Strictモードの場合は、無視されるだけでエラーは発生しない
'use strict'

const obj = {
  x: 0,
  y: 1,
}

// 試す時は以下の① 〜 ③どっちか1つだけ残して残りをコメントアウト、④ 〜 ⑤ も適宜にコメントアウトする

Object.preventExtensions(obj) // ・・・ ①
Object.seal(obj) // ・・・ ②
Object.freeze(obj ) // ・・・ ③

obj.x = 100 // ・・・ ④
delete obj.y // ・・・ ⑤
obj.y = 2 // ・・・ ⑥
実行結果
// Object.preventExtensionsでプロパティの追加のエラーが発生する
Uncaught TypeError: Cannot add property z, object is not extensible

// Object.sealでプロパティの追加 / 削除のエラーが発生する
Uncaught TypeError: Cannot delete property 'y' of #<Object>
Uncaught TypeError: Cannot add property z, object is not extensible

// Object.freezeでプロパティの追加 / 削除 / 値の変更のエラーが発生する
Uncaught TypeError: Cannot assign to read only property 'x' of object '#<Object>'
Uncaught TypeError: Cannot delete property 'y' of #<Object>
Uncaught TypeError: Cannot add property z, object is not extensible

クラス定義

  • classブロック配下はStrictモード
  • class {...} の形式で、クラスリテラルを表すことができる
class MyClass {
  x = null
  // プライベートプロパティ: コンストラクー、メソッドから生成することはできる
  #privateProp = null

  // コンストラクター
  constructor(x, y) {
    this.x = x
    // クラスにプロパティを追加
    this.y = y
    console.log('constructor is done')
  }

  // メソッド
  method1() {
    console.log('method1 is done')
  }

  accessPrivateMember() {
    console.log(`#privateProp: ${this.#privateProp}`)
    this.#privateMethod()
  }

  // プライベートメソッド
  #privateMethod() {
    console.log('private method is done')
  }

  // ゲッター
  set privateProp(value) {
    this.#privateProp = value
  }

  // セッター
  get privateProp() {
    return this.#privateProp
  }
}

// インスタンス生成時にコンストラクターが実行される
const mc = new MyClass(0, 1)

// インスタンスにプロパティを追加
console.log(mc.y)

// インスタンスに追加したプロパティを削除
delete mc.y
console.log(mc.y)

// インスタンスからプロパティを削除
console.log(mc.x)
delete mc.x
console.log(mc.x)

// プロトタイプにプロパティを追加
MyClass.prototype.y = 1
console.log(mc.y)
console.log(MyClass.prototype.y)

// プロトタイプに追加したプロパティを削除
// インスタンス経由でプロトタイプに追加したプロパティを削除することは出ない
delete MyClass.prototype.y
console.log(mc.y)
console.log(MyClass.prototype.y)

// インスタンスにメソッドを追加
mc.method2 = () => {
  console.log('method2 is done')
}

// プロトタイプにメソッドを追加
MyClass.prototype.method3 = () => {
  console.log('method3 is done')
}

// クラスのメソッドを実行
mc.method1()
// インスタンスに追加したメソッドを実行
mc.method2()
// プロトタイプに追加したメソッドを実行
mc.method3()
MyClass.prototype.method3()

// プロトタイプに追加したメソッドを削除
// インスタンス経由でプロトタイプに追加したメソッドを削除することは出ない
delete MyClass.prototype.method3
console.log(mc.method3)
console.log(MyClass.prototype.method3)

// クラスのGetter / Setter でプライベート変数にアクセス
mc.privateProp = 2
console.log(mc.privateProp)

// メソッド経由でプライベートメンバーにアクセス
mc.accessPrivateMember()
実行結果
// インスタンス生成時にコンストラクターが実行される
constructor is done

// インスタンスにプロパティを追加
1

// インスタンスに追加したプロパティを削除
undefined

// インスタンスからプロパティを削除
0
undefined

// プロトタイプにプロパティを追加
1
1

// プロトタイプに追加したプロパティを削除
undefined
undefined

// クラスのメソッドを実行
method1 is done

// インスタンスに追加したメソッドを実行
method2 is done

// プロトタイプに追加したメソッドを実行
method3 is done
method3 is done

// プロトタイプに追加したメソッドを削除
undefined
undefined

// クラスのGetter / Setter でプライベートプロパティにアクセス
2

// メソッド経由でプライベートメンバーにアクセス
#privateProp: 2
private method is done
クラスリテラルでクラスを定義
// クラスリテラルを変数に代入
const MyClass = class {
  #x = null

  constructor(value) {
    this.#x = value
  }
  
  get x() {
    return this.#x
  }
}

console.log(new MyClass(0).x)
実行結果
0

クラスのコンストラクターでの初期化簡略

class MyClass {
  // 規定値付きコンストラクター
  constructor(x = 0, y = 1) {
    // { x, y } は { x: x, y: y } の省略形
    Object.assign(this, { x, y })
  }
}

console.log(new MyClass())
実行結果
MyClass { x: 0, y: 1 }

クラスの静的プロパティ / メソッド

class MyClass {
  static x = 0

  static getValue() {
    return this.x + 1
  }
}

console.log(MyClass.x)
console.log(MyClass.getValue())
実行結果
0
1

クラス定数

Getter構文を利用することでクラスの静的プロパティをクラス定数として扱うことができる。

class MyClass {
  static get VALUE1() {
    return 1
  }

  static get VALUE2() {
    return 2
  }
}

console.log(MyClass.VALUE1)
console.log(MyClass.VALUE2)
実行結果
1
2

静的イニシャライザー

静的イニシャライザーを利用することで、クラスをロードした時に一度だけ実行する処理を定義できる。

class MyClass {
  static list = null

  static {
    this.list = []
  }
}

console.log(MyClass.list)
実行結果
[]

thisの束縛

thisは呼び出す場所、文脈によって中身が変化するため、thisに特定のオブジェクトを明示的に参照するようにすることがthisの束縛。

以下のメソッドを使用することでthisの束縛を行うことができる。

  • bind
  • call
  • apply
bindメソッドの構文
/*
  func: 関数
  thisArgs: 関数呼び出し時にthisで参照するオブジェクト
  args: 関数に渡す引数 (可変長引数)
*/
func.bind(thisArgs, args, ...)
const obj = {
  x: 0,
  func1() {
    // thisはオブジェクト自身を参照
    console.log(this.x)
  },
  func2(func) {
    func()
  },
}

// objのメソッド内のthisが自身のオブジェクトを参照
obj.func1()

// グローバルオブジェクトにobjと同じ名前のプロパティを追加
globalThis.x = 100
function doFunc() {
  // グローバルオブジェクトに追加したプロパティを参照するthis
  console.log(this.x)
}
// objのメソッドに定義したメソッドを渡して実行
obj.func2(doFunc)

// メソッド内のthisがobjを参照するように設定、そしてメソッドを実行
doFunc.bind(obj)()

// objのメソッド内でobjを参照するように設定、そしてメソッドを実行
obj.func2(doFunc.bind(obj))
実行結果
// objのメソッド内のthisが自身のオブジェクトを参照
0

// objのメソッドに定義したメソッドを渡して実行
100

// メソッド内のthisがobjを参照するように設定、そしてメソッドを実行
0

// objのメソッド内でobjを参照するように設定、そしてメソッドを実行
0
callメソッドの構文

bindメソッドではthisは束縛作るが、関数の呼び出しを行なわない。
callメソッドを使用することでthisの束縛と関数の呼び出しをまとめて行うことができる。

/*
  func: 関数
  thisArgs: 関数呼び出し時にthisで参照するオブジェクト
  args: 関数に渡す引数 (可変長引数)
*/
func.call(thisArgs, args, ...)
const obj = {
  x: 0,
}

// グローバルオブジェクトにobjと同じ名前のプロパティを追加
globalThis.x = 100
function doFunc(arg1, arg2) {
  console.log(this.x)
  console.log(`arg1: ${arg1}, arg2: ${arg2}`)
}

doFunc.call(obj, 1, 2)
実行結果
0
arg1: 1, arg2: 2
applyメソッドの構文

applyメソッドの動作はcallメソッドと同じで、違いとしては引数に渡すのが可変長引数か配列。

/*
  func: 関数
  thisArgs: 関数呼び出し時にthisで参照するオブジェクト
  argsArray: 関数に渡す引数 (配列)
*/
func.apply(thisArgs, argsArray)
const obj = {
  x: 0,
}

// グローバルオブジェクトにobjと同じ名前のプロパティを追加
globalThis.x = 100
function doFunc(arg1, arg2) {
  console.log(this.x)
  console.log(`arg1: ${arg1}, arg2: ${arg2}`)
}

doFunc.apply(obj, [1, 2])
実行結果
0
arg1: 1, arg2: 2

不変クラス

不変オブジェクト同様不変クラスを定義することで意図しない変更を制御できるため、バグの発生を防ぐことができる。また、インスタンスの状態変化を意識しなくて良い。

不変クラスを定義する際に以下のことを注意する必要がある。

  1. プロパティはすべてプライベート宣言
  2. プロパティ / メソッドの追加を禁止
  3. 参照型のプロパティ受け渡しは明示的に複製する
  4. プロパティの変更はメソッド経由で新しいインスタンスを生成して行う
    • 不変クラスではプロパティに対しての変更は基本行なわないが、どうしても特定のプロパティに対して変更したい状況ができてる場合はメソッドを用意して行う
    • 新しいインスタンスを生成し、特定のプロパティのみを差し替えた上で生成したインスタンスを返却して扱うことで現在のインスタンスに対しての変更を及ぼさない
    • メソッドの命名規則は「with + 変更したいプロパティ名」に統一する

また、クラス/プロパティが増えるとコードが冗長になってしまうので、どこまで実装するかは考慮する必要がある。
例えば、1. 〜 3. までのみを実施するなど。

class MyClass {
  #value = null
  #list = []

  constructor(value, list) {
    this.#value = value
    this.#list = [...list]
    // クラスに対してプロパティの変更、削除、追加に設定
    Object.freeze(this)
  }

  get prop() {
    return this.#value
  }

  get list() {
    // 参照型のプロパティになので、インスタンス側で影響を及ぼさないように制御
    return [...this.#list]
  }

  method() {
    console.log(`#value: ${this.#value}, #list: ${this.#list}`)
  }

  // 変更したプロパティに対しての制御
  // 命名規則: with + は変更したいプロパティ名
  withValue(value) {
    // 現在のインスタンスに対して変更を加えないように新しくインスタンスを生成して、それを扱う
    return new MyClass(value, this.#list)
  }
}
// クラスのプロトタイプに対してプロパティの変更、削除、追加に設定
Object.freeze(MyClass.prototype)

const mc = new MyClass(0, [1, 2])
mc.method()

// クラスのプロパティを変更
const newMc = mc.withValue(100)
newMc.method()
実行結果
#value: 0, #list: 1,2
#value: 100, #list: 1,2

クラスの継承

サブクラスからは、superキーワードを使用してスーパークラスにアクセスできる。ただし、以下のことについて注意が必要。

  • super.メソッドでスーパークラスのメソッドにアクセスできる
  • super.プロパティでスーパークラスのプロパティにアクセスできない
  • super()でスーパークラスのコンストラクターをオーバーライドできる
    • サブクラスとスーパークラスの両方にコンストラクターが定義されている場合は、オーバーライドは必須
class SuperClass {
  prop1 = 0
  prop2 = 1
  #privateProp1 = 0
  #privateProp2 = 1

  constructor(message = 'super class constructor') {
    console.log(message)
  }

  method1() {
    console.log('super class method1')
  }

  method2() {
    console.log('super class method2')
  }

  #privateMethod1() {
    console.log('super class private method1')
  }

  #privateMethod2() {
    console.log('super class private method2')
  }
}

class SubClass extends SuperClass {
  // スーパークラスのプロパティをオーバーライド
  prop1 = 100
  prop3 = 3
  // スーパークラスのプライベートプロパティをオーバーライド
  #privateProp1 = 100
  #privateProp3 = 3

  constructor() {
    // 試す時はコメントアウトする
    // スーパークラスにコンストラクターが定義されている場合は、オーバーライドは必須
    super('sub class override super class constructor')
    console.log('sub class constructor')
  }

  method1() {
    console.log('sub class override super class method1')
  }

  // サブクラスのメソッドをオーバーライド
  method3() {
    console.log('sub class method3')
  }

  // サブクラスのプライベートメソッドをオーバーライド
  #privateMethod1() {
    console.log('sub class override super class #privateMethod1')
  }

  #privateMethod3() {
    console.log('sub class #privateMethod3')
  }

  accessClassMember() {
    console.log(`prop1: ${this.prop1}, prop2: ${this.prop2}, prop3: ${this.prop3}`)
    console.log(`#privateProp1: ${this.#privateProp1}, #privateProp3: ${this.#privateProp3}`)

    this.method1()
    this.method2()
    this.method3()
    this.#privateMethod1()
    this.#privateMethod3()
  }

  // スーパークラスのメンバーにアクセスする
  accessSuperClass() {
    // スーパークラスのメソッドを実行
    super.method1()
    super.method2()

    // 確認する時はコメントアウトを外す
    // メソッド内でスーパークラスのコンストラクターをオーバーライドできない
    // super()

    // スーパークラスのプロパティにアクセスできない
    // console.log(`prop1: ${super.prop1}, prop3: ${super.prop2}`)
    // super.#privateProp1
    // super.#privateProp2

    // スーパークラスのプライベートメソッドにアクセスできない
    // super.#privateMethod1()
    // super.#privateMethod2()
  }
}

const subClass = new SubClass()
subClass.accessClassMember()
subClass.accessSuperClass()
実行結果
// コンストラクターのオーバーライド
sub class override super class constructor
sub class constructor

// accessClassMember
prop1: 100, prop2: 1, prop3: 3
#privateProp1: 100, #privateProp3: 3
sub class override super class method1
super class method2
sub class method3
sub class override super class #privateMethod1
sub class #privateMethod3

// accessSuperClass
super class method1
super class method2

ミックスイン (mixin)

ミックスインは、再利用可能な機能をまとめたオブジェクトのこと。単体で使用することは想定しておらず、他のクラスやオブジェクトに組み込んだ使用する。
基本的にメソッドのみを定義し、プロパティを持たせない。

const mixin = {
  mixinMethod() {
    // thisは組み込まれるクラス / オブジェクトを指す
    for (const [key, value] of Object.entries(this)) {
      console.log(`${key}: ${value}`)
    }
  }
}

class MyClass {
  x = 0
  y = 1
  z = 2

  method() {
    console.log('method3 is done')
  }
}

Object.assign(MyClass.prototype, mixin)
new MyClass().mixinMethod()
実行結果
x: 0
y: 1
z: 2

オブジェクト、クラスインスタンスのプロパティ / メソッドの存在チェック

const obj = {
  x: 0,
  method1() {},
}

console.log('x' in obj)
console.log('y' in obj)
console.log('method1' in obj)
console.log('method2' in obj)

obj.y = 2
obj.method2 = () => {}
console.log('z' in obj)
console.log('method2' in obj)

// クラスも同様にinで存在チェックできる
class MyClass {
  x = 0

  method1() {}
}

const mc = new MyClass()
console.log('x' in mc)
console.log('y' in mc)
console.log('method1' in mc)
console.log('method2' in mc)

mc.y = 1
mc.method2 = () => {}
console.log('y' in mc)
console.log('method2' in mc)
実行結果
// オブジェクトに対してのプロパティ / メソッドの存在チェック
true
false
true
false
false
true

// クラスに対してのプロパティ / メソッドの存在チェック
true
false
true
false
true
true

モジュールのimport

モジュール環境を有効にした場合、Strictモードが自動的に有効となる。

インポートの構文 - 個別インポート
/*
  name: インポートする要素
  path: モジュールのパス
*/
import { name, ... } from path
インポートの構文 - まとめてインポート
/*
  name: インポートしたすべての要素をまとめた名前
*/
import * as name from path
index.html
<!DOCTYPE html>
<html lang="en">
<!-- ・・・ 省略 ・・・ -->
  <script type="module" src="/main.js"></script>
<!-- ・・・ 省略 ・・・ -->
</html>
module.js
const CONST_VALUE_1 = 1
const CONST_VALUE_2 = 2
export const CONST_VALUE_3 = 3

function func1() {
  console.log('func1 is done')
}

function func2() {
  console.log('func2 is done')
}

export function func3() {
  console.log('func3 is done')
}

class MyClass1 {
  constructor() {
    console.log('MyClass1 constructor is done')
  }

  method() {
    console.log('MyClass1 method is done')
  }
}

class MyClass2 {
  constructor() {
    console.log('MyClass2 constructor is done')
  }

  method() {
    console.log('MyClass2 method is done')
  }
}

export class MyClass3 {
  constructor() {
    console.log('MyClass3 constructor is done')
  }

  method() {
    console.log('MyClass3 method is done')
  }
}

// まとめてexportすることもできる
export { CONST_VALUE_1, CONST_VALUE_2, func1, func2, MyClass1, MyClass2 }
main.js
// モジュールから使用するものだけをimport
import { CONST_VALUE_1, CONST_VALUE_3, func1, func3, MyClass1, MyClass2 } from './module.js'

console.log(CONST_VALUE_1)
console.log(CONST_VALUE_3)

func1()
func3()

new MyClass1().method()
new MyClass2().method()
実行結果
1
3
func1 is done
func3 is done
MyClass1 constructor is done
MyClass1 method is done
MyClass3 constructor is done
MyClass3 method is done
まとめてインポート

as句でモジュールの別名を指定する必要がある。

module.js
const CONST_VALUE = 0

function func() {
  console.log('func is done')
}
class MyClass {
  constructor() {
    console.log('MyClass constructor is done')
  }

  method() {
    console.log('MyClass method is done')
  }
}

export { CONST_VALUE, func, MyClass }
main.js
import * as app from './module.js'

console.log(app.CONST_VALUE)
app.func()
new app.MyClass().method()
実行結果
0
func is done
MyClass constructor is done
MyClass method is done
インポート時に別名をつける

as句を利用することで、インポートするメンバーに対して別をつけることができる。

module.js
const CONST_VALUE = 0

function func() {
  console.log('func is done')
}
class MyClass {
  constructor() {
    console.log('MyClass constructor is done')
  }

  method() {
    console.log('MyClass method is done')
  }
}

export { CONST_VALUE, func, MyClass }
main.js
import { CONST_VALUE, func, MyClass as aliasClass } from './module.js'

console.log(CONST_VALUE)
func()
new aliasClass().method()
実行結果
0
func is done
MyClass constructor is done
MyClass method is done

export default (規定のエクスポート)

export defaultは以下の特徴がある。

  • 同一ファイルに1つだけ宣言できる
  • クラス / 関数名を省略することができる
  • import側が名前を自由につけることができる
    • エクスポート側でにクラス / 関数名を定義していても同じ
module.js
export const CONST_VALUE = 0

// export defaultは1つだけ宣言できる
export default function () {
  console.log('export default func is done')
}
main.js
import
  ExportDefaultFunc, // export defaultの要素をインポート、名前は自由につけることができる
  { CONST_VALUE }    // exportの要素をインポート
from './module.js'

ExportDefaultFunc()
実行結果
export default func is done
module.js
// import側で呼び出す際に名前を自由に変更できる
export default function ExportDefaultFunc() {
  console.log('export default func is done')
}
main.js
// export defaultの要素をインポート、名前は自由につけることができる
import ExportFunc from './module.js'

ExportFunc()
実行結果
export default func is done

動的インポート

import命令は戻り値としてPromiseを返すので、async、await構文を使用して置き換えることもできる。

動的インポートの構文
/*
  path: モジュールのパス
  module: インポートされたモジュール
  statements: モジュールを利用したコード
*/
import(path).then(module => {
  ...statements...
})
module.js
export function func() {
  console.log('func is done')
}

export class MyClass {
  #value = null

  constructor(value) {
    this.#value = value
  }

  method() {
    console.log(`#value: ${this.#value}`)
  }
}
main.js
import('./module.js').then(modules => {
  module.func()
  new module.MyClass(0).method()
})

// asyncとawaitで置き換え
async function asyncFunc() {
  const module = await import('./module.js')
  module.func()
  new module.MyClass(0).method()
}

asyncFunc()
実行結果
func is done
#value: 0

// asyncとawaitで置き換え
func is done
#value: 0
RSI技術ブログ

Discussion