Prototype 기반의 Javascript

Javascript의 Prototype과 OOP

Javascript

지난 포스트인 Javascript와 객체 지향 프로그래밍 - 객체 지향 프로그래밍(OOP)에서는 객체 지향 프로그래밍의 기본적인 개념, 기본 구성요소, OOP의 기법 또는 특성으로 많이 언급되는 추상화, 캔슐화, 은닉화, 상속성, 다형성 그리고 OOP의 장단점에 대해 공부해보면서 객체 지향 프로그래밍에서 중요시하는 것은 무엇이며 프로그래밍 설계 및 구현시 이반되는 장점에 대해 알 수 있었습니다.

이번 포스트에서 다를 주제는 Prototype 기반의 프로그래밍입니다.

Prototype 기반의 프로그래밍?

Prototype 이란 사전적 의미는 원형, 원본 이다. 그럼 Prototype 기반의 프로그래밍은 무엇일까? Javascript에서 어디에 쓰는 개념인고…?
일단 이전 포스트인 Javascript와 객체 지향 프로그래밍 - 객체 지향 프로그래밍(OOP)에서 언급한 Class에 대해 복기해보면 객체 지향 프로그래밍을 구현하기 위해 추상화된 속성과 메서드를 Class 라는 하나의 틀(template)을 정의하고, 클래스에 의해 생성된 새로운 객체(object)를 클래스의 인스턴스라고 부르며 클래스의 속성과 메서드를 그대로 상속받아 OOP의 기법인 캡슐화와 은닉화, 추상화, 상속성과 다형성의 개념을 구현할 수 있다.

위와 같은 Class의 개념이 Javascript에서는 Prototype 이다. 즉, 프로토타입 기반 프로그래밍이란 객체의 원형인 프로토타입을 이용해 새로운 객체를 만들어내는 프로그래밍 기법으로, 새롭게 생성된 객체는 자기 자신의 원형(Prototype)을 가지며 원형의 속성과 메서드를 상속받거나 확장할 수 있다.

객체 생성법

Javascript에서 객체를 생성하는 방법은 총 3가지로 객체 리터럴, Object 생성자 함수, 생성자 함수가 있으며 Javascript에서는 new 연산자와 함께 생성자 함수를 사용해 인스턴스 객체를 생성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var obj = {
name : 'BKJang',
job : 'Developer'
}


// Object 생성자 함수
var obj = new Object();
obj.name = 'BKJang';
obj.job = 'Developer';


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

// 인스턴스 생성
var obj = new Person('BKJang', 'Developer');

생성자 함수와 new 연산자

생성자 함수란 객체 인스터스를 생성하는 함수로, 선언된 일반 함수를 new 키워드와 함께 호출 및 실행하는 함수로, Javascript에서는 두 가지 타입의 생성자 함수가 존재합니다.

  • ArrayObject 와 같은 내장 생성자 함수: 런타임 환경의 실행 컨텍스트 환경에서 자동으로 사용 가능
  • 커스텀 생성자 함수: 객체 타입으로 프로퍼티와 메서드 정의

객체 생성을 위해 우리가 사용할 생성자 함수 타입은 두번째로, 위에서 객체 생성 방법 중 굳이 생성자 함수를 사용하는 이유는 동일한 프로퍼티와 메서드를 갖는 복수의 객체를 생성할때 객체 리터럴 방식보다 유용하기 때문이며 객체 인스턴스를 생성할때 생성자 함수의 this 가 반환되면서 인스턴스 각자의 실행 컨텍스트를 갖게 되어 독립적 실행환경을 유지할 수 있기 때문입니다.

new 연산자

Javascript에서 new 연산자는 사용자 정의 객체 타입 또는 내장 객체 타입의 인스턴스를 생성할때 사용되며 아래와 같은 문법을 따릅니다.

1
new constructor[([arguments])]
  • constructor: 객체 인스턴스의 타입을 기술 또는 명세하는 함수
  • arguments: constructor와 함께 호출될 값 목록

생성자 함수와 함께 new 연산자를 사용하면…

