Object.create(null)

package.json の Conditional Exports では順序が意味を持つ

Conditional Exports とは

package.json 内の exports フィールドには, 以下のように条件付きでエクスポートするファイルを指定できる (conditional exports).

{
  "name": "@susisu/example",
  "type": "module",
  "exports": {
    ".": {
      "require": "./lib/index.cjs",
      "default": "./lib/index.js"
    }
  }
}

たとえばこの例では CommonJS ファイル内から require("@susisu/example") のように参照した場合は require に指定された ./lib/index.cjs が, ESM のファイル内から import "@susisu/example" のように参照した場合は default に指定された ./lib/index.js が使われることになる.

requiredefault はそれぞれ条件を表していて, パッケージが参照された場合は条件にマッチしたものが使われるようになっている. どのような条件がサポートされているかランタイムやバンドラによって異なるので, 各々のドキュメントを参照されたい.

順序が意味を持つとはどういうことか

先程の例の defaultrequire の順番を入れ替えてみる.

{
  "name": "@susisu/example",
  "type": "module",
  "exports": {
    ".": {
      "default": "./lib/index.js",
      "require": "./lib/index.cjs"
    }
  }
}

もしこのようになっていると, CommonJS ファイル内から require("@susisu/example") のように参照した場合も, ESM のファイル内から import "@susisu/example" のように参照した場合も, default に指定された ./lib/index.js が使われる. CommonJS ファイルからも ESM である ./lib/index.js を参照することになるので, 場合によってはエラーになるかもしれない.

これは条件の比較が package.json に書いてある順番に先頭から行われるためで, default はあらゆる状況にマッチするので常に default に指定されたファイルが参照されることになってしまう. 条件の種類によって特別扱いがあるとか, 指定の強い (対象範囲の狭い) 条件ほど優先的に比較されるといったことはない.

ソース

Modules: Packages | Node.js v23.0.0 Documentation

Within the "exports" object, key order is significant. During condition matching, earlier entries have higher priority and take precedence over later entries. The general rule is that conditions should be from most specific to least specific in object order.

Package exports | webpack

In an object where each key is a condition, order of properties is significant. Conditions are handled in the order they are specified.

一方 JSON の仕様では

ECMA-404 - Ecma International

The JSON syntax does not impose any restrictions on the strings used as names, does not require that name strings be unique, and does not assign any significance to the ordering of name/value pairs. These are all semantic considerations that may be defined by JSON processors or in specifications defining specific uses of JSON for data interchange.

とあるように, JSON の仕様としてはオブジェクト内の順序が意味を持つかどうかについては特に規定しておらず, どう解釈して利用するかは処理系の実装や別の仕様に任せている.

一方 JavaScript の仕様では

JSON.parse でオブジェクトをパースする場合, 一部挙動が異なるものの, 解釈は概ね JavaScript のオブジェクトリテラルと同じとなっている.

したがって, Object.keys などでキーを列挙する場合は大抵の場合はキーの登場順になるので, 順序が重要な場合はこれをそのまま使うことができる.

> Object.keys(JSON.parse('{ "require": "./lib/index.cjs", "default": "./lib/index.js" }'))
[ 'require', 'default' ]
> Object.keys(JSON.parse('{ "default": "./lib/index.js", "require": "./lib/index.cjs" }'))
[ 'default', 'require' ]

ただしキーが配列のインデックスとして有効な値 (0 <= x <= 2 ** 32 - 2 = 4294967294) である場合はこの限りではなく, 他のキーよりも優先して列挙されるため注意が必要 (参考).

> Object.keys(JSON.parse('{ "require": "./lib/index.cjs", "4294967294": "./lib/number.js", "default": "./lib/index.js" }'))
[ '4294967294', 'require', 'default' ]
> Object.keys(JSON.parse('{ "require": "./lib/index.cjs", "4294967295": "./lib/number.js", "default": "./lib/index.js" }'))
[ 'require', '4294967295', 'default' ]

そもそも条件として未実装ならキーがあってもなくても同じなので, こんなことは全く気にしなくて良い. 実際 Node.js の実装も素朴に JSON.parse を使っていそうだった.

条件を追加できる立場にあって, かつ無意味に仕様や実装を複雑にする嫌がらせをしたい場合はぜひご利用ください (?)

null or undefined #kyotoasterisk とその補足など

Kyoto.なんか #6 で発表しました.

speakerdeck.com

以下はその補足情報など.

仕様書中の出現頻度

null と undefined がそれぞれの仕様でどの程度使われているのかは, 仕様書中の出現頻度を見るだけでもある程度わかりりそうです. ということで ECMAScript 2024WHATWG の標準 (2024-09-07 時点) のうちいくつかの仕様書の中での出現頻度を見てみましょう.

Spec #null #undefined
ES2024 300 939
DOM 368 120
Fetch 276 10
HTML 1778 275
URL 98 9

見ての通り, 顕著に登場頻度に差があることがわかりますね.

なお上記の null の出現数には WebIDL の nullable (T?) を含めていないため, Web 標準における実際の null の登場頻度はもっと多くなるはずです.

仕様書の null 全部読む

上記の通り ES2024 の仕様には null が 300 回しか登場しません. この程度なら null の登場箇所は全部読めるなと思ったので読みました.

いくつか手元のメモから面白かった箇所を抜粋します:

  • null がオブジェクトの prototype となるのは, プログラマがそうした場合を除くとたぶん以下のいずれか
  • Date.prototype.toJSON は時刻が finite でないとき null を返す
  • String.prototype.matchAll が返すイテレータは null を返す的なことが書いてあるが, たぶん普通はそんなことはない
  • Array.prototype.join は配列中の null や undefined を無視する
  • Object.assign は引数中の null や undefined を無視する
  • Map, Set, WeakMap, WeakSet のコンストラクタは引数に null や undefined が渡されると無視する

ECMAScript の言語仕様での例外的なケースについて

スライド中でも紹介した通り, ECMAScript の言語仕様中で null が使われるのは (仕様内部での利用を除けば) 以下の 3 つです.

このうち JSON で null が使われる理由は明白で, JSON の仕様でそう定義されているためです.

正規表現についてはおそらく歴史的な事情で, 今となっては特に深い意味はないものと思われます. (この理由に関する情報を昔見かけたことがある気もしますが, 真偽が定かでないのでここで広めるのはやめておきます.)

プロトタイプについては, ES5 より前には仕様内部で用いられていたものが, ES5 以降に Object.getPrototypeOf などが追加されたことで正式に表に出てくるようになったものと考えています.

Web 標準仕様で例外的なケースについて

例えば CustomElementRegistry#get が custom element が見つからない時に undefined を返すようです.

オブジェクトや文字列に対しては null を使うのでは?

文字列は JavaScript ではプリミティブ値の一種ですが, 他の言語ではオブジェクト側に分類されることもあります. 実際, ECMAScript の言語仕様における例外ケースや Web 標準の慣例は, こういった他の言語の仕様や慣習に由来するものでほぼ間違いないでしょう.

一方, その他のプリミティブ値に対してであったり, プリミティブ値とオブジェクトが混在する場合に null と undefined のどちらを使うのかは, これだけでは説明できないはずです. ECMAScript の言語仕様でもこういった場合に null を使っても良いはずですが, 原則 undefined を使うように統一されています.

また TypeScript を使う場合に Map<string, string> と型を制限したからといって get() したときに null が返るかというとそんなことはないので, やはり出自も考えるほうが混乱しないでしょう.