JavaScript の基礎

2024年3月30日

JavaScript とは

JavaScript とは、主に Web アプリケーションを開発するのに用いられているプログラミング言語である。Java とは関係ない。当初は、「ブラウザでなんか余計なことをする怪しいやつ」というイメージがあったが、現在は Web の中核技術の一つになっている。

言語仕様としては ECMAScript (エクマスクリプト、ES) と呼ばれる。バージョンは、ES5、ES2015 (ES6)、ES2016、... などとなっている。JavaScript は ECMAScript の実装という位置づけである。

動作環境

JavaScript は、インタープリタ型言語であり、まずたいていのブラウザで動く。Firefox なら、F12 を押して、[コンソール] を選び、そこにコマンドを打ち込めばよい。

>> 1 + 2
← 3

ブラウザ以外の主な動作環境としては、Node.js や Deno などがある。

JavaScript の動作環境のことをホスト環境という。JavaScript は、コアの言語仕様と、ホスト環境に依存した部分が分離されている。ファイルを扱う機能などは後者にあたり、ホスト環境によって書き方が異なる。

ここではプログラミング言語としての JavaScript について考える。動作環境は Node.js を想定する。

インストール

Linux (Ubuntu 20.04 LTS) の場合、ここ からパッケージをダウンロードし、下記のように展開する。

$ tar xvJf node-v18.17.1-linux-x64.tar.xz

あとは bin ディレクトリにパスを通せばよい。

~/.bashrc

export PATH=~/opt/node-v18.17.1-linux-x64/bin:$PATH

Node.js の実行

node と打てば Node.js が起動する。

$ node
Welcome to Node.js v18.17.1.
Type ".help" for more information.
> a = 1
1

> a + 1
2

終了するには、Ctrl+D を入力するか、Ctrl+C を 2 回入力するか、.exit を入力する。

> .exit

コードをファイルに記述しておくこともできる (これをスクリプトという)。拡張子は .js や .mjs が使われる。

hello.js

console.log("Hello");

次のように実行する。

$ node hello.js
Hello

式

JavaScript のコードは式や構文からなり、式はセミコロン (;) で区切られる。ただ、セミコロンは多くの場合省略できる。

> a = 1 + 2
3

> a =
... 1 +
... 2
3

セミコロンについては、「必ず付けたほうがお行儀がよい」派と、「可能な限り余計なものをは付けるな」派がある。セミコロンを付けないほうが「モダン」な感じがする (付けると C っぽいし、付けないとPython っぽい) が、場合によっては妙な罠にハマることもある (特に、変なところで改行した場合)。

コメント

C/C++ 同様、/* ... */ や // ... でコメントを入れることができる。

/* 文字列を表示 */
console.log("Hello");

変数の定義

JavaScript の変数は、ちょこっと計算するだけなら、Python のように名前に値を = で割り当てればよい。これを代入 (assignment) という。

> a = 1

正式には、let で定義する。

> let a = 1

変数名は、大文字と小文字が区別される。

古いバージョンのプログラムだと、var が使われているのを見るだろう。

> var a = 1

var はややこしいところがある。以下のプログラムを見てみよう。

console.log(a);
let a = 1;

最初に未定義の変数 a が使われている。これを実行すると、当然怒られる。

console.log(a);
            ^

ReferenceError: Cannot access 'a' before initialization

ここで、let を var に変えるとどうか。

console.log(a);
var a = 1;

この場合、undefined を出力する。

$ node var.js
undefined

どういうことかというと、var で定義された変数については、そのスコープ (有効範囲) 内では、順番の前後関係なしにその変数が有効になってしまうのである (「巻き上げ」という)。ただ、値の代入の影響は及ばないので、未定義の値 undefined を持つことになる。これはややこしいので、新しいコードについては let を使う。

何も付けないで変数を定義した場合は、let 同様に怒られる (var のようには動作しない)。

console.log(a);
a = 1;
console.log(a);
            ^

ReferenceError: a is not defined

ただ、Python でうっかりやらかすように、タイプミスで新しい変数を定義してしまう事故が起こりうるので、変数定義には let を使うことを原則にしたほうがよい。

let なしでの変数定義を禁止したければ、strict モード (仕様が厳しめになる) にする方法がある。ファイルの先頭に "use strict" と書く。

"use strict";
a = 1;

そうすると怒られる。

a = 1;
  ^

ReferenceError: a is not defined

class など、新しめの機能を使うと、勝手に strict モードになる。

let は、なんかカッチリした変数定義に見えるが、値の型に縛りはない。

> let a = 1
> a = "hello"
'hello'

定数を定義するものとして、const がある。この場合、値を変更できない (正確にいうと、変数への再代入ができない)。

> const a = 1
> a = 2
Uncaught TypeError: Assignment to constant variable.

JavaScript の変数とオブジェクト

JavaScript のデータはすべてオブジェクトである。変数は、C 言語のような「データが入る箱」ではなく、オブジェクトの名前と考えたほうがよい。代入はオブジェクトに対する名付けであり、変数同士の代入は、オブジェクトの名付けが新たに設定されるだけである。要するに、変数同士の代入は、オブジェクトの参照が渡るだけである。したがって、オブジェクトのコピーを作りたい場合は、明示的な操作が必要であることに注意する。

また、const は代入を禁止しているだけであって、名付けの変更が禁止されているのであり、オブジェクトの値の書き換え禁止を意味しない。

データ型

データの型は typeof で調べられる (関数ではなく演算子)。

> typeof 1
'number'

数値

JavaScript には整数型がなく、数値型は倍精度浮動小数点数になる。

> a = 1.2
1.2

> typeof a
'number'

数学的な処理を行うために、標準で Math オブジェクトが用意されている。

> Math.PI
3.141592653589793

> Math.E
2.718281828459045

> Math.round(1.2)
1

文字列から数値に変換する関数として、parseInt()、parseFloat() がある。

> parseInt("1.2")
1

> parseFloat("1.2")
1.2

変な計算をした場合は、エラーになるのではなく、NaN (Not a Number) や Infinity という特別な値をとる。

> Math.sqrt(2)
1.4142135623730951

> Math.sqrt(-1)
NaN

> Math.sqrt("a")
NaN

> 1/0
Infinity

文字列

"..." あるいは '...' で文字列になる。

> a = "Hello"
'Hello'

> typeof a
'string'

"\u" で Unicode の番号を指定できる。

> "\u0153"
'œ'

文字列に二重引用符を入れたい場合は、バックスラッシュをつける。

> "s = \"xxx\""
's = "xxx"'

あるいは、'...' の中ならそのまま二重引用符を使える。

> 's = "xxx"'
's = "xxx"'

長い文字列は、バックスラッシュで改行できる。

> "Hello, \
... world."
'Hello, world.'

バックスラッシュはエスケープと呼ばれる特別なものである。上で見た "\u" や改行 "\n" のようなものはエスケープ文字と呼ばれ、特別な文字とみなされる。バックスラッシュ自体を表示したい場合は、"\\" とする。

> console.log("\n")


> console.log("\\n")
\n

テンプレートリテラル

`...` で文字列に変数を埋め込める。

> greet = "Hello"
'Hello'

> whom = "world"
'world'

> `${greet}, ${whom}`
'Hello, world'

数値でもよい。

> a = 1
1

> `a = ${a}`
'a = 1'

計算もできる。

> `a = ${2*a}`
'a = 2'

途中の改行はそのまま改行になる。

> s = `Hello,
... world`
'Hello,\nworld'

> console.log(s)
Hello,
world

各文字のアクセス

文字列の各文字にアクセスするには、添字を使う (0 から始まる)。

> s = "Hello, world."
'Hello, world.'

> s[0]
'H'

各文字は変更できない。

slice() によってもっと柔軟なアクセスができる。たとえば、添字 7 以降を取り出すには以下のようにする。

> s.slice(7)
'world.'

1, 4 だと添字 1 から 3 までを取り出す。

> s.slice(1, 4)
'ell'

負の値も使えて、-1 なら最後の文字、-2, -1 なら最後から 2 番目の文字、-6 なら最後から 6 文字目以降となる。

> s.slice(-1)
'.'

> s.slice(-2, -1)
'd'

> s.slice(-6)
'world.'

文字列の長さ

length で文字列の長さを得られる。

> s.length
13

文字列の連結

文字列の連結には + を用いる。

> "Hello" + ", " + "world" + "."
'Hello, world.'

論理値

false と true を取る論理型がある。

> b = true
true

> typeof b
'boolean'

false、true はそれぞれ 0、1 と同じ意味である。

> b + 1
2

undefined と null

JavaScript には、undefined と null という特殊な値がある。undefined は変数の値が未定義 (あるいはオブジェクトのプロパティなどが未定義) のときに使われる。

たとえば、値を指定しないで変数を定義した場合、その値は undefined になる。

> let a
> a
undefined

また、console.log() など、値を返さない関数は、形式的に undefind が返される。

null は、ユーザー側で変数の値が特殊な状況にあることを表したいときに使われる。自分で使う場合は、undefined だとたまたまそうなっている場合と区別がつかないため、ふつうは null を使ったほうがよい。

> a = null
null

列挙型

JavaScript では、enum は予約語になっているが、列挙型は今の所ない。ただ、それっぽいものを作ることはできる。

> const CatType = {
... SABATORA: "sabatora",
... CHATORA: "chatora",
... MIKE: "mike" 
... }
> a = CatType.SABATORA

あるいは

> const CatType = {
... SABATORA: 0,
... CHATORA: 1,
... MIKE: 2
... }
> a = CatType.SABATORA

シンボル

一意の値を作るためにシンボルというものがある。

> const SABATORA = Symbol()
> const CHATORA = Symbol()
> const MIKE = Symbol()
> a = SABATORA
Symbol()

> typeof a
'symbol'

文字列を設定したほうがわかりやすいかもしれない。

