🎫

JavaScriptの識別子

2021/12/05に公開

JavaScriptの識別子として使えるもの・使えないものに関して整理しました。

識別子、予約語、キーワード

ECMAScriptでは識別子、予約語、キーワードを以下のように使っています

  • 識別子 (identifier) とは、ざっくり言うとローカル変数名に使える名前のことです。が、この定義は細かいことを言うと状況によってぶれがあり複雑です。これを細かく見ていくのが本記事の主眼です。
    • 例: foo
    • 規格上は Identifier がこれに相当しますが、様々な例外を勘案するとこれをそのまま使うのが自然とは言えないため、以降では微妙に異なる定義を採用します。
  • 予約語 (reserved word) とは、識別子と同じ形式であるにも関わらず識別子としては使えないもののことです。
    • 例: if, public (strict modeの予約語だが、使い道はない)
    • 非例: async (識別子として自由に使うことができる)
  • キーワード (keyword) とは、識別子と同じ形式であるにも関わらず構文上識別子とは異なる特別な役割を担っているトークンのことです。
    • 例: if, async (async functionの一部として使われた場合)

おことわり

本稿の内容は2021-12-05時点でのECMAScriptの規格に基づきます。識別子の特別扱いの規則は規格中に分散して存在しているため、必ずしも網羅的に記述できていないかもしれません。

モジュールで使える識別子

let foo = 42;

モジュールソースコード内で識別子に使えるのは、以下の条件を満たす文字列です。 (エスケープを除く; エスケープについては後述)

  • 1文字以上で、最初の文字は以下のいずれか。
    • Unicodeの ID_Start 属性を満たす文字 (ASCIIの [a-zA-Z] に相当)
    • _
    • $
  • 2文字目以降は全て、以下のいずれか。
  • 以下に挙げる(広義の)予約語に一致しない。
    • arguments await break case catch class const continue debugger default delete do else enum eval export extends false finally for function if implements import in instanceof interface let new null package return static super switch this throw true try typeof var void while with yield

本稿ではこれをベースラインの「識別子」として扱います。後述するように一部条件によって範囲が変わりますが、「識別子」が使えるのは以下のような構文です。 (foo で表されている部分が識別子)

  • IdentifierReference
    • console.log(foo);
      • 以下の亜種を含む:
        • console.log({ foo }); (オブジェクト初期化の短縮記法)
  • BindingIdentifier
    • const foo = obj;
      • 以下の亜種を含む:
        • const { foo } = obj; (オブジェクト分解の短縮記法)
        • const { bar: foo } = obj;
        • const { ...foo } = obj;
      • 以下の亜種を含む: let, var
      • 以下の亜種を含む: for-of / for-await-of / for-in 内の同様の束縛
      • 以下の亜種を含む: 関数引数、try-catchの catch() 内の束縛
    • function foo() {} (function式、function宣言の両方)
    • class foo {} (function式、function宣言の両方)
    • import { foo } from "";
    • import { bar as foo } from "";
  • LabelIdentifier
    • foo: console.log(42);
    • break foo; / continue foo;

ここから先はしばらく重箱の隅をつついていきます。重箱の隅が面倒くさい人は「識別子+予約語」のセクションまでスキップするといいでしょう。

Unicode識別子について

const π = 3.14159265358979323846;

全ての非ASCII文字が識別子として使えるわけではありません。Unicodeはプログラミング言語の識別子として使える文字についてUAX #31という技術文書を公開しており、JavaScriptのUnicode識別子はここで書かれている方針に大まかに従っています。UAX #31に大まかに従っているプログラミング言語として他にもRustやPythonなどがありますが、細かい仕様は同じではありません。たとえば……

  • Unicode正規化の扱い
    • JavaScript ... 識別子は元のコードポイント列で比較し、Unicode正規化は行わない。
    • Rust ... NFCで正規化する。
      • ID_Start, ID_Continue のかわりに XID_Start, XID_Continue を用いる。
    • Python ... NFKCで正規化する。
      • ID_Start, ID_Continue のかわりに XID_Start, XID_Continue を用いる。
  • 接合子の扱い
    • JavaScript ... ゼロ幅非接合子とゼロ幅接合子を2文字目以降で許可する。
    • Rust, Python ... ゼロ幅非接合子とゼロ幅接合子を許可しない。
// OK
const π = 3.14159265358979323846;

// error
const= 440;

識別子のUnicodeエスケープ

let \u0066\u006F\u006F = 0;

JavaScriptでは識別子の位置でUnicodeエスケープを書くことができます。エスケープは以下のいずれかの形式です。

  • 4桁固定 (\u0066)
  • 可変 (\u{66})

エスケープを使っても、実際に定義できる識別子の範囲は広がりません。範囲外の文字を使った場合はエラーとなります。

// Error (ID_Start外の文字のため)
// const \u266A = 440;

