자바스크립트는 객체지향 패러다임 하에 설계되었지만, 그 방식에 있어 객체지향 언어의 클래스(Class)가 아니라 프로토타입(Prototype) 기반으로 설계되었다. 그래서 프로토타입의 방식으로 클래스 언어를 모방하기 위해 여러 가지 인스턴스 생성 패턴들이 시도되었고, 그 결과 ES6에서는 class 문법이 도입되었다. 그러나 엄연히 표현적인 부분만 바뀌었을 뿐 내부적으로는 프로토타입 기반으로 동작한다.
자바스크립트에서 함수를 생성하면, 기본적으로 인스턴스를 생성할 수 있게 생성자(Constructor)의 역할도 부여한다. 그래서 콘솔에 console.dir(<함수명>)
를 해보면 prototype
프로퍼티가 생성되어 있음을 알 수 있다. 여기서 prototype
속성의 값은 인스턴스가 될 원형 객체가 되고, 인스턴스에서는 접근자 속성 __proto__
을 통해 prototype
원형 객체를 참조한다.
이와 같은 구조 속에 OOP의 4가지 컨셉이 모두 녹아있다. (이 부분은 따로 블로깅 하겠다) 다소 복잡하더라도 다음의 기본 유형을 머리에 담고 있으면 이해에 도움이 된다. 여기서 나는 로봇을 예시를 들었다. (붕어빵 예시는 내게 별로 도움이 안됐다)
[Constructor]
생성자 함수
예시) 로봇 조립 공정 (매개변수로 로봇 특징이 들어감)
[Constructor].prototype
인스턴스 객체의 원형 객체
예시) 로봇 설계도(또는 설명서)
[instance]
인스턴스 객체
예시) 로봇
[instance].__proto__
인스턴스 객체가 가지게 되는 접근자(accessor) 속성으로써, 자신을 만들어준 생성자 함수의 프로토타입(원형 객체)을 참조
예시) 로봇이 자신의 설계도(설명서) 참조하기
// 로봇 조립 공정 (매개변수로 로봇 특징이 들어감)
const Robot = function(name, color) {
this.name = name;
this.msg = `I am ${this.name}`;
}
// 로봇 설계도에 말하기 기능 추가
Robot.prototype.say = function() {
console.log(this.msg);
}
// 설계도에 따라 robot 생산
const robot = new Robot("Terminator");
// robot 말하기 실행
robot.say(); // "I am Terminator"
robot instanceof Robot // -> true
robot.__proto__ === Robot.prototype // -> true
위의 코드는 '로봇' 생성자 함수(조립 공정)를 만들어 인스턴스 로봇을 만들어낸다. 그리고 robot.say()
의 결과로 "I am Terminator"이 출력된다. 여기서 인스턴스 robot은 직접적인 say 속성이 없다는 점이 핵심인데, 그래서 robot은 __proto__
를 통해 자신의 설계도(설명서) 참조하여 say 메서드를 참조해온다. 그래서 만들어진 로봇들은 모두 프로토타입 체이닝을 통해 상위의 속성과 메서드들을 기본적으로 갖출 수 있게 된다. 하지만 이 경우, __proto__
는 반드시 생략하고 속성이나 메서드를 참조해야하는데, 이는 __proto__
가 접근자 속성으로서 자바스크립트 엔진에서 생략해서 참조하도록 설계되었기 때문이다.
이제 더 복잡하게 파고들어 보자. 다음 그림을 이해할 수 있다면 성공이다.
요약하면,
정도이다. 사실 그림의 라인을 따라가며 하나하나 하려 했지만, 꽤 복잡하기 때문에 공통 유형으로 묶어 설명 해보겠다.
[Constructor].prototype
이 코드가 참조하는 값의 타입은 객체이고, [Constructor]
가 실행되면 생성되는 인스턴스 객체의 원형 객체이다. 이 원형 객체에 속성 또는 메소드를 정의하여 인스턴스 객체들이 accessor __proto__
를 통해 참조하는 방법으로 상속을 구현한다.
[Constructor].prototype.constructor
이 코드가 참조하는 값은 생성자 함수 [Constructor]
이다. 생성자 함수가 실행되어 인스턴스 객체가 생성되었을 때, 인스턴스가 자신을 생성한 함수 [Constructor]
를 __proto__
를 통해 참조할 수 있도록 원형 객체에 정보로써 넣어주는 것이다.
여기서 주의해야 할 점은, 상위(Super) 생성자를 상속한 하위(Sub) 생성자를 통해 인스턴스를 만들려는 경우이다. 하위 생성자 함수를 만들고 프로토타입 객체에 내장된 constructor 속성에 생성자 자신을 재할당해주지 않는다면, 하위 생성자 자신이 인스턴스를 new
로 생성하더라도 상위 생성자의 인스턴스가 생성되게 된다. 왜냐하면 상위 생성자로부터 프로토타입 객체를 그대로 상속받았기 때문이다. 그래서 하위 생성자 자신의 프로토타입에서 참조되는 constructor의 값을 생성자 자신으로 오버라이드(Override) 해주어야 한다.
[Constructor].prototype.constructor = [Constructor]
단, ES6 Class 문법에서는 이 재할당 과정을 알아서 해주므로 신경쓰지 않아도 된다.
[Constructor].__proto__
__proto__
는 accessor 속성으로써, 자신을 만들어준 생성자 함수의 프로토타입(원형 객체)을 참조한다. 그런데 여기서 "아니, [Constructor]
가 생성자 함수인데 자신을 만든 생성자 함수는 또 뭐야?"라고 생각할 수도 있다. 하지만 잘 생각해보면 생성자 함수도 결국 함수이고, 하나의 함수 인스턴스이다. 그래서 자신을 만들어준 생성자 함수도 존재하는 것이다.
예를 들어 Object
는 객체를 생성하는 생성자 함수이면서, 함수를 생성하는 함수 Function
의 인스턴스이다.
Object.__proto__ === Function.prototype // -> true
[Constructor].prototype.__proto__
[Constructor].prototype
의 값은 객체이고, 동시에 객체의 인스턴스이다. 다음의 .__proto__
는 자신의 생성자 함수의 prototype
을 참조하는 접근자 속성이므로 이 유형은 결국 Object.prototype
을 참조한다.
[instance].__proto__
인스턴스는 __proto__
속성을 통해 자신의 생성자에 있는 prototype을 참조한다. (프로토타입 체이닝)
※ 주의
❌ [instance].__proto__.method
⭕️ [instance].method
__proto__
는 단지 접근자 속성이며, prototype 자체가 아니다. 그러므로 참조 오류가 발생한다.
[instance].prototype
[instance]
는 함수. 즉, Function
의 인스턴스인 경우이다. 그러므로 [Constructor].__proto__
와 동일하다.
예전부터 객체에 console.dir
찍어봤을 때 나타나는 그 미쳐버린(?) 구조를 이해하고 극복해야 할 필요성을 느꼈고, 미루고 미루다 이번에 정리해봤다. 어려운 개념을 쉽게 설명했다는 뿌듯함이 생기지만, 나만 그렇게 생각할지도 모르겠다. 블로깅 후에도 계속 피드백을 받아서 수정하고, 잘못된 부분은 고쳐 나가야겠다.