본문 바로가기
Javascript

자바스크립트(javascript) 상속

by 램쥐뱅 2016. 12. 13.

1. 함수와 객체 정의


자바스크립트 파싱 단계가 끝나고 나서 생성자를 이용해 객체가 정의되면 단순히 객체만 생성되는 것이 아니라 그 객체와 관련된 상속 관계도 결정된다. 따라서 상속을 이해하려면 함수와 객체의 생성 절차를 살펴보는것이 도움이 된다.


지금부터 정리하는 글에서는 내용이 함수에 대한 이야기인지 객체에 대한 이야기인지를 잘 구분하여야 한다.




함수 정의 절차



생성자의 prototype 속성은 프로토타입 객체를 가리키고 프로토타입 객체의 constructor 속성은 Person 생성자를 가리킨다.

이제 이런 최종적인 그림이 되기까지의 과정을 밟아본다.




프로토타입 객체 정의


자바스크립트에서 정의하고 있는 프로토타입 객체의 실체는 Object 객체이다.

이는 자바스크립트의 상속 관계에서 최상위 부모가 Object임을 가리킨다.


프로토타입 객체의 실체는 Object 인스턴스이다.


이말은 즉 Person을 정의하는 동안 다음과 같은 코드가 실행되는 것이라 볼수있다.


Person.prototype = new Object(); // Person의 프로토타입 객체는 Object 인스턴스


위의 코드대로라면 다음과 같은 구조가 될 것이다.



그림에서 Person 의 프로토타입 객체가 Object 인스턴스임을 알 수 있다. 그러나 이것으로 Person의 최종적인 그림이 완성되지는 않는다.

생성자를 가리키는 프로토타입 객체의 constructor가 없다.




constructor 속성 변경


프로토타입 객체에 constructor 속성을 추가하려면 다음과 같은 코드가 실행될 것이다.


Person.prototype.constructor = Person;    // 생성된 인스턴스의 생성자는 Person


이제 Person 인스턴스의 constructor를 통해 constructor에 접근하면 Person 프로토타입 객체에서 정의한 Person 생성자에 대한 참조가 반환된다.


모든 함수가 자신의 최종적인 모습이 되려면 프로토타입 객체정의와 constructor 속성 변경 두가지 작업을 거치게 된다.


Person은 Object를 상속받지만, 이후로는 Person을 상속받는 함수를 정의할 것이다. 이처럼 사용자 정의 함수를 다시 상속하는 경우는 앞의 두 절차를 개발자가 직접 구현해주어야 한다.




객체 생성


function Person(name) {

this.name = name;

}


var mySon = new Person("Kim"); // Person 생성자를 이용해 mySon 인스턴스를 생성


자바스크립트에서 인스턴스를 생성하는 절차는 일반 객체지향 언어와는 다르다.

사용자 정의 생성자라 하더라도 제일 먼저 Object 인스턴스를 생성하게 된다. 그런 다음 사용자 정의 생성자에서 정의한 구조로 인스턴스에 멤버를 추가하게 된다.



Person 인스턴스 과정을 나열해 보면.


1. 자바스크립트가 new Person 을 만나면 위 그림과 같이 최초로 Object 인스턴스를 생성한다.

2. Person 생성자에서 this.name = name 처럼 this로 정의한 인스턴스 멤버가 추가된다.

3. this로 정의한 멤버가 인스턴스에 모두 구성되면 생성이 완료된다.


사용자 정의 객체를 생성하더라도 생성되는 최초의 인스턴스는 Object의 구조를 가진다. 그런 다음 생성된 Object 인스턴스의 멤버 구조를 편집해서 최종적으로 인스턴스 구조를 만드는 것이다.


new Person으로 생성된 인스턴스도 결국 Person에서 정의한 인스턴스 멤버가 추가된 Object 인스턴스다.


객체 리터럴을 통해 객체가 메모리에 정의되는 절차도 "new Object"를 이용해 생성되는 절차와 동일하다. 먼저 사용자 정의의 멤버가 없는 Object 객체를 만든 다음 리터럴에서 정의한 멤버가 추가 된다.


var obj = {

age : 7,

increaseAge : function(){ this.age ++; }

};


위 코드가 실행되면 인스턴스 멤버가 없는 Object 인스턴스가 먼저 생성되고 멤버인 age, increaseAge 가 추가되어 인스턴스가 완성된다.




자세한 객체 생성 과정


new Person을 만나면서 부터 과정을 그림으로 자세히 표현하면 다음과 같다.



자바스크립트 엔진이 new Person을 만나면


1. 우선 메모리에 인스턴스 멤버가 없는 비어있는 Object 인스턴스를 만든다

2. 이렇게 생성된 Object 인스턴스를 this에 할당한다.

3. Person 생성자 내부의 코드가 실행된다. 생성자 내부에서는 this가 가리키는 객체에 인스턴스 멤버 name을 정의해서 초기화 한다.

4. 인스턴스 내부에 생성자의 프로토타입 객체를 참조하는 속성인 __proto__를 추가해 생성자의 프로토타입 객체와 인스턴스를 연결하는 역할을 하게한다.

5. new Person의 마지막 절차로 this가 가리키는 인스턴스를 반환한다.


Object 인스턴스 생성 절차


1. 생성된 인스턴스 , this에 할당.

2. 생성자 Person을 호출.

