Post

[모던 자바스크립트 Deep Dive]19장 프로토타입

서론

  • 자바스크립트는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어다.
    • C++나 자바는 클래스 기반 객체지향 프로그래밍 언어다.
  • 자바스크립트에서 원시 타입 값을 제외하고 나머지 모두는 객체이다.

19.1 객체지향 프로그래밍

  • 객체지향 프로그래밍: 객체의 집합으로 프로그램을 표현하는 프로그래밍 패러다임
    • 전통적인 명령형 프로그래밍은 프로그램을 명령어 또는 함수의 목록으로 본다.
  • 추상화: 다양한 속성 중 프로그램에 필요한 속성만 간추려 표현하는 것
  • 객체: 객체의 상태를 나타내는 데이터(프로퍼티)와 상태 데이터를 조작할 수 있는 동작(메서드)을 하나의 논리적인 단위로 묶은 복합적 자료구조

19.2 상속과 프로토타입

  • 상속: 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받을 수 있다.
    • 객체지향 프로그래밍의 핵심 개념
    • 자바스크립트는 프로토타입을 기반으로 상속을 구현

상속을 이용한 코드 재사용

상속은 코드 재사용을 통해 불필요한 중복을 제거하는 데 유용하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    return Math.PI * this.radius ** 2;
  };
}

const circle1 = new Circle(1);
const circle2 = new Circle(2);

// Circle 생성자 함수는 인스턴스를 생성할 때마다 동일한 동작을 하는
// getArea 메서드를 중복 생성하고 모든 인스턴스가 중복 소유한다.
// getArea 메서드는 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직하다.
console.log(circle1.getArea === circle2.getArea); // false

console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172
  • 위의 예시는 getArea 메서드를 중복 생성 및 중복으로 소유한다.
  • 인스턴스가 동일한 메서드를 중복으로 소유하는 것은 메모리 낭비이며, 인스턴스 생성시마다 메서드를 생성하므로 퍼포먼스에도 좋지 않다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
}

// Circle 생성자 함수가 생성한 모든 인스턴스가 getArea 메서드를
// 공유해서 사용할 수 있도록 프로토타입에 추가한다.
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있다.
Circle.prototype.getArea = function () {
  return Math.PI * this.radius ** 2;
};

// 인스턴스 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);

// Circle 생성자 함수가 생성한 모든 인스턴스는 부모 객체의 역할을 하는
// 프로토타입 Circle.prototype으로부터 getArea 메서드를 상속받는다.
// 즉, Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea 메서드를 공유한다.
console.log(circle1.getArea === circle2.getArea); // true

console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172
  • 위의 예시에서 모든 인스턴스는 상위 객체인 Circle.prototype의 모든 프로퍼티와 메서드를 상속받는다.
  • 동일한 메서드를 상속을 통해 공유하기 때문에 코드를 재사용할 수 있다.

19.3 프로토타입 객체

  • 프로토타입 객체(프로토타입)
    • 객체 간 상속을 구현하기 위해 사용
    • 상위 객체의 역할을 하는 객체
    • 다른 객체에 공유 프로퍼티와 메서드 제공
    • 모든 객체는 하나의 프로토타입을 갖는다.
      • [[Prototype]] 내부 슬롯의 값이 null인 경우 프로토타입은 없는 것
    • 모든 프로토타입은 생성자 함수와 연결되어 있다.
      • 프로토타입은 constructor 프로퍼티를 통해 생성자 함수에, 생성자 함수는 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있다.
  • [[Prototype]] 내부 슬롯
    • 모든 객체가 소유
    • 프로토타입의 참조를 값으로 가짐
    • 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장
    • 직접 접근할 수 없지만 __proto__ 접근자 프로퍼티를 이용해 간접 접근 가능
      • __proto__ 접근자 프로퍼티를 이용해 프로토타입에 간접적으로 접근할 수 있다.

19.3.1 __proto__ 접근자 프로퍼티

Prototype_internal_slot

  • 모든 객체는 __proto__ 접근자 프로퍼티를 이용해 자신의 프로토타입, 즉 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있다.

__proto__는 접근자 프로퍼티다.

Prototype_descriptor

1
2
3
4
5
6
7
8
9
const obj = {};
const parent = { x: 1 };

// getter 함수인 get __proto__가 호출되어 obj 객체의 프로토타입을 취득
obj.__proto__;
// setter함수인 set __proto__가 호출되어 obj 객체의 프로토타입을 교체
obj.__proto__ = parent;

