objtoString()을 정의한 적이 없는데 왜 호출이 가능할까요?

자바스크립트의 모든 객체는 프로토타입 체인 을 통해 다른 객체의 프로퍼티를 참조합니다. class 문법도 내부적으로는 이 프로토타입 메커니즘 위에서 동작합니다.

프로토타입이란

자바스크립트의 모든 객체는 다른 객체에 대한 숨겨진 참조 를 가지고 있습니다. 이 참조 대상을 프로토타입(prototype) 이라고 합니다.

JS
const obj = { name: 'Alice' };
console.log(obj.toString()); // "[object Object]"

objtoString을 정의하지 않았는데 호출이 됩니다. obj의 프로토타입인 Object.prototypetoString이 있기 때문입니다.

__proto__ vs prototype

이 두 개념은 혼동하기 쉽습니다.

prototype

  • 생성자 함수(또는 클래스)가 가지는 프로퍼티
  • new로 인스턴스를 만들 때, 인스턴스의 프로토타입이 될 객체
JS
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function () {
  return `Hi, I'm ${this.name}`;
};

__proto__

  • ** 모든 객체가 가지는 접근자 프로퍼티**
  • 자신의 프로토타입 객체를 가리킴
  • 표준은 Object.getPrototypeOf()를 권장
JS
const alice = new Person('Alice');

console.log(alice.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true

정리하면 이렇습니다.

대상소유자역할
prototype생성자 함수인스턴스의 프로토타입이 될 객체
__proto__모든 객체자신의 프로토타입을 가리키는 참조

프로토타입 체인

객체에서 프로퍼티를 찾을 때, 현재 객체에 없으면 ** 프로토타입을 따라 올라가면서 탐색 **합니다. 이 탐색 경로가 프로토타입 체인입니다.

JS
const alice = new Person('Alice');

// 탐색 경로
// alice → Person.prototype → Object.prototype → null
JS
// 직접 확인해보기
console.log(alice.hasOwnProperty('name'));  // true — alice 자체에 있음
console.log(alice.hasOwnProperty('greet')); // false — Person.prototype에 있음

console.log('greet' in alice); // true — 체인 전체를 탐색

체인의 끝은 null

JS
Object.getPrototypeOf(Object.prototype); // null

모든 프로토타입 체인의 끝은 null입니다. 프로퍼티를 찾지 못하면 undefined를 반환합니다.

Object.create

Object.create(proto)는 지정한 객체를 프로토타입으로 하는 새 객체를 만듭니다.

JS
const animal = {
  speak() {
    return `${this.name} makes a sound`;
  }
};

const dog = Object.create(animal);
dog.name = 'Rex';
console.log(dog.speak()); // "Rex makes a sound"

Object.create(null) — 순수 딕셔너리

JS
const dict = Object.create(null);
dict.key = 'value';

console.log(dict.toString);        // undefined — 상속받은 메서드 없음
console.log('key' in dict);        // true
console.log('toString' in dict);   // false

Object.create(null)은 프로토타입이 없는 깨끗한 객체를 만듭니다. 외부 입력을 키로 사용하는 맵 용도에 안전합니다.

class는 문법적 설탕이다

ES2015의 class 문법은 프로토타입 기반 상속을 깔끔하게 작성하는 방법일 뿐, 새로운 상속 모델이 아닙니다.

JS
// class 문법
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound`;
  }
}

// 위와 동일한 프로토타입 기반 코드
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  return `${this.name} makes a sound`;
};

extends와 super의 내부

JS
class Dog extends Animal {
  constructor(name) {
    super(name); // Animal.call(this, name) 과 유사
  }
  bark() {
    return 'Woof!';
  }
}

const rex = new Dog('Rex');

프로토타입 체인을 확인해보면 이렇습니다.

JS
console.log(rex.__proto__ === Dog.prototype);           // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true

class만의 차이점

그렇다고 완전히 같은 것은 아닙니다. class에는 몇 가지 제약이 있습니다.

  • new 없이 호출 불가 — 일반 함수처럼 Dog()으로 호출하면 TypeError
  • class 내부는 자동으로 strict mode
  • 메서드가 열거 불가(non-enumerable)
  • super 키워드 사용 가능

프로토타입 오염 (Prototype Pollution)

프로토타입 오염은 ** 공유 프로토타입 객체의 프로퍼티를 외부 입력으로 변경하는 공격 **입니다.

JS
// 위험한 코드 예시
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

// 공격자가 보낸 JSON
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);

// 모든 객체에 영향
const user = {};
console.log(user.isAdmin); // true — 오염됨!

방어 방법

  • Object.create(null)로 프로토타입 없는 객체 사용
  • hasOwnProperty 검사
  • __proto__, constructor, prototype 키 필터링
  • Object.freeze(Object.prototype) (부작용 주의)
JS
// 안전한 merge
function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor') continue;
    // ...
  }
}

instanceof의 동작 원리

instanceof는 프로토타입 체인을 따라 올라가면서 확인합니다.

JS
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true

내부적으로는 이런 로직입니다.

JS
// instanceof 의사 코드
function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

주의할 점

프로토타입 오염 공격

외부 입력을 그대로 __proto__ 키에 대입하면, 모든 객체의 공유 프로토타입이 변조됩니다. 재귀적 merge 함수에서 __proto__, constructor, prototype 키를 필터링하지 않으면 보안 취약점이 됩니다. Object.create(null)로 프로토타입 없는 객체를 사용하거나, Object.keys()로 열거하여 __proto__를 건너뛰어야 합니다.

hasOwnProperty 없이 in 연산자로 체크하면 오탐

'toString' in obj는 프로토타입 체인 전체를 탐색하므로 true를 반환합니다. 해당 객체 자체의 프로퍼티인지 확인하려면 obj.hasOwnProperty('key') 또는 Object.hasOwn(obj, 'key')를 사용해야 합니다.

class와 생성자 함수의 미묘한 차이

classnew 없이 호출하면 TypeError가 발생하지만, 생성자 함수는 일반 함수로 호출이 가능합니다. 이 차이를 모르면 class를 생성자 함수와 완전히 동일하다고 오해할 수 있습니다.

정리

항목설명
프로토타입모든 객체가 가지는 다른 객체에 대한 숨겨진 참조
prototype vs __proto__prototype은 생성자 함수 소유, __proto__는 모든 객체 소유
프로토타입 체인현재 객체 → __proto__ → ... → null 순서로 프로퍼티 탐색
class프로토타입 기반 상속의 문법적 설탕 + strict mode, new 강제
프로토타입 오염공유 프로토타입 변경으로 모든 객체에 영향을 미치는 취약점
댓글 로딩 중...