3. 인스턴스 멤버 초기화.

4. 비공개 __proto__ 속성 추가.

5. this가 가리키는 최종 인스턴스를 생성자의 반환값으로 반환.




사용자 정의 값 반환


생성자는 대부분 return 문을 생략한다. return 문이 생략되면 앞에서 말한 것처럼 new Person의  결과로 방금 생성된 인스턴스 (this 가 가리키는)가 반환된다.

셍성자에서 this가 아닌 값을 반환하면 어떻게 될까? 그렇다면 생성자가 반환하는 값이 할당된다.


function Person(name) {

this.name = name;

return f(this);

}


function f(obj){

//obj 사용


return obj;

}


위 코드를 실행하면 new Person은 f의 결과값을 반환한다.

그럼 생성자를 일반 함수처럼 Person("Kim") 로 호출하는 것과 new Person("Kim") 로 호출하는 것의 차이는 무엇일까?

바로 this의 차이다.




공유 함수의 this


생성자 내부에서 사용되는 this는 최초 생성자를 사용해서 만들어지는 인스턴스를 가리킨다.

자바스크립트에서는 메서드 코드가 생성자 외부에 존재할 수 있다는 사실 때문에 this를 이해하기 힘들 때가 있다.


function dog(name) {

this.name = name;

this.setNewName = setNewName;

}


function cat(name) {

this.name = name;
this.setNewName = setNewName;

}


function setNewName(newName) {

this.name = newName;

}


이제 setNewName 은 어떤 객체에 의해서도 호출될 수 있다. 이 상황에서 아래와 같은 코드가 실행된다면?


var d = new dog("강아지");

d.setNewName("덕구");


var c = new cat("고양이");

c.setNewName("나비");


setNewName() 함수에서의 this는 함수를 호출하는 객체 d, c에 따라 결정된다.

우선 기본 원칙은 함수가 객체의 메서드로 사용되는 경우 메서드 내부에서 사용되는 this는 "."의 좌측에 오는 객체가 된다.


this는 "해당 함수를 호출하는 객체"를 가리킨다.




프로토타입 메서드의 this


// 생성자 정의

function Person(name) {

this.name = name;

}


// 프로토타입 멤버인 setNewName 정의

Person.prototype.setNewName = function(newName) { this.name = newName; };


프로토타입 멤버에는 prototype 속성을 이용해 접근할 수도 있고 인스턴스로 접근할 수도있다.


// prototype으로 setNewName에 접근

Person.prototype.setNewName("김씨");


// 인스턴스로 setNewName에 접근

var mySon = new Person("");

mySon.setNewName("박씨");


이러한 경우 setNewName에서 사용하는 this는 각각 어떤 객체를 가리킬까? 앞에서 본 기본 원칙대로 생각하면 된다.

첫번째 경우의 this Person.prototype을 가리킨다. 따라서 함수가 실행되고 나면 name 속성이 Person.prototype이 가리키는 객체, 즉 Person의 프로토타입 객체에 추가된다.

두번째 경우의 this는 당연히 mySon을 가리킨다. 그래서 mySon의 name 속성이 업데이트 된다.




루트 객체 참조


다음과 같은 경우는 어떻게 될까?

setNewName() 처럼 그냥 일반 함수처럼 setNewName("이씨"); 로 호출하면 setNewName() 내부의 this는 어떤 객체를 가리킬까?

이렇게 생성자 용도로 만든 함수가 아니 setNewName 과 같은 함수를 "."로 객체를 지정하지 않고 함수를 직접 호출해버리면,

this는 루트객체를 가리키게 된다. 그래서 루트 객체에 name 속성이 추가되어 버린다.


이것은 대부분 원하지 않는 결과일 것이다. 만약 루트 객체의 기존 멤버로 name이라는 속성(변수)이 정의되어 있었다면 이 값을 덮어써 버리게 된다. 그러면 루트 객체의 name을 사용하고 있는 다른 모듈이 정상적으로 작동하지 않을 수도 있다. 따라서, this를 사용하는 함수는 반드시 다른 객체의 멤버(메서드)에 할당되어 호출되도록 주의해야 한다.




this 변경


모든 함수에는 멤버로 calapply 메서드가 있다. 이 두 메서드를 이용하면 this가 가리키는 객체를 변경하는 효과를 낼수 있다.


// 라이브러리 제공

var library = {

loop : function(elements, callback) {

for(var i; i<elements.length; i++) {

callback.apply(elements[i]);

}

};

}


// 라이브러리 함수 호출

library.loop(elements, function(){

this.show();    // this는 어떤 객체를 가리키는가? (위에서 elements[i]가 this가됨)

});


어떤 library 에서 loop 라는 메서드를 제공한다고 할때. loop의 첫 번째 매개변수는 배열 객체이고 두 번째 매개변수는 각 배열의 값을 대상으로 해서 콜백할 함수이다. loop내부에서는 첫 번째 매개변수로 전달된 배열 객체를 for 문으로 돌면서 각 요소에 callback 함수를 apply를 이용해 호출한다. apply에 인자로 전달되는 객체는 callback 함수 내부에서 사용하는 this에 할당된다.