console.log(obj.x); // 1
  • 내부 슬롯은 프로퍼티가 아니므로 직접 접근하거나 호출할 수 없다.
  • __proto__[[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있게 한다.
  • __proto__를 통해 프로토타입에 접근할 때 getter 함수인 [[Get]]이 호출된다.
  • __proto__를 통해 새로운 프로토타입을 할당할 때 setter 함수인 [[Set]]이 호출된다.

__proto__ 접근자 프로퍼티는 상속을 통해 사용된다.

1
2
3
4
5
6
7
8
9
10
11
const person = { name: 'Lee' };

// person 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(person.hasOwnProperty('__proto__')); // false

// __proto__ 프로퍼티는 모든 객체의 프로토타입 객체인 Object.prototype의 접근자 프로퍼티
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}

// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용할 수 있다.
console.log({}.__proto__ === Object.prototype); // true
  • __proto__ 접근자 프로퍼티는 Object.prototype의 프로퍼티다.
    • 객체가 직접 소유하는 프로퍼티가 아니다.
  • 모든 객체는 상속을 통해 Object.prototype의 프로퍼티인 __proto__ 접근자 프로퍼티를 사용할 수 있다.

__proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유

  • 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서 프로토타입에 접근할 때 접근자 프로퍼티를 사용한다.
  • 접근자 프로퍼티는 확인 없이 프로토타입을 교체하는 일을 막는다.
    • 이는 순환 참조(circular reference)하는 프로토타입 체인의 생성을 막을 수 있다.
    1
    2
    3
    4
    5
    6
    7
    
      const parent = {};
      const child = {};
        
      // child의 프로토타입을 parent로 설정
      child.__proto__ = parent;
      // parent의 프로토타입을 child로 설정
      parent.__proto__ = child; // TypeError: Cyclic __proto__ value
    

__proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 건 권장하지 않는다.

  • __proto__ 접근자 프로퍼티를 사용할 수 없는 객체가 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    
      // obj는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속받을 수 없다.
      const obj = Object.create(null);
        
      // obj는 Object.__proto__를 상속받을 수 없다.
      console.log(obj.__proto__); // undefined
        
      // 따라서 Object.getPrototypeOf 메서드를 사용하는 편이 좋다.
      console.log(Object.getPrototypeOf(obj)); // null
    
  • 대신 Object.getPrototypeOf 메서드와 Object.setPrototypeOf 메서드를 사용을 권장한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      const obj = {};
      const parent = { x: 1 };
        
      // obj 객체의 프로토타입을 취득
      Object.getPrototypeOf(obj); // obj.__proto__;
      // obj 객체의 프로토타입을 교체
      Object.setPrototypeOf(obj, parent); // obj.__proto__ = parent;
        
      console.log(obj.x); // 1
    

19.3.2 함수 객체의 prototype 프로퍼티

  • 함수 객체만 가지고 있는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.
    • 일반 객체는 prototype 프로퍼티를 갖지 않고 Object.prototype의 프로퍼티인 __proto__ 접근자 프로퍼티를 사용해 프로토타입에 접근한다.
    1
    2
    3
    4
    5
    
      // 함수 객체는 prototype 프로퍼티를 소유한다.
      (function () {}).hasOwnProperty('prototype'); // -> true
        
      // 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
      ({}).hasOwnProperty('prototype'); // -> false
    
  • 화살표 함수와 ES6 메서드 축약 표현으로 정의한 메서드는 non-structor이므로 prototype 프로퍼티를 갖지 않는다.
    • 프로토타입 또한 생성하지 않는다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      // 화살표 함수는 non-constructor다.
      const Person = name => {
        this.name = name;
      };
        
      // non-constructor는 prototype 프로퍼티를 소유하지 않는다.
      console.log(Person.hasOwnProperty('prototype')); // false
        
      // non-constructor는 프로토타입을 생성하지 않는다.
      console.log(Person.prototype); // undefined
        
      // ES6의 메서드 축약 표현으로 정의한 메서드는 non-constructor다.
      const obj = {
        foo() {}
      };
        
      // non-constructor는 prototype 프로퍼티를 소유하지 않는다.
      console.log(obj.foo.hasOwnProperty('prototype')); // false
        
      // non-constructor는 프로토타입을 생성하지 않는다.
      console.log(obj.foo.prototype); // undefined
    
  • 모든 객체가 갖는 Object.prototype의 프로퍼티인 __proto__ 접근자 프로퍼티와 함수 객체만이 갖는 prototype 프로퍼티는 동일한 프로토타입을 가리킨다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      // 생성자 함수
      function Person(name) {
        this.name = name;
      }
        
      const me = new Person('Lee');
        
      // 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다.
      console.log(Person.prototype === me.__proto__);  // true
    
    • __proto__ 접근자 프로퍼티
      • 모든 객체가 소유
      • 프로토타입의 참조를 값으로 가짐
      • 모든 객체가 사용
      • 객체가 자신의 프로토타입에 접근 또는 교체하기 위해 사용
    • prototype 프로퍼티
      • constructor가 사용
      • 프로토타입의 참조를 값으로 가짐
      • 생성자 함수가 사용
      • 생성자 함수가 자신이 생성할 인스턴스의 프로토타입을 할당하기 위해 사용

19.3.3 프로토타입의 constructor 프로퍼티와 생성자 함수

1
2
3
4
5
6
7
8
9
// 생성자 함수
function Person(name) {
  this.name = name;
}

const me = new Person('Lee');

// me 객체의 생성자 함수는 Person이다.
console.log(me.constructor === Person);  // true

19.4 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입

  • 생성자 함수에 의해 생성된 인스턴스
    • 프로토타입의 constructor 프로퍼티에 의해 인스턴트를 생성한 생성자 함수와 연결
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      // obj 객체를 생성한 생성자 함수는 Object다.
      const obj = new Object();
      console.log(obj.constructor === Object); // true
        
      // add 함수 객체를 생성한 생성자 함수는 Function이다.
      const add = new Function('a', 'b', 'return a + b');
      console.log(add.constructor === Function); // true
        
      // 생성자 함수
      function Person(name) {
        this.name = name;
      }
        
      // me 객체를 생성한 생성자 함수는 Person이다.
      const me = new Person('Lee');
      console.log(me.constructor === Person); // true
    
  • 리터럴 표기법에 의해 생성된 객체
    • 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수는 객체를 생성한 생성자 함수가 아닐 수 있다.
      • 예를 들어, 객체 리터럴로 생성된 객체는 Object 생성자 함수가 생성한 객체가 아니다.
    • 하지만 (리터럴 표기법으로 생성한 객체의 생성자 함수 = 프로토타입의 constructor 프로퍼티에 연결된 생성자 함수)라고 생각해도 무리는 없다.
      • 프로토타입과 생성자 함수는 단독으로 존재할 수 없고, 언제나 쌍으로 존재해 prototype, constructor 프로퍼티에 의해 연결되어야 하기 때문이다.
      • 이는 리터럴 표기법에 의해 생성된 객체는 상속을 위해 프로토타입이 필요하며,
      • 따라서 가상적인 생성자 함수를 가져야 하기 때문이다.
    • 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입

      리터럴 표기법생성자 함수프로토타입
      객체 리터럴ObjectObject.prototype
      함수 리터럴FunctionFunction.prototype
      배열 리터럴ArrayArray.prototype
      정규 표현식 리터럴RegExpRegExp.prototype

19.5 프로토타입의 생성 시점

  • 프로토타입은 생성자 함수가 생성되는 시점에 생성된다.
  • 프로토타입과 생성자 함수는 단독으로 존재할 수 없고, 언제나 쌍으로 존재하기 때문이다.

19.5.1 사용자 정의 생성자 함수와 프로토타입 생성 시점

  • constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입 생성

    1
    2
    3
    4
    5
    6
    7
    
      // 함수 정의(constructor)가 평가돼 함수 객체를 생성하는 시점에 프로토타입도 생성
      console.log(Person.prototype); // {constructor: ƒ}
        
      // 생성자 함수
      function Person(name) {
        this.name = name;
      }
    

    constructor

    • 생성된 프로토타입은 constructor 프로퍼티만을 갖는 객체이다.
      • 프로토타입의 constructor 프로퍼티는 생성자 함수를 가리킨다.

        constructor2

    • 생성된 프로토타입의 프로토타입은 Object.prototype이다.

      Object.prototype

  • non-constructor는 프로토타입 생성하지 않음

    1
    2
    3
    4
    5
    6
    7
    
      // 화살표 함수는 non-constructor다.
      const Person = name => {
        this.name = name;
      };
        
      // non-constructor는 프로토타입이 생성되지 않는다.
      console.log(Person.prototype); // undefined
    

19.5.2 빌트인 생성자 함수와 프로토타입 생성 시점

  • 빌트인 생성자 함수가 생성되는 시점에 프로토타입 생성
  • 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성
    • 프로토타입이 생성되면 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩
  • 객체가 생성되기 전에 생성자 함수와 프로토타입은 이미 객채화되어 존재
    • 이후 객체를 생성하면, 생성된 객체의 [[Prototype]] 내부 슬롯에 프로토타입을 할당

19.6 객체 생성 방식과 프로토타입의 결정

  • 객체 생성 방식
    • 객체 리터럴, Object 생성자 함수, 생성자 함수, Object.create 메서드, 클래스(ES6)
    • 모두 추상 연산 OrdinaryObejctCreate를 이용해 객체 생성
  • 추상 연산 OrdinaryObejctCreate
    • 추상 연산: ECMAScript 사양에서 내부 동작 구현 알고리즘을 표현한 것으로 일종의 의사 코드
    • 생성할 객체의 프로토타입을 인수로 전달받음
      • 이 프로토타입을 [[Prototype]] 내부 슬롯에 할당한 뒤, 객체를 반환한다.
    • 생성할 객체에 추가할 프로퍼티 목록을 옵션으로 전달받음
      • 전달된 경우 빈 객체에 이를 추가한다.

19.6.1 객체 리터럴에 의해 생성된 객체의 프로토타입

1
2
3
4
5
const obj = { x: 1 };

// 객체 리터럴에 의해 생성된 obj 객체는 Object.prototype을 상속받는다.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x'));    // true
  • 객체의 프로토타입: Object.prototype
    • 추상 연산 OrdinaryObejctCreate에 전달되는 프로토타입이 Object.prototype이기 때문
  • 따라서 Object.prototype 객체를 상속받는다.

19.6.2 Object 생성자 함수에 의해 생성된 객체의 프로토타입

1
2
3
4
5
6
const obj = new Object();
obj.x = 1;

// Object 생성자 함수에 의해 생성된 obj 객체는 Object.prototype을 상속받는다.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x'));    // true
  • 객체의 프로토타입: Object.prototype
    • 추상 연산 OrdinaryObejctCreate에 전달되는 프로토타입이 Object.prototype이기 때문
  • 따라서 Object.prototype 객체를 상속받는다.
  • 객체 리터럴에 의해 생성된 객체와 동일한 구조를 갖는다.
    • Object 생성자 함수는 빈 객체를 생성한 뒤 프로퍼티를 추가해야 하지만 객체 리터럴은 객체 리터럴 내부에 프로퍼티를 추가한다는 차이점 존재

19.6.3 생성자 함수에 의해 생성된 객체의 프로토타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name) {
  this.name = name;
}

