JavaScriptプログラムの基本的な構成要素のひとつが演算子です。多くの方が普段のJavaScriptプログラミングで演算子を使っているでしょう。
では、あなたは「演算子とは何か」という問いに答えられますか? 演算子と演算子以外の違いはどこにあるのでしょうか?
演算子とは何かという定義は、人によって考え方が違うでしょう。筆者の個人的な考えとしては、演算子は「1つ以上の式から別の式を構成する構文を特徴づけるトークン」であると考えています。
しかし、JavaScriptには仕様書があります。仕様書はJavaScript (ECMAScript) に関する最も信頼できる情報源ですから、何が演算子で何が演算子でないのかについてもたいへん強力な基準を与えてくれることが期待できます。
そこで、この記事では何が仕様書で演算子と呼ばれているのかについて全て解説します。併せて、演算子っぽいけど演算子とは呼ばれていないものについても解説します。この記事を読んで、「JavaScriptの演算子とは何か」について正確な理解を得ましょう。
ついでに、折角ですからそれぞれの演算子に対して簡単な解説を挟みます。初心者の方はJavaScriptの演算子を網羅的に理解する機会とし、中級者以上の方はこの機会に演算子について復習しましょう。
なお、演算子は英語でoperatorと呼ばれます。この記事では、仕様書でoperatorと呼ばれている構文を出てくるのが早い順に全て列挙します。この記事で参照するのはDraft ECMA-262 / February 15, 2020 版です。
グルーピング演算子 ( )
最初の演算子は( )
です。これは(a + b) * 2
とかに出てくる( )
であり、計算の順序を定める効果を持ちます。
( )
は演算子に見えないという方がいるかもしれませんが、12.2.10 The Grouping OperatorでGrouping Operatorと呼ばれているので演算子に数えます。
new
演算子
new
は、new Array(10)
のような式に現れる演算子であり、クラス等のコンストラクタを呼びインスタンスを作る効果があります。この例の場合はlength
は10のArray
インスタンスとなります1。
個人的にはこれが演算子というのは認めがたいのですが、仕様書が演算子であると言っているので演算子です(12.3.5 The new Operator)
インクリメント・デクリメント演算子
++
と--
です。これは文句なしに演算子ですね。12.4.4 Postfix Increment Operator~12.4.7 Prefix Decrement Operatorが該当します。仕様上は前置と後置が別々の演算子として定義されています。まあ構文が違いますから妥当ですね。
foo++
や++foo
はfoo
に1を足し、foo--
や--foo
は1を引きます。前置と後置の違いは返り値であり、前置の場合は計算の後のfoo
の値が、後置の場合は計算の前のfoo
の値が返ります。
delete
演算子
12.5.3 The delete Operatorで定義されている演算子で、delete foo
やdelete obj.foo
のような構文を持ちます。(すでにnew
が出ていますが)演算子といっても必ずしも記号ではないという例ですね。
delete
演算子は与えられた変数やプロパティを削除する演算子です。
プロパティの削除
プロパティの削除は話が簡単です。delete
演算子によって削除されたプロパティは文字通り存在しなくなります。次の例では最初obj
は{ foo: 123 }
でしたが、delete
の後はobj
は{}
と同じになります。
const obj = { foo: 123 };
console.log(obj.foo); // 123
delete obj.foo;
console.log(obj.foo); // undefined
console.log(obj.hasOwnProperty("foo")); // false
ただし、configurable属性がfalseに設定されたプロパティは削除できません。そのようなプロパティはObject.defineProperty
などの手段で作ることができます(第3引数のオブジェクトでconfigurable: true
を指定しなければデフォルト値のfalse
となります)。
const obj = {};
Object.defineProperty(obj, "foo", {
value: 456
});
console.log(obj.foo); // 456
delete obj.foo; // エラーが発生(strictモードの場合)
configurable
がfalse
の場合は、プロパティは消えませんがエラーも起こりません。
変数の削除
delete
演算子は変数も削除できます。しかし、皆さんがこの機能を使うのは非常にまれでしょう。なぜなら、strictモードではdelete
で変数を消すのは禁止されており、文法エラーとして扱われるからです。
また、strictモードでなかったとしても、どんな変数でも消せるわけではありません。消すことができる変数はたいへん限られています。具体的には、消すことができるのはグローバル変数のみです。それも、var
を使わずに宣言されたグローバル変数に限ります2。ちなみに、strictモードではvar
を使わずにグローバル変数を作ることはできない(存在しない変数にいきなり代入するとエラーが発生する)ので、strictモードではdelete
は無意味となります。この意味でも、delete
による変数の削除は非常に非strictモード的な機能であると言えます。
// varを使わずに作られたグローバル変数
a = 123;
// varを使って作られたグローバル変数
var b = 456;
delete a;
delete b;
console.log(typeof a); // "undefined" (aが消えた)
console.log(typeof b); // "number" (bは消えていない)
function foo() {
var abc = 123;
console.log(abc); // 123
delete abc; // abc はグローバル変数ではないので消されない
console.log(abc); // 123
}
foo();
ただし、delete
が消すことができる変数はもう一つあります。それは**eval
経由で作られた変数**です。この場合は、var
で作られた変数であっても消すことができます。
function foo() {
eval("var abc = 123;");
console.log(abc); // 123
delete abc;
console.log(abc); // abcが存在しないのでエラーが発生
}
たとえeval
で作られた変数であっても、上の例のfoo
の中で作られた変数abc
はfoo
内のローカル変数となります。eval
経由で作られた変数の場合、特別にローカル変数をdelete
演算子で消すことができるのです。
delete
演算子の返り値
delete
演算子は式を作る構文であり、式ということは値があります。delete
演算子の返り値は真偽値であり、基本的には「削除に成功したらtrue
、失敗したらfalse
」です。ただし、最初から存在しない変数を削除しようとしていた場合はtrue
になるので要注意です。
全く使わない演算子である割に何だか奥が深いですね。
void
演算子
void 式
という構文を持つ演算子で、式を評価するものの、結果を無視して常にundefined
を返します(12.5.4 The void Operator)。
そんなもの何に使うのかと思いきや、実は**undefined
を得る**という重要な使いみちがあります。undefined
を使いたいとき、void 0
と書けば3文字節約できます。
また、単に文字数を節約するだけでなく、変数undefined
が信頼できない場面でも有効です。というのも、我々はundefined
と書けばundefined
という値を得ることができますが、これはundefined
という名前のグローバル変数がundefined
が入った状態で用意されているからです。ということは、undefined
という名前の変数を作ってしまえばundefined
と書いてもundefined
が得られないのです。void
はこのような状況でもundefined
を供給することができます。
{
const undefined = "hi";
console.log(undefined); // "hi"
console.log(void 0); // undefined
}
例えば、TypeScriptで??
演算子(左辺がnull
またはundefined
なら右辺を返し、それ以外なら左辺を返す)は次のようにトランスパイルされます。void 0
が使われていることが分かりますね3。
// トランスパイル前
const foo = bar ?? 0;
// トランスパイル後
const foo = (bar !== null && bar !== void 0 ? bar : 0);
ちなみに、null
は変数ではなくリテラルなので、null
と書けば常にnull
であることが保証されます。
typeof
演算子
typeof 式
とすると、その式
の値に応じていろいろな文字列が得られる演算子です(12.5.5 The typeof Operator)。プリミティブは細かに種類を判別することができますが、オブジェクトに関しては"object"
と"function"
の2種類の結果しかありません。プリミティブに対しては、"string"
, "number"
, "boolean"
, "bigint"
, "symbol
", "undefined"
という種類の結果が得られます。比較的有名な話ですが、typeof null
は歴史的経緯により"object"
となります。これを"null"
にしようという試みもありましたが、頓挫しました。
存在しない変数に対してtypeof 変数
としてもランタイムエラーではなく"undefined"
が得られることから、グローバル変数が存在するかどうかを判別する貴重な手段として重宝されています(これだとundefined
が入っているグローバル変数が存在している可能性も残りますが、それが問題になることはほとんどありません)。
単項+
演算子
ここからはしばらく演算子らしい演算子が続きます。
12.5.6 Unary + Operatorで定義される単項+
演算子は、+式
の形で用いることで与えられた式の値を数値に変換します。
数値に変換するのはNumber(式)
でもできるのですが、短いプログラムが好きな人達には+式
が好まれています。これはTypeScriptでも認められており、TypeScriptでは+
に渡された値がnumber
型以外でも型エラーとはなりません(結果はnumber
型になります)。
console.log(+"1e4"); // 10000
console.log(+true); // 1
console.log(+{ toString(){ return "-345" } }); // -345
ちなみに、与えられる式がBigIntの場合は+式
とNumber(式)
で違いがあります。
console.log(Number(12345n)); // 12345
console.log(+12345n); // ランタイムエラーが発生
これは、BigIntはNumber
という明示的な方法を使って数値に変換しなければいけないということです。そうなっている理由は、BigIntからNumberへの変換では数値の精度が落ちる可能性がありバグの元となるため、暗黙のうちにこれが行われるのは望ましくないからです。
単項-
演算子
単項演算子(12.5.7 Unary - Operator)は-式
という形で、式
の符号を反転させた数値を得る演算子です。-
はBigIntにも対応しており、符号が反転されたBigIntが返ります。一見対称性が高いように見える+
と-
ですが、BigIntに関してはこのような非対称性があるのです。
const bignum = 12345n;
console.log(-bignum); // -12345n
console.log(+bignum); // ランタイムエラー
BigInt以外が渡された場合は+
と同様に全部数値(Number)に変換されます(ただし、オブジェクトをプリミティブ値に変換してBigIntになった場合はBigIntとして扱われます)。
ちなみに、TypeScriptでは-
演算子にnumber
型・bigint
型以外を与えるのはコンパイルエラーとなります。これも+
と非対称的な点ですね。
~
演算子
ビット反転の演算子です(12.5.8 Bitwise NOT Operator ( ~ ))。一見何も説明する必要が無さそうなビット反転演算子ですが、JavaScriptでは一筋縄ではいきません。二つほど説明すべきことがあります。
一つは、数値(Number)のときは数値は32ビットとして扱われるということです。JavaScriptではそれ以上の数値も扱うことができますが、ビット演算の場合はそれ以上のビットは切り捨てられます。また、結果は符号あり32ビット整数として解釈されます。
console.log(~0); // -1 (0xffffffff は2の補数表現で-1を表したものであるため)
console.log(~-1); // 0
console.log(~0x100000000); // -1 (32ビットを超えるので0と同様に扱われる)
もう一つはBigIntの場合についてです。~
演算子はBigIntも扱うことができますが、BigIntは任意精度の整数であるためビット数という概念がなく、「全てのビットを反転する」などということができないのです。そのため、BigIntのビット反転演算は別の方法で定義されています。
6.1.6.2.2 BigInt::bitwiseNOT ( x )によれば、x
がBigIntの場合「~x
の値は-x-1n
である」と定義されています。例えば、~123n
は-124n
となります。これは、通常の数値が32ビット整数に収まっている場合の挙動に一致しますから、それに倣ったものです。別の言い方をすれば、BigInt値を無限のビット数を持つ値であるとみなしてビット演算を行なっているとも言えます。
!
演算子
これは言うことがあまりありません。与えられた式を真偽値に変換して真偽を反転させる演算子です(12.5.9 Logical NOT Operator ( ! ))。true
とfalse
を逆にしたいときに使いましょう。また、値を真偽値に変換するのにBoolean(値)
ではなく!!値
として文字数を節約する人もいます。
**
演算子
ES2016で密かに追加された演算子で、累乗を意味します(https://tc39.es/ecma262/#sec-exp-operator)。例えば2 ** 3
は8
です。また、この演算子は右結合で、x ** y ** z
はx ** (y ** z)
として解釈されます。
この演算子は構文に関して面白い点が1つほどあります。それは、-2 ** 4
のような式が構文エラーとなることです。つまり、**
の左に単項+
演算子・単項-
演算子を使えないのです。これは、-2 ** 4
が(-2) ** 4
なのか-(2 ** 4)
なのか分かりにくいからです。エラーを避けるには、必要に応じてこのように括弧を補う必要があります。
ECMAScriptの比較的新しい機能では、このように解釈が曖昧な構文ができそうな場合に、どちらかに寄せるのではなくどちらも許さないというデザインが取られることがあります。**
はその先駆けのひとつです。
**
はNumberとBigIntのどちらにも対応していますが、BigIntは**
の右が負の数の場合にランタイムエラーとなります。**
の左が整数で右が負の数の場合結果が分数となり、BigIntでは表現できないからです。
ちなみに、JavaScriptでは0の0乗は1です。
*
, /
, %
演算子
乗法の演算子です。仕様書では12.7 Multiplicative Operatorsとして3種類まとめて定義されています。算術演算子全てに言えることですが、NumberとBigIntを混ぜて演算することはできません。両辺が同じ型である必要があります。
割り算に関しては、0で割った場合の挙動がNumberとBigIntで異なります。Numberの場合は左辺の値に応じてInfinityやNaNといった値が発生しますが、BigIntの場合は0で割るとランタイムエラーです。
このようにBigIntの計算はランタイムエラーが発生しやすいものになっています。これはNaNなどに起因する変なバグをなるべくわかりやすく防ぎたいという方向性と、そもそもBigIntにはNaNとかInfinityの概念がないという現実によるものです。
+
, -
演算子
こちらは単項ではなく二項演算子の+
・-
で、やはり仕様書では12.8 Additive Operatorsで定義されています。特に何の変哲もありませんが、+
は足し算だけでなく文字列の連結にも使われます。
ビットシフト演算子
12.9 Bitwise Shift Operatorsで定義される3つの演算子<<
, >>
, >>>
です。ビット演算なので、Numberの場合には32ビット符号付き整数として扱われるのも~
の場合と一緒です。ビットシフト演算子はNumberとBigIntで大きく扱いが異なる演算子で、似たような計算でもNumberとBigIntで異なる結果となります。
console.log(1 << 33); // 2
console.log(1n << 33n); // 8589934592n
console.log(4 << -1); // 0
console.log(4n << -1n); // 2n
console.log(4 >> -2); // 0
console.log(4n >> -2n); // 16n
BigIntの場合は話が単純で、ビットシフトはただ単に値に2の累乗数を左辺に掛けたり割ったりする計算となります。
一方Number型の場合は、まず右辺の数値(ビットシフトの距離を表す数値)が下位5ビットを取ることによって0〜31の整数に変換されます。
console.log(123 << 2); // 246
console.log(123 << 32); // 123 (32は0に変換されるのでビットシフトは起こらない)
>>
と>>>
の違いは左辺が負の数のときに現れます。>>
は符号を保ったままま右シフトが行われる一方、>>>
は符号を無視します。また、>>>
の結果は常に正の整数です。
console.log(-5 >> 1); // -3
console.log(-5 >>> 1); // 2147483645
console.log(-5 >> 0); // -5
console.log(-5 >>> 0); // 4294967291
>>
と>>>
の挙動の違いは、最上位ビットの扱いと右ビットシフトにより空く部分をどうするかです。BigIntの場合は最上位ビットという概念がありませんが、これはBigIntでは>>>
をサポートしないという方法で解決します。
console.log(-5n >> 1n); // -3n
console.log(-5n >>> 1n); // ランタイムエラーが発生
6種類の比較演算子
12.10 Relational Operatorsでは、<
, >
, <=
, >=
, instanceof
, in
という6種類の演算子がRelational Operators (比較演算子)として紹介されています。最初の4種類はいいとして、残りの2つが異質ですね。正直、この6種類をひとまとめに扱う意味がよく分かりませんが、仕様がそう言っているのですからこれらは仲間なのです。
とはいえ、まずは最初の4種類から見ていきます。
数値・文字列の比較演算子
<
, >
, <=
, >=
は、数値の比較だけでなく文字列同士の比較を行うことができます。両辺が文字列だった場合、文字同士をコードポイント4で比較することによる辞書順で大小判定が比較されます。文字列と数値の比較になった場合は数値に寄せられます。
instanceof
演算子
instanceof
演算子は演算子の中で最も長いものです。obj instanceof Class
で、obj
がClass
のインスタンスであるかどうかを判定して真偽値を返します。インスタンスであるかどうかの判定は、obj
のプロトタイプチェーンを辿ってコンストラクタにClass
を持つものが存在するかどうかを調べることで行われます。
ただし、実はinstanceof
の挙動はクラス側の[Symbol.hasInstance]
メソッドで制御できます。
class Foo {
static [Symbol.hasInstance](obj) {
return obj.foo === 100;
}
}
const obj = { foo: 0 };
console.log(obj instanceof Foo); // false
obj.foo = 100;
console.log(obj instanceof Foo); // true
in
演算子
in
演算子はオブジェクトが指定した名前のプロパティを持つかどうか判定して真偽値を返すプロパティです。Object.prototype.hasOwnProperty
とは異なり、プロトタイプチェーンを辿ってプロパティがあるかどうか判定します。
console.log("hasOwnProperty" in {}); // true
等価演算子
等価演算子は、12.11 Equality Operatorsで定義されている==
, !=
, ===
, !==
の4種類の演算子です。日本語で何と呼べば分からない演算子第一位(当社調べ)です。
基本的には===
, !==
だけ使っていれば事足りる場面がほとんどで、==
や!=
は避けるべきです。これは、==
は両辺の型が違う場合に型変換によって型を揃えて比較しようとするからです。詳しいことはこちらの既存記事をご参照ください。
ビット演算の二項演算子
ビット演算の二項演算子は12.12 Binary Bitwise Operatorsで定義されています。もうすこし短い訳があればいいのですが、「ビット演算の二項演算子」というのはいささか直訳的ですね。
これらもビット演算なので、Numberの場合は32ビット整数に変換の上ビット演算が行われる一方、BigIntの場合はビット数の制限がありません。
console.log((2 ** 32) | 1); // 1
console.log((2n ** 32n) | 1n); // 4294967297n
二項論理演算子
二項論理演子と言われて何のことかお分かりでしょうか。これは12.13 Binary Logical Operatorsの直訳であり、||
と&&
のことです。また、ES2020からは??
が追加されました。??
はプロポーザル時代はNullish Coalescing Operator(nullish合体演算子)と呼ばれていましたが、仕様書にはこの用語は出てきません。x ?? y
という式はCoalesceExpressionと呼ばれているので、Coalescing OperatorとかCoalesce Operatorと呼ぶのはありかもしれません。
意味は今さらなので省略しますが、これらに共通する特徴は短絡評価です。つまり、左辺を先に評価して、結果が左辺となることが分かったなら右辺は評価されません。
結合の優先順位は&&
> ||
> ??
ですが、??
の右に||
や&&
が来ることはできません。
w && x || y ?? z // OK ((w && x) || y) ?? z と同じ意味
w && x ?? y || z // 構文エラーが発生
(w && x ?? y) || z // OK ((w && x) ?? y) || z と同じ意味
これは、||
や??
は左辺の値が偽(あるいはnullish)の値だったときのフォールバックとして付加されることが多いところ、x ?? y || z
としたときの挙動が分かりにくいことによる処置です。
条件演算子
条件演算子はオペランドを3つ持つ唯一の演算子であり、それゆえ三項演算子と呼ばれることもあるやつです。仕様書を見れば、正式名称が条件演算子 (Conditional Operator) であることは一目瞭然ですね(12.14 Conditional Operator ( ? : ))。
代入演算子
代入演算子は=
, +=
, -=
, *=
, /=
, %=
, <<=
, >>=
, >>>=
, &=
, |=
, ^=
, **=
の総称です()。
&&=
や||=
は存在しませんが、これらを追加するというプロポーザルは存在します。2020年2月現在ではStage 2です。これらは+=
などとは多少意味が違う点があります。具体的には、a += b
はa = a + b
と同じである一方、a ||= b
はa || (a = b)
と同じ意味になります。つまり、a
が真なら代入すら行われないのです。このことは、セッタによって代入に副作用が仕込まれていた場合に目に見える違いとなって現れるのです。
コンマ演算子
JavaScriptでは,
は色々な場面で使われますが、実は演算子としての,
も存在します。それがコンマ演算子(12.16 Comma Operator ( , ))です。コンマ演算子は一番結合度の低い演算子となっています。
x, y
という式は、x
とy
を順に評価してy
の値を返します。x
は評価こそされるもののその結果は使用されません。これは、何だかvoid
演算子と似ているところがあります。コンマ演算子を我々が普段使うことはほとんどありませんが、適当なminifierを用いてソースコードを最小化するとコンマ演算子が結構使われているのを見ることができます。
無理やりコンマ演算子を使ってみるとこんな感じです。
for (let i=0; i < 10; i++) console.log("i is"), console.log(i);
for
文のボディ部分は{ }
を使わなければ書ける文はひとつだけですが、コンマ演算子を用いると複数の関数呼び出しを一つの式で表すことができます。一つの式で表すことができればそれは式文というひとつの文になりますから、{ }
無しでfor
文の中に複数の関数呼び出しを書くことができるのです。こうすると、{ }
で関数呼び出しを並べるよりも文字数を2文字ほど省略できます。たいへん嬉しいですね。
コンマは他の構文要素でも使われる記号ですから、コンマ演算子が活躍できる場所は限られています。例えば、関数呼び出しの引数リストの中ではコンマ演算子は使えません。,
は引数の区切りとして扱われるからです。無理やりコンマ演算子を使うには、( )
を使ってひとつの式であると明示する必要があります。
f(x, y); // (x, y)というひとつの式を渡しているのではなくxとyという2つの式を渡している
f((x, y)); // (x, y)というひとつの式を渡している
以上でECMAScriptで演算子 (operator) と呼ばれているものを全て列挙できました。
演算子なのかどうか微妙なもの
仕様書はJavaScriptという言語を定める絶対的な基準となる文書ですが、人間が作るものですからミスもあります。仕様書には、演算子でないと思われるものを演算子と呼んでいるように見える箇所が2箇所あります。折角ですから、この記事で触れておきます。
追記(2020/03/12): この記事を書いて以降に仕様書が改訂され、以下で紹介する2箇所では演算子 (operator) の語は使われなくなりました。
super
Table 7: Additional Essential Internal Methods of Function Objectsは関数オブジェクトが持つ内部スロットを定義しています(内部スロットについてはJavaScriptの「継承」はどう定義されるのか? 仕様書を読んで理解するで詳しく解説しています)。その2行目、[[Construct]]内部メソッドの説明に次のように書かれています(強調は筆者)。
Creates an object. Invoked via the new or super operators.
「new
またはsuper
演算子」と書かれており、super
が演算子であるかのような記述があります。一方、super
は仕様書の12.3.7 The super Keywordで定義されており、こちらでは「super
キーワード」と呼ばれていますからsuper
は演算子でないと思われます。
ちなみに、super
はクラス宣言の中で使うことができる構文であり、コンストラクタの中のsuper()
で親クラスのコンストラクタを呼び出したり、super.foo
で親クラスのプロパティ・メソッドを参照することができます。
追記(2020/03/12): この部分は“new operator or super call”と改訂され、superは演算子とは呼ばれなくなりました。
Optional Chaining
Optional ChainingはES2020で追加された?.
という構文で、foo?.bar
はfoo.bar
と似た動作をしますが、foo
がnull
やundefined
のときはエラーにならずに結果がundefined
となります。Optional Chainingについては以下の記事で詳しく解説しています。
仕様書では?.
による構文はOptional Chainとだけ呼ばれており(12.3.9 Optional Chains)、演算子とはされていません。しかしながら、一箇所だけ?.
を演算子と呼んでいる箇所があります。それは12.3.1.1 Static Semantics: Early Errorsです。NOTEの中に次のような記述があります(強調は筆者)。
...so that it would be interpreted as two valid statements. The purpose is to maintain consistency with similar code without optional chaining operator:
foo.bar
の.
は演算子とはどこにも書かれていませんから、そこから類推すると?.
が演算子とは考えにくいですね。上記の記述はミスであると考えられます。
追記(2020/03/12): この部分は“...without optional chaining:”と改訂され、optional chainingは演算子とは呼ばれなくなりました。
演算子ではないもの
ここまでで触れられなかったものは、仕様書でまったく演算子と呼ばれていないので明らかに演算子ではありません。どのようなものが演算子でないのか確認しましょう。
関数呼び出しの( )
func(1, 2, 3)
のような関数呼び出しに出てくる( )
は演算子ではありません。個人的にはnew Class(1, 2, 3)
のnew
が演算子であることを考えると少し違和感がありますが、我々ごときが感じる違和感など仕様書の前には塵ひとつほどの価値しかありません。
各種リテラル
次の式にあるような{ }
や[ ]
は演算子ではなく、リテラルと呼ばれています。
const obj = { foo: 0 };
const arr = [1, 2, 3];
括弧だから演算子ではないというわけでもないのが難しいところです。最初に見たように( )
は演算子でした。
プロパティアクセス
foo.bar
とかfoo?.bar
に出てくる.
や?.
は演算子と呼ばれていません。また、foo[name]
のような形のプロパティアクセスで使われる[ ]
もやはり演算子ではありません。
yield
・await
これらはyield 式
やawait 式
という形で使うことができる式であり、一見すると演算子でもおかしくありません(構造としてはtypeof 式
などと全く同じです)。
しかし、仕様ではこれらは演算子とはされず、YieldExpressionやAwaitExpressionのように定義されていますから、「yield式」や「await式」と呼ぶのが正しそうです。
スプレッド構文・レスト構文の...
ES2015以降、JavaScriptには...
という構文がたびたび登場します。...
はスプレッド構文とレスト構文に大別され、前者は配列リテラルやオブジェクトリテラルの中、そして関数呼び出しの引数リストの中で使われる一方、後者は分割代入の配列・オブジェクトパターンや、関数宣言時の引数リストで使われます。
...
は「スプレッド演算子」のように呼ぶ人が多くいるものの、仕様書では全く演算子と呼ばれていません。...
を演算子と呼んでいた方はぜひ今日から改めましょう。
これが演算子ではない理由は、「式を作らない」からであると推察しています。すなわち、...
は配列リテラルなど、他の構文の一部としてしか使うことができず、単独で式をなすものではないのです。
const arr = [...foo]; // ←これはOK
const arr = ...foo; // ←こんな構文は無い
変数宣言の=
先ほど解説した通り=
は代入演算子の一種ですが、実は変数宣言のときの=
は厳密には代入演算子ではありません。
let foo = 123; // ←この = は代入演算子ではない
foo = 456; // ←この = は代入演算子
前者は変数宣言の構文の一部(Initializer)として、代入演算子とは別個に定義されているのです。また、関数引数などのデフォルト値の定義にも=
が使われますが、こちらもInitializerであり代入演算子ではありません。
実際に仕様書を見ると、Initializerの定義には次のよう書かれています(画像で引用)。
一方で、代入演算子の=
はAssignmentExpressionの定義の中で定義されています(画像で引用)。
この2つを見ると、それぞれに=
が使われており、同じ=
という記号でも出自を異にするものであることが分かります。我々が普段=
を使う際はそんなことをいちいち気にしなくても何とかなってしまうのですが。
まとめ
この記事ではECMAScriptに存在する「演算子」を全種類(ES2020時点)解説しました。併せて、演算子っぽいが演算子ではないものも解説しました。
結局演算子と演算子でないものの違いは何なのかという問いについては、残念ながら単純明快な答えはありません。演算子は必ず式を作るという点は間違い無さそうですが、式を作るなら何でも演算子というわけではありません(foo.bar
の.
やawait
式のawait
は演算子ではありませんでしたね)。演算子であると定義されているものが演算子であると言うほか無いのです。
いずれにせよ、どれが演算子でどれが演算子でないかを丸暗記することに意味はありません。プログラムを書く身としては、どんな方法でもいいのでJavaScriptの構文を理解し、使いこなせるようになることが重要なのです。
その一方で、仕様書はJavaScriptプログラミングにおいて唯一完全に信頼できる情報源であり、質の高いプログラムを書くにあたって仕様書が果たす役割は小さくありません。ですから、JavaScriptの解説記事を書くような場合には演算子とそれ以外の区別ができていない(=仕様書を理解していない)と非常に信頼性が低くなります。皆さんもJavaScriptの解説記事を書くときはぜひ正確性に気をつけましょう。また、JavaScriptの解説記事を読む際も演算子をひとつの試金石としてみるのはいかがでしょうか。
-
配列の場合は実際にはただのインスタンスではなくexotic objectが作られるのでちょっと特殊ですが。 ↩
-
関数の中ではないトップレベルのコードにおいては、
var
で宣言された変数はグローバル変数として扱われます。一方、let
やconst
で宣言された変数はグローバル変数として扱われません。 ↩ -
null
またはundefined
であることを判定するならbar != null
でいいのではと思われたかもしれませんが、!=
を使うとdocument.allがdocument.all == null
と判定されてしまうのでまずいのです。 ↩ -
JavaScriptの文字列はUTF-16でエンコーディングされているため正確にはコードユニットですが。 ↩