이제 library의 loop를 호출하는 코드를 보면, 두번 째 콜백 함수에 익명함수를 정의해서 전달하였다. 이때 코드와 같은 콜백 함수의 내부에서 사용하는 this는 library를 가리킬 것으로 예상할 수 있다. 그러나 실제로 콜백 함수 내에서 사용하는 this는 for문에서 인자로 전달한 배열의 각 요소가 된다.

즉, callback 함수가 호출될 때마다 내부의 this 는 for문에 의해 선택된 현재 요소로 변경 되는 것이다.

jQuery 같은 라이브러리의 each 같은 함수가 앞의 loop와 유사한 함수다.


call, apply를 적용해서 호출되는 함수의 내부에서 사용하는 this는 call, apply에서 제공하는 값으로 변경된다.







2. 자바스크립트 상속


함수와 객체가 정의되는 절차를 알아봤다.

계속해서 함수와 객체가 정의되는 과정에서 결정되는 상속 관계를 알아보자.

자바스크립트가 지원하는 함수와 객체의 정의 절차를 이해하고 결과적으로 구성된 상속 관게와 구조를 이해하면 그것을 모델로 삼아서 직접 상속을 구현할 수 있다.




프로토타입 멤버 상속


객체가 해당 생성자의 프로토타입 멤버를 상속할 수 있는 내부적인 원리는 어떻게 될까? 이것은 인스턴스에서 생성자의 프로토타입 멤버를 사용할 수 있는 근거가 된다.



인스턴스를 생성할 때 자바스크립트는 인스턴스 내부에 그 생성자의 프로토타입 객체를 참조하기 위해 __proto__라는 이름의 속성을 추가해 인스턴스가 프로토타입 객체의 멤버를 상속하는 메커니즘을 구현하는 근거로 삼는다고 했다.


new 키워드로 생성자를 이용해 인스턴스를 생성할때 __proto__ 가 만들어지는데 이때 prototype이 가리키고 있는 객체를 이용해서 만들어진다.



아래 그림은 프로토타입 관계도 이다.



프로토타입 멤버는 해당 생성자로 생성된 모든 인스턴스가 공유할 수 있다고 했다.

이것이 가능한 이유는 모든 인스턴스가 __proto__속성을 이용해 자신의 프로토타입 객체에 접근해서 멤버를 검색할 수 있기 때문이다.


인스턴스를 통해서도 프로토타입 객체에 접근할 수 있다는 것은 중요한 사실이다.


모든 Person 인스턴스에서 Person 프로토타입 멤버를 사용할 수 있다는 의미는 다른 말로 표현하면 Person 인스턴스는 Person의 프로토타입 멤버를 "상속"할수 있다는 것이다.


상속이라는 관점에서 그림으로 표현아면 아래와 같다.



mySon 인스턴스가 Person 생성자의 프로토타입 객체를 상속하고 있음을 보여준다.

mySon.constructor로 값을 읽는다고 했을때 constructor는 mySon에는 정의되지 않는 속성이다. 대신 프로토타입 객체로부터 상속을 받은 멤버다.


모든 인스턴스는 해당 생성자의 프로토타입 멤버를 상속한다.



앞에서 함수를 정의하면서 "프로토타입 객체도 Object 인스턴스"라고 했다. 이를 그림으로 표현하면 다음과 같다.





결국 mySon 인스턴스는 생성자인 Person의 프로토타입 객체의 멤버를 상속하고, 다시 Person의 프로토타입 객체는 Object인스턴스인 관계로 Object프로토타입 객체의 멤버를 상속한다는 사실을 알수있다. 이 상속 관계에서 체인을 볼수 있다.




프로토타입 체인


mySon.toString()과 같은 코드가 런타임에 실행된다고 할때 toString을 검색하는 과정을 생각해보자.

자바스크립트는 가장 먼저 mySon 1.인스턴스에서 접근할 수 있는 멤버 가운데 toString을 검색할 것이다. 인스턴스 멤버에서 toString을 찾지 못하면 2.해당 인스턴스가 상속 관계로 연결되어 있는 person 프로토타입 객체로 거슬러 올라간다. 그래서 그곳의 프로토타입 멤버에서 검색하게 된다. 그곳에서 해당 속성을 찾게 되면 그 값을 사용한다. mySon의 프로토타입 객체에서도 멤버를 찾지 못하면 mySon과 연결된 Person 프로토타입 객체는 Object 인스턴스이고 이는 Object 생성자의 프로토타입 객체와 Person 프로토타입 객체가 연결되어 있다는 의미다. 그래서 3.상위의 Object 프로토타입 객체로 가서 멤버를 찾게 된다.

이렇게 인스턴스를 거쳐 상위 프로토타입 객체로 멤버를 검색해나가는 과정을 프로토타입 체인(prototype chaining)이라고 한다.


-> : 속성을 찾지못했을 때 거슬러 올라가는 과정.

Person 의 인스턴스 mySon  검색 -> mySon 의 생성자는 Person, Person.prototype 검색 -> Person.prototype 은 Object 인스턴스, Object 인스턴스의 생성자는 Object, Object.prototype 검색.


인스턴스는 내부에 자신의 프로토타입 객체를 가리키는 숨겨진 멤버(__proto__)를 가진다. 이 멤버를 이용해 프로토타입 체인 이 구성된다.



모든 객체의 최상위 부모는 Object 라는 말을 이제 이해할 수 있을 것이다.