> const SABATORA = Symbol("sabatora")
> const CHATORA = Symbol("chatora")
> const MIKE = Symbol("mike")
> a = SABATORA
Symbol(sabatora)

> typeof a
'symbol'

他のものと絶対にかぶらない値がほしい場合などに使える。

配列

次のような形で配列を定義できる。

> a = [1, 2, 3]
[ 1, 2, 3 ]

> typeof a
'object'

配列の要素は型が混在してもよい。

> [1, "2"]
[ 1, '2' ]

続きの数値なら次のような書き方ができる。

> [...Array(10).keys()]
[
  0, 1, 2, 3, 4,
  5, 6, 7, 8, 9
]

1 から始まりとするなら、ちょっと工夫がいる。

> [...Array(10).keys()].map(i => i + 1)
[
  1, 2, 3, 4,  5,
  6, 7, 8, 9, 10
]

奇数列がほしいなら、次のようになる。

> [...Array(5).keys()].map(i => 2*i + 1)
[ 1, 3, 5, 7, 9 ]

繰り返しを行いたい場合は、次のようにできる。

> [].concat(...Array(3).fill([1, 2]))
[ 1, 2, 1, 2, 1, 2 ]

空の数値リストがほしければ、次のようにできる。

> Array(10).fill(0)
[
  0, 0, 0, 0, 0,
  0, 0, 0, 0, 0
]

要素へのアクセス

各要素にアクセスするには、添字を使う (0 から始まる)。

> a[0]
1

slice() を使うと、たとえば次のように 1 要素目から 2 要素目までの部分配列を取り出せる。

> a.slice(0, 2)
[ 1, 2 ]

配列の長さ

長さを知りたければ length を見ればよい。

> a.length
3

ちなみに、長さはなぜか変更できる。

> a = [1, 2, 3]
[ 1, 2, 3 ]

> a.length = 10
10

> a
[ 1, 2, 3, <7 empty items> ]

要素の追加や取り出し

push() で末尾に要素を追加できる。

> a.push(4)
4

> a
[ 1, 2, 3, 4 ]

逆に末尾の要素を取り出すには pop() を使う。

> a.pop()
4

> a
[ 1, 2, 3 ]

途中に要素を挿入するには splice() を用いる。

> a.splice(1, 0, 5)
[]

> a
[ 1, 5, 2, 3 ]

splice() の第1引数が挿入位置、第2引数が置き換える個数 (の場合は挿入なので 0)、その後がデータである。

任意の要素を取り出すのにも splice() を用いる。

> a.splice(1, 1)
[ 5 ]

> a
[ 1, 2, 3 ]

配列の連結

末尾に配列を追加したい場合は、concat() を用いる。ただし、元の配列は変更されない。

> a.concat([5, 6, 4])
[ 1, 2, 3, 5, 6, 4 ]

> a
[ 1, 2, 3 ]

配列の代入

配列の要素をそれぞれ変数に入れるには、次のようにできる。

> [a, b] = [1, 2]
[ 1, 2 ]

> a
1

> b
2

これを分割代入という。

配列の要素を先頭とそれ以外に分けて変数に入れるには、次のようにできる。

> [a, ...b] = [1, 2, 3]
[ 1, 2, 3 ]

> a
1

> b
[ 2, 3 ]

ここで、... を rest (残余) 宣言という。

これを使うと、変数の値を入れ替えることができる。

> [a, b] = [b, a]
[ 2, 1 ]

> a
2

> b
1

配列を別の変数に代入する場合には注意が必要である。

> a = [1, 2, 3]
[ 1, 2, 3 ]

> b = a
[ 1, 2, 3 ]

> b[0] = 2
2

> b
[ 2, 2, 3 ]

> a
[ 2, 2, 3 ]

上の例では、b の要素の値を変更すると、a の要素の値も変わってしまう。代入はオブジェクトを入れる箱に名前を付けるだけのため、b は a と同じオブジェクトを見ているからである。b に a のコピーを割り当てたい場合は、slice() を用いる。

> a = [1, 2, 3]
[ 1, 2, 3 ]

> b = a.slice()
[ 1, 2, 3 ]

> b[0] = 2
2

> b
[ 2, 2, 3 ]

> a
[ 1, 2, 3 ]

あるいは、次のようにしてもよい。

> b = [...a]

ここで ... を spread (展開) 演算子という。

入れ子の配列

配列は入れ子にできる (配列を配列の要素にできる)。

> a = [[1, 2], [3, 4]]
[ [ 1, 2 ], [ 3, 4 ] ]

> a[0]
[ 1, 2 ]

> a[0][0]
1

入れ子の配列の場合、コピーには注意が必要である。

> a = [[1, 2], [3, 4]]
[ [ 1, 2 ], [ 3, 4 ] ]

> b = [...a]
[ [ 1, 2 ], [ 3, 4 ] ]

> b.push(5)
3

> b
[ [ 1, 2 ], [ 3, 4 ], 5 ]

> a
[ [ 1, 2 ], [ 3, 4 ] ]

ここまではよい。配列である要素に値を追加したらどうなるか。

> [0].push(3)
3

> b
[ [ 1, 2, 3 ], [ 3, 4 ], 5 ]

> a
[ [ 1, 2, 3 ], [ 3, 4 ] ]

a のほうも変わってしまう。入れ子になっている場合、中身まで再帰的にコピーしないためである。これを浅い (shallow) コピーという。

深い (deep) コピーは、この場合は次のようにすればよい。

> a = [[1, 2], [3, 4]]
[ [ 1, 2 ], [ 3, 4 ] ]

> b = JSON.parse(JSON.stringify(a))
[ [ 1, 2 ], [ 3, 4 ] ]

> b[0].push(3)
3

> b
[ [ 1, 2, 3 ], [ 3, 4 ] ]

> a
[ [ 1, 2 ], [ 3, 4 ] ]

何をしているかというと、一旦 JSON 形式にシリアライズ (文字列化) して、元に戻している。

集合

配列に少し似たものとして集合 (set) がある。

> a = new Set([1, 2, 2, 0, 5])
Set(4) { 1, 2, 0, 5 }

> typeof a
'object'

集合では値は一意で、順番は関係ない。

要素を追加・削除するにはそれぞれ add()、delete() を用いる。

> a.add(7)
Set(5) { 1, 2, 0, 5, 7 }

> a
Set(5) { 1, 2, 0, 5, 7 }

> a.delete(7)
true

> a
Set(4) { 1, 2, 0, 5 }

has() で要素を持っているかどうかを確認できる。

> a.has(0)
true

> a.has(3)
false

合併などの演算機能は特にない。

配列に変換するには、次のようにすればよい。

> Array.from(a)
[ 1, 2, 0, 5 ]

> [...a]
[ 1, 2, 0, 5 ]

辞書

キーに対して値を持つ辞書 (連想配列) を実現するものとして、Map がある。

> a = new Map([["A", 1], ["B", 2]])
Map(2) { 'A' => 1, 'B' => 2 }

> a.get("B")
2

空の状態から作っていくこともできる。

> a = new Map()
Map(0) {}

> a.set("A" ,1)
Map(1) { 'A' => 1 }

> a.set("B", 2)
Map(2) { 'A' => 1, 'B' => 2 }

キーを持っているかどうかは次のように調べられる。

> a.has("A")
true

> a.has("C")
false

実は、Map とは別に、Python の辞書っぽいものを作れる。

> a = {"A": 1, "B": 2}
{ A: 1, B: 2 }

> a["B"]
2

空の状態から作っていくこともできる。

> a = {}
{}

> a["A"] = 1
1

> a["B"] = 2
2

> a
{ A: 1, B: 2 }

キーを持っているかどうかは次のように調べられる。

> "A" in a
true

> "C" in a
false

これは辞書ではなくて、次で述べるオブジェクトそのものである。

オブジェクト

オブジェクト指向プログラミング言語において、クラスは複数の変数や関数をひとまとめにしたもので、その実体がオブジェクトである。JavaScript にも最近のバージョンにはクラスがあるが、もともとはもっと荒削りなオブジェクト観を持っている。

JavaScript のオブジェクトは、最も単純には次のようにすれば作成できる。

> a = {}
{}

> typeof a
'object'

これは上の説明では空の辞書のような感じだったが、これは空のオブジェクトである。オブジェクトにプロパティ (「属性」と呼びたいところだが、あとで「プロパティの属性」というのが出てくるため避ける) をもたせるには、次のようにする。

> prof = {"name": "omochi", "type": "sabatora", "age": 4}
{ name: 'omochi', type: 'sabatora', age: 4 }

> prof.name
'omochi'

これは、いわゆる JSON (JavaScript Object Notation) の書き方である。JavaScript 上で記述する場合は、プロパティは文字列でなくてもよい。

> prof = {name: "omochi", type: "sabatora", age: 4}
{ name: 'omochi', type: 'sabatora', age: 4 }

> prof.name
'omochi'

以下のようにもできる。

> prof = {}
{}

> prof.name = "omochi"
'omochi'

> prof.type = "sabatora"
'sabatora'

> prof.age = 4
4

> prof
{ name: 'omochi', type: 'sabatora', age: 4 }

「辞書」のようにしてもよい。

> prof = {}
{}

> prof["name"] = "omochi"
'omochi'

> prof["type"] = "sabatora"
'sabatora'

> prof["age"] = 4
4

> prof
{ name: 'omochi', type: 'sabatora', age: 4 }

プロパティに空白を含むこともできるが、この場合は辞書形式でしかアクセスできない。

> prof["cat type"] = "sabatora"
'sabatora'

プロパティは動的に生成できる。

> prof["cat" + "_type"] = "sabatora"
'sabatora'

> prof.cat_type
'sabatora'

オブジェクトの定義のときにプロパティを動的に生成したい場合は、次のようにする。

> prof = {["cat" + "_type"]: "sabatora"}
{ cat_type: 'sabatora' }

これは算出プロパティと呼ばれる。

メソッドは、オブジェクトに関数を持たせたものになる。

