JSのクラス・プロトタイプ・モジュールを jsc で実際に動かす

JavaScript 講座 4:プロトタイプチェーン、class と関数の違い、#private、this を失う TypeError、循環 import、1 個 40B 対 249B の実測。

シリーズ:JavaScript 講座 4/6(中級)。 関数や配列・オブジェクトは書けるものの、現場のコードで class、extends、#private、import に出会うと中で何が起きているのか分からない、という人向けです。読み終えると、プロパティがどこで見つかるかを予測でき、クラスと普通のオブジェクトを使い分け、this が消えて落ちるバグを避け、循環 import に引っかからずに ES モジュールでコードを分けられます。

研修で「JavaScript のクラスって Java のクラスと同じですよね?」と聞かれることがよくあります。見た目は似ていますが、中身は別物です。JavaScript には設計図としてのクラスはなく、オブジェクトが別のオブジェクトを指す「つながり」があるだけです。ここを知らないと、コールバックに渡したメソッドで this が消えたり、共有データが知らないうちに書き換わったりします。この記事では、すべてを実際のエンジンで動かし、本物の出力とエラーメッセージをそのまま載せます。

実行環境: macOS 26.6.2 に同梱の JavaScriptCore(フレームワーク版 21624.5.1.11.3)を jsc シェルで実行、Apple Silicon Mac(M1 Pro)。Node.js、Deno、ブラウザは実行していません。挙動が違うところはその都度書きます。第 1 回で作ったエイリアスをそのまま使います。

$ alias jsc=/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Helpers/jsc

jsc では print() で出力します。このシェルに console はありません。

プロトタイプチェーン:プロパティは実際どこで見つかるか

すべてのオブジェクトは [[Prototype]] という隠れたリンクで別のオブジェクト(または null)を指しています。obj.name を読んだとき obj 自身に name がなければ、エンジンはリンクをたどってもう一度探します。名前が見つかるか null に着くまで繰り返す、これが継承のすべてです。Object.create(p) はプロトタイプが p の新しいオブジェクトを作ります。

// proto.js — the prototype chain, step by step
const animal = { legs: 4, describe() { return `${this.name} has ${this.legs} legs`; } };
const cat = Object.create(animal);      // cat's prototype is animal
cat.name = "Tama";
print(cat.describe());                  // found on animal, this = cat
print(Object.hasOwn(cat, "legs"), "legs" in cat);
print(Object.getPrototypeOf(cat) === animal);
print(Object.getPrototypeOf(animal) === Object.prototype);
print(Object.getPrototypeOf(Object.prototype));
cat.legs = 3;                           // shadows, does not change animal
print(cat.legs, animal.legs);
const bird = Object.create(animal); bird.name = "Piyo";
animal.legs = 2;                        // every object without its own legs sees this
print(bird.describe());
for (const k in cat) print("for-in:", k);
print(Object.keys(cat));
$ jsc proto.js
Tama has 4 legs
false true
true
true
null
3 4
Piyo has 2 legs
for-in: name
for-in: legs
for-in: describe
name,legs

この出力から読み取るべき点は 3 つです。

  • 自分のプロパティと継承したプロパティ。 Object.hasOwn(cat, "legs") は false、"legs" in cat は true でした。in はチェーンをたどり、hasOwn(ES2022)はたどりません。
  • 書き込みはチェーンをたどりません。 cat.legs = 3 は cat に新しい自前のプロパティを作り(シャドーイング)、animal.legs は 4 のままでした。逆に、あとで animal.legs を変えると、自前の legs を持たない bird の見え方が変わりました。プロトタイプ上の共有された可変データは典型的なバグの元です。
  • for...in は継承した列挙可能なキーまで回ります(describe が出ました)。Object.keys は回りません。データには Object.keys/Object.entries を使いましょう。

describe の中の this は animal ではなく cat でした。メソッドがどこで見つかったかに関係なく、this は呼び出したときにドットの左にあったオブジェクトです。