サロゲート文字も範囲外のため使えません。サロゲートペアは分割せずにエスケープする必要があります。 (文字列リテラル内のエスケープではこの限りではありません)

// OK (BMP外の文字を1つのコードポイントで表現している)
const \u{20BB7} = 42;

// Error (サロゲートペアを分割しているため)
// const \uD842\uDFB7 = 42;

予約語のUnicodeエスケープについて

Unicodeエスケープを使っても、予約語と同じ名前の識別子を定義することはできません。

// Error (`if` という名前の変数は定義できない)
const \u{69}\u{66} = 0;

ただし、規格上はあらかじめ識別子としてパースした後、ASTの静的検査で弾くことになっています。特に、Unicodeエスケープされた予約語は構文上のキーワードとして使うことはできません。

// Error (if文にはならない)
// \u{69}\u{66} (true) console.log(42);

予約語ではないが、構文上の役割を持つトークン

以下に挙げる識別子はいつでも識別子として使うことができますが、特定の位置では構文的な意味を持ちます。

  • as ... import { A as B } from ""; など
  • async ... async function() {} など
  • from ... import {} from ""; など
  • get/set ... { get foo() {}, set foo() {} } など
  • of ... for (const x of a) {} など
  • meta/target ... import.meta, new.target など

これらのトークンが特別な意味で使われるときは、Unicodeエスケープは使えません。

グローバル変数

以下に挙げるものは単なる既定のグローバル変数です。 (全ての既定のグローバル変数を列挙しているわけではありません)

  • undefined
  • Infinity, NaN
  • globalThis
// OK
const undefined = 42;

this, super, import.meta, new.target

this, super, import.meta, new.target は構文上は識別子とは全く別ですが、それぞれ特別なローカル束縛を参照するという意味では識別子に近いものとも考えられます。識別子ではないため、Unicodeエスケープは使えません。

yield, await

yield式、await式は以下のように同名の識別子の関数呼び出しとの曖昧性があります。

await(42);
yield(42);

そのため、文脈によって解釈が異なります。まず以下のような禁止ルールがあります。

yield await
非strictモード 文脈依存 文脈依存
strictモード
(モジュール以外)
× 文脈依存
モジュール内 × ×

※ここでのモジュールとはES Modulesを指す

さらに、上の表で「文脈依存」と書かれたものはそれぞれ [Yield], [Await] フラグがついていないときだけ使えます。これらのフラグは以下の構文でセット・アンセットされ、内側の構文に継承されます。

[Yield] [Await] 備考
スクリプト - -
モジュール -
function() {} - - 引数部と本体
() => {} - - 引数部と本体
{ foo() {} } - - 引数部と本体
{ get foo() {} } - - 引数部と本体
{ set foo(val) {} } - - 引数部と本体
async function() {} - 引数部と本体
async () => {} - 引数部と本体
{ async foo() {} } - 引数部と本体
function*() {} - 引数部と本体
{ foo*() {} } - 引数部と本体
async function*() {} 引数部と本体
{ async foo*() {} } 引数部と本体
class { static {} } -
function foo() {} - - 関数名部分 / 式のみ (宣言は除く)
async function foo() {} - 関数名部分 / 式のみ (宣言は除く)
function* foo() {} - 関数名部分 / 式のみ (宣言は除く)
async function* foo() {} 関数名部分 / 式のみ (宣言は除く)
  • 凡例
    • ➕ ... セットされる
    • - ... アンセットされる

基本的には、意味的にyield/awaitが使える箇所では [+Yield]/[+Await] がセットされていると考えてよいですが、以下のような例外があります。

  • ジェネレーター系関数定義の引数部は [+Yield] ですが、実際にここでyield式を使うと静的検査でエラーになります。
  • async系関数定義の引数部は [+Await] ですが、実際にここでawait式を使うと静的検査でエラーになります。
  • class static blockの引数部は [+Await] ですが、実際にここでawait式を使うと静的検査でエラーになります。
  • function, async function, generator, async generatorには関数名を含めることができますが、この扱いは当該構文が式か宣言かによって異なります。
    • function宣言、async function宣言、generator宣言、async generator宣言の名前部分に書ける識別子は、それを含むブロックの条件に準じます。
    • function式、async function式、generator式、async generator式の名前部分に書ける識別子は、定義されようとしている関数本体の条件に準じます。
// 以下、非strict modeを仮定

// OK
function foo(x = await(42)) {}
// Error (await式としてパースされるが、await式はここでは使えない)
async function foo(x = await(42)) {}

// OK
async () => function await() {};
// Error
// async () => {function await() {}};

// OK
() => {async function await() {}};
// Error
// () => async function await() {};

strictモードの予約語とeval/arguments

以下の名前はstrictモードでは使えません

  • arguments eval implements interface let package private protected public static yield

このうち、 arguments, eval, let, yield についてはさらなる特殊ルールがあります。 (yieldについては前節で説明済みのため省略)