// 프로토타입 메서드 추가
Person.prototype.sayHello = function () {
  console.log(`Hi! My name is ${this.name}`);
};

const me = new Person('Lee');
const you = new Person('Kim');

me.sayHello();  // Hi! My name is Lee
you.sayHello(); // Hi! My name is Kim
  • 객체의 프로토타입: 생성자 함수의 prototype 프로퍼티에 바인딩된 객체
    • 추상 연산 OrdinaryObejctCreate에 전달되는 프로토타입이 생성자 함수의 prototype 프로퍼티에 바인딩된 객체이기 때문
    • 위 예시에서 생성자 함수의 prototype 프로퍼티에 바인딩된 객체는 Person.prototype이다.
      • Person.prototype가 생성될 때 프로퍼티는 constructor뿐이다.
  • 따라서 Object.prototype 객체를 상속받는다.
  • 객체 리터럴에 의해 생성된 객체와 동일한 구조를 갖는다.
    • Object 생성자 함수는 빈 객체를 생성한 뒤 프로퍼티를 추가해야 하지만 객체 리터럴은 객체 리터럴 내부에 프로퍼티를 추가한다는 차이점 존재

19.7 프로토타입 체인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name) {
  this.name = name;
}

// 프로토타입 메서드
Person.prototype.sayHello = function () {
  console.log(`Hi! My name is ${this.name}`);
};

