Javascript prototype inheritance
최근 Javascript 는 그야말로 어디에나 쓰인다고 해도 과언이 아닌데, 그 범용성 만큼이나 문법이 변태같다 특이하다. 이는 functional
한 언어인 동시에 object oriented
코딩을 지원하기 때문에 더더욱 그렇다. 게다가 class
도 아니고 prototype
방식으로 OOP 를 구현하여 더더욱 알수 없는 물건이 되었다.
Javascript 의 상속을 이해하기 위해서는 우선 new
키워드를 알아야 할 필요가 있다.
What's new
?
new Person()
이 코드는 어떻게 동작할까? new
키워드는 우선 새로운 object 를 생성한다. 그리고 뒤에 오는 Person 함수의 prototype 을 해당 object 의 prototype 으로 설정한 뒤, apply 를 호출하여 object 를 함수의 this 로 바인딩하며 실행해준다. 말이 길다 코드를 보자 그래서 new 키워드는 다음과 같이 다시 쓸 수 있다.
function Person(saying) {
this.saying = saying
}
Person.prototype.talk = function() {
console.log('I say:', this.saying)
}
function spawn(constructor) {
var obj = {} // 우선 object 를 생성하고
Object.setPrototypeOf(obj, constructor.prototype) // 프로토타입을 설정한 후
var argsArray = Array.prototype.slice.apply(arguments)
return constructor.apply(obj, argsArray.slice(1)) || obj // apply 로 this 에 obj 를 바인딩
}
var crockford = spawn(Person, 'SEMICOLANS!!!1one1')
crockford.talk()
제대로 상속 되었는지 확인해보면, 아래처럼 true
가 출력된다.
console.log(crockford instanceof Person)
> true
new
키워드 없이 프로토타입 함수를 사용한다면 어떻게 될까?
var eminem = Person('Slim Shady!')
eminem.talk()
> TypeError: undefined is not an object (evaluating 'eminem.talk')
eminem 에는 talk
라는 메소드가 없다는 에러 메시지가 나온다. 일단 talk 는 그렇다 치고, 코드 상의 this.saying = saying
이 어딘가 사라진 것은 아닐텐데, 어떻게 된걸까?
console.log(window.saying)
> "Slim Shady!"
function 을 호출할때 객체를 바인딩 해주지 않았기 때문에 this === window
가 되어[1], saying 은 글로벌 객체에 바인딩 되었다. 왠지 그럼 window.talk()
도 될것 같으니 한번 해보자.
window.talk()
> TypeError: window.talk is not a function. (In 'window.talk()', 'window.talk' is undefined)
아니 이건 또 왜 에러가 나오지 하겠지만, 사실 당연한 것이다. Person.prototype 에 talk 메소드를 만들어 주었지만, new 키워드가 없이는 prototype 이 window 객체에 바인딩 되지 않아서 object 검색 패스에서 찾을 수 없게 된다. 그렇다면 window.saying 을 함수로 호출하고 싶으면 어떻게 해야 할까?
Person.prototype.talk.call(window) // window 대신 this 를 사용해도 무방
> I say: "Slim Shady!"
혹은
var talk = Person.prototype.talk.bind(window)
talk()
> I say: "Slim Shady!"
도대체 이게 뭔가 싶다.
__proto__
와 prototype
우선 코드부터 보자.
Person.prototype.rap = function () {
console.log('Bling bling pot dolizzle fizzle shiz, ' + this.saying)
}
var nas = new Person('Nas is like')
nas.rap()
> Bling bling pot dolizzle fizzle shiz, Nas is like
지금 이 상태로 쭉 코드를 작성해왔다고 가정했을때, nas
는 분명 rap()
을 할 수 있을 것 같은데, crockford
도 할 수 있을까?
crockford.rap()
> Bling bling pot dolizzle fizzle shiz, SEMICOLANS!!!1one1
정답은 그렇다
이다. 왜냐하면 이는 javascript 의 object property 검색 방식과 prototype 바인딩 구조 때문이다.
var jayZ = new Person('u huh')
변수 jayZ
는 name 이라는 property 를 가지고 있는데, 재밌는 것은 jayZ.prototype 을 로그로 확인해보면 undefined
가 출력된다. prototype
은 또 어디로 증발했을까?
console.log(jayZ.__proto__)
> Person {talk: function, rap: function}
prototype
은 함수에만 존재하고, new
키워드로 객체가 되면 __proto__
로 참조 되도록 구성된다. 즉 prototype 에 등록된 메소드는 jayZ 의 메소드가 되는 것이 아니라, jayZ 의 프로토타입의 원형이 되는 객체인 __proto__
로 연결 된다.
객체의 프로퍼티에 접근하기 위해서는 ECMAscript 의 object property specification 에 따라 jayZ
자신의 객체를 확인 한 후 없으면 __proto__
를 검색하게 되고, 여기에도 없으면 Person 의 prototype 인 Function
의 property 를 찾는 절차로 Look Up 이 이루어진다.[2] 드럽게 복잡하네~ (좀 더 자세한 내용은 MDN 객체 모델의 세부사항 항목을 참고하자.)
Prototype 의 inheritance
자 이제 그럼 상속을 구현해보자. 상속은 어떻게 구현해야 할까? 우선 다시 한번 Person
prototype 을 다음과 같이 작성하자.
function Person () {
this.race = 'human'
}
console.log(Person.prototype)
> Person {}
Person 이라고 하는 함수의 prototype 은 Person 자신 인 것 까지는 알겠는데, 그 뒤를 보면 {}
라고 하는 object literal 이 표시 된다. 즉, 함수의 prototype 은 객체라는 것이다. 때문에 상속을 위해서는 함수의 prototype 은 객체 형태로 대입시켜야 한다. 상위 클래스를 extends
하기 위해서는 다음과 같이 코드를 작성하면된다.
function Person () {
this.race = 'human'
}
function Korean () {
this.lang = 'Korean'
}
Korean.prototype = new Person()
var chulsoo = new Korean()
console.log(chulsoo.lang + ' is ' + chulsoo.race)
> Korean is human
일단 작동은 잘 하는것 같다. 다음 코드를 실행해보자.
console.log(chulsoo instanceof Person, chulsoo instanceof Korean)
> true true
그렇다면 부모 클래스의 생성자를 가져다 쓰는 경우는 어떨까?
function Person (name) {
this.name = name
}
function Korean () {
this.lang = 'Korean'
}
Korean.prototype = new Person()
var chulsoo = new Korean('Chul-Soo')
console.log(chulsoo.name + ' speaks ' + chulsoo.lang)
> undefined speaks Korean
어라? 뭔가 잘못한 듯 잘한듯 모호한 코드가 되었는데 어쨋든 원치 않는 결과물이 나온 것 같다.
Korean prototype 이 Person 의 constructor
[3] 를 __proto__
객체에 가지고 있을테니 이걸 호출하면 될것도 같다. 해보자.
chulsoo.__proto__.constructor('Chul-Soo')
console.log(chulsoo.name + ' speaks ' + chulsoo.lang)
> Chul-Soo speaks Korean
되긴 되는데 뭔가 찝찝한 마음을 지울수가 없다. Korean 함수가 만들어질때 이 Person 의 constructor 를 호출 하게끔 해줄 순 없을까?
다음과 같이 코드를 써보자. 한~~~참 위로 올라가면 function 을 단순하게 호출하면 this 가 바인드 되어 있지 않기 때문에 global (window) 객체를 참조하게 되어있다고 언급하였다. Korean 함수를 new 로 호출하면 객체를 바인딩 해주는 것이니 이 this 를 Person 함수에 바인딩 해주면 어떨까? 코드는 다음과 같다.
function Korean(name) {
Person.call(this, name)
this.lang = 'Korean'
}
Korean.prototype = new Person()
var chulsoo = new Korean('Chul-Soo')
console.log(chulsoo.name + ' speaks ' + chulsoo.lang)
> Chul-Soo speaks Korean
이제야 비로소 제대로 작동한다. 물론 Korean 함수는 apply
와 arguments
를 사용해서 다음과 같이 쓸 수도 있다.
function Korean() {
Person.apply(this, arguments)
this.lang = 'Korean'
}
재밌는 것은 경우에 따라 다를 수 있겠지만 Korean.prototype = new Person()
을 제거한 다음 코드도 잘 작동한다.[4]
function Korean(name) {
Person.apply(this, arguments)
this.lang = 'Korean'
}
var chulsoo = new Korean('Chul-Soo')
console.log(chulsoo.name + ' speaks ' + chulsoo.lang)
> Chul-Soo speaks Korean
다만, 이 경우에는 instanceof 연산자로 Person 을 확인하면 false 를 리턴하게 된다.
console.log(chulsoo instanceof Person, chulsoo instanceof Korean)
> false true
ES6 의 class
이처럼 prototype 방식의 상속이 복잡할 뿐만 아니라 방법 또한 다양하다. 따라서 ES6 표준에서는 syntactic sugar
인 class
를 소개하고 있는데, 사용방식이 훨씬 더 직관적이다.
class Person {
constructor(name) {
this.name = name
}
}
class Korean extends Person {
constructor(name) {
super(name)
this.lang = 'Korean'
}
}
var chulsoo = new Korean('Chul-Soo')
console.log(chulsoo.name + ' speaks ' + chulsoo.lang)
> Chul-Soo speaks Korean
아니면 위와 유사하게 es6 의 spread operator 를 사용하여 다음과 같이 작성할 수도 있다.
class Korean extends Person {
constructor() {
super(...arguments)
this.lang = 'Korean'
}
}
Babel이 보편적으로 사용되고 있는 지금 굳이 javascript 에서 OOP 를 구현해야 한다면 class 를 쓰는 것이 당연하겠지만, prototype 의 특성을 이해하지 못하고서는 실제 어떤식으로 작동되는지 단편적인 사실 밖에는 알 수 없다. 솔직히 굳이 알아야 하나 싶긴 하지만...
Object.create
마지막으로 Object.create 로 객체를 생성하는 방법에 대해서 알아보자. Object.create 는 EC5 에서 도입된 방식으로 프로로타입 객체를 첫번째 인수로 넘겨주고, 그 뒤에는 object 형태로 값을 대입해주면 된다. 말보단 코드
var person = {
name: 'Chul-Soo'
}
var chulsoo = Object.create(person, {
lang: {
value: 'Korean'
}
})
console.log(chulsoo.name + ' speaks ' + chulsoo.lang)
> Chul-Soo speaks Korean
Object.create
를 사용하기 위해 구현 방식을 일부 수정해보았다. 이러한 방식으로 객체를 생성하면 instanceof
사용 방식이 달라진다.
console.log(chulsoo instanceof person.constructor)
> true
person 은 객체 이므로 prototype
이 없는데다가 __proto__
는 객체이기때문에, constructor
를 비교해야 제대로 어디서 상속받은 객체인지 제대로 확인할 수 있다. 다른 방법으로는 isPrototypeOf 메소드를 사용해도 된다.
console.log(person.isPrototypeOf(chulsoo))
> true
대부분의 개발자들이 Javascript 에서 상속 방식으로 Object.create
를 추천하는데, 이는 가독성도 좋고, 사용하기 편리하기 때문이다. prototype inheritance 는 언급한 것 이외에도 Object.setPrototypeOf
등 다양한 방식으로 구현할 수 있고, 이에 따라 특성도 조금씩 달라지기 때문에, 가급적이면 Object.create
를 추천한다.
... 지만
그냥 함수형으로 composition 을 사용하는 편이 더 낫다. 다음번에는 그걸 사용해보자.
참고자료
- FunFunFunction - The 'new' keyword
- FunFunFunction - Object.create
- MDN - 상속과 프로토타입
- Basic Inheritance with Object.create
- Javascript 기초 - Object prototype 이해하기
Javascript 에서는 closure 로 감싸지 않은 경우 var, function 키워드 등으로 선언한 변수(혹은 함수)는 window (global) 객체에 property 로 할당된다. 때문에 this 를 호출하면 window 객체가 불려지는 것이다. ↩︎
표준 문서를 찾지 못해서 정확하진 않으나 대체로 유사한 방식으로 동작한다고 보면 된다. ↩︎
__proto__ 의 prototype function 이 바로 이 constructor 다. ↩︎
prototype 으로 주입해준 메소드나 프로퍼티가 있다면 물론 액세스가 불가능하게 될 것이다. ↩︎