まず let についてはシンプルです。strictモードでない場合でも、 let/const を使った束縛 (let/const 宣言および for-in, for-of, for-await-oflet/const束縛) では let という名前を使うことができません。

// OK (非strict modeの場合)
var let = 42;

// Error
// let let = 42;

argumentsとevalはstrictモードでの新規束縛や再代入は禁止されていますが、既存束縛の利用は可能です。この場合、arguments/evalはあくまで識別子の一種のため、Unicodeエスケープしてもarguments/evalとして利用可能です。

"use strict";
// OK
function f() { return \u0061rguments; }

strict modeか否かに関わらず、arguments/evalは以下のように特別扱いされます。

  • arguments という名前の変数が関数内で参照され、かつ引数部で同名の束縛が存在しないときだけ、関数開始時に自動的に arguments に対するローカル束縛が作成される。
    • このときArgumentsオブジェクトが作られて arguments に入れられる。
  • eval という名前の変数参照が eval(...) の形で直接使われ、かつ eval によって解決された値が globalThis.eval の初期値 (%eval%) と等しいとき、当該 eval() 呼び出しは呼び出し元のスコープを参照できる状態で評価される。
console.log((() => {
  const undefined = true;
  return eval("undefined");
})());
// => true

console.log((() => {
  const undefined = true;
  const myEval = eval;
  return myEval("undefined");
})());
// => undefined

識別子+予約語

JavaScriptの一部の構文では、予約語を含む広義の識別子を指定することができます。

典型的な使用例として default, catch, finally があります。

// defaultは予約語だが使える
export { myCounter as default };
const component = await import("./someRoute").default;

// catch, finallyは予約語だが使える
fetch("...")
  .catch((e) => { ... })
  .finally(() => { ... });

識別子+予約語+文字列リテラル

ES2022以降ではエクスポート名に文字列リテラルを指定できるようになりました。

export { Note as "♪" };

エクスポート名には識別子としては正しくない名前も指定できます。ただし、Unicodeとしてwell-formedである必要があります (ペアを形成しないサロゲート文字が出現してはいけない)。

// Error
// export { f as "\uD83D --- \uDE02" }; // error

プロパティ名

クラス本体、オブジェクトリテラル、分割代入、分割初期化ではプロパティの指定にプロパティ名という構文要素が用いられます。これは以下の4つからなります。

  • 識別子+予約語
  • 文字列リテラル
    • テンプレート文字列リテラルは含まない
  • 数値リテラル
    • bigintリテラルも含む
  • [] で囲まれた式 (computed property name)
const obj = {
  foo: 42,
  default: 42,
  "♪": 42,
  42: 42,
  123456789123456789123456789n: 42,
  [Symbol.toStringTag]: "Foo",
};

モジュールエクスポートと異なり数値リテラルとcomputed property nameが使えます。Computed property nameが使えるため、symbol名を持つプロパティを設定・取得できます。また、モジュールエクスポートと異なり、サロゲートペアが対応していない文字列も許されています。

特別なプロパティ名

以下のプロパティ名は特別扱いされます。いずれもcomputed property nameの計算結果として出てきた場合は特別扱いされません。

  • constructor (クラス本体内)
    • インスタンスメソッドの形で定義された場合、実際にはコンストラクタの実装を与える。
    • インスタンスメソッド以外の形で定義されたらエラー。
  • prototype (クラス本体内)
    • staticメソッドまたはstaticフィールドの形で定義されたらエラー。
  • __proto__ (オブジェクトリテラル内)
    • プロパティを作るのではなく Object.setPrototypeOf が実行される。

プライベート識別子

#foo のように # で始まるトークンはプライベート識別子です。 #foo でひとつのトークンであり、 # foo のように間を空けて書くことはできません。

# の後に続く部分は識別子のフォーマットと同じですが、 #default#if など一般的には予約語とされるものも使えます。例外として #constructor は使えません

プライベート識別子はクラス宣言内で特別なプロパティ名として使えます。クラス本体でプロパティ名構文と同様に使えるほか、 in 演算子の左辺、メンバー式の右辺などでも使えます。

TypeScriptの型名

(この節の記述はTypeScript 4.5時点での挙動に基づきます)

TypeScriptの型名はJavaScriptの識別子のルールに大まかには準じていますが、追加の予約語として以下が定義されています。 (元々JavaScriptで予約語に入っているものも含まれていますが、ここでは実装に準じてそのまま列挙しています)

  • any asserts bigint boolean false infer keyof never null number object readonly string symbol true void undefined unique unknown

元々のJavaScriptの予約語、たとえば debugger なども使えません (構文解析後の静的検査で弾かれます)。strict modeで public などの識別子が使えないのも同様です。ただし、 await, arguments, eval は禁止されていません。

JavaScriptのメンバー式と同様、ドットの直後に出てくる型名 (React.FCimport("qs").ParsedQs など) では予約語の制約はありません。

Discussion