생성자와 인스턴스 — new 연산자의 동작 원리

JavaScript new 연산자가 내부에서 하는 4단계 작업, constructor의 반환값 규칙, 인스턴스와 프로토타입의 관계를 단계별로 설명합니다.

· 6 min read · PALDYN Team

지난 글에서 class 키워드의 기본 구조와 내부 동작을 살펴봤습니다. 클래스를 실제로 인스턴스로 만드는 것은 new 연산자입니다. new가 내부에서 무슨 일을 하는지 알면 생성자가 반환값을 가질 때 어떤 일이 벌어지는지, 왜 this가 새 객체를 가리키는지 자연스럽게 이해됩니다.

클래스-new-인스턴스 관계 다이어그램

new 연산자의 4단계 동작

new MyClass(args)를 실행하면 JavaScript 엔진은 아래 4단계를 수행합니다.

  1. 빈 객체 생성: Object.create(MyClass.prototype)으로 프로토타입이 연결된 빈 객체를 만듭니다.
  2. this 바인딩: 새로 만든 객체를 constructor 내부의 this로 설정합니다.
  3. 생성자 실행: constructor를 호출해서 인스턴스 프로퍼티를 초기화합니다.
  4. 반환: 생성자가 객체를 반환하면 그 객체를, 그렇지 않으면 step 1에서 만든 객체를 반환합니다.

코드로 흉내 내면 이렇습니다.

function myNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype);
  const result = Constructor.apply(obj, args);
  return (result !== null && typeof result === 'object')
    ? result
    : obj;
}

생성자와 인스턴스 프로퍼티

constructor 안에서 this.xxx = value로 설정한 값은 인스턴스 자체에 저장됩니다. 메서드처럼 프로토타입에 공유되지 않습니다.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  distance() {
    return Math.sqrt(
      this.x ** 2 + this.y ** 2
    );
  }
}
const p = new Point(3, 4);
console.log(p.distance()); // 5

p.x, p.yp 객체에 직접 존재하지만, p.distancePoint.prototype.distance를 통해 접근합니다. Object.hasOwn(p, 'distance')false입니다.

생성자와 인스턴스 생성 코드

생성자의 반환값 규칙

생성자에서 명시적으로 값을 반환하면 규칙이 적용됩니다.

class Weird {
  constructor() {
    this.value = 1;
    return { custom: 'object' }; // 객체를 반환하면 이것이 인스턴스
  }
}
const w = new Weird();
console.log(w.custom); // 'object'
console.log(w.value);  // undefined

class Normal {
  constructor() {
    this.value = 1;
    return 42; // 원시값은 무시됨
  }
}
const n = new Normal();
console.log(n.value); // 1 (원시값 반환은 무시)
  • 객체 반환: 해당 객체가 new의 결과가 됩니다.
  • 원시값 반환 또는 반환 없음: new가 만든 원래 객체가 반환됩니다.

이 규칙은 팩토리 패턴이나 캐싱 패턴 구현에서 활용할 수 있지만, 일반적인 코드에서는 생성자가 값을 반환하지 않는 것이 관례입니다.

여러 인스턴스와 공유 메서드

같은 클래스에서 생성된 여러 인스턴스는 메서드를 프로토타입을 통해 공유합니다.

const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
const p3 = new Point(5, 12);

// 각 인스턴스는 독립적인 x, y를 가짐
console.log(p1.x, p2.x, p3.x); // 0, 3, 5

// 메서드는 하나만 존재, 공유
console.log(p1.distance === p2.distance); // true
console.log(p1.distance === Point.prototype.distance); // true

이것이 클래스(프로토타입 상속)가 메모리 효율적인 이유입니다. 함수를 생성자 내부에서 this.method = function () {}로 정의하면 인스턴스마다 별도의 함수 객체가 생성됩니다.

constructor를 생략해도 될 때

constructor를 정의하지 않으면 기본 생성자가 자동으로 추가됩니다.

class Empty {}
// 내부적으로 아래와 동일
class Empty {
  constructor() {}
}

서브클래스(상속받은 클래스)에서 생략하면 약간 다릅니다.

class Parent {
  constructor(value) { this.value = value; }
}
class Child extends Parent {
  // constructor 생략 시 아래와 동일
  // constructor(...args) { super(...args); }
}
const c = new Child(42);
console.log(c.value); // 42

서브클래스에서 constructor를 직접 작성했다면 super()를 반드시 this 사용 전에 호출해야 합니다.

instanceof로 인스턴스 확인

instanceof는 객체가 특정 클래스의 인스턴스인지 프로토타입 체인을 따라 확인합니다.

const p = new Point(1, 2);
console.log(p instanceof Point);  // true
console.log(p instanceof Object); // true (프로토타입 체인)
console.log(p instanceof Array);  // false

실무에서 instanceof의 한계(같은 클래스의 다른 realm 인스턴스 등)는 instanceof와 Symbol.hasInstance 글에서 자세히 다룹니다.

constructor 프로퍼티

모든 함수(클래스 포함)의 prototype 객체에는 constructor 프로퍼티가 있어 자기 자신을 가리킵니다.

class Point {}
console.log(Point.prototype.constructor === Point); // true

const p = new Point();
console.log(p.constructor === Point); // true (프로토타입을 통해)
console.log(p.constructor.name);     // 'Point'

이를 이용하면 런타임에 어떤 클래스로 만들어진 인스턴지 확인하거나, 동일한 클래스의 새 인스턴스를 동적으로 만들 수 있습니다.

function clone(instance) {
  return new instance.constructor();
}

지난 글: 클래스 문법 입문 — ES6 class 키워드 완전 이해

다음 글: 정적 멤버 — 클래스 레벨 프로퍼티와 메서드


읽어주셔서 감사합니다. 😊