> prof = {
... name: "",
... set_name: function(name) { this.name = name }
... }
> prof.set_name("omochi")
> prof
{ name: 'omochi', set_name: [Function: set_name] }

メソッドは次のような省略記法が可能である。

> prof = {
... name: "",
... set_name(name) { this.name = name }
... }

JavaScript のクラスは、このオブジェクトをベースに成立している。

条件付きプロパティアクセス

存在しないプロパティにアクセスした場合、undefined になるだけである。

> a = {}
{}

> a.b
undefined

ただし、undefined のものにプロパティアクセスすると、当然だがエラーになる。

> a.b.c
Uncaught TypeError: Cannot read properties of undefined (reading 'c')

undefined のプロパティにアクセスした場合も、undefined になってほしい場合がある。この場合に使うのが、条件付きプロパティアクセスである。ドット . の代わりに ?. を使う。

> a.b?.c
undefined

分割代入

オブジェクトのプロパティを個別の変数に一度に取り出すことができる。

> prof = {name: "omochi", type: "sabatora", age:4}
{ name: 'omochi', type: 'sabatora', age: 4 }

> {name, age} = prof
{ name: 'omochi', type: 'sabatora', age: 4 }

> name
'omochi'

> age
4

これを分割代入 (destructuring) という。{name, age} = prof で、指定した名前のプロパティだけが取り出される。

JSON

JSON (JavaScript Object Notation) は JavaScript のオブジェクトの文字列表記である。次のようにすると、オブジェクトのプロパティを文字列化する。

> JSON.stringify(prof)
'{"name":"omochi","age":4}'

文字列からオブジェクト化するには、次のようにする。

> JSON.parse('{"name":"annko","age":4}')
{ name: 'annko', age: 4 }

オブジェクトのコピー

オブジェクトを別の変数に代入して変更すると、元のオブジェクトも変更されてしまう。

> prof = {name: "omochi", age: 4}
{ name: 'omochi', age: 4 }

> prof2 = prof
{ name: 'omochi', age: 4 }

> prof2.name = "annko"
'annko'

> prof
{ name: 'annko', age: 4 }

これは、変数間の代入がオブジェクトの参照を渡しているだけだからである。コピーするには、spread (展開) 演算子と呼ばれる 3 点のドット (...) を用いて、新しいオブジェクトの中に既存のオブジェクトのプロパティを展開すればよい。

> prof = {name: "omochi", age: 4}
{ name: 'omochi', age: 4 }

> prof2 = {...prof}
{ name: 'omochi', age: 4 }

> prof2.name = "annko"
'annko'

> prof
{ name: 'omochi', age: 4 }

ただし、これは配列同様、浅いコピーである。

> a = {a: [1, 2]}
{ a: [ 1, 2 ] }

> b = {...a}
{ a: [ 1, 2 ] }

> b.a.push(3)
3

> b
{ a: [ 1, 2, 3 ] }

> a
{ a: [ 1, 2, 3 ] }

深いコピーをするには、配列同様、一旦 JSON にする方法がある。

> a = {a: [1, 2]}
{ a: [ 1, 2 ] }

> b = JSON.parse(JSON.stringify(a))
{ a: [ 1, 2 ] }

> b.a.push(3)
3

> b
{ a: [ 1, 2, 3 ] }

> a
{ a: [ 1, 2 ] }

オブジェクトの場合、これでは問題がある場合がある。この方法では関数を扱えないのである。

> a = {a: [1, 2], f: function(){}}
{ a: [ 1, 2 ], f: [Function: f] }

> JSON.stringify(a)
'{"a":[1,2]}'

これに対応するには、以下のようにできるかもしれない。

> b = {}
{}

> for (let p in a) {
... if (typeof a[p] === "function") b[p] = a[p]
... else b[p] = JSON.parse(JSON.stringify(a[p]))
... }

> b
{ a: [ 1, 2 ], f: [Function: f] }

> b.a.push(3)
3

> b
{ a: [ 1, 2, 3 ], f: [Function: f] }

> a
{ a: [ 1, 2 ], f: [Function: f] }

関数はすなおに代入して、関数でなければコピーしている。class を使うなら、コピーコンストラクタを用意しておいたほうがよいかもしれない。

オブジェクトの変更禁止

JavaScript には const があるが、これは再代入を禁止しているだけである。したがって、オブジェクトの値自体は変更できる。

> const a = []
> a.push(1)
1

> a
[ 1 ]

オブジェクトの変更自体を禁止したい (定数にしたいというより、immutable にしたい) 場合は、Object.freeze() を使う。

> const a = []
> Object.freeze(a)
[]

> a.push(1)
Uncaught TypeError: Cannot add property 0, object is not extensible
    at Array.push ()

プロパティを追加したり、変更したりできない (ただし、エラーは出ない)。

ただし、Object.freeze() は「浅い」フリーズで、中身までは面倒見てくれない。

> const a = { b: [] }
> Object.freeze(a)
{ b: [] }

> a.b.push(1)
1

a がもつ b 自体はフリーズされていない。これを解決するには、再帰的にフリーズする。

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
function deepFreeze(object) {
    const propNames = Reflect.ownKeys(object);
    for (const name of propNames) {
        const value = object[name];

        if ((value && typeof value === "object") || typeof value === "function") {
            deepFreeze(value);
        }
    }
    return Object.freeze(object);
}

const a = { b: [] }
deepFreeze(a);
a.b.push(1); // エラー

式と演算子

式はデータと演算子で構成される。

数値については、以下のような演算子が使える。

演算子例意味
+a + b加算
-a - b減算
*a * b乗算
/a / b除算 (浮動小数点数)
//a // b除算 (整数)
**a ** b累乗
%a % b剰余
--a負符号
++i++, ++iインクリメント (1 足す)
--i--, --iデクリメント (1 引く)

i += 1 で i = i + 1 を意味するような代入演算子というものもある。また、& や << などのビット演算子というものもある。

制御構造

ブロック

{...} でブロックを作る。

> {
... let a = 1
... let b = 2
... a + b
... }
3

この場合、ブロックの外では a、b にアクセスできない。

> a
Uncaught ReferenceError: a is not defined

let を付けなければ、ブロックの外でもアクセスできる (すなわち、グローバル変数になる)。そういう意味で、ここでの let は重要である。

if...else 文

if (v === 1) {
  console.log("sabatora");
} else if (v === 2) {
  console.log("chatora");
} else {
  console.log("mike");
}

if のあとの括弧の中は、条件式になり、値が 0 以外ならその下のブロックの中身が実行され、値が 0 だと実行されない。else if は次の条件式で、else はどれにも当てはまらない場合である。if の下は 1 文ぶら下がるようになっていて、上の例はそこにブロックがぶら下がっている状態になっている。また、else if も else に if 文がぶら下がっている状態である。ブロック内に文が 1 行しかないなら、次のようにも書ける。

if (v === 1)
  console.log("sabatora");
else if (v === 2)
  console.log("chatora");
else
  console.log("mike");

値を分岐させたい場合は、if...else 文の代わりに、三項演算子を使うこともできる。

let i = v === 1 ? 0 : 1;

とまあ、ここまでは一部を除いて C 言語にそっくりである。条件文では、以下のような比較演算子や論理演算子が使える。

比較演算子

値を比較する演算子として、<、>、<=、>= がある。等しいを表すものは === である。"等しくない" は !== である。

なぜ == ではなくて、=== なのか? 一応 == もあるのだが、ちょっと条件がゆるいのである。

> "1" == true
true

> "1" === true
false

上の例の場合、== だと、"1" が 1 に変換され、true が 1 に変換され、どちらも 1 だから "1" は true に等しい、となってしまう。これは意図しない動作をうっかり作り込む原因になりうるため、== の使用は避けられ、=== の使用が推奨される。

比較演算子は、文字列の比較も行える。

> "abc === "abc"
true

> "abc" < "abd"
true

配列には使えない。一見、使えるように見えるが、オブジェクトを比較しているだけである。

> a = [1, 2, 3]
[ 1, 2, 3 ]

> a === a
true

> a === [1, 2, 3]
false

論理演算子

条件を複数記述するための論理演算子として、&& (AND)、|| (OR) がある。また、式の頭に ! をつけると否定になる。途中で結果が決まったらその先は評価しない (短絡という)。たとえば、&& で早々に false になったり、|| で早々に true になったりすると、その先は評価されない。関数を呼んでいたりすると、呼ばれないことになる。

たとえば、x が 0 以上でかつ y が 5 以下であるときになにかしたい、という場合は、次のようになる。