const me = new Person('Lee');

// hasOwnProperty는 Object.prototype의 메서드다.
console.log(me.hasOwnProperty('name')); // true

// me 객체의 프로토타입은 Person.prototype이다.
Object.getPrototypeOf(me) === Person.prototype; // true

// Person.prototype의 프로토타입은 Object.prototype이다.
// 프로토타입의 프로토타입은 언제나 Object.prototype이다.
Object.getPrototypeOf(Person.prototype) === Object.prototype; // true
  • 프로토타입 체인: 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 메커니즘
    • 객체의 프로퍼티나 메서드에 접근할 때, 접근하려는 프로퍼티나 메서드가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 상위 프로토타입의 프로퍼티를 순차적 검색
  • Object.prototype
    • 프로토타입 체인의 최상위 객체, 프로토타입 체인의 종점
    • 모든 객체가 상속받음
    • [[Prototype]] 내부 슬롯 값은 null
    • 여기서도 존재하지 않는 프로퍼티나 메서드에 접근하면 undefined 반환(에러 발생 아님)
  • 스코프 체인: 식별자 검색을 위한 메커니즘
    • 프로토타입 체인과 협력해 식별자와 프로퍼티를 검색한다.

프로토타입 체인 검색 과정

1
2
3
// hasOwnProperty는 Object.prototype의 메서드다.
// me 객체는 프로토타입 체인을 따라 hasOwnProperty 메서드를 검색하여 사용한다.
me.hasOwnProperty('name'); // true
  • 스코프 체인에서 me 식별자를 검색한 뒤 me 객체의 프로토타입 체인에서 메서드 검색 시작
    1. me 객체에서 hasOwnProperty 메서드 검색
    1. 없으므로 [[Prototype]] 내부 슬롯에 바인딩된 Person.prototype으로 이동
      1. Person.prototype 객체에서 hasOwnProperty 메서드 검색
    2. 없으므로 [[Prototype]] 내부 슬롯에 바인딩된 Object.prototype으로 이동
      1. Object.prototype 객체에서 hasOwnProperty 메서드 검색
    3. 존재하므로 자바스크립트 엔진은 Object.prototype.hasOwnProperty 메서드 호출
    4. 이때 메서드의 this에는 me 객체 바인딩

      1
      
       Object.prototype.hasOwnProperty.call(me, 'name');
      

19.8 오버라이딩과 프로퍼티 섀도잉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Person = (function () {
  // 생성자 함수
  function Person(name) {
    this.name = name;
  }

  // 프로토타입 메서드
  Person.prototype.sayHello = function () {
    console.log(`Hi! My name is ${this.name}`);
  };

  // 생성자 함수를 반환
  return Person;
}());

const me = new Person('Lee');

// 인스턴스 메서드
me.sayHello = function () {
  console.log(`Hey! My name is ${this.name}`);
};

