6.2 상속
자바스크립트는 클래스를 기반으로 하는 전통적인 상속을 지원하지는 않는다. 하지만 자바스크립트 특성 중 객체 프로토타입 체인을 이용하여 상속을 구현해낼 수 있다.
이러한 상속의 구현 방식은 크게 두 가지로 구분할 수 있는데,
하나는 클래스 기반 전통적인 상속 방식을 흉내 내는 것이고, 다른 하나는 클래스 개념 없이 객체의 프로토타입으로 상속을 구현하는 방식이다.
—>
프로토타입을 이용한 상속(Prototypal inheritance)
6.2.1 프로토타입을 이용한 상속
예제 6-4
function create_object(o) { // o를 부모로 하는 object 생성
function F() {}
F.prototype = o;
return new F();
}
이 코드는 더글라스 크락포드가 자바스크립트 객체를 상속하는 방법으로 오래 전에 소개한 코드이다. 조금 과장해서 말하면 이 세 줄의 코드를 이해하면 자바스크립트에서의 프로토타입 기반의 상속을 다 배운 것이나 다름없다.
create_object()
함수는 인자로 들어온 객체를 부모로 하는 자식 객체를 생성하여 반환한다.
- 새로운 빈 함수 객체
F
생성 F.prototype
프로퍼티에 인자로 들어온 객체를 참조- 함수 객체
F
를 생성자로 하는 새로운 객체를 만들어 반환
이렇게 프로토타입의 특성을 활용하여 상속을 구현하는 것이 프로토타입 기반의 상속.
앞에서 소개한 create_object()
함수는 ECMAScript 5에서 Object.create()
함수로 제공되므로, 따로 구현할 필요는 없다.
예제 6-5
var person = {
name : "zzoon",
getName : function() {
return this.name;
},
setName : function(arg) {
this.name = arg;
}
};
function create_object(o) {
function F() {};
F.prototype = o;
return new F();
}
var student = create_object(person);
student.setName("me");
console.log(student.getName()); // (출력값) me
프로토타입 기반 상속의 특징
- 클래스에 해당하는 생성자 함수를 만들지 않음
- 그 클래스의 인스턴스를 따로 생성하지 않음
- 단지 부모 객체에 해당하는
person
객체와 이 객체를 프로토타입 체인으로 참조할 수 있는 자식 객체student
를 만들어서 사용.
여기에서 자식은 자신의 메서드를 재정의 혹은 추가로 기능을 더 확장시킬 수 있어야 한다.
student.setAge = function(age) {...}
student.getAge = function() {...}
단순히 이렇게 그 기능을 확장시킬 수는 있지만, 이렇게 구현하면 코드가 지저분해진다.
자바스크립트에서는 범용적으로 extend()
라는 이름의 함수로 객체에 자신이 원하는 객체 혹은 함수를 추가시킨다.
jQuery 1.0의 extend
함수는 다음과 같이 구현되었다.
jQuery.extend = jQuery.fn.extend = function(obj,prop) {
if (!prop) { prop = obj; obj = this; }
for ( var i in prop ) obj[i] = prop[i];
return obj;
};
이 코드를 분석해본다.
jQuery.extend = jQuery.fn.extend = ...
jQuery.fn
은 jQuery.prototype
—> jQuery
함수 객체와 jQuery
함수 객체의 인스턴스 모두 동일한 extend
함수를 만들어두겠다는 뜻.
즉, jQuery.extend()
로 호출할 수도 있고,
var elem = new jQuery(...);
elem.extend();
의 형태로도 호출할 수 있음을 뜻한다.
if ( !prop ) { prop = obj; obj = this; }
이는 extend 함수의 인자가 하나만 들어오는 경우에는 현재 객체(this
)에 인자로 들어오는 객체의 프로퍼티를 복사함을 의미한다.
for (var i in prop ) obj[i] = prop[i];
그리고 두 개가 들어오는 경우에는 첫 번째 객체에 두 번째 객체의 프로퍼티를 복사하겠다는 것을 뜻한다.
루프를 돌면서 prop
의 프로퍼티를 obj
로 복사한다.
하지만 이 코드에는 약점이 있다. obj[i] = prop[i]
는 얕은 복사를 의미한다. 즉, 문자 혹은 숫자 리터럴 등이 아닌 객체(배열, 함수 객체 포함)인 경우 해당 객체를 복사하지 않고, 참조한다. 이는 두 번째 객체의 프로퍼티가 변경되면 첫 번째 객체의 프로퍼티도 같이 변경됨을 의미한다. 이것을 의도해서 작성한 경우가 아니라면, 작성자가 의도하지 않은 결과가 나오고 이를 디버깅하는 것은 그렇게 쉬운 일이 아니다. 그러므로 보통 extend
함수를 구현하는 경우 대상이 객체일 때는 깊은 복사를 하는 것이 일반적이다.
깊은 복사를 하려면 복사하려는 대상이 객체인 경우 빈 객체를 만들어 extend
함수를 재귀적으로 다시 호출하는 방법을 쓴다. 함수 객체인 경우는 그대로 얕은 복사를 진행하기 때문에 주의.
다음은 jQuery 1.7의 extend
함수 중 일부 코드.
for ( ; i < length ; i++) {
// Only deal with non-null/undefined values
// 인자로 넘어온 객체의 프로퍼티를 option으로 참조시키고,
// 이 프로퍼티가 null이 아닌 경우 블록 안으로 진입한다.
if ((options = arguments[i]) != null) {
// Extend the base object
for (name in options) {
// src는 반환될 복사본 target의 프로퍼티를 가리키고
src = target[name];
// copy는 복사할 원본의 프로퍼티를 가리킨다.
copy = options[name];
// Prevend never-ending loop
if (target === copy) {
continue;
}
// Recurse if we're mergint plain objects or arrays
// deep 플래그 : jQuery의 extend 함수는 사용자가 첫 번째 인자로 Boolean 값을 넣어 깊은 복사를 할 것인지 선택할 수 있게 한다.
// copy 프로퍼티가 객체이거나 배열인 경우 재귀 호출을 하려고 블록 안으로 진입한다.
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray == jQuery.isArray(copy)))){
// copy가 배열인 경우 빈 배열을, 객체인 경우 빈 객체를 clone에 할당한다.
// 여기서 src가 같은 배열, 혹은 같은 객체이면 clone에 해당 배열 혹은 객체를 참조시킨다.
// 이는 복사본에 같은 이름의 프로퍼티가 있는 경우, 원본과 똑같은 배열이거나 객체라면 새롭게 할당시키지 않고, 복사본의 해당 프로퍼티에 추가될 수 있게 하기 위해서다.
if (copyIsArray) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : [];
} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}
// Never move original objects, clone them
// extend 함수를 다시 호출한다. clone이라는 빈 객체에 copy 객체를 복사함을 의미한다. copy 객체 안에 다시 객체 혹은 배열이 있는 경우 다시 재귀 호출이 이루어진다.
target[name] = jQuery.extend(deep,clone,copy);
// Don't bring in undefined values
// copy 프로퍼티가 객체 혹은 배열이 아닌 경우 undefined인지를 살피고 target 객체에 할당한다.
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
// Return the modified objects
// 만들어진 복사본을 반환한다.
return target;
}
예제 6-6
var person = {
name : "zzoon",
getName : function() {
return this.name;
},
setName : function(arg) {
this.name = arg;
}
};
function create_object(o) {
function F() {};
F.prototype = o;
return new F();
}
function extend(obj, prop) {
if (!prop) { prop = obj; obj = this; }
for ( var i in prop ) {
console.log(i);
obj[i] = prop[i]
};
return obj;
};
var student = create_object(person);
var added = {
setAge : function(age) {
this.age = age;
},
getAge : function() {
return this.age;
}
};
extend(student, added);
console.dir(student);
student.setAge(25);
console.log(student.getAge()); // (출력값) 25
extend()
함수는 사용자에게 유연하게 기능 확장을 할 수 있게 하는 주요 함수일 뿐만 아니라, 상속에서도 자식 클래스를 확장할 때 유용하게 사용되므로 기억!
6.2.2 클래스 기반의 상속
앞 절에서는 객체 리터럴로 생성된 객체의 상속을 소개했지만, 여기서는 클래스의 역할을 하는 함수로 상속을 구현.
예제 6-7
function Person(arg) {
this.name = arg;
}
Person.prototype.setName = function (value) {
this.name = value;
};
Person.prototype.getName = function() {
return this.name;
};
function Student(arg) {
}
var you = new Person("iamhjoo");
Student.prototype = you;
var me = new Student("zzoon");
me.setName("zzoon");
console.log(me.getName()); // zzoon
앞 예제에서 Student
함수 객체를 만들어서, 이 함수 객체의 프로토타입으로 하여금 Person
함수 객체의 인스턴스를 참조하게 만들었다.
이렇게 하면 Student
함수 객체로 생성된 객체 me
의 Pertotype
링크가 생성자의 프로토타입 프로퍼티 Student.prototype
인 you
를 가리키고, new Person()
으로 만들어진 객체의 Prototype
링크는 Person.prototype
을 가리키는 프로토타입 체인이 형성된다.
—> 객체 me
는 Person.prototype
프로퍼티에 접근할 수 있고, setName()
과 getName()
을 호출할 수 있다.
그러나 예제 6-7은 문제가 있다. 먼저 me
인스턴스를 생성할 때 부모 클래스인 Person
의 생성자를 호출하지 않는다.
var me = new Student(“zzoon”);
이 코드로 me
인스턴스를 생성할 때 “zzoon”을 인자로 넘겼으나, 이를 반영하는 코드는 어디에도 없다. 결국 생성된 me
객체는 빈 객체이다. setName()
메서드가 호출되고 나서야 me
객체에 name
프로퍼티가 만들어진다.
이렇게 부모의 생성자가 호출되지 않으면, 인스턴스의 초기화가 제대로 이루어지지 않아 문제가 발생할 수 있다. 이를 해결하려면 Student
함수에 다음 코드를 추가하여 부모 클래스의 생성자를 호출해야 한다.
function Student(arg) {
Person.apply(this, arguments);
}
Student
함수 안에서 새롭게 생성된 객체를 apply
함수의 첫 번째 인자로 넘겨 Person
함수를 실행시킨다. 이런 방식으로 자식 클래스의 인스턴스에 대해서도 부모 클래스의 생성자를 실행시킬 수 있다.
클래스 간의 상속에서 하위 클래스의 인스턴스를 생성할 때, 부모 클래스의 생성자를 호출해야 하는데, 이 경우에 필요한 방식이다.
현재는 자식 클래스의 객체가 부모 클래스의 객체를 프로토타입 체인으로 직접 접근한다. 하지만 부모 클래스의 인스턴스와 자식 클래스의 인스턴스는 서로 독립적일 필요가 있다.
이 구조는 자식 클래스의 prototype
에 메서드를 추가할 때 문제가 된다.
예제 6-8
function Person(arg) {
this.name = arg;
}
Function.prototype.method = function(name, func) {
this.prototype[name] = func;
}
Person.method("setName", function(value) {
this.name = value;
});
Person.method("getName", function() {
return this.name;
});
function Student(arg) {
}
function F() {};
F.prototype = Person.prototype;
Student.prototype = new F();
Student.prototype.constructor = Student;
Student.super = Person.prototype;
var me = new Student();
me.setName("zzoon");
console.log(me.getName());
예제 6-8의 프로토타입 체인 형성 과정은 6.2.1 프로토타입을 이용한 상속 방식과 매우 유사하다. 어차피 함수의 프로토타입을 이용한 것이니 비슷할 수밖에 없다. 여기에서도 빈 함수 F()
를 생성하고, 이 F()
의 인스턴스를 Person.prototype
과 Student
사이에 두었다. 그리고 이 인스턴스를 Student.prototype
에 참조되게 한다.
빈 함수의 객체를 중간에 두어 Person
의 인스턴스와 Student
의 인스턴스를 서로 독립적으로 만들었다. 이제 Person
함수 객체에서 this
에 바인딩되는 것은 Student
의 인스턴스가 접근할 수 없다. 이 상속이 앞서 소개된 상속보다 좀 더 나은 코드이다. 의 저자 스토얀 스테파노프는 상속 관계를 즉시 실행 함수와 클로저를 활용하여 최적화된 함수로 소개하였는데, 그 코드는 다음과 같다.
var inherit = function(Parent, Child) {
var F = function() {};
return function(Parent, Child) {
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.super = Parent.prototype;
};
}();
앞 코드에서 클로저 (반환되는 함수) 는 F()
함수를 지속적으로 참조하므로, F()
는 가비지 컬렉션의 대상이 되지 않고 계속 남아있다. 이를 이용해 함수 F()
의 생성은 단 한 번 이루어지고 inherit
함수가 계속해서 호출되어도 함수 F()
의 생성을 새로 할 필요가 없다.
#책/인사이드자바스크립트
'language > JavaScript' 카테고리의 다른 글
6. 객체지향 프로그래밍 (3) (0) | 2019.12.02 |
---|---|
6. 객체지향 프로그래밍 (1) (0) | 2019.11.29 |
5. 실행 컨텍스트와 클로저 (3) (0) | 2019.11.28 |
5. 실행 컨텍스트와 클로저 (2) (0) | 2019.11.27 |
5. 실행 컨텍스트와 클로저 (1) (0) | 2019.11.26 |