if (x >= 0 && y <= 5) {
    ...

この場合、そもそも x が 0 より小さかったら、y の式はまったく評価されない。

switch 文

switch 文は、if 文の特殊なものである。

switch(v) {
case 1:
    console.log("sabatora");
    break;
case 2:
    console.log("chatora");
    break;
case 3:
    console.log("mike");
    break;
default:
    console.log("none");
    break;
}

v の値に応じて、対応する case の下の文が実行される。値は整数以外に文字 (文字列ではない) でもよい。break は必須でなく、書き忘れると下の条件に流れ落ちる (フォールスルーという)。最後の default の break は必要ないのだが、習慣として書かれる。default がいらない場合も習慣として default が書かれる (うっかり通ることがある)。

for ループ

for ループは規定の回数だけ繰り返したりするときに使う。

for(let i = 0; i < 10; i++){
    console.log(i);
}

括弧の中はセミコロンで区切られており、それぞれ初期化、ループ条件、更新となっている。変数 i のスコープはループブロック内で閉じている。

ここまでは C 言語と同様だが、JavaScript には、for-of ループという配列のような反復可能なものについてのループがある。

> for (let i of ["sabatora", "chatora", "mike"])
... console.log(i)
sabatora
chatora
mike

文字列でもよい。

> for (let i of "sabatora")
... console.log(i)
s
a
b
a
t
o
r
a

Map だと、キーと値のペアを巡回する。

> a = new Map([["A", 1], ["B", 2]])
Map(2) { 'A' => 1, 'B' => 2 }

> for (let [k, v] of a) {
... console.log(`${k} ${v}`)
... }
A 1
B 2

また、オブジェクトのプロパティを巡回する for-in ループというものもある。

> prof = {name: "omochi", type: "sabatora", age: 4}
{ name: 'omochi', type: 'sabatora', age: 4 }

> for (let i in prof)
... console.log(`${i} ${prof[i]}`)
name omochi
type sabatora
age 4

ただし、for-of ループでのオブジェクトのプロパティの巡回も可能である。

> for (let i of Object.keys(prof))
... console.log(i)
name
type
age

> for (let i of Object.values(prof))
... console.log(i)
omochi
sabatora
4

> for (let [k, v] of Object.entries(prof))
... console.log(`${k} ${v}`)
name omochi
type sabatora
age 4

while ループ

while ループは、条件が成り立つあいだ繰り返す。

> i = 0
0

> while (i < 10) {
... console.log(i)
... i++
... }
0
1
2
3
4
5
6
7
8
9
9

do...while ループ

do...while ループは、while ループに似ているが、条件判定が最後にあるため、少なくとも 1 回はループの内容が実行される。

> do {
... console.log(i)
... i++
... } while (i < 10)
0
1
2
3
4
5
6
7
8
9
9

continue と break

continue でループを次に進めることができる。

> for (let i = 0; i < 10; i++) {
... if (i % 2 === 0) continue
... console.log(i)
... }
1
3
5
7
9

break でループを抜けることができる。

> for (let i = 0; i < 10; i++) {
... if (i === 5) break
... console.log(i)
... }
0
1
2
3
4

continue、break にはラベルを使うことができる。これで入れ子になったループを一気に抜けたりできる。

loop: for (let i = 0; i < 5; i++) {
    for (let j = 0; j < 5; j++) {
        if (i === 3) break loop;
        console.log(`${i} ${j}`);
    }
}

loop: が任意のラベルの設定である。

例外処理

try...catch でエラー (例外) を受け止めることができる。

> try {
... f() // 存在しない関数
... } catch (error) {
... console.error(`error: ${error}`)
... }
error: ReferenceError: f is not defined

必ず実行してほしいコードがあるなら、finally 節をつければよい。

> try {
... f()
... } catch (error) {
... console.error(`error: ${error}`)
... } finally {
... console.log("finally")
... }
error: ReferenceError: f is not defined
finally

throw で例外を投げることができる。

> try {
... throw new Error("error!")
... } catch (error) {
... console.error(`error: ${error}`)
... }
error: Error: error!

プログラムの停止

プログラムを停止するのは、JavaScript というより Node.js の領域である。process.exit() でプログラムは停止する。

process.exit();

引数として終了コードをとる。

なにか致命的なことが起こったときに緊急停止する process.abort() というものもある。

JavaScript の言語仕様にあるものだが、debugger という文を置いておくと、そのままデバッグ環境に入れる環境がある (ブラウザの開発環境など)。

関数

関数は、引数を受け取って値を返す処理のまとまりである。次のように定義する。

> function f(x, y) {
... return x + y 
... }
> f(1, 2)
3

複数の値を返したければ、配列を使う。

> function f(x, y) {
... return [x + y, x - y]
... }
> f(1, 2)
[ 3, -1 ]

関数は何も返さなくてもよい。

> function f(x) {
... x[0] = 0
... }

これは undefined を返している。

> x = [1, 2, 3]
[ 1, 2, 3 ]

> console.log(f(x))
undefined

関数を変数に入れることもできる。

> function f(x, y) {
... return x + y 
... }
> g = f
[Function: f]

> g(1, 2)
3

JavaScript の関数は、同じ名前で引数が異なる関数を多重定義することはできない。

条件付き呼び出し

JavaScript には undefined があるため、未定義のものをそのまま扱うことがある。そのため、undefined で関数を呼んでしまった場合にエラーにしないで undefined を返すようにする方法がある。

undefined でふつうに関数を呼ぶと、当然ながらエラーになる。

> a = undefined
undefined
> a()
Uncaught TypeError: a is not a function

関数名に ?. を付けて呼ぶと、エラーにならずに値が undefined になる。

> a?.()
undefined

これにより、値が undefined だったらなにか起こったと判断することができる。

引数

関数内で引数に値を代入しても、呼び出し元の変数の値は変わらない。

> function f(x) {
... x = 0
... }
> x = 1
1

> f(x)
> x
1

ただし、引数のオブジェクトの値 (内部状態) を変更した場合、呼び出し元のオブジェクトの値も変わってしまう。

> function f(x) {
... x[0] = 0
... }
> x = [1, 2, 3]
[ 1, 2, 3 ]

> f(x)
> x
[ 0, 2, 3 ]

これは、変数同士の代入同様、引数にオブジェクトの参照が渡っているためである。

キーワード引数

JavaScript にキーワード引数の仕組みは直接的にはないが、分割代入を用いて似たようなことはできる。

> function f({x, y}) {
... return x - y 
... }
> f({x: 3, y: 2})
1

> f({y: 2, x: 3})
1

デフォルト値

引数にデフォルト値を設定することができる。デフォルト値がある引数は値を指定してもしなくてもよい。ただし、省略できるのは後ろの引数だけである。

> function f(x=3, y=2) {
... return x - y 
... }
> f()
1

> f(4)
2

> f(6, 3)
3

JavaScript の場合、デフォルト値が設定されていない変数も省略できる。その場合、引数の値は undefined になる。

JavaScript のデフォルト値は、Python と少し異なるところがある。それは、デフォルト値がすべての呼び出しで共有されないことである。たとえば、Python だと次のようになる。

>>> def f(x, a=[]):
...     a.append(x)
...     return a
... 
>>> f(1)
[1]

>>> f(2)
[1, 2]

>>> f(3)
[1, 2, 3]

これは、引数 a のデフォルト値である空のリストオブジェクトが、すべての呼び出しで共有されてしまっているためである。JavaScript だとこうならない。

> function f(x, a=[]) {
... a.push(x)
... return a
... }
> f(1)
[ 1 ]

> f(2)
[ 2 ]

> f(3)
[ 3 ]

つまり、呼び出しのたびに引数 a は空の配列オブジェクトが設定されているのである。

可変長引数

可変長引数にするには、...args の形にする。これを rest (残余) パラメータという。

> function sum(...args) {
... let s = 0
... for (let i of args) {
... s += i
... }
... return s
... }
> sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
55

rest パラメータは引数リストの最後に位置する必要がある。

... は、配列の要素を引数に展開するのにも使われる。この場合は spread (展開) 演算子と呼ばれる。

> sum(...[1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
55

再帰呼び出し

関数の再帰呼び出しはふつうにできる。

> function fibonacci(n) {
... if (n === 0) return 0
... else if (n === 1) return 1
... else return fibonacci(n - 2) + fibonacci(n - 1)
... }
> fibonacci(10)
55

無名関数

関数名を書かなければ、無名関数になる。

> f = function (x, y) { return x + y }
> f(1, 2)
3

アロー関数

アロー関数と呼ばれるもので、無名関数を定義できる。

> f = (x, y) => x + y 
> f(1, 2)
3

アロー関数の場合、ブロックを使わなければ式の値が返り値になる。ブロックを使う場合には return がいる。

> f = (x, y) => { return x + y }

アロー関数は、map() のように配列の要素に関数を適用するなどのときに使う。

> [1, 2, 3].map(x => 2*x)
[ 2, 4, 6 ]

引数の束縛

関数の引数の一部を固定 (束縛) して、新しい関数を作ることができる。

> f = (x, y) => x + y 
> f1 = f.bind(null, 1)
> f1(2)
3

もちろん、新しい関数を直接作ってもよい。

> f2 = y => f(2, y)
> f2(3)
5

クロージャ

次のように、呼ぶたびに 1 増えた数値を返す関数を定義したい。

> f()
1

> f()
2

> f()
3

そのためには、ローカルの環境にオブジェクトを閉じ込める形で関数を定義すればよい。この仕組みをクロージャという。目的の関数は、次のように実現できる。

> f = (() => { let x = 0; return () => ++x; })()
> f()
1

> f()
2

> f()
3

f には関数が入っていて、その関数は親関数内の x を参照し続けるため、こうなる。

これは、次のようにも書ける。

> f = (x => _ => ++x)(0)
> f()
1

> f()
2

> f()
3

おまけだが、関数はオブジェクトの一種なので、プロパティを持てるため、次のようなことができる。

> function f() { return ++f.x }
> f.x = 0
0

> f()
1

> f()
2

> f()
3

クラス

C++ などのオブジェクト指向プログラミング対応言語では、データと手続きをひとまとまりとして、オブジェクトの設計図であるクラスを構成することができる。JavaScript にも一応このタイプのクラスの仕組みがある。「一応」とつけているのは、JavaScript の場合は特殊な事情があるためである。

class Prof {
    constructor(name, type, age) {
        this.set_name(name);
        this.set_type(type);
        this.set_age(age);
    }

    set_name(name) { this.name = name; }
    set_type(type) { this.type = type; }
    set_age(age) { this.age = age; }

    show_profile() {
        console.log(`name: ${this.name}`);
        console.log(`type: ${this.type}`);
        console.log(`age: ${this.age}`);
    }
}

construct() はオブジェクト作成時に自動的に呼ばれるコンストラクタである (1 つしか持てない)。その他の関数 (メソッド) も含め、function なしで書かれている。プロパティやメソッド (まとめてフィールドと呼ばれる) にアクセスするには this を使う。

オブジェクトを作るには、次のようにする。

prof1 = new Prof("omochi", "sabatora", 3);

オブジェクトのメソッドは、次のように実行できる。

prof1.show_profile();

実行結果

name: omochi
type: sabatora
age: 3

上のクラス定義は、次のようにも書ける。

function prof(name, type, age) {
    let o = Object.create(prof.methods);
    o.set_name(name);
    o.set_type(type);
    o.set_age(age);
    return o;
}

prof.methods = {
    set_name(name) { this.name = name; },
    set_type(type) { this.type = type; },
    set_age(age) { this.age = age; },
    show_profile() {
        console.log(`name: ${this.name}`);
        console.log(`type: ${this.type}`);
        console.log(`age: ${this.age}`);
    }
}

この場合、オブジェクトの作成は次のようにする。

prof1 = prof("omochi", "sabatora", 3);

あるいは、次のようにも書ける。

function Prof(name, type, age) {
    this.set_name(name);
    this.set_type(type);
    this.set_age(age);
}

Prof.prototype = {
    set_name(name) { this.name = name; },
    set_type(type) { this.type = type; },
    set_age(age) { this.age = age; },
    show_profile() {
        console.log(`name: ${this.name}`);
        console.log(`type: ${this.type}`);
        console.log(`age: ${this.age}`);
    }
}

この場合のオブジェクトの作成は class の場合と同じである。

prof1 = new Prof("omochi", "sabatora", 3);

prof() は Object.create() でオブジェクトを作成して返している。Prof() は new を付けて呼び出されることを前提とした関数で、それによってオブジェクトが作成される。prof.methods は Object.create() に渡すためのもので、名前に意味はないが、Prof.prototype については、オブジェクト作成に自動的に使われるものなので、この名前に意味がある。

こんな感じで、見た目は C++ のクラスに似ているようで、中身はかなり異なっている。

アクセッサ

上の例では、プロパティを隠蔽 (カプセル化) するために値を設定するための関数 (セッター) を用意したが、直接プロパティに値を入れることもできてしまう。これを防ぐ仕組みとして、set/get がある。

class Prof {
    constructor(name, type, age) {
        this.name = name;
        this.type = type;
        this.age = age;
    }

    set name(name) { this._name = name; }
    set type(type) { this._type = type; }
    set age(age) { this._age = age; }
    get name() { return this._name; }
    get type() { return this._type; }
    get age() { return this._age; }

    show_profile() {
        console.log(`name: ${this.name}`);
        console.log(`type: ${this.type}`);
        console.log(`age: ${this.age}`);
    }
}

上の例では、this.name などの代わりに this._name というプロパティにデータを入れている。これでも直接プロパティを触れはするが、アンダーバーによって少し「隠している感」は出る。set name()/get name() などのメソッドは、this.name などでアクセスしたときに自動的に適用される関数である。プロパティに直接アクセスしているように見える操作でも、いちいちメソッドが介在するため、プロパティをカプセル化できる (こういうメソッドをアクセッサという)。上の例だとあまり意味がないように見えるが、プロパティの値の設定値を制限したり、値を計算して返したりといったことができる。

プライベートフィールド

上述の例では、プロパティにいきなり値を設定しているが、初期値を与えることもできる。

class Prof {
    _name = "omochi";
    _type = "sabatora";
    _age = 3;
    
    constructor(name, type, age) {
        if (name !== undefined) this.name = name;
        if (type !== undefined) this.type = type;
        if (type !== undefined) this.age = age;
    }
    ...

コンストラクタに引数が指定されない場合にも対応させた。コンストラクタに引数を与えないでオブジェクトを作成してみる。

prof = new Prof();
prof.show_profile();

結果

name: omochi
type: sabatora
age: 3

ここで、プロパティのアンダーバーをシャープ # に変えると、プライベートプロパティになる。

class Prof {
    #name = "omochi";
    #type = "sabatora";
    #age = 3;
    
    constructor(name, type, age) {
        if (name !== undefined) this.name = name;
        if (type !== undefined) this.type = type;
        if (type !== undefined) this.age = age;
    }

    set name(name) { this.#name = name; }
    set type(type) { this.#type = type; }
    set age(age) { this.#age = age; }
    get name() { return this.#name; }
    get type() { return this.#type; }
    get age() { return this.#age; }

    show_profile() {
        console.log(`name: ${this.name}`);
        console.log(`type: ${this.type}`);
        console.log(`age: ${this.age}`);
    }
}

アンダーバーを付けたプロパティの場合は気分の問題であり、アクセスしようと思えばアクセスできたが、プライベートプロパティの場合、アクセスしようとすると怒られる。

nsole.log(prof.#name);
                ^

SyntaxError: Private field '#name' must be declared in an enclosing class

じゃあ最初からそうしろよ...という話だが、以前はこの機能がなかったので工夫していたということである。

メソッドのほうも、# を付けるとクラス内からしか呼べなくなる。

静的フィールド

個々のオブジェクトではなく、クラス自体に属するプロパティやメソッドがほしい場合がある。そういうときは静的フィールドを使う。

たとえば、クラスの使い方の例を表示させたいとする。

class Prof {
    static example = 'const prof = new Prof("omochi", "sabatora", 3)';
    static show_example() {console.log(this.example)};
    ...

static を付けると静的フィールドになる。使い方は以下のようになる。

Prof.show_example();

結果

const prof = new Prof("omochi", "sabatora", 3)

オブジェクトから静的メソッドを呼んだりはできない。

継承

既存のクラスをベースにして別のクラスを作ることができる。これを継承という。

class Prof2 extends Prof {
    constructor(name, type, age) {
        super(name, type, age);
        this.food = [];
    }

    add_food(food) {
        this.food.push(food);
    }

    show_profile() {
        super.show_profile();
        console.log(`food: ${this.food}`);
    }
}

extends でベースとなるクラスを指定している。ベースのクラスをスーパークラスという。継承で作られるクラスをサブクラスという。サブクラスからスーパークラスにアクセスするには、super を使う。

prof2 = new Prof2("annko", "chatora", 4);
prof2.add_food("karikari");
prof2.show_profile();

実行結果

name: annko
type: chatora
age: 4
food: karikari

ところで、配列の要素にはどんな型でも入れられるが、すべての要素が show_profile() というメソッドをもつと想定して実行させることができる。

class Prof3 {
    show_profile() {
        console.log("name: tamago");
        console.log("type: mike");
        console.log("age: 5");
    }
}

prof_list = [prof1, prof2, new Prof3()];
for (p of prof_list) {
    p.show_profile();
    console.log();
}

実行結果

name: omochi
type: sabatora
age: 3

name: annko
type: chatora
age: 4
food: karikari

name: tamago
type: mike
age: 5

これは、C++ だと多相性 (仮想関数) を用いて実現するもので、実際 Prof と Prof2 は親子だからよいのだが、Prof3 にいたっては赤の他人であり、Prof の親戚のように振る舞っているだけである。こういう動作を、「アヒルのように振る舞うならそれはアヒルだ」ということわざ?からダックタイピングという。

無名クラス

無名関数と同様に、無名クラスがある。

const Prof = class {
    constructor(name, type, age) {
        this.set_name(name);
        this.set_type(type);
        this.set_age(age);
    }

    set_name(name) { this.name = name; }
    set_type(type) { this.type = type; }
    set_age(age) { this.age = age; }

    show_profile() {
        console.log(`name: ${this.name}`);
        console.log(`type: ${this.type}`);
        console.log(`age: ${this.age}`);
    }
}

これだけだとあまり意味がないが、クラス定義を動的に生成したりするのに使える。

function add_food(base) {
    return class extends base {
        constructor(name, type, age) {
            super(name, type, age);
            this.food = [];
        }

        add_food(food) {
            this.food.push(food);
        }

        show_profile() {
            super.show_profile();
            console.log(`food: ${this.food}`);
        }
    }
}

const Prof2 = add_food(Prof);

これはどういうことなのだろうか。もっとも単純なクラスを作ってみる。

> A = class {}
> typeof A
'function'

型は関数になっている。コンストラクタを new を付けて呼ぶ特別な関数とみなせなくはない。関数はオブジェクトである。つまり、クラスもオブジェクトということになる。

フィールドの動的追加

JavaScript のオブジェクトには、あとから柔軟にフィールドを追加できる。たとえば、オブジェクトにメソッドを追加できる。

prof1 = new Prof("omochi", "sabatora", 3);
prof2 = new Prof("annko", "chatora", 4);

prof1.f = () => {console.log("f");}
prof1.f();
prof2.f(); // エラー

オブジェクトにメソッドを追加した場合、当然ながらそのオブジェクトからしか呼べない。

クラス自体にメソッドを追加したい場合は、クラスの prototype を使う。

Prof.prototype.g = () => {console.log("g")};
prof1.g();
prof2.g();

クラスの prototype に追加したものは、そのクラスのオブジェクト全体に追加されることになる。

プロパティを持つかどうか

オブジェクトがプロパティを持つかどうかは、in と hasOwnProperty() で調べられる。in は、継承したものも含めてプロパティを持つかを調べる。hasOwnProperty() は継承したものは含まず、そのオブジェクト自身が持つプロパティを調べる。

上述の prof2 に対してプロパティ "name" を持つかどうか調べる。

console.log("name" in prof2);
console.log(prof2.hasOwnProperty("name"));

結果は以下のようになる。

true
false

hasOwnProperty() と同じことを行う Object.hasOwn() というものもある。

console.log(Object.hasOwn(prof2, "name"));

プロパティの属性

プロパティには属性というものがある。属性は、Object.getOwnPropertyDescriptors() で調べられる。

console.log(Object.getOwnPropertyDescriptors(prof1));

結果

{
  _name: {
    value: 'omochi',
    writable: true,
    enumerable: true,
    configurable: true
  },
  _type: {
    value: 'sabatora',
    writable: true,
    enumerable: true,
    configurable: true
  },
  _age: { value: 3, writable: true, enumerable: true, configurable: true }
}

ここで、writable はプロパティの値を更新できるかどうか、enumerable は for-in ループで訪問されるかどうか、configurable はプロパティを削除したり属性を変更したりできるかどうかである。

プロパティを指定して調べることもできる。

console.log(Object.getOwnPropertyDescriptor(prof1, "_name"));
{
  value: 'omochi',
  writable: true,
  enumerable: true,
  configurable: true
}

プロパティの属性を変えるものとして、Object.seal() と Object.freeze() がある。

Object.seal(prof1);
Object.freeze(prof1);

Object.seal() では configurable が false になり、Object.freeze() ではさらに writable も false になる。

Object.seal()、Object.freeze() すると、オブジェクトにフィールドを追加することができなくなる。これはプロパティの属性の問題ではなく、オブジェクトの問題である。Object.preventExtensions() を適用すると、オブジェクトにプロパティを追加できなくなる。Object.seal()、Object.freeze() を使うと、これが適用されるのである。オブジェクトにこれらの関数が適用されているかどうかは、それぞれ Object.isExtensible()、Object.isSealed()、Object.isFrozen() で調べられる。

個別のプロパティの属性を設定したい場合は、Object.defineProperty() を用いる。

Object.defineProperty(prof1, "_name", {
    writable: false,
    configurable: false
});
console.log(Object.getOwnPropertyDescriptor(prof1, "_name"));

結果

{
  value: 'omochi',
  writable: false,
  enumerable: true,
  configurable: false
}

これは、新しいプロパティを定義するのにも使える。

Object.defineProperty(prof1, "_name", {
    value: "omochi",
    writable: false,
    enumerable: true,
    configurable: false
});

非同期処理

JavaScript はもともと Web ブラウザで動的な UI (ユーザーインターフェイス) を実現するためのものである。JavaScript は基本的にシングルスレッドで動くが、逐次処理をしてしまうと、処理が行われている間 UI が固まってしまって、ユーザー体験上都合が悪い。そこで、JavaScript ではシングルスレッドながら非同期処理ができるようになっている。

コールバック関数

非同期処理を行うと、関数の返り値をその場で受け取ることができないことが起きる。そこで、準備ができた時点で呼び出してもらうための仕組みとして、コールバック (callback) 関数がある。

次の例は、Node.js の fs モジュールを用いたファイルの読み込みである。

const fs = require("fs");

fs.readFile("test.txt", "utf8", (err, data) => {
    if (err) {
        console.log(err);
    } else {
        console.log(data);
    }
});

fs.readFile() の第 3 引数に関数を渡しているが、これがあとで呼び出される。

コールバック関数の欠点は、コールバック関数内で非同期処理を呼び出し続けるような処理だと、どんどん入れ子が深くなっていってしまうことである。これをコールバック地獄 (callback hell) というらしい。

Promise

コールバック地獄を避けるために考えたされたのが Promise である。これは、一旦あとで値を返すことを約束した Promise オブジェクトを返しておいて、準備ができたら値を取り出すという仕組みである。

const promise = fs.promises.readFile("test.txt", "utf8");

promise
    .then((data) => { console.log(data); })
    .catch((err) => { console.log(err); });

fs.promises.readFile() は、Promise 版の readFile() である。これは、一旦 Promise オブジェクトを返す。それに対して then() と catch() でそれぞれ結果とエラーを受け取れる。上記は次のように書いてもよい。

fs.promises.readFile("test.txt", "utf8")
    .then((data) => { console.log(data); })
    .catch((err) => { console.log(err); });

then() の中でさらに Promise 版の非同期処理を実施する場合、then() に then() をつないでいけばよい。これにより、深い入れ子がいらなくなり、コールバック地獄を避けることができる。

async/await

自前の関数を非同期処理に対応したければ、async をつければよい。

async function f() {
    ...
}

async 関数の返り値は Promise 型になる。

非同期処理では処理の順番が確定しない。あくまでコードの順番に処理したいときのために、await がある。これは async 関数内で使える。

たとえば、それぞれ "x"、"y"、"z" を出力する関数 fx()、fy()、fz() があるとする。

function fx() {
    for (let i = 0; i < 5; i++) {
        console.log("x");
    }
}

function fy() {
    for (let i = 0; i < 5; i++) {
        console.log("y");
    }
}

function fz() {
    for (let i = 0; i < 5; i++) {
        console.log("z");
    }
}

fx();
fy();
fz();

実行結果は次のようになるはずである。

x
x
x
x
x
y
y
y
y
y
z
z
z
z
z

これを async 関数にする。ただし、そのままだとおそらく順番に処理されるだけなので、setTimeout() で待ちを入れる。setTimeout() は第 1 引数の関数を第 2 引数で指定した数値のミリ秒後に実行する。setTimeout() は非同期関数で、そのままだとすぐ次に行ってしまうので、await を付けて次に行かないようにしている。

async function fx() {
    for (let i = 0; i < 5; i++) {
        await setTimeout(() => {}, 200);
        console.log("x");
    }
}

async function fy() {
    for (let i = 0; i < 5; i++) {
        await setTimeout(() => {}, 100);
        console.log("y");
    }
}

async function fz() {
    for (let i = 0; i < 5; i++) {
        console.log("z");
    }
}

fx();
fy();
fz();

順番がバラバラになるはずである。

z
z
z
z
z
x
y
x
y
x
y
x
y
x
y

fy() と fz() だけはこの順番に実行したいとすると、次のように async 関数の中で await で並べればよい。

async function fxy() {
    await fy();
    await fz();
}

fx();
fxy();

次のように、"x" は非同期に実行されるが、"z" は "y" のあとに出力されるはずである。

x
y
x
y
x
y
x
y
x
y
z
z
z
z
z

つまり、await はあくまで async 関数内で逐次処理 (同期処理) になるだけで、他の非同期処理の邪魔はしない。

for-of ループについては、ループ本体を await にする for await-of ループがある。

モジュール

JavaScript では別ファイルにあるコードを取り込めるようになっている。Node.js が採用しているものと、JavaScript (ECMAScript) 標準のものがある。

Node.js のモジュール

Node.js では、モジュールとして CommonJS というものを採用している。この形式では、モジュールを取り込むのに require() を用いる。

const fs = require("fs");

fs は Node.js に組み込まれているファイルシステムモジュールである。fs の関数を使うには、たとえば、"text.txt" というファイルを新規書き込みしようとする場合は、次のようにする。

const output = fs.createWriteStream("test.txt");

自分でモジュールを作りたい場合は、以下のようにする。たとえば、関数 fibonacci() をもつ fibonacci モジュールは次のようになる。

fibonacci.js

module.exports = { fibonacci };

function fibonacci(n) {
    if (n === 0) return 0
    else if (n === 1) return 1
    else return fibonacci(n - 2) + fibonacci(n - 1)
}

これを読み込むには、次のようにする。

> const fibonacci = require("./fibonacci.js")
> fibonacci.fibonacci(10)
55

module.exports に入れたものを require() の左辺で受ける感じになっている。module.expoerts に渡しているものがメソッドとして fibonacci() を持つオブジェクトなので、呼び出しもそのようになっている。関数 1 つしかないなら、module.exports に直接関数を渡してもよい。

fibonacci() を直接受け取るには、(オブジェクトなので) 分割代入を使えばよい。

> const {fibonacci} = require("./fibonacci.js")
> fibonacci(10)
55

JavaScript のモジュール

JavaScript 標準のモジュールの仕組みは ECMAScript modules と呼ばれるもので、import を使う。ただし、Node.js のコマンドラインでは使えないし、ふつうのスクリプトでも使えない。モジュールにする必要がある (拡張子を .mjs にするなど)。

fs モジュールをインポートするには、次のようにする。

import fs from "fs";
const output = fs.createWriteStream("test.txt");

最初の fs という名前は任意である。この方法では、デフォルトエクスポート (後述) のものをインポートする。組み込みモジュールであることを明確にするために、"node:" を付けることもできる。

import fs from "node:fs";

デフォルトでないエクスポートをインポートする場合は、次のようにする。

import * as fs from "fs";

ただし、この場合はデフォルトエクスポートはインポートされない。

また、個別の関数をインポートする場合は、分割代入風の書き方ができる。

import { createWriteStream } from "fs";
const output = createWriteStream("test.txt");

自分でモジュールを作りたい場合は、次のようにする。

fibonacci.mjs

export default { fibonacci };

function fibonacci(n) {
    if (n === 0) return 0
    else if (n === 1) return 1
    else return fibonacci(n - 2) + fibonacci(n - 1)
}

読み込むには、次のようにする。

import fibonacci from "./fibonacci.mjs";

上の例では、export default としているが、これはデフォルトエクスポートと呼ばれる。これの default を外すと、上の import の方法ではインポートされなくなる。

export { fibonacci };

エラー

import fibonacci from "./fibonacci.mjs"
       ^^^^^^^^^
SyntaxError: The requested module './fibonacci.mjs' does not provide an export named 'default'

デフォルトでないエクスポートをインポートするには、次の書き方にする。

import * as fibonacci from "./fibonacci.mjs";

ただし、この場合はデフォルトエクスポートはインポートされない。

最後に、次のようなインポート方法がある。

import "./fibonacci.mjs";

これだと何もインポートされないが、コードは実行されるので、コードだけ実行したい場合に使う。

動的インポート

上述のインポートは静的インポートといって、インポート時にコードをすべて読みこんでしまう。実行中に適宜必要なものを読みたい場合のために、動的インポートである import() が用意されている (こちらはコマンドラインでも使える)。これはデフォルトでないエクスポートのインポートに当たる。

> import("./fibonacci.mjs").then(fib => console.log(fib.fibonacci(10)))
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 55,
  [Symbol(trigger_async_id_symbol)]: 39
}
> 55

import() は Promise を返す。

パッケージ

Node.js には、モジュールを共有するためのパッケージ管理ツール npm がある。

たとえば、グラフを書きたいとする。まず、作業のためのディレクトリを作って、そこに移動する。

$ mkdir plot
$ cd plot

グラフを書くためのパッケージ Chart.js と chartjs-node-canvas をインストールするには、次のようにする。

$ npm install chartjs-node-canvas chart.js

コードを用意する。ここでは、sin 関数をプロットする。

plot.mjs

import { promises as fs } from "fs";
import { ChartJSNodeCanvas } from "chartjs-node-canvas";

const width = 400;
const height = 400;
const backgroundColour = "white";
const canvas = new ChartJSNodeCanvas({ width, height, backgroundColour });

const n = 50;
const x = Array.from({length:n+1}, (_, i) => i*2*Math.PI/n);
const y = Array.from({length:n+1}, (_, i) => Math.sin(x[i]));

const config = {
    type: "line",
    data: {
        labels: x,
        datasets: [{
            label: "sin(x)",
            data: y,
            backgroundColor: ["rgb(255, 99, 132)"],
            borderColor: ["rgb(255, 99, 132)"],
            borderWidth: 1,
            radius: 0,
        }]
    },
    options: {
        scales: {
            x: {
                type: "linear",
                title: {
                    display: true,
                    text: "x"
                }
            }
        }
    }
};

(async () => {
    const buffer = await canvas.renderToBuffer(config);
    fs.writeFile("out.png", buffer, "base64");
})();

実行

$ node plot.mjs

out.png ファイルができて、グラフが描画されていれば OK。

プロジェクト

npm は基本的にプロジェクトごとにパッケージをインストールする。

プロジェクトを作るには、まずディレクトリを作って移動する。

$ mkdir proj
$ cd proj

npm init を実行する。いろいろ質問してくるので、適当に答える。

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install ` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (proj)
...

どうせ全部デフォルトにするなら、-yes を指定すればよい。

$ npm init -yes

package.json ができる。

package.json

{
  "name": "proj",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

npm でパッケージをインストールすると、ここに依存パッケージの情報が書き込まれる。

$ npm install chartjs-node-canvas chart.js

package.json

{
  "name": "proj",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "chart.js": "^3.9.1",
    "chartjs-node-canvas": "^4.1.6"
  }
}

また、パッケージ自体はプロジェクトディレクトリの node_modules ディレクトリにインストールされている。つまり、このプロジェクト限りでパッケージがインストールされていることになる。パッケージを入れすぎてわけがわからなくなっても、プロジェクトごとに管理している限り、プロジェクトを作り直せばよい。

さて、上記のグラフ描画のプログラムをプロジェクト化する。plot.mjs をプロジェクトディレクトリに配置する。そして、package.json を以下のように書き換える。

{
  ...

  "main": "plot.mjs",
  "scripts": {
    "start": "node plot.mjs"
  },

  ...
}

以下のように実行する。

$ node start

> proj@1.0.0 start
> node plot.mjs

out.png ができていれば OK。

ところで、packages.json の scripts で実行コマンドを指定してるのなら、main は何の意味があるのか。これはモジュールとして読み込まれたときに意味をなす。node_modules/chart.js などの中身を覗くとわかるように、それぞれのディレクトリに packages.json が置いてある。これらはそれぞれがパッケージになっている。つまり、プロジェクトはそのままパッケージになるのである。main はモジュールとして読み込まれた際の起点である。

作成したプロジェクトがモジュールとして機能することを確認するには、プロジェクトディレクトリを node_modules に入れてみればよい。

$ cd ..
$ mkdir node_modules
$ cp -r proj node_modules

コマンドラインで読み込んでみる。

$ npm
Welcome to Node.js v18.17.1.
Type ".help" for more information.
> import("proj")
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 148,
  [Symbol(trigger_async_id_symbol)]: 6
}

out.png が出力されるはずである。

プロジェクトの配布

プロジェクトを配布したい場合がある。node_modules ごと配布すると、これが結構サイズがある (上の例だと 207 MB ある)。どうせネットからインストールできるのなら、これは無駄である。その場合、node_modules 以外を配布すればよい。

$ cp proj proj-dist
$ cd proj-dist
$ rm -rf node_modules

配布後、パッケージをインストールするには、npm install だけを実行する。

$ npm install

そうすると、package.json、package-lock.json に従ってパッケージをインストールしてくれる。package-lock.json はパッケージのバージョンを固定するものである。これがないと最新のものがインストールされるので、開発環境と異なる環境になり、プログラムが動かなくなることがある。

コマンドライン引数

Node.js をシェルで実行するときにスクリプトと引数を指定した場合、それらは process.argv で取得できる。

args.js

console.log(process.argv.slice(2));

1 番目に Node.js のパスが、2 番目にスクリプトのパスが入り、それ以降が引数になる。

$ node args.js 1 2 3
[ '1', '2', '3' ]

時間の計測

コード内である間の時間を計測する場合は、次のようにする。

const startTime = performance.now();

...

const endTime = performance.now();
const elapsedTime = (endTime - startTime)/1000.;

console.log(`Elapsed Time: ${elapsedTime.toFixed(3)} s`);

文字列

文字列は文字を変更できない。文字列を編集したければ変換するしかない。そのためには、一旦配列に変換したほうがよいかもしれない。

> Array.from("abc")
[ 'a', 'b', 'c' ]

どうせループを回すなら、配列に変化せずに直接回すことができる。

> for (let c of "abc") {
... console.log(c)
... }
a
b
c

たとえば、小文字を大文字に変換する関数を雑に作るとすると、次のようになるだろう。

toupper.js

function toupper(x) {
    const r = [];
    for (let c of x) {
        if ("a" <= c && c <= "z") {
            r.push(String.fromCharCode(c.charCodeAt(0)
                + ("A".charCodeAt(0) - "a".charCodeAt(0))));
        } else {
            r.push(c);
        }
    }
    return r.join("");
}

console.log(toupper("abc"));
console.log(toupper("aB6"));

結果

$ node toupper.js
ABC
AB6

文字列を繰り返す

文字列を繰り返すには、repeat() を用いる。

> "=".repeat(20)
'===================='

連結と分割

配列を文字列として連結したり、文字列を配列に分割したりするには、それぞれ join()、split() を用いる。

> ["sabatora", "chatora", "mike"].join()
'sabatora,chatora,mike'

> ["sabatora", "chatora", "mike"].join(" ")
'sabatora chatora mike'

> "sabatora,chatora,mike".split(",")
[ 'sabatora', 'chatora', 'mike' ]

文字を探す

文字列から文字列を探すには、search()、lastIndexOf() を使う。

> "penguin".search("n")
2

> "penguin".lastIndexOf("n")
6

正規表現

/.../ の形で正規表現を表すことができる。

> /\s*$/

正規表現もオブジェクトである。

match() で条件に合致しているものを取り出したりできる。

> m = "This is a penguin.".match(/\s(p.*)[\s\.]/)
[
  ' penguin.',
  'penguin',
  index: 9,
  input: 'This is a penguin.',
  groups: undefined
]

> m[1]
'penguin'

括弧でくくったところ (この場合は "(p.*)") を取り出せる。

replace() で文字の置き換えができる。

> "This is a penguin.    ".replace(/\s*$/, "")
'This is a penguin.'

配列

大きさだけ確保する

とりあえず配列の大きさだけを確保するには、次のようにする。

> a = Array(3)
[ <3 empty items> ]

ある値で埋める

全要素を特定の値にしたい場合などは、fill() が使える。

> a = Array(3).fill(0)
[ 0, 0, 0 ]

結合する

配列を結合するには、concat() を使う。

> a = [1, 2, 3]
[ 1, 2, 3 ]

> b = [4, 5, 6]
[ 4, 5, 6 ]

> a.concat(b)
[ 1, 2, 3, 4, 5, 6 ]

配列の変換

Array.from() を使うと、配列から別の配列を作ることができる。

> Array.from([1, 2, 3], (v, i) => [v, i])
[ [ 1, 0 ], [ 2, 1 ], [ 3, 2 ] ]

map() に似ているが、値と添字を得られるし、オリジナルは残る。

続きの数値の配列を得たければ、次のようにできる。

> Array.from(Array(5), (_, i) => i)
[ 0, 1, 2, 3, 4 ]

次のようにしてもよい。

> Array.from({length:5}, (_, i) => i)
[ 0, 1, 2, 3, 4 ]

> Array.from({length:5}, (_, i) => 2*i + 1)
[ 1, 3, 5, 7, 9 ]

> Array.from({length:11}, (_, i) => i*1/10)
[
    0, 0.1, 0.2, 0.3,
  0.4, 0.5, 0.6, 0.7,
  0.8, 0.9,   1
]

範囲の数値列がほしいということなら、他にもいろいろヘンテコなことができる。

> Array(10).fill(0).map((s => _ => s++)(0))
[
  0, 1, 2, 3, 4,
  5, 6, 7, 8, 9
]

> Array(10).fill(0).map((s => _ => s++)(1))
[
  1, 2, 3, 4,  5,
  6, 7, 8, 9, 10
]

> Array(10).fill(0).map((s => _ => s += 2)(-1))
[
   1,  3,  5,  7,  9,
  11, 13, 15, 17, 19
]

最初と最後を取り出す

リストの最初と最後の要素を取り出すには、slice() を用いる。

> a = [...Array(100).keys()].map(i => i + 1)
[
   1,  2,  3,   4,  5,  6,  7,  8,  9, 10, 11, 12,
  13, 14, 15,  16, 17, 18, 19, 20, 21, 22, 23, 24,
  25, 26, 27,  28, 29, 30, 31, 32, 33, 34, 35, 36,
  37, 38, 39,  40, 41, 42, 43, 44, 45, 46, 47, 48,
  49, 50, 51,  52, 53, 54, 55, 56, 57, 58, 59, 60,
  61, 62, 63,  64, 65, 66, 67, 68, 69, 70, 71, 72,
  73, 74, 75,  76, 77, 78, 79, 80, 81, 82, 83, 84,
  85, 86, 87,  88, 89, 90, 91, 92, 93, 94, 95, 96,
  97, 98, 99, 100
]

> a.slice(0, 3)
[ 1, 2, 3 ]

> a.slice(-3)
[ 98, 99, 100 ]

0, 3 は、0、1、2、-3 は -3、-2、-1 となる。-1 が最後の要素の位置である。

要素が含まれるかどうか

要素が含まれるかどうかは includes() で調べられる。

> ["a", 2, "?"].includes("a")
true

> ["a", 2, "?"].includes(2)
true

> ["a", 2, "?"].includes(1)
false

添字がほしい場合は indexOf()、lastIndexOf() で得られる。

> ["a", 2, "?", "a"].indexOf("a")
0

> ["a", 2, "?", "a"].indexOf(2)
1

> ["a", 2, "?", "a"].lastIndexOf(1)
-1

> ["a", 2, "?", "a"].lastIndexOf("a")
3

見つからない場合は -1 を返す。

includes() と indexOf() は NaN に対して挙動が異なる。

> [NaN].includes(NaN)
true

> [NaN].indexOf(NaN)
-1

Python で使われる in は、JavaScript ではオブジェクトのプロパティの有無を調べるものなので注意。

> "a" in {"a": 1, "b": 2}
true

> 1 in {"a": 1, "b": 2}
false

要素の並び替え

要素を並び替えるには sort() を用いる。

> a = [1, 2, 3, 5, 6, 4]
[ 1, 2, 3, 5, 6, 4 ]

> a.sort()
[ 1, 2, 3, 4, 5, 6 ]

これは、a の値自体を並び替える。

逆にするだけなら reverse() を使う。

> a = [1, 2, 3, 5, 6, 4]
[ 1, 2, 3, 5, 6, 4 ]

> a.reverse()
[ 4, 6, 5, 3, 2, 1 ]

逆順で並べるなら sort() と reverse() を続けて適用すればよい。

> a = [1, 2, 3, 5, 6, 4]
[ 1, 2, 3, 5, 6, 4 ]

> a.sort().reverse()
[ 6, 5, 4, 3, 2, 1 ]

重複要素の削除

重複要素を削除するには、一旦集合にしてしまえばよい。

> a = [1, 2, 2, 5, 4, 6, 3]
[
  1, 2, 2, 5,
  4, 6, 3
]

> [...new Set(a)]
[ 1, 2, 5, 4, 6, 3 ]

最大値と最小値

要素の最大値、最小値を得るには、それぞれ Math.max()、Math.min() を用いる。

> a = [...Array(3)].map(x => Math.random())
[ 0.7115525770751512, 0.1561609827702084, 0.792420191250369 ]

> Math.max(...a)
0.792420191250369

> Math.min(...a)
0.1561609827702084

添字がほしい場合は、indexOf() を用いる。

> a.indexOf(Math.max(...a))
2

> a.indexOf(Math.min(...a))
1

絶対値

要素の絶対値がほしい場合は、次のようにすればよい。

> a = [1, -2, 3, -4, 5]
[ 1, -2, 3, -4, 5 ]

> a.map(x => Math.abs(x))
[ 1, 2, 3, 4, 5 ]

合計

要素の合計は自分で計算するしかないが、以下のようにできる。

> a = [1, 2, 3, 4, 5]
[ 1, 2, 3, 4, 5 ]

> a.reduce((a, b) => a + b)
15

足していく途中がほしい場合は、次のようにすればよい。

> a.map((s => v => s += v)(0))
[ 1, 3, 6, 10, 15 ]

要はクロージャの応用である。

積

各要素の積を取るには、以下のようにできる。

> a = [1, 2, 3, 4, 5]
[ 1, 2, 3, 4, 5 ]

> a.reduce((a, b) => a*b)
120

積の途中がほしい場合は、次のようにすればよい。

> a.map((s => v => s *= v)(1))
[ 1, 2, 6, 24, 120 ]

演算の積算

上ですでに示しているが、reduce() を使うと、ある演算子を要素に適用して積算した結果が得られる。

> a = [1, 2, 3, 4, 5]
[ 1, 2, 3, 4, 5 ]

> a.reduce((a, b) => a + b)
15

> a.reduce((a, b) => a*b)
120

初期値を指定できる。

> a.reduce((a, b) => a*b, -1)
-120

reduce() には逆順に関数を適用する reduceRight() というものもある。

> a.reduce((x, y) => `(${x} => ${y})`)
'((((1 => 2) => 3) => 4) => 5)'

> a.reduceRight((x, y) => `(${x} => ${y})`)
'((((5 => 4) => 3) => 2) => 1)'

これも上で示しているが、積算の途中がほしい場合は、map() を使って工夫する。

> a.map((s => v => s += v)(0))
[ 1, 3, 6, 10, 15 ]

階差

階差を作るには、次のようにすればよい (ちょっと苦しいか)。

> [1, 2, 3, 4, 5].map((s => v => s *= v)(1))
[ 1, 2, 6, 24, 120 ]

> a.map(((a, b) => v => {b = a; a = v; return a - b})(0, 0)).slice(1)
[ 1, 4, 18, 96 ]

各要素への関数の適用

これまでふつうに使ってきたが、配列の各要素へ関数を適用して別の配列を作るには、map() を使う。

> a = [...Array(5).keys()].map(i => i + 1)
[ 1, 2, 3, 4, 5 ]

> a.map(x => 2*x + 1)
[ 3, 5, 7, 9, 11 ]

単純に各要素に関数を適用したい場合は、for を使えばよい。

> b = []
[]
> for (x of [...Array(5).keys()].map(i => i + 1)) {
... b.push(2*x + 1)
... }
5

> b
[ 3, 5, 7, 9, 11 ]

これと同じことを行うのに forEach() がある。

> [...Array(5).keys()].map(i => i + 1).forEach(x => b.push(2*x + 1))
> b
[ 3, 5, 7, 9, 11 ]

抽出

filter() で条件に合った要素だけを取り出すことができる。

> a = [...Array(10).keys()].map(i => i + 1)
[
  1, 2, 3, 4,  5,
  6, 7, 8, 9, 10
]

> a.filter(x => x % 2 == 0)
[ 2, 4, 6, 8, 10 ]

条件に合う値をひとつだけ見つける find()、findLast() もある。

> [1, 2, 3, 4, 5].find(x => x % 2 == 0)
2

> [1, 2, 3, 4, 5].findLast(x => x % 2 == 0)
4

条件に合う値の添字を返す findIndex()、findLastIndex() もある。

> a = [...Array(10).keys()].map(i => i + 2)
[
  2, 3, 4,  5,  6,
  7, 8, 9, 10, 11
]

> a.findIndex(x => x % 2 == 0)
0

> a.findLastIndex(x => x % 2 == 0)
8

条件に合う値すべての添字がほしい場合は、自分で頑張る。

> b = []
[]

> a.forEach((i => x => {if (x % 2 == 0) b.push(i); i++})(0))
> b
[ 0, 2, 4, 6, 8 ]

条件判定

どれかの要素で条件が成り立つかを調べるには some() が使える。また、すべての要素で条件が成り立つかどうかは every() でわかる。

> a = [1, 3, 5, 8, 9]
[ 1, 3, 5, 8, 9 ]

> a.some(x => x % 2 == 0)
true

> a.every(x => x % 2 == 0)
false

> a = [1, 3, 5, 7, 9]
[ 1, 3, 5, 7, 9 ]

> a.every(x => x % 2 == 1)
true

条件に合うものが何個あるかを数えるには、filter() で出てきた長さを得ればよい。

> a = [1, 3, 5, 8, 9, 10]
[ 1, 3, 5, 8, 9, 10 ]

> a.filter(x => x % 2 == 0).length
2

map() と reduce() を使う方法もある。

> a.map(x => x % 2 == 0).reduce((a, b) => a + b)
2

要素の置き換え

条件に合った要素を別の値で置き換えるのは、map() でできる。

> a = [1, 2, 3, 4, 5].map(x => x % 2 == 0 ? null : x)
[ 1, null, 3, null, 5 ]

> a.map(x => x === null ? 0 : x)
[ 1, 0, 3, 0, 5 ]

ファイル

JavaScript 自体はファイルを扱えないので、ここでは Node.js の話になる。Node.js でファイルを扱う場合は、fs モジュールを用いる。

ファイルの読み書きには、様々な方法が用意されている。もし、次々にファイルに書き込んでいくような状況であれば、fs.createWriteStream() が使える。

const fs = require("fs");
const output = fs.createWriteStream("test.txt");
output.write("test");
output.end();

ファイルから1行ずつ読み込んでいくような状況であれば、fs.createReadStream() と readline モジュールを併用する。

const readline = require("readline");
const input = fs.createReadStream("test.txt");
const rl = readline.createInterface({
    input: input,
    crlfDelay: Infinity
});

rl.on("line", (line) => {
    console.log(line);
});

出力

test

ファイルの内容がまとまっているなら、fs.writeFileSync()/fs.readFileSync() などを使うことができる。

const lines = ["aaa", "bbb", "ccc"];
fs.writeFileSync("test.txt", lines.join("\n"));

const lines2 = fs.readFileSync("test.txt");
console.log(lines2.toString());

出力

aaa
bbb
ccc

書式

JavaScript には C 言語の printf() 風の書式を扱える仕組みはないので、テンプレートリテラルと toFixed() などを使って対処する。

> a = 1.23456
1.23456

> console.log(`a = ${a.toFixed(3)}`)
a = 1.235

> b = "m"
'm'

> console.log(`a = ${a.toFixed(3)} [${b}]`)
a = 1.235 [m]

数値の表示である幅を取りたいなら、String の padStart() を使う。

> a = 123
123

> String(a).padStart(5, " ")
'  123'

0 で埋めることもできる。

> String(a).padStart(5, "0")
'00123'

参考

  • Ethan Brown: 初めての JavaScript, オライリー・ジャパン (2017).
  • Cay S. Horstmann: JavaScript モダンプログラミング完全ガイド, インプレス (2020).
  • David Flanagan: JavaScript 第7版, オライリー・ジャパン (2021).
  • MDN Web Docs