チェーン図。new Circle(1) で作ったオブジェクト c は、データプロパティ name と r を自分で持つ。そのプロトタイプは Circle.prototype で、area と constructor がある。さらに上は Shape.prototype で、toString と自前の area がある。その上が hasOwnProperty などを持つ Object.prototype、最後が null で、ここで探索が止まり undefined になる。
c.area() と c.toString() が見つかるまで。エンジンはまずオブジェクト自身を見て、名前が見つかるか null に着くまでプロトタイプチェーンを上にたどる。

class は関数。ただし目に見える違いが 4 つある

ES2015 より前の「クラス」は、コンストラクタ関数と .prototype に載せたメソッドでした。class も同じ形を作るので、次の 2 つはどちらも 5 を返します。

function PointFn(x, y) { this.x = x; this.y = y; }
PointFn.prototype.len = function () { return Math.hypot(this.x, this.y); };
class Point { constructor(x, y) { this.x = x; this.y = y; } len() { return Math.hypot(this.x, this.y); } }
print(typeof Point, typeof PointFn);
print(new PointFn(3, 4).len(), new Point(3, 4).len());
for (const k in new PointFn(3, 4)) print("fn  for-in:", k);
for (const k in new Point(3, 4)) print("cls for-in:", k);
print(Object.getOwnPropertyDescriptor(Point.prototype, "len").enumerable,
      Object.getOwnPropertyDescriptor(PointFn.prototype, "len").enumerable);
const p = PointFn(1, 2);   // forgot new (sloppy mode)
print(p, typeof globalThis.x, globalThis.x);
try { Point(1, 2); } catch (e) { print(e); }
$ jsc classvsfn.js
function function
5 5
fn  for-in: x
fn  for-in: y
fn  for-in: len
cls for-in: x
cls for-in: y
false true
undefined number 1
TypeError: Cannot call a class constructor Point without |new|
  1. new を付け忘れると。 古い関数版の PointFn(1, 2) は undefined を返し、グローバルオブジェクトに x と y を黙って書き込みました(globalThis.x が 1)。クラスは TypeError で拒否します。これだけでも class を選ぶ理由になります。
  2. メソッドが列挙されない。 関数版では len が for...in に漏れました。
  3. クラス本体は strict モード。 this への影響は「this を失ったメソッド」で見ます。
  4. Point.prototype は読み取り専用。 instanceof の節で確認します。

private #フィールド、getter、static ブロック

ES2022 で本物の非公開が入りました。# で始まる名前はクラス本体の中にしか存在しません。ES2022 のクラス機能を一度に使った小さな口座クラスです。