그리고 이보다 더 중요한 것은 "자바스크립트의 상속은 프로토타입 기반의 상속이다"라는 것이다. 이는 우리가 직접 타입을 정의할 때 어떻게 멤버를 설계할 것인가에 대한 힌트를 제시한다. 즉 객체의 공통되는 기능과 상속되어야 하는 기능은 인스턴스 멤버 대신 프로토타입 멤버를 기반으로 객체를 설계하는 방법이 권장된다는 것이다.


모든 객체의 최상위 부모는 Object다. 자바스크립트의 상속은 프로토타입 기반의 상속이다.


다른 장에서 "변수 스코프 체인" 이라는 것을 배웠었는데 이것은 함수의 내부 변수를 탐색해가는 과정에서 등장한 개념이다.


이번 글에서 정리는 "프로토타입 체인"은 객체의 멤버를 탐색해가는 과정과 관련된 개념이다.




Object 멤버


Object는 인스턴스 멤버는 정의하지 않고 프로토타입 멤버만 정의한다.

결국 모든 객체는 Object의 프로토타입 멤버를 상속받게 된다는 의미다.


모든 객체는 Object의 프로토타입 멤버를 상속한다.


인스턴스 멤버를 정의하지 않고 있다고 해서 Object 생성자가 다음과 같이 비어있지는 않다.


function Object(){

}


Object도 일반 함수처럼 생각해서 new 없이 다음과 같이 호출할 수도 있다.


var o = Object(2);


위와 같이 호출하면 인자로 전달되는 값이 Object 객체로 변환되어 반환된다.

즉, Object(2)는 숫자 2를 값으로 갖는 Object 객체를 반환한다. 이런 식으로 Object 함수는 다른 값과 Object 객체의 변환이 필요한 경우 사용할 수 있다.




Object 프로토타입 멤버


Object 프로토타입 멤버는 Object 인스턴스를 통해 접근할 수 있다.


1. Object 프로토타입 속성 멤버

 속성 

 설명 

 obj.constructor

 Object 객체 obj를 생성한 함수(생성자)에 대한 참조


2. Object 프로토타입 메서드 멤버

 메서드

 설명

 obj.hasOwnProperty("멤버명")

 인스턴스 멤버인지 상속된 멤버인지 구분한다.

 지정한 멤버가 obj의 직접적인 멤버인지 확인하는 메서드 이므로 이 메서드는 프로토타입 체    인을 거슬러 올라가서 체크하지 않는다. obj가 지정한 멤버를 가지고 있지 않거나 상속받은 멤  버인 경우는 false를 반환한다.


 var son = new object();

 son.age = 5;

 son.hasOwnProperty("age");       // true

 son.hasOwnProperty("bark");      // false

 son.hasOwnProperty("toString");  // false (toString은 상속된 멤버)


 obj.isPrototypeOf(obj1)

 obj가 다른 객체 obj1의 프로토타입인지 확인할 수 있다.

 obj는 주로 "생성자명.prototype" 이 된다.

 obj1이 "생성자명"이 나타내는 셍성자의 객체인지를 확인할 수 있다. obj1이 객체가 아니거나  obj가 obj1의 프로토타입이 아닌 경우 false를 반환한다.


 var son = new Object();

 Object.prototype.isPrototypeOf(son) // true (son은 Object객체)

 Function.prototype.isPrototypeOf(son.toString); // true (toString은 함수)

 Array.prototype.isPrototypeOf([1,2,3]); // ture ([1,2,3]은 배열)

 (son.constructor == Object); // true son.constructor이 가리키는 곳은 Object

 Object.prototype.isPrototypeOf(Function.prototype); // ture Function.prototype이 가리키  는 인스턴스는 Object 타입이다. 

 obj.propertyIsEnumerable("멤버명")

 인스턴스 멤버 가운데 인자로 전달된 문자열과 이름이 같은 사용자 정의 속성이 존재하면 true  를 반환한다.

 하지만 상속을 고려하지 않아 부모 타입에 인자와 동일한 이름의 속성이 존재하더라도 false  를 반환한다.

 

 var p = new Person();

 p.propertyIsEnumerable("toString"); // false (Object에 정의되어있지만 상속고려 안함) 


 obj.toString()

 Object를 상속받는 현재 인스턴스를 대표하는 문자열을 반환한다.

 문자열을 기대하는 상환에서 toString()없이 인스턴스만 인자로 전달하면 자동으로 해당

 인스턴스의 toString()이 호출된다.


 alert(son); // "[object Object]"

 obj.valueOf()

 만약 지정한 obj 객체가 Number, String, Boolean 객체라면 이 함수는 해당 래퍼 객체가 감싸  고 있는 원시값을 반환한다. 만약 원시값이 없다면 그 객체는 [object Object]와 같은 표현을  반환한다.




Object 프로토타입 멤버 상속


프로토타입 멤버의 상속으로 인해 Person 인스턴스에서 Object의 모든 프로토타입 멤버를 사용할 수 있게 되는 것처럼 함수, 배열 객체, 사용자 정의 객체 등은 모두 Object의 프로토타입 멤버를 상속한다.







3. Function 상속



Function 프로토타입 멤버


Obejct 처럼 Function 도 function 또는 new Function으로 생성된 모든 함수 객체가 상속할 프로토타입 멤버를 정의하고 있다.


모든 함수는 Function에서 정의한 프로토타입 멤버를 상속한다.




call/apply


