자바 스크립트는 프로토타입 기반 객체 지향 언어라고 한다. 프로토타입?이라는 것을 이용하여 상속을 구현한다고 하는데, (클래스의 개념이 도입된 ES6 전에는 말이다.) 이 프로토 타입이라는 개념이 다른 언어에서는 듣지도 보지도 못한 것이라 이해하는데 꼬박 하루가 걸렸다. 그렇게 하루동안 공부한 내용을 정리한다. 생활코딩과 오승환님 블로그가 많은 도움이 되었다.
자바스크립트에 모든 함수는 생성될 때
prototype
속성이 부여되고, 모든 객체는 생성될 때__proto__
속성이 부여된다. 이 부분은 생소하고 잘 와닿지 않겠지만, 자바스크립트가 이렇게 설계된 것이므로 그냥 받아드리는 수 밖에 없다.
자바스크립트에서는 객체 생성자 함수를 생성할 때, 단순히 함수가 생성되는 게 아니라, 2가지 일이 일어난다.
constructor
속성이 자동으로 부여되고, 이 constructor
속성은 생성된 생성자 함수를 가리킨다.prototype
이라는 속성이 부여되고, 이 속성은 프로토타입 객체를 가리킨다.이미지 출처: 오승환님 블로그
생성된 함수와 프로토타입 객체는 위와 같이 서로를 상호 참조한다.
이러한 프로토타입 객체의 속성 값을 이용하여, 인스턴스에서 공통적으로 사용할 부분들을 정의할 수 있다.
인스턴스들은 __proto__
속성을 통해 생성자 함수의 프로토타입 객체와 연결되는데, 이는 인스턴스들이 모두 부모 생성자의 프로토타입 객체와 연결되어 있기 때문에, 프로토타입 객체에 부여한 속성 값들에 접근할 수 있다는 것을 의미한다. 예를 들어,
function Person(name, first, second, third){
this.name=name;
this.first=first;
this.second=second;
}
// 프로토타입 객체에 sum 메소드를 저장.
Person.prototype.sum = function(){
return 'prototype : '+(this.first+this.second);
}
var lee = new Person('lee', 10, 10);
console.log("lee.sum()", lee.sum()); // 'lee.sum()' 'prototype : 20'
lee
객체에는 sum
메소드가 없었지만, __proto__
속성의 링크를 타고 프로토타입 객체를 참조하여, 그 안에 있는sum
메소드를 실행했다.
var kim = new Person('kim', 10, 20);
kim.sum = function(){
return 'this : '+(this.first+this.second);
}
console.log("kim.sum()", kim.sum()); // 'kim.sum()' 'this : 30'
다만 kim.sum()
이 호출되었을때, kim
객체 안에 sum
메소드가 있다면, 프로토타입 객체까지 갈 필요없이 kim
객체에서 sum
메소드가 실행된다.
__proto__
를 이용한 객체간 상속 구현위에서 __proto__
속성은 각각의 인스턴스를 부모 프로토타입 객체와 연결을 해주어, 부모 프로토타입 객체 속의 속성을 사용할 수 있도록 해주었다. 만약 그러한 연결을 객체끼리 해준다면, 하나의 객체에서 다른 객체의 속성에 자유롭게 접근할 수 있게 될것이다. 마치 객체끼리 서로를 상속하는 듯이 말이다.
__proto__
를 이용한 객체 상속var superObj = {superVal:'super'}
var subObj = {subVal:'sub'}
subObj.__proto__ = superObj;
console.log('subObj.superVal =>', subObj.superVal); // 'subObj.superVal =>' 'super'
위에서 보듯, subObj
는 superVal
가 없지만, 서로 간의 상속을 통해 superObj
의 superVal
을 문제 없이 접근할 수 있다.
혼동하면 안되는 것이,subObj.superVal = 'sub'
을 할 경우, 서로가 상속하고 있다고 해서 superObj
의 superVal
을 수정하는 것이 아니라는 것이다. 이 표현식의 의미는 subObj
에 새로이 superVal
속성을 만들라는 것이다.
// 코드는 위에서 이어진다.
subObj.superVal = 'sub';
console.log('superObj.superVal =>', superObj.superVal); // 'superObj.superVal =>' 'super'
만약 subObj
를 통하여 superObj
를 변경하고 싶다면, subObj.__proto__.superVal = 'sub'
로, __proto__
를 타고 superObj
로 접근하여야 한다.
Object.create()
__proto__
가 자바스크립트에 구현은 되있지만, 이는 비표준으로 취급된다. (비표준으로 취급할꺼면 구현을 왜 해두고 사용하는지는 이해가 안가지만 말이다.) 따라서, 표준으로 취급되는 동등한 코드를 사용하도록 하자. Object.create()
가 그것인데, 괄호 안에 들어가는 객체를 상속함을 의미한다.
var superObj = {superVal:'super'}
var subObj = Object.create(superObj);
subObj.subVal = 'sub';
console.log(subObj); // { subVal: 'sub', __proto__: { superVal: 'super' } }
console.log(subObj.__proto__ === superObj); // true
자바스크립트는에서 함수는 혼자 있으면 개인이고, new가 앞에 있으면 객체를 만드는 신이고, call을 뒤에 붙이면 용병이고, bind를 붙이면 분신술을 부리는 놀라운 존재입니다. 자바스크립트의 함수의 놀라움을 느껴보세요. -생활코딩
call()
함수를 call()
을 통해서 호출하는 것으로 this
의 컨텍스트를 자유자재로 바꿀수 있다.
var kim = {name:'kim', first:10, second:20}
var lee = {name:'lee', first:10, second:10}
function sum(prefix){
return prefix+(this.first+this.second);
}
// sum();
console.log("sum.call(kim)", sum.call(kim, '=> ')); // 'sum.call(kim)' '=> 30'
console.log("lee.call(kim)", sum.call(lee, ': ')); // 'lee.call(kim)' ': 20'
위와 같이, sum.call()
에 kim
을 인자로 넘겼을 시, this
의 컨텍스는 kim
이고, lee
를 넘겼을 시, 컨텍스트는 lee
이다.
첫번째 인자가 this
의 컨텍스트를 결정하고, 이후로 주어지는 인자는 패러미터이다.
bind()
함수를 bind()
를 통해서 호출하면, 주어진 인자로 this
의 컨텍스트가 결정된 새로운 함수를 리턴한다.
var kim = {name:'kim', first:10, second:20}
var lee = {name:'lee', first:10, second:10}
function sum(prefix){
return prefix+(this.first+this.second);
}
// sum();
var kimSum = sum.bind(kim, '-> ');
console.log('kimSum()', kimSum()); // 'kimSum()' '-> 30'
call
과는 달리, 새로운 함수가 생성 되었고, 따라서 kimSum()
으로, 새로운 함수를 호출하였다.
ES6에 도입된 클래스 문법를 이용해서 구현하는 것과 비교하려면 여기를 참조
function Person(name, first, second){
this.name = name;
this.first = first;
this.second = second;
}
Person.prototype.sum = function(){
return this.first + this.second;
}
constructor
부분은 동일하게 this
키워드를 이용하여 구현하고, 클래스 메소드는 파생된 인스턴스에서 접근가능 하도록 프로토타입 객체에 속성을 부여해 구현한다.
function PersonPlus(name, first, second, third){
Person.call(this, name,first,second); // 여기서 this 는 PersonPlus를 의미
this.third = third;
}
PersonPlus.prototype.avg = function(){
return (this.first+this.second+this.third)/3;
}
상속되는 서브 클래스의 생성자에서는, call
키워드를 통해 Person
안 this
의 컨텍스트를 PersonPlus
로 변환한뒤, 나머지 name
, fisrt
, second
인자를 넘겨주는 것으로 슈퍼 클래스의 생성자를 상속 한다.
// 첫번째 방법
PersonPlus.prototype.__proto__ = Person.prototype;
// 두번째 방법
PersonPlus.prototype = Object.create(Person.prototype);
PersonPlus.prototype.constructor = PersonPlus;
PersonPlus.prototype.avg = function(){
return (this.first+this.second+this.third)/3;
}
두가지 접근법이 있다. 첫번째로, PersonPlus
의 프로토타입 객체에 있는 __proto__
속성을 Person
의 프로토타입 객체에 연결시켜, PersonPlus
객체가 Person
객체 속에 있는 sum
을 호출할 수 있도록 한다. 제일 깔끔한 방법이지만, 비표준이라는 단점이 있다. (모든 브라우저가 __proto__
를 지원하지는 않는다.)
두번째로, 좀더 표준 표현인 Object.create()
를 이용하는 것. 이렇게 되면 Person
의 프로토타입 객체를 기반으로 하는 새로운 객체를 만들어 PersonPlus
의 프로토타입 객체를 교체한다. 이 접근법은 2가지 문제점이 일으킨다.
Person
의 프로토타입 객체로 교체가 되어버렸으므로, PersonPlus
의 프로토타입 객체의 constructor
는 Person
을 가리킨다. 따라서, 이를 다시 PersonPlus
로 가리키도록 재설정해주어야 한다.PersonPlus
프로토타입 객체는 아무곳에도 연결되지 못한 채 버려졌기 떄문에, 그 안에 만들어준 avg
메소드를 호출할 수 있는 방법이 없어진다. 따라서 PersonPlus
의 프로토타입 객체에 다시 만들어주어야 한다. function Person(name, first, second){
this.name = name;
this.first = first;
this.second = second;
}
Person.prototype.sum = function(){
return this.first + this.second;
}
function PersonPlus(name, first, second, third){
Person.call(this, name,first,second);
this.third = third;
}
// PersonPlus.prototype.__proto__ = Person.prototype;
PersonPlus.prototype = Object.create(Person.prototype);
PersonPlus.prototype.constructor = PersonPlus;
PersonPlus.prototype.avg = function(){
return (this.first+this.second+this.third)/3;
}
var kim = new PersonPlus('kim', 10, 20, 30);
console.log("kim.sum()", kim.sum()); // 'kim.sum()' 30
console.log("kim.avg()", kim.avg()); // 'kim.avg()' 20
console.log('kim.constructor', kim.constructor); // 'kim.constructor' ƒ PersonPlus()
프로토타입이 무엇인지 그리고 프로토타입을 이용해서 상속을 어떻게 구현하는지에 대해 알아보았다.
PersonPlus.prototype = Object.create(Person.prototype);
PersonPlus.prototype.constructor = PersonPlus;
위와 같이 때려맞추는 식의 코드를 내제하는 것처럼 문제의 요소가 꽤 있다. 프로토타입이 무엇인지 공부하는 목적으로 알고만 있고, 실제 구현은 클래스 문법을 통해서 하도록 하자. ES6를 지원 안하는 브라우저가 있다면 걱정말자. 갓 바벨이 알아서 잘 때려맞춰줄 것이니 👍
js공부하면서 prototype에 대해 항상 궁금했는데 감사합니다 !!