생성자 함수와 함께 new 연산자를 사용하면 아래의 단계를 거쳐 객체 인스턴스가 생성됩니다.

  1. 비어있는 객체({})를 만듭니다.
  2. 생성자 함수의 Prototype Object에 연결된 새 객체(__proto__)를 프로퍼티에 추가합니다.
    • 따라서, new 연산자를 사용함으로써 생성자 함수 prototype에 추가된 프로퍼티와 객체는 생성자 함수에 의해 생성된 모든 인스턴스에서 접근가능(accessible)하다.
  3. 새롭게 생성된 객체 인스턴스를 this 컨텍스트로 바인딩한다.
    • i.e.) 생성자 함수에서 this에 대한 모든 참고는 현재 첫번째 단계에서 생성된 객체를 참고한다.
  4. 만약 함수가 객체를 반환하지 않는다면 this를 반환한다.

사용자 정의 객체를 생성하기 위해서는…

사용자 정의 객체를 생성하기 위해서는 총 2개의 과정이 필요합니다.

  1. 이름과 속성을 가진 함수를 작성함으로써 객체 타입을 정의한다.
  2. new 연산자와 함께 객체의 인스턴스를 생성한다.
예제 및 실행문맥 분석 (parsing)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 생성자 함수 정의
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.introduce = function () {
console.log(`This Car's Model is ${this.model} made by ${this.make} in ${this.year}`)
}
}

// 2. 객체 인스턴스 생성
var myCar = new Car('Eagle', 'Talon TSi', 1993);
myCar.make
> "Eagle"
myCar.introduce()
> "This Car's Model is 'Talon TSi' made by 'Eagle in 1993"

new Car(...)를 실행하면

  1. Car.prototype 으로부터 상속된 새로운 객체(인스턴스)가 생성된다.
  2. make, model, year 인자와 함께 생성자 함수 Car가 호출되며 새롭게 생성된 객체에 this가 바인딩 됩니다. new Carnew Car()와 동일하고, 예를 들어 인자가 지정되지 않았다면 인자 없이 Car를 호출합니다.
    3, 생성자 함수에 의해 반환된 객체는 전체 new 표현식의 결과입니다. 만약 생성자 함수가 객체를 반환하지 않는다면, 1 단계에서 생성된 객체가 대신 사용됩니다. (일반적으로 생성자 함수는 값(value)를 반환하지 않으나 만약 객체 생성 과정을 재정의(overide)하려는 경우 그렇게 할 수도 있습니다)

위와 같이 동일한 프로퍼티 또는 멤버와 메서드를 같는 객체를 효율적으로 생성할 수 있는 방법이 생성자 함수이다. 인스턴스가 생성되면 각 인스턴스는 make, model, year라는 프로퍼티와 introduce와 같은 메서드를 동일하게 갖게 된다. 즉, 인스턴스가 생성될 때마다 동일한 프로퍼티와 메서드가 계속 생성되는 것이다. 필요한 만큼. 만약 인스턴스가 매우 많아지거나 각 사이즈가 늘어난다면 메모리를 낭비하게 되는 구조가 된다. 이를 해결하기 위해 개념이 바로 Prototype 기반의 객체지향 프로그래밍 이다.

prototype에 대해 공부해본 후 생성자 함수 Car를 수정하고 확장해보자.

Javascript에서 Prototype이란?

먼저, 프로토타입 기반 프로그래밍에 대해 다시 복기해보면, 아래와 같이 정의했었다.

프로토타입 기반 프로그래밍이란 객체의 원형인 프로토타입 객체를 이용해 새로운 객체를 만들어내는 프로그래밍 기법으로, 새롭게 생성된 객체는 자기 자신의 원형(prototype)을 가지며 원형의 프로퍼티와 메서드를 상속받거나 확장할 수 있다.

그리고 생성자 함수와 new 연산자 섹션의 2번에서 우리는
생성자 함수와 new 연산자를 통해 인스턴스를 생성했을때, 생성자 함수의 Prototype Object에 연결된 새 객체(__proto__)를 프로퍼티에 추가한다는 것을 배웠다. 따라서, 생성자 함수에 추가된 속성과 객체는 생성자 함수에 의해 생성된 모든 인스턴스에서 접근 가능(accessible)하게 해주며 Javascript에서 OOP의 개념이 가능하게 해준다.

어떻게 이게 가능할까?