// 인스턴스 메서드가 호출된다. 프로토타입 메서드는 인스턴스 메서드에 의해 가려진다.
me.sayHello(); // Hey! My name is Lee
  • 프로퍼티 섀도잉: 상속 관계에 의해 프로퍼티가 가려지는 현상
    • 프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면, 프로토타입 프로퍼티를 덮어쓰지 않고 인스턴스 프로퍼티로 추가한다. 이때 인스턴스 메서드가 프로토타입 메서드를 오버라이딩하여 가리게 된다.
  • 오버라이딩: 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해 사용하는 방식
    • 오버로딩: 함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현 + 매개변수에 의해 매서드를 구별하여 호출하는 방법
  • 하위 객체를 통해 프로토타입에 get 액세스는 허용되나 set 액세스는 허용되지 않는다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      // 인스턴스 메서드를 삭제한다.
      delete me.sayHello;
      // 인스턴스에는 sayHello 메서드가 없으므로 프로토타입 메서드가 호출된다.
      me.sayHello(); // Hi! My name is Lee
        
      // 프로토타입 체인을 통해 프로토타입 메서드가 삭제되지 않는다.
      delete me.sayHello;
      // 프로토타입 메서드가 호출된다.
      me.sayHello(); // Hi! My name is Lee
    
    • 하위 객체를 통해 프로토타입의 프로퍼티 변경 및 삭제는 불가능하다.
    • 프로토타입 프로퍼티 변경 및 삭제를 위해선 프로토타입 체인을 통한 접근이 아닌 직접 접근이 필요하다.

      1
      2
      3
      4
      5
      6
      7
      8
      
        Person.prototype.sayHello = function () {
          console.log(`Hey! My name is ${this.name}`);
        };
        me.sayHello(); // Hey! My name is Lee
              
        // 프로토타입 메서드 삭제
        delete Person.prototype.sayHello;
        me.sayHello(); // TypeError: me.sayHello is not a function
      

19.9 프로토타입의 교체

  • 프로토타입은 생성자 함수 또는 인스턴스에 의해 임의의 다른 객체로 변경 가능
  • 프로토타입 교체를 통해 객체 간 상속 관계를 동적 변경 가능
    • 하지만 번거롭기 때문에 직접 상속이나 클래스를 이용하는 게 더 편리하다.

19.9.1 생성자 함수에 의한 프로토타입의 교체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Person = (function () {
  function Person(name) {
    this.name = name;
  }

  // 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
  Person.prototype = {
    sayHello() {
      console.log(`Hi! My name is ${this.name}`);
    }
  };

  return Person;
}());

// me 객체의 생성자 함수는 Person이 아닌 Object가 된다.
const me = new Person('Lee');

// 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴된다.
console.log(me.constructor === Person); // false
// 프로토타입 체인을 따라 Object.prototype의 constructor 프로퍼티가 검색된다.
console.log(me.constructor === Object); // true

me

  • 생성자 함수가 미래에 생성할 객체의 프로토타입객체 리터럴로 교체할 수 있다.
  • 객체 리터럴에는 constructor 프로퍼티가 없다.
    • constructor 프로퍼티는 자바스크립트 엔진이 프로토타입 생성 시 암묵적으로 추가하는 프로퍼티다.
  • 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결 파괴
    • 연결을 되살리기 위해 constructor 프로퍼티를 추가할 수 있다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      
        const Person = (function () {
          function Person(name) {
            this.name = name;
          }
              
          // 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
          Person.prototype = {
            // constructor 프로퍼티와 생성자 함수 간의 연결을 설정
            constructor: Person,
            sayHello() {
              console.log(`Hi! My name is ${this.name}`);
            }
          };
              
          return Person;
        }());
              
        const me = new Person('Lee');
              
        // constructor 프로퍼티가 생성자 함수를 가리킨다.
        console.log(me.constructor === Person); // true
        console.log(me.constructor === Object); // false
      

19.9.2 인스턴스에 의한 프로토타입의 교체

  • 인스턴스의 __proto__ 접근자 프로퍼티 또는 Object.setPrototypeOf 메서드를 이용해 프로토타입 교체 가능
    • 즉 이미 생성된 객체의 프로토타입을 변경할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Person(name) {
  this.name = name;
}

const me = new Person('Lee');

// 프로토타입으로 교체할 객체
const parent = {
  sayHello() {
    console.log(`Hi! My name is ${this.name}`);
  }
};

// me 객체의 프로토타입을 parent 객체로 교체한다.
Object.setPrototypeOf(me, parent);
// 위 코드는 아래의 코드와 동일하게 동작한다.
// me.__proto__ = parent;

me.sayHello(); // Hi! My name is Lee

// 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴된다.
console.log(me.constructor === Person); // false
// 프로토타입 체인을 따라 Object.prototype의 constructor 프로퍼티가 검색된다.
console.log(me.constructor === Object); // true
  • 생성자 함수에 의해 프로토타입을 교체할 때는 생성자 함수의 prototype 프로퍼티가 여전히 교체된 프로퍼티를 가리키지만, 인스턴스에 의해 프로토타입을 교체할 때는 생성자 함수의 prototype 프로퍼티가 교체된 프로퍼티를 가리키지 않는다.
  • 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결 파괴
    • 연결을 되살리기 위해 constructor 프로퍼티를 추가할 수 있다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      
        function Person(name) {
          this.name = name;
        }
              
        const me = new Person('Lee');
              
        // 프로토타입으로 교체할 객체
        const parent = {
          // constructor 프로퍼티와 생성자 함수 간의 연결을 설정
          constructor: Person,
          sayHello() {
            console.log(`Hi! My name is ${this.name}`);
          }
        };
              
        // 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결을 설정
        Person.prototype = parent;
              
        // me 객체의 프로토타입을 parent 객체로 교체한다.
        Object.setPrototypeOf(me, parent);
        // 위 코드는 아래의 코드와 동일하게 동작한다.
        // me.__proto__ = parent;
              
        me.sayHello(); // Hi! My name is Lee
              
        // constructor 프로퍼티가 생성자 함수를 가리킨다.
        console.log(me.constructor === Person); // true
        console.log(me.constructor === Object); // false
              
        // 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리킨다.
        console.log(Person.prototype === Object.getPrototypeOf(me)); // true
      