class Account {
  #balance = 0;                 // private field (ES2022)
  static #count = 0;            // private static
  static { print("static block ran"); }   // static initialization block (ES2022)
  constructor(owner) { this.owner = owner; Account.#count++; }
  deposit(n) { this.#check(n); this.#balance += n; return this; }
  #check(n) { if (!(n > 0)) throw new RangeError(`bad amount: ${n}`); }
  get balance() { return this.#balance; }        // read-only from outside
  static isAccount(o) { return #balance in o; }  // brand check (ES2022)
  static get count() { return Account.#count; }
}
const a = new Account("kim").deposit(100).deposit(50);
print(a.balance, Account.count);
a.balance = 999;                 // no setter: silently ignored in sloppy mode
print(a.balance);
print(Object.keys(a), JSON.stringify(a));
print(Account.isAccount(a), Account.isAccount({ balance: 1 }));
try { a.deposit(-5); } catch (e) { print(e); }
try { Account.prototype.deposit.call({}, 10); } catch (e) { print(e); }
$ jsc account.js
static block ran
150 1
150
owner {"owner":"kim"}
true false
RangeError: bad amount: -5
TypeError: Cannot access private method or acessor (evaluating 'this.#check')
  • a.balance = 999 は何もせず、何も言いませんでした。 getter だけで setter がなく、代入した場所が strict でない普通のスクリプトだったからです。strict なコードでは同じ代入が TypeError: Attempted to assign to readonly property. になりました(jsc -e '"use strict"; ...' で確認)。古い非モジュールのスクリプトにはこの種のバグが潜んでいます。
  • private フィールドは Object.keys にも JSON.stringify にも出ません。非公開の状態をシリアライズしたいなら toJSON() を書きます。
  • #balance in o は、本当にこのクラスで作ったオブジェクトかを確かめます。"balance" in o のようなダックタイピングでは区別できません。
  • よそのオブジェクトにメソッドを呼ぶと、最初の private アクセスで失敗します。JavaScriptCore のメッセージは本当に "acessor" と綴りを間違えています。検索するときは表示どおりに。

外から private 名にアクセスすると、実行時エラーですらありません。構文解析の時点の SyntaxError なので、ファイルの 1 行目すら動きません。

$ cat outside.js
print("first line runs?");
class A { #x = 1 }
const a = new A();
print(a.#x);
$ jsc outside.js
Exception: SyntaxError: Cannot reference undeclared private names: "#x"
at outside.js:5
$ echo $?
3

"first line runs?" は最後まで表示されませんでした(jsc は最終行の次の 5 行目を指しました。ファイル全体へのエラーという意味です)。一方 a["#x"] は undefined でした。文字列キー "#x" はまったく別の普通のプロパティです。

よくある間違い 1:自分自身を呼ぶ setter

こう書きがち: 入力を trim して同じ名前に保存する setter。

class User {
  set name(v) { this.name = v.trim(); }   // BUG: calls itself
}
try { new User().name = " lee "; } catch (e) { print(e); }

実際に起きること: setter の中の this.name = ... がまた setter を呼び、終わりがありません。

$ jsc setter_loop.js
RangeError: Maximum call stack size exceeded.

直し方: 値は別の場所、できれば private フィールドに置きます。#name; set name(v) { this.#name = v.trim(); } get name() { return this.#name; }

よくある間違い 2:this を失ったメソッド

こう書きがち: メソッドをコールバック(setTimeout、イベントリスナー、array.map)に渡す、または変数に入れる。

class Counter {
  count = 0;
  inc() { this.count++; return this.count; }
  incArrow = () => { this.count++; return this.count; };
}
const c = new Counter();
const f = c.inc;
try { f(); } catch (e) { print(e); }
setTimeout(c.inc, 0);           // same bug, later
const g = c.incArrow; print(g(), g());
const h = c.inc.bind(c); print(h());
print(Object.keys(c));

実際に起きること:

$ jsc lostthis.js
TypeError: undefined is not an object (evaluating 'this.count')
1 2
3
count,incArrow
Exception: TypeError: undefined is not an object (evaluating 'this.count')
inc@lostthis.js:3:15
$ echo $?
3

this は関数を定義した場所ではなく呼び方で決まります。f() にはドットの左のオブジェクトがなく、クラス本体は strict なので this はグローバルオブジェクトではなく undefined です。setTimeout のほうは後になって誰にも捕まえられずに落ち、プログラムは終了コード 3 で終わりました。Chrome や Node では Cannot read properties of undefined (reading 'count') と別の文言になります(この記事では実行していないので、両方の文言で検索してください)。

直し方: c.inc.bind(c)、包む () => c.inc()、またはアロー関数のクラスフィールド。最後の行に注目してください。incArrow はインスタンスごとにできる自前のプロパティ(Object.keys に出る)で、inc はプロトタイプに 1 つだけです。ここにはコストがあるので測りました。

実測:アロー関数フィールドはオブジェクト 1 個あたりいくらか

同じ小さなクラスを 4 通りに書き、インスタンスを 10 万個生かしたまま、フル GC 後のヒープサイズを読みました。fullGC() と gcHeapSize() は jsc 専用のヘルパー関数です。Node なら --expose-gc と process.memoryUsage() を使います(ここでは未実行)。

// mem.js — heap cost per instance: prototype method vs per-instance function
// usage: jsc mem.js -- <variant>   (run each variant in a fresh process)
class ProtoMethod { constructor(i) { this.i = i; } get() { return this.i; } }
class ArrowField  { get = () => this.i; constructor(i) { this.i = i; } }
class BoundInCtor { constructor(i) { this.i = i; this.get = this.get.bind(this); } get() { return this.i; } }
class PrivateField { #i; constructor(i) { this.#i = i; } get() { return this.#i; } }
const variants = { ProtoMethod, ArrowField, BoundInCtor, PrivateField };
const N = 100_000;
const name = arguments[0];
const C = variants[name];
fullGC(); const before = gcHeapSize();
const t0 = performance.now();
const keep = new Array(N);
for (let i = 0; i < N; i++) keep[i] = new C(i);
const t1 = performance.now();
fullGC(); const after = gcHeapSize();
let sum = 0; for (const o of keep) sum += o.get();   // all instances still alive and usable
print(`${name.padEnd(12)} bytes/instance=${((after - before) / N).toFixed(1)} create=${(t1 - t0).toFixed(1)}ms sum=${sum}`);

それぞれ新しいプロセスで 5 周しました(1 周目のみ掲載。バイト数は 5 回とも同じでした)。

$ for v in ProtoMethod ArrowField BoundInCtor PrivateField; do jsc mem.js -- $v; done
ProtoMethod  bytes/instance=40.4 create=2.4ms sum=4999950000
ArrowField   bytes/instance=249.4 create=5.7ms sum=4999950000
BoundInCtor  bytes/instance=136.4 create=5.4ms sum=4999950000
PrivateField bytes/instance=72.4 create=2.2ms sum=4999950000
書き方 1 個あたりのバイト 10 万個の生成(5 回の中央値)
プロトタイプのメソッド 40.4 1.8 ms
private #i + プロトタイプのメソッド 72.4 2.2 ms
コンストラクタで bind() 136.4 5.4 ms
アロー関数のクラスフィールド 249.4 5.7 ms
棒グラフ。プロトタイプのメソッドはインスタンスあたり 40.4 バイト。同じクラスを private #フィールドで書くと 72.4 バイト。コンストラクタで bind すると 136.4 バイト、アロー関数のクラスフィールドは 249.4 バイトで、プロトタイプのメソッドの約 6 倍だった。
同じクラスを 4 通りに書き、10 万個を生かしたままのインスタンス 1 個あたりのヒープ使用量(fullGC 後の gcHeapSize、配列スロット 8 バイト込み)。5 回とも同値、macOS 26.6.2 の jsc、Apple M1 Pro。

keep 配列のスロット 8 バイトを含む値です。アローフィールドはインスタンスごとに新しい関数オブジェクト(とクロージャ)を作るので、ここではプロトタイプのメソッドの約 6 倍のメモリを使い、生成も約 3 倍遅くなりました。画面部品 50 個なら誤差ですが、データレコード 100 万件ならおよそ 200MB の差です。コールバックに渡すことが多いオブジェクト(イベントハンドラ)はアローフィールド、データ的なオブジェクトはプロトタイプのメソッド、が目安です。private フィールドは JavaScriptCore ではオブジェクトあたり約 32 バイト多くかかりました。理由までは追っておらず、V8 では違う可能性があります。

extends、super、instanceof

class Shape {
  constructor(name) { this.name = name; }
  area() { return 0; }
  toString() { return `${this.name}(${this.area().toFixed(2)})`; }
}
class Circle extends Shape {
  constructor(r) { super("circle"); this.r = r; }
  area() { return Math.PI * this.r ** 2; }
}
class Square extends Shape {
  constructor(s) { super("square"); this.s = s; }
  area() { return this.s ** 2; }
  toString() { return "[" + super.toString() + "]"; }
}
const shapes = [new Circle(1), new Square(2)];
print(shapes.join(" "));
const c = shapes[0];
print(c instanceof Circle, c instanceof Shape, c instanceof Object, c instanceof Square);
print(Object.getPrototypeOf(Circle.prototype) === Shape.prototype, Object.getPrototypeOf(Circle) === Shape);
class Broken extends Shape { constructor() { this.x = 1; super("b"); } }
try { new Broken(); } catch (e) { print(e); }
class NoSuper extends Shape { constructor() { } }
try { new NoSuper(); } catch (e) { print(e); }
class Stack extends Array { peek() { return this[this.length - 1]; } }
const s = Stack.from([1, 2, 3]);
const m = s.map(x => x * 10);
print(s.peek(), m.peek(), m instanceof Stack, Array.isArray(m));
$ jsc inherit.js
circle(3.14) [square(4.00)]
true true true false
true true
ReferenceError: 'super()' must be called in derived constructor before accessing |this| or returning non-object.
ReferenceError: 'super()' must be called in derived constructor before accessing |this| or returning non-object.
3 30 true true

Shape.toString が this.area() を呼ぶと、サブクラスのものが動きます。探索はオブジェクトから始まるので、いちばん具体的なメソッドが勝ちます。super() の前に this に触れても、super() を呼ばなくても、同じ ReferenceError です。Array の継承もでき、Stack で map を呼ぶと結果も Stack でした。

instanceof はクラス名を見ていません。「Circle.prototype がこのオブジェクトのチェーンのどこかにあるか」を聞いているだけです。

class Duck {}
const d = new Duck();
print(d instanceof Duck);
Duck.prototype = {};             // ignored: class prototype is non-writable
print(d instanceof Duck);
function Goose() {}
const g = new Goose();
Goose.prototype = {};            // allowed for plain functions
print(g instanceof Goose);
// another realm (like an iframe): jsc's createGlobalObject()
const other = createGlobalObject();
const arr = other.eval("[1, 2, 3]");
print(arr instanceof Array, Array.isArray(arr));
// duck typing via Symbol.hasInstance
class Iterable { static [Symbol.hasInstance](o) { return o != null && typeof o[Symbol.iterator] === "function"; } }
print("abc" instanceof Iterable, new Map() instanceof Iterable, {} instanceof Iterable);
try { print(d instanceof {}); } catch (e) { print(e); }
$ jsc instanceof.js
true
true
false
false true
true true false
TypeError: {} is not a function. (evaluating 'd instanceof {}')

別のグローバルオブジェクト(「realm」。ブラウザなら iframe やワーカー。createGlobalObject() はそれをまねる jsc 専用ヘルパー)で作った配列は、Array.prototype が違うので instanceof Array になりません。確実なのは Array.isArray です。

モジュール:jsc で動くもの、動かないもの

モジュールは独自のスコープを持つファイルで、export/import(ES2015)でだけ値を共有します。jsc は拡張子が .mjs か、-m を付けたときにモジュールとして実行します。

// counter.mjs
export let count = 0;
export function inc() { count++; }
export default class Greeter { hi(n) { return `hi ${n}`; } }
print("counter.mjs evaluated");
// main.mjs
import Greeter, { count, inc } from "./counter.mjs";
import * as again from "./counter.mjs";       // same module, not evaluated twice
print(count); inc(); inc();
print(count, again.count);                     // live binding
try { count = 10; } catch (e) { print(e); }
print(new Greeter().hi("module"));
print(typeof this, typeof globalThis);
const cfg = await Promise.resolve({ debug: true });   // top-level await
print("tla:", cfg.debug);
const lazy = await import("./counter.mjs");
print(lazy.count, lazy === again);
print(import.meta.url.replace(/^.*\/mod\//, "~/demo/"));
$ jsc main.mjs
counter.mjs evaluated
0
2 2
TypeError: Attempted to assign to readonly property.
hi module
undefined object
tla: true
2 true
~/demo/main.mjs

モジュールは何回 import されても1 回だけ実行されます。import した let はライブバインディングで、書き出し側が変えると 0 から 2 に一緒に変わりますが、取り込み側からは読み取り専用です。トップレベルの this は undefined、トップレベル await(ES2022)と import()(ES2020)も動きました。import.meta.url の絶対パスは自分で削っています。

書き方を混ぜたときに出会うエラーです。

$ jsc classic.js          # line 1: import { inc } from "./counter.mjs";
Exception: SyntaxError: Unexpected token '{'. import call expects one or two arguments.
at classic.js:1
$ jsc -m classic.js       # same file, as a module: works
counter.mjs evaluated
1
$ jsc -e 'const fs = require("fs")'
Exception: ReferenceError: Can't find variable: require
global code@[Command Line]:1:19
$ jsc noext.mjs           # import { inc } from "./counter";
Exception: Error: Could not open file '~/demo/mod/counter'.
$ jsc badname.mjs         # import { incr } from "./counter.mjs";
Exception: SyntaxError: Importing binding name 'incr' is not found.
$ jsc sloppy.mjs          # x = 5;
Exception: ReferenceError: Can't find variable: x
module code@~/demo/mod/sloppy.mjs:1:2

モジュールは常に strict なので、宣言していない変数への代入はグローバルを作らずエラーになります。ブラウザでも拡張子まで書いたファイル名が必要です。バンドラや Node の CommonJS の解決は拡張子を補ってくれるので、多くのプロジェクトで動く "./counter" がここでは失敗します。

CommonJS(require()、module.exports)は Node の古いモジュール方式です。jsc にはなく、Node は実行していないので以下は説明のみです。CommonJS は同期で読み込み、require 時点の module.exports を受け取ります。ESM のバインディングはライブです。Node は .mjs/.cjs の拡張子か package.json の "type" で両者を区別します。

よくある間違い 3:循環 import と TDZ

こう書きがち: 2 つのモジュールがお互いの定数をトップレベルで import する。

// a.mjs
import { B_NAME } from "./b.mjs";
export const A_NAME = "A";
print("a.mjs sees", B_NAME);
// b.mjs
import { A_NAME } from "./a.mjs";
export const B_NAME = "B";
print("b.mjs sees", A_NAME);      // a.mjs has not run yet

実際に起きること: a.mjs を始めると先に b.mjs が実行されますが、その時点で a.mjs の const 行はまだ実行前です。バインディングはあっても、第 2 回で見た TDZ(一時的デッドゾーン)の中にいます。

$ jsc a.mjs
Exception: ReferenceError: Cannot access 'A_NAME' before initialization.
module code@~/demo/mod/b.mjs:3:27

直し方: 循環を断つ(共有定数は 3 つ目のモジュールへ)か、取り込んだ値を両方のモジュールが終わったあとに動く関数の中でだけ読みます。b.mjs が function hello() { return "b.mjs sees " + A_NAME; } を export し、a.mjs がそれを呼ぶように変えると a.mjs sees B | b.mjs sees A と表示されました。

判断表

状況 使うもの 理由
振る舞いを持つデータ、インスタンスが多い プロトタイプのメソッドを持つ class メソッドは 1 つだけ。実測 40B/個
コールバックに渡すメソッド(UI ハンドラ) アローフィールドか 1 回だけ bind this を保つ代わりにインスタンスごとにメモリ
本当に隠したい状態、所属チェック #private、#x in obj 外から触れず、シリアライズされない
値の入れ物にすぎない オブジェクトリテラル クラス不要、JSON の往復がきれい
ファイル間でコード共有 ES モジュール(ここでは .mjs か -m) ライブバインディング、strict、ブラウザでも動く
「配列か?」 Array.isArray instanceof は realm をまたぐと失敗
2 つのモジュールがお互いの定数を必要とする 共有用の 3 つ目のモジュール TDZ の ReferenceError を避ける

練習問題

  1. 実行する前に出力を予想し、jsc で確かめてください。const base = { greet() { return "hi " + this.n; } }; const o = Object.create(base); o.n = "A"; const fn = o.greet; print(o.greet()); print(typeof fn.call({ n: "B" }));
  2. よくある間違い 1 の setter を直して、u.name = " lee " で "lee" が保存され、u.name で読めて、JSON.stringify(u) が {"name":"lee"} を出すようにしてください。

解答

  1. hi A、次に string。fn.call({ n: "B" }) は "hi B" を返します。call が this を明示的に決めます。
  2. 解答例(jsc で確認済み):
class User {
  #name = "";
  set name(v) { this.#name = String(v).trim(); }
  get name() { return this.#name; }
  toJSON() { return { name: this.#name }; }
}
const u = new User(); u.name = "  lee ";
print(u.name, JSON.stringify(u));   // lee {"name":"lee"}

正直な限界・バージョンメモ

  • エンジンは 1 つだけ。 すべて macOS 26.6.2 の JavaScriptCore でのみ実行しました。エラーの文言はエンジンごとに違い(Chrome や Node の V8 は別の言い回し)、ここで示した挙動は ECMAScript 仕様が定めるものです。
  • バージョン: class、extends、super、Symbol.hasInstance、ES モジュールは ES2015。動的 import() と import.meta は ES2020。public・private フィールド、private メソッド、static ブロック、#x in obj、Object.hasOwn、トップレベル await は ES2022。すべてこの jsc で動きました。古いエンジンは試していません。
  • jsc 専用: print、fullGC、gcHeapSize、createGlobalObject、-m オプションは jsc シェルにしかありません。メモリの数値は JavaScriptCore のヒープ集計なので、V8 では変わる可能性があります。
  • ここで実行していないもの: Node.js と CommonJS ローダ、ブラウザ、<script type="module">、バンドラ、TypeScript。それらの拡張子ルールやエラー文言は説明だけです。

前回: 第 3 回:オブジェクト、配列とメソッド · 次回: 第 5 回:非同期、Promise、イベントループ

シリーズ:1 型と等価比較 · 2 関数、スコープ、クロージャ · 6 性能、メモリ、数値 · 関連:Java のクラス・レコード・インターフェース · Dart のクラス・ミックスイン・拡張 · プログラミング学習ロードマップ

よくある質問

JavaScript の class は結局ただの関数ですか?

ほぼそうですが、完全に同じではありません。typeof クラスは "function" で、メソッドはクラス名.prototype に載ります。一方で、new なしで呼ぶと "Cannot call a class constructor Point without |new|" の TypeError になる、メソッドが列挙されず for...in に出ない、クラス本体は常に strict モード、クラス名.prototype を差し替えられない、という違いを実際に確認しました。

"undefined is not an object (evaluating 'this.count')" はなぜ出るのですか?

メソッドをオブジェクトから切り離して(const f = obj.inc や setTimeout(obj.inc, 0))そのまま呼んだからです。this は呼び方で決まるので、strict モードのクラス内では undefined になります。bind、包むアロー関数、アロー関数のクラスフィールドで直せます。最後の方法はインスタンスごとにメモリを使います(実測 249B 対 40B)。

jsc シェルで ES モジュールや require() は使えますか?

ES モジュールは使えます。.mjs ファイルか jsc -m で実行すると、import/export、ライブバインディング、動的 import()、トップレベル await がすべて動きました。require・module・exports はないので、CommonJS は "ReferenceError: Can't find variable: require" で失敗します。Node.js はこの記事では実行していません。

  • #javascript
  • #中級
  • #クラス
  • #プロトタイプ
  • #モジュール
  • #jsc
  • #javascriptcore

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