Javascript에는 아래와 같이 크게 2가지 개념의 protoype 이 존재하며 이는 Javascript의 함수와 객체에 대한 내부 구조를 더 살펴봐야 한다.

  • 함수의 prototype 프로퍼티가 가리키고 있는 Prototype Object
  • 자기 자신을 만들어낸 인스턴스 객체의 원형을 의미하는 Prototype Link

함수와 객체의 구조

Javascript의 모든 객체는 생성과 동시에 정의된 프로퍼티와 메서드를 가진 프로토타입 객체(Prototype Object) 라는 새로운 객체를 복제(Cloning)하여 만드는데 함수의 경우에도 객체 타입으로써 정의 및 분석(parsing) 단계에서 함수 내부에 prototype 프로퍼티를 추가한 후 복제된 프로토타입 객체(Prototype Object) 를 참조하도록 한다. 또한, 프로토타입 객체(Prototype Object)constructor 프로퍼티를 갖는 구조로써, 이는 함수를 참조하는 구조를 갖는다.

단계를 나열하면 아래와 같다.

  1. 생성자 함수 function Car (make, model, year) 정의 및 prototype 프로퍼티 추가
  2. 생성자 함수 Car의 원형인 프로토타입 객체(Prototype Object) - Car Prototype Object 생성 및 constructor 프로퍼티 추가
  3. 생성자 함수 Carprototype 프로퍼티는 Car Prototype Object 참조
  4. Car Prototype Objectconstructor는 생성자 함수 Car 참조

생성자 함수와 인스턴스 그리고 Prototype Object의 관계도

즉, Car Prototype Objectnew 연산자와 생성자 함수에 의해 생성될 새로운 인스턴스가 참조할 원형 객체(Prototype Object) 이다. 또한 생성된 인스턴스는 아래와 같은 구조를 갖는데 예를 들어, myCar 인스턴스는 생성자 함수를 참조한 프로퍼티 이외에 __proto__ 프로퍼티를 가지고 있는데 바로 이 프로퍼티가 myCar 라는 객체를 만들어내기 위해 사용된 프로토타입 객체 (Car protototype object)에 대한 숨겨진 연결 이며 이를 Prototype Link라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 인스턴스 `myCar`
myCar {
introduce: ƒ ()
make: "Eagle"
model: "Talon TSi"
year: 1993
__proto__: {
constructor: ƒ Car(make, model, year)
__proto__: Object
}
}

// 인스턴스의 __proto__의 생성자와 생성자 함수의 prototype 프로퍼티의 생성자 비교
myCar.__proto__.constructor === Car.prototype.constructor
> true

예시를 기반으로 생성자 함수, 인스턴스 그리고 프로토타입 객체(Prototype Object) 에 대해 정리해보면,

  • constructor는 생성자 함수 본인이고,
  • prototype은 생성자 함수에 정의한 모든 객체가 공유할 원형으로 하위로 물려줄 연결에 대한 속성
  • __proto__는 생성자 함수를 new로 호출할 때, 정의해두었던 prototype을 참조한 객체로서 상위에서 물려받은 객체의 프로토타입에 대한 정보
  • prototype은 생성자 함수에 사용자가 직접 넣는 거고, __proto__는 new를 호출할 때 prototype을 참조하여 자동으로 만들어짐
  • 생성자에는 prototype, 생성자로부터 만들어진 객체에는 __proto__

Prototype Chain (프로토타입 체인)

우리는 프로토타입 객체(Prototype Object)프로토타입 링크(Prototype Link) 에 대해 살펴봤습니다. 생성자 함수의 prototype 프로퍼티가 함수의 프로토타입 객체(Prototype Object)를 참고하고 있으며 new 연산자와 생성자 함수에 의해 생성한 인스턴스는 __proto__ 프로퍼티를 통해서 함수 객체의 원형을 참조하고 있음을 알 수 있었다. 따라서 생성된 인스턴스들은 생성자 함수의 프로토타입 객체(Prototype Object) 을 계속 주시하고 있으며 생성자 함수의 prototype 프로퍼티에 프로퍼티 또는 메서드를 추가할 경우 프로토타입 링크(Prototype Link) 의 관계인 인스턴스도 이를 공유받아 추가된 속성들을 활용할 수 있습니다. 이는 그 어떠한 상위 프로토타입 객체도 마찬가지입니다. 이러한 개념이 바로 프로토타입 체인(prototype chain) 이고 다른 객체에 정의된 메소드와 속성을 한 객체에서 사용할 수 있도록 하는 원리입니다.