19.10 instanceof 연산자

1
2
3
4
5
6
7
8
9
10
11
12
// 생성자 함수
function Person(name) {
  this.name = name;
}

const me = new Person('Lee');

// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Person); // true

// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Object); // true
  • instanceof 연산자
    • 좌변: 객체를 가리키는 식별자 우변: 생성자 함수를 가리키는 식별자
    • 만약 우변의 피연산자가 함수가 아니면 TypeError 발생
    • 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변 객체의 프로토타입 체인에 존재하면 true로 평가
  • 프로토타입이 교체되는 경우

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
      // 생성자 함수
      function Person(name) {
        this.name = name;
      }
        
      const me = new Person('Lee');
        
      // 프로토타입으로 교체할 객체
      const parent = {};
        
      // 프로토타입의 교체
      Object.setPrototypeOf(me, parent);
        
      // Person 생성자 함수와 parent 객체는 연결되어 있지 않다.
      console.log(Person.prototype === parent); // false
      console.log(parent.constructor === Person); // false
        
      // Person.prototype이 me 객체의 프로토타입 체인 상에 존재하지 않아 false로 평가
      console.log(me instanceof Person); // false
        
      // Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가
      console.log(me instanceof Object); // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
      // 생성자 함수
      function Person(name) {
        this.name = name;
      }
        
      const me = new Person('Lee');
        
      // 프로토타입으로 교체할 객체
      const parent = {};
        
      // 프로토타입의 교체
      Object.setPrototypeOf(me, parent);
        
      // Person 생성자 함수와 parent 객체는 연결되어 있지 않다.
      console.log(Person.prototype === parent); // false
      console.log(parent.constructor === Person); // false
        
      // parent 객체를 Person 생성자 함수의 prototype 프로퍼티에 바인딩한다.
      Person.prototype = parent;
        
      // Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
      console.log(me instanceof Person); // true
        
      // Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
      console.log(me instanceof Object); // true
    
  • 프로토타입이 교체되어 constructor 프로퍼티와 생성자 함수 간 연결이 파괴돼도, 생성자 함수의 prototype 프로퍼티와 프로토타입 간 연결은 파괴되지 않기 때문에 instanceof는 영향받지 않는다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
      const Person = (function () {
        function Person(name) {
          this.name = name;
        }
        
        // 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
        Person.prototype = {
          sayHello() {
            console.log(`Hi! My name is ${this.name}`);
          }
        };
        
        return Person;
      }());
        
      const me = new Person('Lee');
        
      // constructor 프로퍼티와 생성자 함수 간의 연결은 파괴되어도 
      // instanceof는 아무런 영향을 받지 않는다.
      console.log(me.constructor === Person); // false
        
      // Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
      console.log(me instanceof Person); // true
      // Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
      console.log(me instanceof Object); // true
    

19.11 직접 상속

19.11.1 Object.create에 의한 직접 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 프로토타입이 null인 객체를 생성, 생성된 객체는 프로토타입 체인의 종점에 위치
// obj → null
let obj = Object.create(null);
console.log(Object.getPrototypeOf(obj) === null); // true
// Object.prototype을 상속받지 못한다.
console.log(obj.toString()); // TypeError: obj.toString is not a function

// obj → Object.prototype → null
// obj = {};와 동일하다.
obj = Object.create(Object.prototype);
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

// obj → Object.prototype → null
// obj = { x: 1 };와 동일하다.
obj = Object.create(Object.prototype, {
  x: { value: 1, writable: true, enumerable: true, configurable: true }
});
// 위 코드는 다음과 동일하다.
// obj = Object.create(Object.prototype);
// obj.x = 1;
console.log(obj.x); // 1
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

const myProto = { x: 10 };
// 임의의 객체를 직접 상속받는다.
// obj → myProto → Object.prototype → null
obj = Object.create(myProto);
console.log(obj.x); // 10
console.log(Object.getPrototypeOf(obj) === myProto); // true

// 생성자 함수
function Person(name) {
  this.name = name;
}

