JS 클래스·프로토타입·모듈: jsc 로 직접 돌려 본 동작
JavaScript 강좌 4: 프로토타입 체인, class 와 함수의 차이, #private, this 를 잃는 TypeError, 순환 import, 객체당 40B 대 249B 실측.
시리즈: JavaScript 강좌 4/6 (중급). 함수와 배열·객체는 쓸 줄 알지만, 실무 코드에서 class, extends, #private, import 를 만나면 안에서 무슨 일이 일어나는지 모르는 분을 위한 글입니다. 다 읽으면 속성이 어디서 찾아지는지 예측하고, 클래스와 그냥 객체 중 하나를 고르고, this 를 잃어 죽는 버그를 피하고, 순환 import 에 당하지 않고 ES 모듈로 코드를 나눌 수 있습니다.
신입 분들이 React·Vue 코드를 처음 열었을 때 자주 묻는 것이 "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
출력에서 읽어야 할 것은 세 가지입니다.
- 자기 속성과 물려받은 속성.
Object.hasOwn(cat, "legs")는false,"legs" in cat은true였습니다.in은 체인을 따라가고hasOwn(ES2022)은 따라가지 않습니다. - 쓰기는 체인을 타지 않습니다.
cat.legs = 3은cat에 새 자기 속성을 만들었고(가리기, shadowing)animal.legs는 4 그대로였습니다. 반대로 나중에animal.legs를 바꾸자 자기legs가 없는bird가 보는 값이 바뀌었습니다. 프로토타입에 공유된 변경 가능 데이터는 전형적인 버그 원인입니다. for...in은 물려받은 열거 가능 키까지 돕니다(describe가 나왔습니다).Object.keys는 아닙니다. 데이터에는Object.keys/Object.entries를 쓰세요.
describe 안의 this 는 animal 이 아니라 cat 이었습니다. this 는 메서드를 어디서 찾았든 호출할 때 점 앞에 있던 객체입니다.
class 는 함수다, 다만 눈에 보이는 차이 네 가지
ES2015 이전의 "클래스"는 생성자 함수와 .prototype 에 붙인 메서드였습니다. class 키워드도 같은 모양을 만들기 때문에 아래 두 방식은 둘 다 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|
new를 빼먹으면. 옛 함수 방식의PointFn(1, 2)는undefined를 돌려주고 전역 객체에x,y를 조용히 써 버렸습니다(globalThis.x가 1). 클래스는TypeError로 거부합니다. 이것만으로도class를 쓸 이유가 됩니다.- 메서드가 열거되지 않습니다. 함수 방식은
len이for...in에 새어 나왔습니다. - 클래스 본문은 strict 모드입니다.
this에 어떤 영향이 있는지는 아래 "this 를 잃은 메서드"에서 봅니다. 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 라서 파일의 첫 줄도 실행되지 않습니다.
$ 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 는 프로토타입에 하나만 있습니다. 여기에는 비용이 있어서 직접 재 봤습니다.
실측: 화살표 함수 필드는 객체 하나에 얼마나 드나
같은 작은 클래스를 네 가지로 쓰고, 인스턴스 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 바퀴 돌렸습니다(첫 바퀴만 보이며, 바이트 수는 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
| 방식 | 인스턴스당 바이트 | 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 |
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 하든 한 번만 실행됩니다. 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
이렇게 씁니다: 두 모듈이 서로의 상수를 최상위에서 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
고치는 법: 순환을 끊거나(공유 상수는 세 번째 모듈로), 가져온 값을 두 모듈이 다 끝난 뒤 실행되는 함수 안에서만 읽습니다. b.mjs 가 function hello() { return "b.mjs sees " + A_NAME; } 를 내보내고 a.mjs 가 그것을 부르게 바꾸자 a.mjs sees B | b.mjs sees A 가 출력됐습니다.
판단표
| 상황 | 쓸 것 | 이유 |
|---|---|---|
| 동작이 있는 데이터, 인스턴스가 많음 | 프로토타입 메서드를 가진 class |
메서드는 하나만, 실측 40B/개 |
| 콜백으로 넘기는 메서드(UI 핸들러) | 화살표 필드 또는 한 번 bind |
this 유지, 대신 인스턴스마다 메모리 |
| 정말 숨길 상태, 소속 검사 | #private, #x in obj |
접근 불가, 직렬화 안 됨 |
| 값 묶음일 뿐 | 객체 리터럴 | 클래스 불필요, JSON 왕복이 깔끔 |
| 파일 간 코드 공유 | ES 모듈(여기서는 .mjs 나 -m) |
라이브 바인딩, strict, 브라우저에서도 동작 |
| "배열인가?" | Array.isArray |
instanceof 는 realm 을 넘으면 실패 |
| 두 모듈이 서로의 상수가 필요 | 공유용 세 번째 모듈 | TDZ ReferenceError 회피 |
연습 문제
- 실행하기 전에 출력을 예측하고 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" })); - 흔한 실수 1 의 setter 를 고쳐서
u.name = " lee "이면"lee"가 저장되고,u.name으로 읽히고,JSON.stringify(u)가{"name":"lee"}를 출력하게 하세요.
정답
hi A, 그다음string.fn.call({ n: "B" })는"hi B"를 돌려줍니다.call이this를 직접 정합니다.- 풀이 예(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"}
솔직한 한계 / 버전 메모
- 엔진 하나. 모두 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 는 이 글에서 실행하지 않았습니다.