지난 포스트인 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 | var obj = { |
생성자 함수와 new 연산자
생성자 함수란 객체 인스터스를 생성하는 함수로, 선언된 일반 함수를 new
키워드와 함께 호출 및 실행하는 함수로, Javascript에서는 두 가지 타입의 생성자 함수가 존재합니다.
Array
와Object
와 같은 내장 생성자 함수: 런타임 환경의 실행 컨텍스트 환경에서 자동으로 사용 가능- 커스텀 생성자 함수: 객체 타입으로 프로퍼티와 메서드 정의
객체 생성을 위해 우리가 사용할 생성자 함수 타입은 두번째로, 위에서 객체 생성 방법 중 굳이 생성자 함수를 사용하는 이유는 동일한 프로퍼티와 메서드를 갖는 복수의 객체를 생성할때 객체 리터럴 방식보다 유용하기 때문이며 객체 인스턴스를 생성할때 생성자 함수의 this
가 반환되면서 인스턴스 각자의 실행 컨텍스트를 갖게 되어 독립적 실행환경을 유지할 수 있기 때문입니다.
new 연산자
Javascript에서 new
연산자는 사용자 정의 객체 타입 또는 내장 객체 타입의 인스턴스를 생성할때 사용되며 아래와 같은 문법을 따릅니다.
1 | new constructor[([arguments])] |
- constructor: 객체 인스턴스의 타입을 기술 또는 명세하는 함수
- arguments: constructor와 함께 호출될 값 목록
생성자 함수와 함께 new
연산자를 사용하면…
생성자 함수와 함께 new
연산자를 사용하면 아래의 단계를 거쳐 객체 인스턴스가 생성됩니다.
- 비어있는 객체(
{}
)를 만듭니다. - 생성자 함수의
Prototype Object
에 연결된 새 객체(__proto__
)를 프로퍼티에 추가합니다.- 따라서,
new
연산자를 사용함으로써 생성자 함수 prototype에 추가된 프로퍼티와 객체는 생성자 함수에 의해 생성된 모든 인스턴스에서 접근가능(accessible)하다.
- 따라서,
- 새롭게 생성된 객체 인스턴스를
this
컨텍스트로 바인딩한다.- i.e.) 생성자 함수에서
this
에 대한 모든 참고는 현재 첫번째 단계에서 생성된 객체를 참고한다.
- i.e.) 생성자 함수에서
- 만약 함수가 객체를 반환하지 않는다면
this
를 반환한다.
사용자 정의 객체를 생성하기 위해서는…
사용자 정의 객체를 생성하기 위해서는 총 2개의 과정이 필요합니다.
- 이름과 속성을 가진 함수를 작성함으로써 객체 타입을 정의한다.
new
연산자와 함께 객체의 인스턴스를 생성한다.
예제 및 실행문맥 분석 (parsing)
1 | // 1. 생성자 함수 정의 |
new Car(...)
를 실행하면
Car.prototype
으로부터 상속된 새로운 객체(인스턴스)가 생성된다.make
,model
,year
인자와 함께 생성자 함수Car
가 호출되며 새롭게 생성된 객체에this
가 바인딩 됩니다.new Car
는new 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
프로퍼티를 갖는 구조로써, 이는 함수를 참조하는 구조를 갖는다.
단계를 나열하면 아래와 같다.
- 생성자 함수
function Car (make, model, year)
정의 및prototype
프로퍼티 추가 - 생성자 함수
Car
의 원형인 프로토타입 객체(Prototype Object) - Car Prototype Object 생성 및constructor
프로퍼티 추가 - 생성자 함수
Car
의prototype
프로퍼티는Car Prototype Object
참조 - Car Prototype Object의
constructor
는 생성자 함수Car
참조
즉, Car Prototype Object는 new
연산자와 생성자 함수에 의해 생성될 새로운 인스턴스가 참조할 원형 객체(Prototype Object) 이다. 또한 생성된 인스턴스는 아래와 같은 구조를 갖는데 예를 들어, myCar 인스턴스는 생성자 함수를 참조한 프로퍼티 이외에 __proto__
프로퍼티를 가지고 있는데 바로 이 프로퍼티가 myCar 라는 객체를 만들어내기 위해 사용된 프로토타입 객체 (Car protototype object)에 대한 숨겨진 연결 이며 이를 Prototype Link
라고 한다.
1 | // 인스턴스 `myCar` |
예시를 기반으로 생성자 함수, 인스턴스 그리고 프로토타입 객체(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 | // #예제 1. |
프로토타입 객체와 프로토타입 링크 에 대해 잘 이해했다면, ‘#예제1’에서 메서드 메서드 x
의 수정이 즉시 반영되지 않는 이유를 금방 눈치챌 수 있을 것입니다. 힌트는 바로 생성자 함수 객체의 메서드를 어디에서 수정했냐 이다. 생성자 함수와 객체 인스턴스는 프로토타입 객체(Prototype Object) 와 연결되어 있으며 생성자 함수 내 메서드의 추가, 변경, 삭제 등의 내부 속성의 변경상태를 공유하기 위해서는 ‘#예제2’ 와 같이 생성자 함수의 prototype
프로퍼티를 통해 정의 및 수정해야 한다. #예제1 의 exam1.x=function () { ~ }
와 같은 수정은 단지 생성자 함수 객체의 메서드를 변경한 것 뿐이다.
잊고 있던 예제를 개선해보자.
Javascript의 prototype
에 대해 공부하면서 잊고 있었던 Car
생성자 함수의 introduce 메서드 할당을 개선해보자.
1 | // 1. 생성자 함수 정의 |
위와 같이 생성자 함수의 내부에 메서드를 할당하는 대신 prototype
프로퍼티에 메서드를 할당해줌으로써 Car Prototype Object
또한 참조 받으며 생성된 인스턴스 객체들 또한 Prototype Link 속성으로 추가된 메서드 또는 프로퍼티를 공유받아 생성 이후에 할당된 기능들도 실행시킬 수 있게 된다.
마치며
Prototype에 대해 학습하면서 몇몇 부분에서 제대로 이해되지 않아 디테일한 부분까지 찾아보다보니 많은 블로그를 찾아보았고 많은 시간을 소모했다고 느꼈지만 끝나고나니 이제서야 왜 Javascript에서 Prototype 기반의 프로그래밍이 중요하고 OOP를 구현하기 위한 기반이라고 했는지 이해하게 되었다.
다음 포스트 주제로는 prototype의 상속에 대해 다뤄보겠습니다.
참고
- new Operator
- ‘new’ 연산자와 생성자 함수
- [javascript] new 연산자와 생성자 함수
- new operator
- 자바스크립트 객체 생성자 함수란?
- Javascript 기초 - Object prototype 이해하기
- Javascript: 프로토타입 (prototype) 이해
- 객체 지향 프로그래밍(생성자와 프로토타입)
- [Javascript ] 프로토타입 이해하기
- 자바스크립트는 프로토타입 기반의 객체지향 프로그래밍 언어이다.
- 함수의 프로토타입 - prototype
- 자바스크립트 객체지향 프로그래밍
- Object prototypes
- Inheritance in JavaScript
- Common Misconceptions About Inheritance in JavaScript
- [JavaScript] 8-1. 객체지향 프로그래밍(클래스 vs 프로토타입)
- [JavaScript] 8-2. 객체지향 프로그래밍(상속)