// obj → Person.prototype → Object.prototype → null
// obj = new Person('Lee')와 동일하다.
obj = Object.create(Person.prototype);
obj.name = 'Lee';
console.log(obj.name); // Lee
console.log(Object.getPrototypeOf(obj) === Person.prototype); // true
  • Object.create 메서드
    • 명시적으로 프로토타입을 지정해 새로운 객체 생성
    • 추상 연산 OrdinaryObejctCreate 호출
    • 첫 번째 매개변수: 생성할 객체의 프로토타입으로 지정할 객체 두 번째 매개변수(옵션): 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이루어진 객체
  • Object.create 메서드의 장점
    • new 연산자 없이 객체 생성
    • 프로토타입을 지정하면서 객체 생성 가능
    • 객체 리터럴이 생성한 객체도 상속받을 수 있음
  • Object.prototype의 빌트인 메서드
    • Object.create 메서드를 통해 프로토타입 체인 종점에 위치한 객체를 생성할 수 있고, 이 객체는 Object.prototype의 빌트인 메서드 사용 불가

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
        // 프로토타입이 null인, 즉 프로토타입 체인의 종점에 위치하는 객체를 생성
        const obj = Object.create(null);
        obj.a = 1;
              
        console.log(Object.getPrototypeOf(obj) === null); // true
              
        // obj는 Object.prototype의 빌트인 메서드를 사용할 수 없다.
        console.log(obj.hasOwnProperty('a')); 
        // TypeError: obj.hasOwnProperty is not a function
      
    • 따라서 Object.prototype의 빌트인 메서드는 모든 객체가 상속받아 호출할 수 있지만, 간접적으로 호출하는 걸 권장

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
        // 프로토타입이 null인 객체를 생성한다.
        const obj = Object.create(null);
        obj.a = 1;
              
        console.log(obj.hasOwnProperty('a')); 
        // TypeError: obj.hasOwnProperty is not a function
              
        // Object.prototype의 빌트인 메서드는 객체로 직접 호출하지 않는다.
        console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); // true
      

19.11.2 객체 리터럴 내부에서 __proto__에 의한 직접 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const myProto = { x: 10 };

// 객체 리터럴에 의해 객체를 생성하면서 프로토타입을 지정하여 직접 상속받을 수 있다.
const obj = {
  y: 20,
  // 객체를 직접 상속받는다.
  // obj → myProto → Object.prototype → null
  __proto__: myProto
};
/* 위 코드는 아래와 동일하다.
const obj = Object.create(myProto, {
  y: { value: 20, writable: true, enumerable: true, configurable: true }
});
*/

console.log(obj.x, obj.y); // 10 20
console.log(Object.getPrototypeOf(obj) === myProto); // true
  • ES6부터 가능
  • Object.create 메서드의 두 번째 인자로 프로퍼티를 정의하는 것은 번거롭다. 객체 리터럴 내부에서 __proto__를 이용해 깔끔하게 직접 상속을 구현할 수 있다.

19.12 정적 프로퍼티/메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 생성자 함수
function Person(name) {
  this.name = name;
}

// 프로토타입 메서드
Person.prototype.sayHello = function () {
  console.log(`Hi! My name is ${this.name}`);
};

// 정적 프로퍼티
Person.staticProp = 'static prop';

// 정적 메서드
Person.staticMethod = function () {
  console.log('staticMethod');
};

const me = new Person('Lee');

// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출한다.
Person.staticMethod(); // staticMethod

// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없다.
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 한다.
me.staticMethod(); // TypeError: me.staticMethod is not a function
  • 정적 프로퍼티/메서드
    • 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드
    • 생성자 함수 객체가 소유한 프로퍼티/메서드
    • 생성자 함수가 생성한 인스턴스로 참조/호출 불가
      • 정적 프로퍼티/메서드는 인스턴스의 프로토타입 체인에 속하지 않기 때문
    1
    2
    3
    4
    5
    
      // Object.create는 정적 메서드다.
      const obj = Object.create({ name: 'Lee' });
        
      // Object.prototype.hasOwnProperty는 프로토타입 메서드다.
      obj.hasOwnProperty('name'); // -> false
    
  • 인스턴스/프로토타입 메서드 내에서 this를 사용하지 않는 경우 해당 메서드는 정적 메서드로 변경 가능

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
      function Foo() {}
        
      // 프로토타입 메서드
      Foo.prototype.x = function () {
        console.log('x');
      };
        
      const foo = new Foo();
      // 프로토타입 메서드를 호출하려면 인스턴스를 생성해야 한다.
      foo.x(); // x
        
      // 정적 메서드
      Foo.x = function () {
        console.log('x');
      };
        
      // 정적 메서드는 인스턴스를 생성하지 않아도 호출할 수 있다.
      Foo.x(); // x
    
    • 인스턴스가 호출한 인스턴스/프로토타입 메서드의 this는 인스턴스를 가리키므로 인스턴스를 참조할 필요가 없다면 정적 메서드로 동작 가능

19.13 프로퍼티 존재 확인

19.13.1 in 연산자

  • 프로퍼티 키를 나타내는 문자열 in 객체로 평가되는 표현식
1
2
3
4
5
6
7
8
9
10
const person = {
  name: 'Lee',
  address: 'Seoul'
};

console.log('name' in person);    // true
console.log('address' in person); // true
console.log('age' in person);     // false