모든 함수는 Function을 상속받아 프로토타입 멤버로 정의되어 있는 call/apply를 다음과 같이 호출할 수 있다.


func.call(객체, 인자값);

func.apply(객체, 인자값 배열);


call/apply 내부에서는 전달받은 인자를 이용해 다시 함수 func를 호출해준다.  

func 함수 내부에서 this를 사용하고 있다면 call/apply의 첫 번째 인자는 func의 내부에 있는 this에 할당되고, 두 번째 인자부터는 func을 호출하는 인자로 사용한다.


call/apply가 생성자에 사용되면 다른 생성자에 정의된 인스턴스 멤버를 가져와서 정의할 수 있다.


function Person(name){

this.name = name;

}


function Korean(city){

this.city = city;

}


Korean에서 Person 에 정의되어 있는 인스턴스 멤버를 가져오고 싶은 경우에 call/apply를 사용할 수 있다는 것이다.


function Korean(name, city){

// 생성자 Person에 대해 call/apply를 호출한다.

Person.call(this, name); // Person.apply(this, [name]);

this.city = city;

}


Korean 생성자 내부에서 Person.call(this, name)을 호출하면 이때의 this는 현재 생성된 Korean 인스턴스가 된다.

결국 Person 생성자 내부에서 this를 통해 멤버를 정의하면 결국 그 멤버는 현재 생성된 Korean 인스턴스의 멤버가 되는 것이다.


위 this 변경 에서 정리한 내용해서도 call/apply를 이용한 예를 확인할수 있었다.




new Korean을 실행하면 Korean 인스턴스를 생성하고 나서 Korean 생성자를 호출한다. 이때 생성된 인스턴스를 Korean의 this에 할당한다.

Korean 생성자 내부에서는 다시 Person.call/apply를 호출하면서 생성된 Korean 인스턴스를 Person 생성자의 this로 전달한다.

결국 Person 생성자 내부의 this.name = name은 생성된 Korean 인스턴스에 name 속성 멤버를 추가하는 결과를 가져온다.

이렇게 추가된 name은 실질적인 Korean 인스턴스의 멤버다.


Korean kor = new Korean("kim","seoul");

