Post

Deep Dive 19. 프로토타입

상속과 프로토타입

1
2
3
4
5
6
7
8
9
10
// 생성자 함수
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 생성자 함수가 생성하는 모든 객체(인스턴스)는 radius프로퍼티와 getArea 메서드를 갖는다. getArea 메서드는 모든 인스턴스가 동일한 내용의 메서드를 사용하기 때문에 단 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직하다. 위 코드는 Circle 생성자 함수가 인스턴스를 생성할 때마다 getArea메서드를 중복 생성하고 모든 인스턴스가 중복 소유한다.

Desktop

이처럼 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 메서드를 중복 소유하는 것은 메모리를 불필요하게 낭비한다.

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

Circle.prototype.getArea = function(){
  return Math.PI * this.radius ** 2;
}

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

프로토타입을 사용하여 상속을 구현하면 불필요한 중복을 제거할 수 있다.

Desktop

prototype 프로퍼티는 생성자 함수가 생성할 객체(인스턴스)의 프로토타입을 가리킨다. 따라서 생성자 함수로서 호출할 수 없는 non-contructor인 화살표 함수와 ES6 메서드 축양 표현으로 정의한 메서드는 prototype 프로퍼티를 소유할 수 없다.

프로토타입 객체

프로토타입 객체란 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다. 프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티를 제공한다. 프로토타입을 상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용한다.

1
2
3
4
5
function Person(name){
  this.name = name;
}

const person = { name : 'Lee'}

모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조다. [[Prototype]]에 저장되는 프로토타입은 객체 생성 방식에 의해 결정된다.

내부 슬롯은 일반적으로 접근할 수 있지만 [[Prototype]]의 경우 __proto__ 접근자 프로퍼티를 이용하여 간접적으로 접근할 수 있다.

Desktop

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

console.log(person.hasOwnProperty('__proto__')); // false
console.log(Object.getOwnPropertyDescriptor(Object.prototype,'__proto__'))
// {get: f, set: f, enumberable: false, configurable : true }

__proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티다. __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유는 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서다.

1
2
3
4
5
const parent = {};
const child = {};

child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value

모든 프로토타입은 consturctor 프로퍼티를 갖는다. 이 constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다.

프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성된다

__proto__를 사용하여 프로토타입 참조를 취득하기 보다는 Object.getPrototypeOf 메서드를 사용하고, 프로토타입을 교체하고 싶은 경우에는 Object.setPrototypeOf를 사용하는 것이 좋다.

프로토타입 체인

1
2
3
4
5
6
7
8
9
10
11
12
13
fuction 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

위 예제를 그림으로 표현하면 다음과 같다.

Desktop

자바스크립트는 객체의 프로퍼티(9메서드 포함)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없드면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인 이라고 부른다.

프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘이다.

1
me.hasOwnProperty('name');

위 예제의 경우, 먼저 스코프 체인에서 me식별자를 검색한다. me식별자는 전역세서 선언되었으므로 전역 스코프에서 검색된다.
me 식별자를 검색한 다음, me 객체의 프로토타입 체인에서 hasOwnProperty 메서드를 검색한다. 이처럼 스코프 페인과 프로토타입 체인은 서로 연관없이 별도로 동작하는 것이 아니라 서로 협력하여 식별자와 프로퍼티를 검색하는 데 사용된다.

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

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

위 예제를 그림으로 표현하면 다음과 같다.

Desktop

프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 프로토타입 체인을 따라 프로토타입 프로퍼티를 검색하여 프로토타입 프로퍼티를 덮어쓰는 것이 아니라 인스턴스 프로퍼티로 추가한다. 이때 인스턴스 메서드 sayHello는 프로포타입 메서드 sayHello를 오버라이딩했고 프로토타입 메서드 sayHello는 가려진다. 이처럼 상속 관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉(property shadowing)이라 한다.

오버라이딩(overriding)이란 상위 클래스가 가지고 있는 메서들르 하위 클래스가 재정의하여 사용하는 방식이다.

1
2
3
4
5
delete me.sayHello;
me.sayHello(); // Hi My name is Lee

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

위 예제는 delete를 두 번 하여 sayHello 메서드를 삭제시키고 있다. 이 경우 인스턴스의 메서드는 삭제되었지만 부모 프로토타입의 메서드는 삭제되지 않았다. 이는 객체를 통해 프로토타입에 get 액세스는 허용되지만 set 액세서는 허용되지 않는다는 것을 알 수 있다.

정적 프로퍼티/메서드

정적(static) 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 말한다.

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.staticProrp = 'static prop';

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

const me = new Person('Lee');

// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출한다.
Person.staticMehod(); // 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

위 코드는 다음과 같이 쓸 수 있다.

1
2
3
4
5
6
7
8
function Person() {}
Person.prototype.name = 'Lee';

const obj = new Person();

console.log(obj.name); // 'Lee'
console.log(obj.hasOwnProperty('name')); // false
console.log('name' in obj); // true
This post is licensed under CC BY 4.0 by the author.