console.log('toString' in person); // true
  • 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인
    • 상속받는 모든 프로토타입의 프로퍼티를 확인
  • ES6에서 도입된 Reflect.has 메서드도 같은 동작을 한다.

    1
    2
    3
    4
    
      const person = { name: 'Lee' };
        
      console.log(Reflect.has(person, 'name'));     // true
      console.log(Reflect.has(person, 'toString')); // true
    

19.13.2 Object.prototype.hasOwnProperty 메서드

1
2
3
4
5
6
const person = { name: 'Lee' };

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age'));  // false

console.log(person.hasOwnProperty('toString')); // false
  • 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인
    • 객체 고유의 프로퍼티만 확인하고 상속받은 프로퍼티라면 false 반환

19.14 프로퍼티 열거

19.14.1 for...in

  • for (변수선언문 in 객체) {...}
1
2
3
4
5
6
7
8
9
10
11
const person = {
  name: 'Lee',
  address: 'Seoul'
};

// for...in 문의 변수 prop에 person 객체의 프로퍼티 키가 할당된다.
for (const key in person) {
  console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul
  • 객체의 프로토타입 체인 상 존재하는 모든 프로토타입의 프로퍼티 중, 프로퍼티 어트리뷰트 [[Enumerable]] 값이 true인 프로퍼티를 순회하며 열거(enumeration)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      const person = {
        name: 'Lee',
        address: 'Seoul',
        __proto__: { age: 20 }
      };
        
      for (const key in person) {
        console.log(key + ': ' + person[key]);
      }
      // name: Lee
      // address: Seoul
      // age: 20
    
  • 객체의 프로퍼티 수만큼 순회
  • 변수선언문에서 선언한 변수에 프로퍼티 키를 할당
  • 프로퍼티 키가 심벌인 경우 열거하지 않는다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      const sym = Symbol();
      const obj = {
        a: 1,
        [sym]: 10
      };
        
      for (const key in obj) {
        console.log(key + ': ' + obj[key]);
      }
      // a: 1
    
  • 상속받은 프로퍼티를 열거하고 싶지 않은 경우 Object.prototype.hasOwnProperty 메서드를 사용할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      const person = {
        name: 'Lee',
        address: 'Seoul',
        __proto__: { age: 20 }
      };
        
      for (const key in person) {
        // 객체 자신의 프로퍼티인지 확인한다.
        if (!person.hasOwnProperty(key)) continue;
        console.log(key + ': ' + person[key]);
      }
      // name: Lee
      // address: Seoul
    
  • for...in문은 순서를 보장하지 않는다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      const obj = {
        2: 2,
        3: 3,
        1: 1,
        b: 'b',
        a: 'a'
      };
        
      for (const key in obj) {
        if (!obj.hasOwnProperty(key)) continue;
        console.log(key + ': ' + obj[key]);
      }
        
      /*
      1: 1
      2: 2
      3: 3
      b: b
      a: a
      */
    
    • 하지만 대부분의 모던 브라우저는 순서를 보장하고, 프로퍼티 키가 숫자(사실은 문자열)이면 정렬도 한다.
  • 배열은 객체이므로 프로퍼티를 가질 수 있다.
    • 따라서 배열은 for...in문 사용을 권장하지 않는다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      const arr = [1, 2, 3];
      arr.x = 10; // 배열도 객체이므로 프로퍼티를 가질 수 있다.
        
      for (const i in arr) {
        // 프로퍼티 x도 출력된다.
        console.log(arr[i]); // 1 2 3 10
      };
        
      // arr.length는 3이다.
      for (let i = 0; i < arr.length; i++) {
        console.log(arr[i]); // 1 2 3
      }
        
      // forEach 메서드는 요소가 아닌 프로퍼티는 제외한다.
      arr.forEach(v => console.log(v)); // 1 2 3
        
      // for...of는 변수 선언문에서 선언한 변수에 키가 아닌 값을 할당한다.
      for (const value of arr) {
        console.log(value); // 1 2 3
      };
    

19.14.2 Object.keys/values/entries 메서드

  • 객체의 고유 프로퍼티만 열거할 때 유용하다.
  • Object.keys 메서드
    • 객체 자신의 열거 가능한 프로퍼티 를 배열로 반환
    1
    2
    3
    4
    5
    6
    7
    
      const person = {
        name: 'Lee',
        address: 'Seoul',
        __proto__: { age: 20 }
      };
        
      console.log(Object.keys(person)); // ["name", "address"]
    
  • Object.values 메서드
    • ES8에서 도입
    • 객체 자신의 열거 가능한 프로퍼티 을 배열로 반환
    1
    2
    3
    4
    5
    6
    7
    
      const person = {
        name: 'Lee',
        address: 'Seoul',
        __proto__: { age: 20 }
      };
        
      console.log(Object.values(person)); // ["Lee", "Seoul"]
    
  • Object.entries메서드
    • ES8에서 도입
    • 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 넣어 반환
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      const person = {
        name: 'Lee',
        address: 'Seoul',
        __proto__: { age: 20 }
      };
        
      console.log(Object.entries(person)); // [["name", "Lee"], ["address", "Seoul"]]
        
      Object.entries(person).forEach(([key, value]) => console.log(key, value));
      /*
      name Lee
      address Seoul
      */
    
This post is licensed under CC BY 4.0 by the author.