명시적 return — 원시값은 무시되고, 객체는 채택된다 (직접 고쳐보세요)
코드
Run
function Foo() { this.x = 1; return 100; // 원시값 → 무시된다 } function Bar() { this.x = 1; return { y: 2 }; // 객체 → 채택된다 (this는 버려짐) } console.log("new Foo() →", new Foo()); // { x: 1 } console.log("new Bar() →", new Bar()); // { y: 2 }
팁:
return 100
을
return { z: 9 }
로 바꾸면 Foo도 그 객체를 돌려준다.
싱글톤 — new를 두 번 해도 인스턴스는 하나 (직접 고쳐보세요)
코드
Run
function Config() { // 이미 인스턴스가 있으면 그걸 객체로 return → 채택된다 if (Config.instance) return Config.instance; this.settings = {}; Config.instance = this; } const a = new Config(); const b = new Config(); console.log("a === b :", a === b); // 같은 객체일까? a.settings.theme = "dark"; console.log("b.settings:", b.settings); // a에서 바꿨는데 b에도 보인다
팁:
return Config.instance;
줄을 지우면 매번 새 인스턴스가 생겨
a === b
가 false가 된다.
Proxy 래핑 — 생성자가 this 대신 Proxy를 return (직접 고쳐보세요)
코드
Run
function Model(data) { Object.assign(this, data); // this를 감싼 Proxy를 return return new Proxy(this, { set(target, key, value) { console.log("변경 감지 →", key, ":", value); target[key] = value; return true; } }); } const m = new Model({ name: "kim" }); m.name = "lee"; // set 트랩이 가로챈다 m.age = 20; // 새 프로퍼티도 감지 console.log("최종 m.name:", m.name);
팁:
return new Proxy(...)
를 지우면 평범한 인스턴스가 되어 변경 감지가 사라진다.
class도 함수다 — 그리고 new 가능 여부는 정의 방식이 가른다 (직접 고쳐보세요)
코드
Run
class Bar {} console.log("typeof Bar :", typeof Bar); // "function" try { Bar(); } // new 없이 호출 catch (e) { console.log("Bar() →", e.constructor.name); } const arrow = () => {}; try { new arrow(); } // 화살표를 new catch (e) { console.log("new arrow() →", e.constructor.name); }
class는 [[Construct]]는 있지만 [[Call]]이 막혀 있고, 화살표는 [[Construct]] 자체가 없다.
prototype은 new 이전, 함수 정의 시점에 이미 존재한다 (직접 고쳐보세요)
코드
Run
function Foo() {} // new를 한 번도 안 했는데... console.log("constructor 백참조 :", Foo.prototype.constructor === Foo); // true console.log("typeof Foo.prototype :", typeof Foo.prototype); // "object" const arrow = () => {}; console.log("arrow.prototype :", arrow.prototype); // 생성자가 아니라 undefined
생성자로 못 쓰는 화살표 함수엔 prototype 프로퍼티가 아예 없다(undefined).
위임의 증거 — 인스턴스를 만든 '후에' 추가한 메서드도 동작한다 (직접 고쳐보세요)
코드
Run
function Animal() {} const a = new Animal(); // 이 순간 speak는 어디에도 없다 // a를 만든 '후에' 부모에 추가 Animal.prototype.speak = function () { return "소리!"; }; // 복사 모델이라면 몰라야 하는데... console.log("a.speak() :", a.speak()); // 호출 시점에 체인에서 찾아온다
복사가 아니라 링크(위임)이기 때문에, 부모를 나중에 바꿔도 살아있는 자식에 즉시 반영된다.
⚠️ instanceof 뒤에 엉뚱한 타입 → 무한 재귀로 RangeError (Run을 눌러 직접 확인)
코드
Run
function Person(name) { if (!(this instanceof Date)) { // Person이 아니라 Date를 넣어버림 return new Person(name); // new로 만들어도 Date가 아니니 또 걸림 } this.name = name; } new Person("kim"); // 출구가 없는 무한 재귀!
new Person()이 만드는 객체는 항상 Person(Date 아님)이라 가드를 영원히 못 빠져나간다.
✅ 올바른 가드: 자기 자신 instanceof / new.target (직접 고쳐보세요)
코드
Run
// ① instanceof 가드 — 뒤에 '자기 자신'을 적는다 function PersonA(name) { if (!(this instanceof PersonA)) return new PersonA(name); this.name = name; } console.log("instanceof :", PersonA("kim").name); // new 없이도 안전 // ② new.target 가드 — 타입을 적을 자리가 없어 실수 불가 function PersonB(name) { if (!new.target) return new PersonB(name); this.name = name; } console.log("new.target :", PersonB("lee").name);
new로 만든 객체가 가드 조건을 '만족'하므로 두 번째 호출에서 빠져나간다.