정확히 말하자면 상속되는 속성과 메소드들은 각 객체가 아니라 객체(인스턴스) 생성자의 prototype 이라는 속성에 정의되어 있습니다.

그리고 위와 같이 객체 인스턴스와 프로토타입 객체 간에 연결을 생성자 함수의 prototype 프로퍼티와 인스턴스 객체의 __proto__ 프로터리를 통해 구성하고 있으며 이 연결을 따라 타고 올라가며 속성과 메소드를 탐색하는 것을 프로토타입 체인(Prototype Chain) 이라고 정리할 수 있습니다.

아래 코드는 프로토타입 체인을 설명하기 위한 예시입니다.

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
// #예제 1.
function exam1 () {
this.x = function () {
console.log('hello');
};
};
exam1.x=function() {
console.log('world');
};
var exam1A = new A();
var exam1B = new A();
exam1A.x();
> hello
exam1B.x();
> hello

// #예제 2.
var exam2 = function () { };
exam2.x=function() {
console.log('hello');
};
exam2.prototype.x = function () {
console.log('world');
};
var exam2A = new exam2();
var exam2B = new exam2();
exam2A.x();
> world
exam2B.x();
> world

프로토타입 객체프로토타입 링크 에 대해 잘 이해했다면, ‘#예제1’에서 메서드 메서드 x의 수정이 즉시 반영되지 않는 이유를 금방 눈치챌 수 있을 것입니다. 힌트는 바로 생성자 함수 객체의 메서드를 어디에서 수정했냐 이다. 생성자 함수와 객체 인스턴스는 프로토타입 객체(Prototype Object) 와 연결되어 있으며 생성자 함수 내 메서드의 추가, 변경, 삭제 등의 내부 속성의 변경상태를 공유하기 위해서는 ‘#예제2’ 와 같이 생성자 함수의 prototype 프로퍼티를 통해 정의 및 수정해야 한다. #예제1exam1.x=function () { ~ } 와 같은 수정은 단지 생성자 함수 객체의 메서드를 변경한 것 뿐이다.

잊고 있던 예제를 개선해보자.

Javascript의 prototype에 대해 공부하면서 잊고 있었던 Car 생성자 함수의 introduce 메서드 할당을 개선해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 생성자 함수 정의
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
// this.introduce = function () {
// console.log(`This Car's Model is ${this.model} made by ${this.make} in ${this.year}`)
// }
}

// 2. `introduce` 메서드를 `Car` 함수의 prototype 속성에 추가
Car.prototype.introduce = function () {
console.log(`This Car's Model is ${this.model} made by ${this.make} in ${this.year}`)
}

// 3. 인스턴스 생성
var myCar = new Car('Eagle', 'Talon TSi', 1993);
myCar.make
> "Eagle"

// 4. 인스턴스에서 `introduce` 메서드 호출
myCar.introduce()
> "This Car's Model is 'Talon TSi' made by 'Eagle in 1993"

위와 같이 생성자 함수의 내부에 메서드를 할당하는 대신 prototype 프로퍼티에 메서드를 할당해줌으로써 Car Prototype Object 또한 참조 받으며 생성된 인스턴스 객체들 또한 Prototype Link 속성으로 추가된 메서드 또는 프로퍼티를 공유받아 생성 이후에 할당된 기능들도 실행시킬 수 있게 된다.

마치며

Prototype에 대해 학습하면서 몇몇 부분에서 제대로 이해되지 않아 디테일한 부분까지 찾아보다보니 많은 블로그를 찾아보았고 많은 시간을 소모했다고 느꼈지만 끝나고나니 이제서야 왜 Javascript에서 Prototype 기반의 프로그래밍이 중요하고 OOP를 구현하기 위한 기반이라고 했는지 이해하게 되었다.

다음 포스트 주제로는 prototype의 상속에 대해 다뤄보겠습니다.


참고