kor.hasOwnProperty("name); // true;


hasOwnProperty 함수는 인자로 전달한 속성이 인스턴스 자신의 멤버이면 true를, 그렇지 않고 상속을 통해 물려받은 멤버라면 false를 반환한다. call/apply를 통해 Korean에 추가한 name 속성은 정말 Korean인스턴스의 속성임을 말해준다.




Function 상속


모든 Object 객체가 Object의 프로토타입 멤버를 상속하듯이 모든 함수도 Function의 프로토타입 멤버를 상속한다.

모든 함수는 "Function 인스턴스"에 해당한다.


모든 함수는 Function의 프로토타입 멤버를 상속받는다.


사용자 정의 함수뿐 아니라 Object 생성자, Array 생성자도 모두 Function 인스턴스이다.


function Person(){

...

}


Person 생성자는 함수로서 new Function()을 통해 생성된 Function 인스턴스다. (Object 인스턴스는 아니다.)

Object 생성자도 Function을 상속한 함수 인스턴스다. 이러한 이유로 생성자를 포함한 모든 함수는 Function 프로토타입 멤버를 사용할 수 있게 된다. 따라서 Person과 Object 생성자도 Function의 프로토타입 멤버인 call, apply 등을 사용할 수 있다.


Person.call();

Object.call();



Person 프로토타입 객체와 Function 프로토타입 객체가 Object 프로토타입 객체를 상속받고 있다.

여기에서 중요하게 볼것은 Person 생성자와 Object 생성자는 Function 인스턴스로서 위 그림에서 처럼 Function 프로토타입의 멤버를 상속받는다는 것이다.


Person.constructor === Function -> true 반환

Person instanceof Function -> true 반환


위와같은 결과가 나오는 이유는 다음과 같다. Person 생성자에는 constructor 멤버가 없다. 따라서 상속 관계에 의해 상위의 Function 프로토타입 객체로 거슬러 올라가 그곳에서 constructor가 발견되고 그 값을 Function 과 비교하는 것이니 당연히 동일하다는 판정이 나는 것이다.

instanceof 사용 시에도 Person은 Function의 함수 인스턴스이므로 true를 반환한다.


위의 그림이 복잡한 것처럼 보이지만 간단히 아래와 같이 정리할 수 있다.


1. 모든 함수는 Function의 프로토타입 멤버를 상속한다.

2. 모든 함수의 프로토타입 객체는 Object의 프로토타입 객체를 상속한다.

3. 모든 생성자의 인스턴스Object 프로토타입 객체를 상속한다.


따라서 다음 코드의 차이는.


Person.toString()

Person.prototype.toString()


첫 번째 코드는 생성자 멤버인 toString() 메서드를 호출하는 코드이고, 두 번째 코드는 생성자의 속성인 prototype 을 통해 toString()을 호출하는 것이다. 이를 그림으로 나타내면.


Function 은 Object에서 상속받은 toString을 그대로 사용하는 것이 아니라 toString을 자체적으로 정의해 사용하고 있다. 그러나 Person.prototype.toString()을 호출하면 결국 object 프로토타입 객체에서 상속받은 toString()을 호출하게 된다.







4. 객체 확장


확장이라 함은 부모 객체의 멤버를 모두 자식 객체로 전달하고 자식만의 멤버를 추가하는 것을 말한다. 일반 객체지향 언어에서는 확장의 방법으로 대개 상속을 이용하는데 자바스크립트에서도 비슷하게 상속을 구현해볼 것이다.




상속


new와 자식 생성자를 이용하는 방법으로서 일반 객체지향 언어의 상속과 유사한 구문을 갖는다.

부모 생성자가 Object인 경우에는 프로토타입 멤버의 상속을 자바스크립트에서 자동으로 대신 수행해줬다.

Person 같은 사용자 정의 생성자를 상속해 좀 더 구체적인 객체를 정의하는 자식 생성자를 정의할 수도 있다.


사용자 정의 생성자를 다시 상속하려면 프로토타입 멤버 상속과 인스턴스 멤버 상속을 직접 구현해야 한다.


인스턴스 멤버와 프로토타입 멤버가 자식으로 전달되는 메커니즘이 다르다.

인스턴스 멤버는 객체 생성 절차를 이용해 자식의 인스턴스로 복사되어 상속되는 것이고, 프로토타입 멤버는 자바스크립트의 상속 메커니즘에 의해 자식 객체로 상속된다.




위 그림과 같은 상속 관계를 구현하기 위해 프로토타입 멤버 상속인스턴스 멤버 상속을 구분해 정리해본다.

자바스크립트에서의 상속은 대부분 프로토타입 멤버 기반의 상속이라고 하지만 인스턴스 멤버도 상속 효과를 낼 수 있는 방법을 정리해본다.




프로토타입 멤버 상속 구현 (프로토타입 상속) : prototype


자바스크립트에서 흔히 사용되는 방법이다.


먼저 Person의 생성자 코드이다.


function Person(){

// 인스턴스 멤버 정의는 없음

}


//Person의 프로토타입 멤버 추가

Person.prototype.species = "human";



다음은 Korean 생성자의 정의다.


function Korean(){

// 인스턴스 멤버 정의는 없음.

}


//Korean의 플로토타입 멤버 추가

Korean.prototype.nationality = "korea";



이제 Korean이 Person의 프로토타입 멤버를 상속하는 코드를 만들자.


Korean.prototype = new Person();

Korean.prototype.constructor = Korean;


Object를 상속하는 Person을 정의할 때는 아래와 같은 작업을 자바스크립트가 알아서 처리해주지만


Person.prototype = new Object();

Person.prototype.constructor = Person;


지금은 Object 가 아닌 Person을 상속받아 다른 사용자 정의 자식 타입을 정의 하려하니 이 경우에는 자바스크립트가 해줬던 내부 작업을 개발자가 직접 구현해야 한다.


Korean의 프로토타입 객체로 Person의 인스턴스를 사용하고 있고 Korean의 프로토타입 객체의 constructor 속성값을 Korean 생성자에 대한 참조로 변경하였다.


결국 Korean의 프로토타입 객체를 Person 인스턴스로 대체하는 작업을 하는 것으로서 두 줄의 코드가 실행되고 나면 Korean의 프로토타입 체인에 Person의 프로토타입 객체가 추가되는 결과가 나타난다.



Person 프로토타입 멤버의 상속이란 결국 프로토타입 체인상에서 Korean의 프로토타입 객체 상위에 Person의 프로토타입 객체를 만들어 끼워넣는 작업이다.


이제 Korean 인스턴스를 생성해서 사용하면 Korean에서 정의하지 않은 species를 사용할 수 있다.


var cc = new Korean();

var species = cc.species;        // "human"

var nationality = cc.nationality;   // "korea"


console.log(cc instanceof Korean)    // true

console.log(cc instanceof Person)   // true




인스턴스 멤버의 상속 구현 : call/apply


프로토타입 멤버뿐 아니라 부모 객체에 있는 인스턴스 멤버도 사용하고 싶을 때가 있다.

부모에 정의된 인스턴스 멤버를 자식의 인스턴스에서도 사용할 수 있게 만드는 방법을 정리해본다.


일반적인 객체 지향 언어에서는 base()super() 같은 메서드를 제공한다. 그래서 자식 객체를 생성할 때 이것이 호출 되어 부모 객체도 생성한다. 부모 객체와 자식 객체가 별도로 생성되어서 자동으로 상속되는 구조다.


다음과 같은 상황에서 상속을 정리해본다.


function Person(name) {

this.name = name;    //인스턴스 멤버

}


function Korean(name, city) {

this.city = city;    //인스턴스 멤버

}


Korean 생성자에서 Person을 직접 호출하면 될까?


function Korean(name, city) {

Person(name);

this.city = city;

}


var mySon = new Korean("Kim", "Seoul");

mySon.name; //undefined;


결과는 name이 korean의 인스턴스에 정의되지 않았다고 나온다.

직접 Korean 생성자에서 Person 생성자를 호출할 때 Person 멤버를 소유하는 객체를 지정하지 않았다. 따라서 Person을 루트 객체의 멤버로 간주하고 Person 내부에서 사용되는 this는 루트 객체를 가리키게 된다. 브라우저에서는 Window 객체를 가리킨다. 따라서 위와 같은 경우는 전역 변수 스코프의 루트 객체에 name 속성을 추가하는 셈이다.


이러한 경우 Function에 정의 되어있는 call 또는 apply를 사용해 Person 내부에서 사용되는 this를 원하는 객체로 바라보게 할 수 있다.


Function의 call, apply를 사용하면 함수를 다른 함수의 컨텍스트에서 실행할 수 있다.


수저오딘 Korean 생성자 코드는 다음과 같다.


function Korean(name, city) {

// 부모 생성자를 호출한다.

// 이때 부모 생성자 내의 this에는 call 또는 apply 함수의 첫 번째 인자를 할당 한다.


// Korean 타입의 인스턴스 this를 Person생성자의 this에 할당한다.

// 결국, 부모 생성자 Person에서는 인자로 전달받은 Korean 인스턴스에 name 속성을 추가하는 셈이다.

Person.apply(this,[name]);    //또는 Person.call(this, name);


this.city = city;

}




상속 구현 통합


앞에서 정리한 프로토타입 상속과 인스턴스 멤버 상속을 함께 구현하는 경우를 생각해보자.


프로토타입 상속을 구현하는 코드를 다시보자.


1. Korean.prototype = new Person();

2. Korean.prototype.constructor = Korean;


첫번째 코드에 의해 Korean의 프로토타입 객체는 Person 인스턴스 객체로 대체된다. 그런데 이때 Person 인스턴스에 있는 name은 인스턴스별로 존재하는 것이기에 프로토타입 멤버로는 대부분 필요없을 것이다.

따라서 다음과 같이 프로토타입 멤버에서 name 속성을 제거하면 상속이 완성된다.


delete Korean.prototype.name;


프로토타입 멤버에서 인스터늣 멤버인 name 속성을 반드시 제거해야만 하는 것은 아니다.

인스턴스 멤버가 프로토타입 멤버로 전환될 때는 일반적으로 불필요하기 때문에 제거하는 것이 보통이지만 그대로 두어도 상관은 없다.

마지막 제거 작업은 선택적이다.


인스턴스 멤버 상속과 프로토타입 멤버 상속을 구현한 코드를 합쳐보자.


function Person(name) {

this.name = name;    //인스턴스 멤버

}

//Person 의 프로토타입 멤버

Person.prototype.species = "human";


function Korean(name, city) {

//인스턴스 멤버 상속

person.apply(this, [name]); // 또는 Person.call(this,name);

this.city = city;    //인스턴스 멤버

}

//Korean 의 프로토타입 멤버

Korean.prototype.nationality = "korea";


//프로토타입 멤버 상속

Korean.prototype = new Person();

Korean.prototype.constructor = Korean;

//상속 구현에서 선택적인 작업

delete Korean.prototype.name; // 삭제하는 이유는 간단함 위에 new person() 으로 생성된 인스턴스의 멤버는 사용될 일이 없어서


이제 Korean 타입의 인스턴스인 mySon을 생성하면 다음과 같은 메모리 구조가 만들어진다.


var mySon = new Korean("Kim", "seoul");



멤버를 모두 출력해보면 아래와 같다.


var mySon = new Korean("kim", "seoul");

var a = [];

for ( var propertyName in mySon)

a.push(propertyName);


console.log(a);    // ["name","city","constructor","species"]




멤버 확장


프로토타입 멤버를 상속하는 것과 Function의 call/apply 함수를 이용해 인스턴스 멤버를 상속하는 방법 이외에 자바스크립트 에서는 상속을 흉내낼 수 있는 다른 간단한 방법도 있다.


상속이랑 결국 부모 객체의 멤버를 모두 자식 객체에서 사용할 수 있게 하는 것이다.

따라서 부모 객체의 모든 멤버를 자식 객체로 복사하는 것도 간단하게 상속을 구현하는 방법이라고 할 수 있다.


다음과 같은 유틸리티 메서드를 Object의 프로토타입 멤버로 만들어두면 편하게 멤버 복사를 마칠 수 있다.


Object.prototype.extend = function(parent) {

for(var property in parent) {

this[property] = parent[property];

}

}


위에서 정의한 메서드는 다음과 같이 사용할 수 있다.


function Person(name) {

this,name = name;

}

Person.prototype.setNewName = function(newName) { this.name = newName; };


// 부모 객체 생성.

var parent = new Person("Person Kim");

// 자식 객체 생성

var child = {};

// 멤버 상속

child.extend(parent);


child.extend(parent)를 실행할 때 extend 메서드 내부에서 사용되는 this는 child 객체를 가리킨다는 것을 알 수 있다.

헷갈린다면 위에서 공유 함수의 this를 참조!







5. 리플렉션


현재 객체가 어떤 생성자에서 생성됐는지, 특정 생성자와 생성자 또는 특정 객체와 객체가 어떤 관계인지 알고싶을때 현재의 인스턴스로부터 생성자에 대한 정보를 역추적 해야한다. 이러한 과정을 리플렉션(reflection)이라고 한다.

리플렉션이랑 런타임에 객체의 값 타입과 멤버의 구조를 밝히는 작업을 말한다.


리플렉션을 활용하면 런타임에 동적으로 객체의 구조를 구성하거나 변경할 수 있는 상당히 유연하고 수준 높은 프로그램을 만들 수 있다.


일반 객체지향 언어에서는 객체를 통해 타입에 대한 정보를 역추적하는 것이 그렇게 어렵지 않다. 언어적 차원에서 리플렉션을 위한 다양한 API을 제공하기 때문이다.


그러나 자바스크립트에서는 일반 객체지향 언어에 비해 그렇게 다양한 API를 제공해주지는 못하고, 리플렉션을 통해 얻을 수 있는 타입에 대한 정보도 그렇게 많지 않다. 따라서 런타임에 자바스크립트가 제공하지 못하는 정보가 필요하다면 함수를 정의할 때 개발자가 직접 필요한 정보를 추가해야 할 수도 있다.




타입 판별 - typeof 연산자


자바스크립트에서 리플렉션을 통해 판별할 수 있는 데이터 타입은 문자열, 숫자, 함수, 객체, 배열 정규식 객체등 자바스크립트에서 기본적으로 제공하는 타입이다.



먼저 변수가 null인 경우는 "==" 연산자를 이용해 먼저 판별하는 것이 좋다. typeof 연산자로는 null을 구분해 낼 수 없기 때문이다.

typeof로 undefined인 경우를 구분 해낼 수 는 있지만 앞의 순서도 에서는 undefined도 "=="를 이용해 null과 함께 판별하는 것으로 하고 있다. 만약 null도 아니고 undefined도 아니라면 아래로 내려가서 typeof 연산자를 사용할 수 있다.


판별 대상인 값이 null도 아니고 undefined도 아니라면 typeof 연산을 적용해 반환되는 문자열로 타입을 구분한다. 이 문자열을 기준으로 타입을 판별하는 것이 첫 번째 방법이다.


 typeof 연산 

 결과값 

 typeof 문자열 변수

 "string" 

 typeof 숫자변수

 "number" 

 typeof 불린변수

 "boolean" 

 typeof 함수변수

 "function"

 typeof 객체참조변수

 "object"

 typeof null

 "object"

 typeof undefined

 "undefined"


typeof를 이용하면 변수가 기본 타입인지 함수 변수인지, 아니면 객체 변수인지 알 수 있다.

그러나 typeof 연산자가 "object"를 반환하면 구체적으로 해당 객체가 어떤 객체인지는 알 수 없다.

구체적인 객체의 타입을 판별해야 한다면 instanceof 연산자를 사용할 수 있다.




상세 타입 판별 - instanceof 연산자


instanceof 연산자의 사용법은 다음과 같다.


객체 instanceof 생성자


이 연산은 "해당 객체가, 지정한 생성자의 인스턴스인가?" 라는 질문과 같다.

예를 들어, obj instanceof Array 가 true 를 반환하면 obj는 Array 타입의 인스턴스라는 의미다.


var obj = [];

obj instanceof Array;        // true

obj = new Date();

obj instanceof Date;        // true

obj = new RegExp();

obj instanceof RegExp;     // true


instanceof 연산자는 타입의 상속관계를 고려해서 결과를 반환한다. 우측에 있는 생성자로 좌측의 객체를 생성한 생성자가 오면 당연히 true를 반환하지만 우측에 좌측 객체의 부모 타입이 와도 true를 반환한다.


function Person(name) {

this.name = name;

}


var mySon = new Person("Kim");


mySon instanceof Person;    // true

mySon instanceof Object;    // true

Object는 프로토타입 체인 관계를 보면 모든 객체의 최상위 부모다. 따라서 mySon instanceof Object도 true를 반환한다.


instanceof는 우측의 생성자가 좌측에 지정한 인스턴스의 프로토타입 체인상에 있는 생성자이면 true를 반환한다.



instanceof는 함수의 경우에도 사용될 수 있다.


var add = function(){};

add instanceof Function    // true

add instanceof Object      // true


add 함수의 프로토타입 체인상 최상위는 Object 프로토타입 객체가 있고, 이 객체의 constructor 속성은 Object를 가리킨다.

즉, add instanceof Object 는 true를 반환하게 된다.




toString 재정의.


Object 의 프로토타입 객체에서 정의하고 있는 toString 메서드는 다음과 같은 형식의 문자열을 반환한다.


"[object 생성자명]"


Object 객체의 경우는 "Object"라는 이름을 내부에 가지고 있어서 Object.prototype.toString()의 결과는 "[object Object]" 를 반환한다.


Object 외의 다른 대부분의 생성자는 toString 메서드를 오버라이딩 해서 나름의 로직을 구현하고 있어 이러한 형태의 결과값을 직접 조회할 수는 없다. Array.prototype.toString(), date.prototype.toString(), Function.prototype.toString() ..

따라서 현재 관심이 있는 객체에 대해 우리가 원하는 형태의 결과를 얻으려면 call() 또는 apply()로 Object의 프로토타입 객체에 정의되어 있는 원래의 toString을 호출해야 한다.


var ar = [];

Object.prototype.toString.apply(ar); 


사용자 정의 함수도 toString()을 재정의 할수 있다.


function Person(name) {

Person.prototype.toString = function() {

return "[object Person]";

};

}


Person.prototype.toString();    // "[object Object]"


var mySon = new Person("kim");

mySon.toString();             // "[object Person]"

Person.prototype.toString();    // "[object Person]"


Person 생성자 내부에서 Person.prototype을 통해 자신만의 toString()을 구현하고 있다. 이렇게 하면 Object 프로토타입 객체에서 상속받은 toString()이 감춰진다.














참조.


자바스크립트 객체지향 프로그래밍.

댓글