April 12th 2019
OOP는 우리가 사물을 인식하는 자연스러운 방식을 반영해, 데이터와 기능을 논리적으로 묶어놓은 프로그래밍 방식입니다. 만약 자동차가 객체라면 그 데이터에는 제조사, 모델 등이 있을 것입니다. 그리고 기능으로는 전진, 후진 등이 있을 수 있습니다. OOP는 사물에 관해 추상적으로(어떠한 자동차인지), 혹은 구체적으로(특정 자동차) 생각하며 프로그래밍을 할 수 있게 합니다.
Contents
프로토타입 체인은 어떠한 객체를 다른 객체와 유사하게 만들고 싶을 때 사용할 수 있습니다. 두 개의 객체가 같은 Property를 가지도록 할 때, 두 객체에 각각 같은 Property 코드를 직접 작성할 수도 있습니다. 하지만, 자바스크립트는 프로토타입 체인을 제공하기 때문에 이러한 중복된 작업을 더 쉽게 할 수 있습니다. 프로토타입 체인을 이용해 우리는 어떠한 객체에서 원하는 속성을 찾을 수 없는 경우 또 다른 객체로 이동해 해당 속성이 있는지 찾아볼 수 있습니다. 이를 통해서 메모리를 아끼고 코드 중복을 방지할 수 있습니다.
기본적인 이야기이지만, 우리가 어떠한 객체의 속성에 접근하면 Javascript Interpreter는 해당 객체에 해당 속성이 있는지 확인합니다. 해당 속성이 있으면 그 값을 반환해 줍니다. 만약 해당 객체에 찾으려는 속성이 없으면 undefined를 반환합니다.
let jone = { age: 20 };
console.log(jone.age); // -> 1
console.log(jone.job); // -> undefined
만약 우리가 jone 객체의 속성을 지닌 또 다른 객체를 생성하려 한다면, 우리는 속성을 그대로 복사해 올 수 있습니다. 아래의 코드에서는 extend라는 가상의 helper function을 이용했지만, 여러분은 for loop을 통해 구현할 수도 있습니다.
요지는 1-time copy이기 때문에 jone과 jonas 모두 'age: 20'이란 프로퍼티를 가지고 있지만, 복사가 이루어진 후에 서로는 별도의 객체입니다. 한쪽을 수정한다고 해서 다른 쪽에 영향을 미치지 않습니다.
아래는 jone 객체의 속성을 카피한 jonas 객체를 생성하고, jonas에 job 속성을 추가했습니다.
let jone = { age: 20 };
console.log(jone.age); // -> 1
console.log(jone.job); // -> undefined
let jonas = extend({}, jone);
jonas.job = 'web developer';
console.log(jonas.age); // -> 20
console.log(jonas.job); // -> web developer
console.log(jonas.salary); // -> undefined
이번에는 jone과 같은 속성을 지닌 또 다른 객체를 생성하기 위해 Object.create 메소드를 이용해 보겠습니다. Object.create 메소드는 전달 받은 객체를 프로토타입으로 한 새로운 객체를 반환합니다.
이 방법으로 생성된 새로운 객체 julie는 jone 객체와 연관성을 가지게 됩니다. 새로운 객체 julie에서 특정 property를 찾을 수 없는 경우, jone 객체에 특정 property가 있는지 탐색을 하게 됩니다.
let julie = Object.create(jone);
julie; // -> {}
julie.job = 'designer';
console.log(julie.age); // -> 20이 출력됨
console.log(julie.salary); // -> undefined
Object.create으로 객체를 생성한 후 job property를 할당했습니다. 그리고 julie 객체에 할당하지 않은 age 속성값을 확인해 보면 20이 출력되는 것을 확인할 수 있습니다.
위와 같이 jone, jonas, julie 세 개의 객체를 만들었습니다. 이제 jone에 새로운 속성을 추가했을 때 다른 방식으로 생성한 두 객체(jonas, julie)에 어떠한 영향이 생기는지 확인해 보겠습니다.
jone.salary = 7000;
console.log(jonas.salary); // -> undefined
console.log(julie.salary); // -> 7000
jonas 객체에서는 salary 속성을 확인할 수 없고, julie 객체는 salary 속성을 확인할 수 있습니다.
위의 간단한 코드를 통해 julie 객체에 없는 속성을 검색할 때, 검색 필드를 상위 객체인 jone에 위임하는 것을 확인할 수 있었습니다. 그렇다면 jone 객체도 검색 필드를 또 다른 객체에 위임할 수 있을까요?
자바스크립트에는 모든 객체가 검색 필드를 위임할 수 있는 최상위 객체가 있습니다. 이 객체는 모든 객체에 제공되는 기본적인 메서드가 위치한 객체입니다. 이것을 우리는 object prototype이라 부릅니다. 이러한 메서드는 객체의 dot notation 또는 bracket notation을 통해서 접근하고 호출할 수 있습니다. 예로 아래와 같이 Object.prototype객체의 toString 메서드를 호출해 보았습니다.
julie.age.toString(); // -> "20"
julie.age['toString'](); // -> "20"
julie 객체에 toString이라는 함수를 명시적으로 정의하지 않았지만 위와 같이 메서드를 사용할 수 있습니다.
Object.prototype의 속성을 살펴보면 constructor 속성이 있습니다. 이 속성으로 특정 객체를 만들기 위해 어떤 함수가 사용됐는지 확인할 수 있습니다. Object.prototype.constructor가 가리키는 것은 별도의 메모리 공간에 저장된 Object(생성자 함수)입니다. Object.prototype과 이 생성자 함수를 혼동하는 경우가 많습니다.
우리가 프로그래밍하며 다루는 객체는 대부분 자체에 .constructor 속성이 없고, 프로토타입 체인을 통해서 확인할 것입니다. prototype객체의 .constructor 속성이 해당 객체를 생성한 생성자 함수를 참조하고 있습니다. 따라서, .constructor 속성은 이 생성자 함수를 반환합니다.
우리가 생성하는 대부분의 객체는 Object.prototype에 검색 필드를 위임할 수 있습니다. 하지만, 자바스크립트의 특정 객체는 일반적인 객체보다 추가된 기능을 가지고 있습니다. 예를 들어, Array 객체는 indexOf, slice 등의 메서드를 가지고 있습니다. 이러한 Array 메서드는 Array.prototype이라는 별도의 prototype에 저장돼 있습니다. 그리고 Array.prototype 또한 Object.prototype에 위임할 수 있습니다. 이렇게 함으로써 Array만의 특정 메서드를 제외한 다른 기능들에 접근이 가능합니다. Array.prototype이 Object.prototype에 검색 필드를 위임하고 있지만, Array.prototype 또한 별도의 .constructor 속성을 가지고 있습니다.
let arr = [1,2,3,4,5];
let arr.constructor // -> Array()
프로그래밍을 하면서 비슷한 목적으로 쓰인 코드가 있다면, 이 코드는 하나의 일반화된 그리고 재사용 할 수 있는 라이브러리 코드로 만들 수 있는 가능성이 있습니다.
도로에 자동차를 만들고 자동차가 움직이는 매우 간단한 프로그램을 만든다고 가정해 봅시다. 먼저 자동차를 나타내는 객체를 만들고 자동차의 위치를 나타내는 loc 속성을 추가했습니다. 그리고 각 자동차를 이동시키기 위해 loc 값을 증가시킵니다.
let audi = { loc: 1 };
audi.loc++;
let kia = { loc: 5 };
kia.loc++;
위의 코드를 보면 같은 역할을 하는 중복된 코드가 눈에 뜨입니다. 객체를 생성하는 부분이 유사하고, location 속성값을 증가시키는 코드도 중복되었습니다. 우선 중복된 loc 속성값을 증가시키는 코드를 아래와 같이 별도의 함수로 만들어 볼 수 있습니다.
let audi = { loc: 1 };
move(audi);
let kia = { loc: 5 };
move(kia);
function move(obj) {
obj.loc++;
}
위와 같이 loc 속성을 증가시키는 코드를 별도의 함수로 만들고, move 함수의 호출로 이전의 코드를 대체했습니다. 현재까지의 코드를 보고 이전의 코드와 비교를 하면 더 좋아진 점이 있는 것인지 의아하게 생각할 수 있습니다. 오히려 별도의 함수가 하나 더 생겼으니까요. 하지만 크게 두 가지 면에서 위의 재사용 코드의 장점이 있습니다.
위의 예제 코드는 매우 간단하지만 실제 프로그래밍을 하면 대부분 여러 줄의 긴 로직을 갖는 경우가 많습니다. 이 긴 코드를 한 번만 작성하고 함수 호출을 하면 프로그래밍이 간단해집니다. 또한, 함수명을 적절하게 지으면 코드 리뷰 시 어떠한 의도를 가지고 작성을 했는지 쉽게 알 수 있습니다.
위의 이유보다 더욱 중요한 점은 refactoring이 매우 쉬워진다는 것입니다. 위의 코드에는 단 두 대의 자동차만 있지만 만약 100대 1000대의 자동차가 있다고 가정해 봅시다. 이 경우 위와 같이 reuse code로 작성을 하면 move function 한 곳에서만 변경사항이 생겼을 때 수정을 하면 되기 때문에 프로그래밍의 큰 수고를 덜어줍니다.
다음으로 중복된 부분인 객체를 생성하고 속성을 할당하는 별도의 decorator function으로 만들겠습니다. 그리고 move 함수를 객체의 메소드로 호출할 수 있도록 carlike 함수에 추가해, 자동차 객체의 메서드로써 호출했습니다.
function carlike(obj, loc) {
obj.loc = loc;
obj.move = move;
return obj;
}
function move() {
this.loc++;
}
let audi = carlike({}, 1);
audi.move();
let kia = carlike({}, 5);
kia.move();
지금까지의 코드를 보면 move 함수가 글로벌 영역에 위치해야 할 필요성이 없어 보입니다. move 함수를 아래와 같이 carlike decorator function에 추가했습니다.
function carlike(obj, loc) {
obj.loc = loc;
obj.move = function() {
obj.loc++;
};
return obj;
}
두 함수로 나누어졌었던 자동차 객체와 관련된 내용이 이제 carlike 함수 한 곳에만 존재합니다. 이렇게 코드를 작성해 코드 리뷰와 관리가 더 수월해질 수 있습니다. 하지만, 반대로 자동차 instance를 생성할 때마다 별도의 메모리 영역에 각 함수가 추가로 생성이 됩니다. 이전에는 글로벌 영역의 move함수를 참조하고 있었기 때문에 하나의 함수만 존재했었습니다.
Class에 대한 기본적인 개념을 알아보겠습니다. Class란 유사한 객체를 생성해 내는 함수를 말합니다. 위에서 작성한 decorator 함수는 객체를 argument로 전달받아 그 객체를 수정 후 반환합니다. Class는 별도의 객체를 전달받지는 않고, 객체 자체를 생성합니다. Class 이름의 첫 글자는 대문자로 작성하는 것이 일반적입니다. 그리고 이렇게 여러 유사한 객체를 생성하는 함수를 contructor function이라고 합니다. contstructor function이 반환하는 객체를 instance라고 부릅니다.
위의 decorator function에 빈 객체를 전달해 속성을 변경했습니다. 이것을 애초에 빈 객체를 전달하지 않고 함수 내에서 객체를 생성할 수 있을 것 같습니다. 그리고 이렇게 자체적으로 객체를 생성하는 것이 Class와 Decorator의 차이점입니다. 이 점을 인지해서 Class 코드를 작성해 보면 아래와 같습니다.
const Car = function(loc) {
let obj = { loc: loc };
obj.move = function() {
obj.loc++;
};
return obj;
};
위에 작성한 constructor function의 단점은 instance를 생성할 때마다 새로운 move method를 별도의 메모리 영역에 할당한다는 것입니다. 이제 우리는 메모리를 아끼기 위해서 한 개의 move method만을 생성하고 이것을 Class의 모든 instance 객체가 공유하게 만들고 싶습니다.
이를 위해서 move method를 global 영역으로 다시 이동시킬 수 있습니다. move method가 global 영역으로 이동하면 closure scope로 접근 가능했던 obj 변수에 접근할 수 없습니다. 따라서 this keyword를 이용해 함수를 호출하는 객체에 접근할 수 있도록 했습니다.
const Car = function(loc) {
let obj = { loc: loc };
obj.move = move;
return obj;
};
let move = function() {
this.loc++;
};
여기까지 Functional Class pattern을 기본 개념에 대해 알아봤습니다. 하지만 위의 코드는 아직 개선의 여지가 있습니다. move method의 이름이 두 곳에서 쓰이고 있습니다. Car Class에 더 많은 method를 추가한다면, 우리는 두 영역을 왔다 갔다 하며 함수를 만들고 그 함수의 이름을 construction function 안에서 다시 불러와야 합니다. 추가하는 method가 많아질수록 실수를 할 여지도 커집니다.
이를 방지하기 위해, Global 영역의 move 함수를 객체로 묶어서 construction fucntion 내에서 iteration 또는 extend 함수로 확장하는 방법을 사용할 수 있습니다.
const Car = function(loc) {
let obj = {loc: loc};
extend(obj, methods);
return obj;
};
let methods = {
move : function() {
this.loc++:
};
};
위 methods 객체는 Car class에서 사용하기 위한 것이기에 Car class function에 묶여 있는 것이 더 좋을 것 같습니다. methods변수를 global 영역에 변수로 할당하는 대신 Car construction function의 속성으로 처리할 수 있습니다.
const Car = function(loc) {
let obj = {loc: loc};
extend(obj, Car.methods);
return obj;
};
Car.methods = {
move: function() {
this.loc++:
},
};
자바스크립트의 Class 생성에 함수는 중요한 역할을 합니다. 유사한 Interface를 가지는 여러 객체를 생성하는 함수를 Class라고 정의했습니다. 이러한 정의는 프로그래밍 언어에 따라 논란이 있지만, 저는 자바스크립트의 Class는 이 정의를 기준으로 현재 포스팅을 작성하겠습니다.
바로 위에서 Functional shared pattern에 대해 학습을 했고, 포스팅 초반에 prototype에 대해서 소개했습니다. 이제 위 코드에 prototype을 적용하면 조금더 효율적인 코드를 작성할 수 있습니다. 앞에서 배운 Object.create로 Car.methods를 프로토타입으로 둔 객체를 Car construction function 안에서 생성합니다.
const Car = function(loc) {
let obj = Object.create(Car.methods);
obj.loc = loc;
return obj;
};
Car.method = {
move: function() {
this.loc++;
}
};
Prototype 객체를 Car.methods로 지정된 객체를 생성함으로써, 우리는 loop나 extend같은 별도의 함수에 의존하지 않고 instance 객체를 생성합니다.
여기까지 Decorator pattern, Functional pattern을 거쳐 매우 길게 설명을 했지만, prototypal pattern으로 Class를 생성하는 방법을 정리해 보면 간단합니다.
위의 방식이 매우 빈번히 널리 사용되었습니다. prototype을 사용하기 위해 methods를 담고 있는 객체를 만들고, Construction Function에 그 객체의 참조값을 속성으로 추가합니다. 여러 Class를 만들고, 또 그 class로 instance 객체를 만들고 prototype을 추가한다면 위의 작업을 반복해서 진행해야 합니다.
그래서 프로그래밍 언어 설계자들은 위 작업에 도움이 되도록 공식적인 방식을 언어 자체에 도입했습니다. 함수를 만들 때마다 자바스크립트 자체에서 method를 담을 수 있는 객체를 하나 만들고 함수의 속성으로 추가해줍니다. 그리고 이 객체의 이름은 prototype이라고 주어집니다. 우리는 위에서 method라고 이름을 붙였지만, 자바스크립트에서는 prototype이라는 혼란스러운 이름을 붙였습니다.
이제 우리는 함수가 생성되면 이 prototype이라는 객체가 자동으로 생성된다는 사실을 알았습니다. 그러면 이 사실을 인지하고 위의 코드를 리팩터링 해보겠습니다.
const Car = function(loc) {
let obj = Object.create(Car.prototype); // 프로토타입으로 사용할 객체를 자동으로 생성되는 prototype으로 변경함.
obj.loc = loc;
return obj;
};
Car.prototype = {
move: function() {
this.loc++;
}
};
// ↓ 우리는 함수 생성 시 prototype 객체가 생성되는 것을 알았습니다.
// ↓ prototype 객체가 만들어져 있으니, 메소드만 추가하여 아래와 같이 리팩토링 할 수 있습니다.
Car.prototype.move = function() {
this.loc++;
};
let audi = Car(10);
audi.move();
let kia = Car(5);
kia.move();
위의 코드는 지금까지 우리가 작성한 methods 객체를, 언어 자체에서 제공하는 prototype 객체로 대체했습니다. 저는 처음에 이 'behind the scene'에서 prototype에 뭔가 특별한 특성이 가미된 것으로 추측하고 어려워했습니다. 하지만, 위의 코드에서 변경된 점은 methods라는 변수명을 prototype으로 변경한 점밖에는 없습니다. 표면적인 이름만 변경되었고 다른 특이점이 발생하지는 않습니다.
혼동하지 말아야 할 사항으로는 생성자 함수 Car 자체는 검색 필드를 Car.prototype에 위임하지 않는다는 점입니다. 예를 들어 위의 코드만 보면 Car.move 메소드를 호출할 수 없습니다. Car 함수로 생성한 객체만이 Car.prototype 객체에 검색 필드를 위임할 수 있습니다. 이것은 우리가 명시적으로 Object.create를 이용해 instance와 Car.prototype 객체에 연관성을 만들었기 때문입니다. Car 함수 자체가 연관성을 만든것은 아닙니다. 예로 아래의 함수 역시 Car.prototype에 검색 필드를 위임하는 객체를 생성합니다.
let Example = function() {
return Object.create(Car.prototype);
};
우리는 audi의 prototype은 Car.prototype이라고 할 수 있습니다. 하지만, 이 관계가 Car와 Car.prototype의 관계와는 다릅니다. Car는 function object이고, Car의 검색 필드는 function prototype으로 위임될 것입니다. 이러한 점 때문에 prototype이란 이름이 모호하게 느껴질 수 있습니다.
모든 .prototype 객체는 .constructor 속성을 가지고 있습니다. 이 속성은 prototype 객체가 종속된 함수를 가리킵니다. 따라서 Car.prototype.constructor는 Car 함수를 가리킵니다. 이 속성은 instance 객체가 어떠한 construction function에 의해 만들어졌는지 확인할 때 사용할 수 있습니다. 모든 instance 객체는 검색 필드를 prototype 객체에 위임하기 때문에, 같은 함수로 생성된 모든 instance 객체의 constructor는 같은 함수를 가리키게 됩니다.
Car.prototype.constructor; // --> Car function을 가리킴
audi.constructor; // --> Car function을 가리킴
그리고 instanceof 연산자는 오른쪽의 생성자 함수의 .prototype 객체를 왼쪽 객체의 prototype chain에서 찾을 수 있는지 확인 후 Boolean을 반환합니다.
audi instanceof Car; // --> true
Prototypal pattern으로 Car class를 만들어 보았습니다. Car.prototype 객체를 prototype으로 갖는 instance 객체를 생성하고 속성을 부여한 후 instance를 반환합니다. 이 함수에서 instance를 만드는 부분과, instance 객체를 리턴하는 부분은 모든 Class에서 동일하게 쓰일 것입니다. 그래서 이 부분을 언어 자체에서 자동적으로 처리하는 기능을 제공했습니다.
new keyword
함수를 호출하기 전 new keyword를 추가하면, 그 함수는 construction mode로 실행이 됩니다. 이 모드에서 자바스크립트 interpreter는 일시적으로 아래의 코드를 함수 바디 안 처음과 끝에 추가해 줍니다.
const Car = function(loc) {
this = Object.create(Car.prototype); // <- new keyword의 역할
// ... some codes ... //
return this // <- new keyword의 역할
};
new 키워드가 Car.prototype 객체를 prototype으로 참조하는 instance 객체를 this keyword에 할당해 주기 때문에 아래와 같이 Car class를 변경할 수 있습니다.
const Car = function(loc) {
this.loc = loc;
};
Car.prototype.move = function() {
this.loc++;
};
let audi = new Car(10);
audi.move();
let kia = new Car(5);
kia.move();
위와 같이 Pseudoclassical Pattern을 만들었습니다. Car.prototype 객체에는 일반적으로 모든 instance가 동일하게 가질 메소드를 지정할 수 있습니다. 그리고 Car 생성자 함수 안에는 instance 객체가 다른 instance와의 다른 점을 표기할 수 있습니다.
Pseudoclassical Pattern과 차이점을 보기 위해 아래 Functional Pattern 코드를 다시 가져왔습니다. Functional Pattern에서는 instance 객체의 같은 점과 다른 점을 모두 함수 안, 한 곳에 표기합니다. 이것은 장점이 될 수 단점이 될 수도 있습니다.
자바스크립트의 Class를 생성하는 여러 패턴 중 어떠한 것이 더 좋다고 단정하기는 어렵습니다. 단지, 상황에 따라 특정한 패턴이 더 좋은 수도 있고 나쁠 수도 있습니다.
지금까지 작성한 Car class는 유사한 instance 객체를 생성하기 좋습니다. 만약, 일반적인 Car의 속성을 똑같이 가지며 별도의 속성을 가지는 Class가 필요하다면 어떻게 할까요? 예로 짐을 싣는 메소드가 필요한 트럭이나, 다른 차를 세우는 기능을 하는 경찰차를 만든다고 가정해 봅시다. 단순하게 생각하면 트럭과 경찰차를 위한 별도의 Class를 생성할 수도 있습니다.
const Truck = function(loc) {
let obj = { loc: loc };
obj.move = function() {
obj.loc++;
};
obj.load = function() {
/* do something */
};
return obj;
};
const CopCar = function(loc) {
let obj = { loc: loc };
obj.move = function() {
obj.loc++;
};
obj.pullOver = function() {
/* do something */
};
return obj;
};
위의 코드는 우리가 원하는 대로 작동을 하지만 많은 내용이 중복되었습니다. 이 문제를 해결하기 위해, 두 Class에 똑같이 적용되는 상위 Class를 별도로 만들고 별도로 적용되는 특성들만 Truck, CopCar에서 처리할 수 있습니다. 상위 Class인 Car를 만들고, 그 결과물을 Truck과 CopCar Class가 이용하도록 코드를 작성해 봅니다.
const Car = function(loc) {
let obj = { loc: loc };
obj.move = function() {
obj.loc++;
};
return obj;
};
const Truck = function(loc) {
let obj = Car(loc);
obj.load = function() {
/* do something */
};
return obj;
};
const CopCar = function(loc) {
let obj = Car(loc);
obj.pullOver = function() {
/* do something */
};
return obj;
};
Superclass와 subclass를 functional pattern으로 작성해 보았습니다. Functional class로 Subclassing을 하는 것은 상대적으로 쉽습니다. 이제 마지막으로 Pseudoclassical Pattern으로 만들어 보겠습니다. 먼저 Superclass로 사용할 Car class를 작성해 봅니다.
const Car = function(loc) {
this.loc = loc;
};
Car.prototype.move = function() {
this.loc++;
};
let audi = new Car(5);
Functional Pattern과 동일한 결과를 얻기 위해, Pseudoclassica Pattern으로 subclass를 만들려면 어떻게 코드를 작성해야 할까요? 잠시 고민을 해보는 시간을 가져봅니다.. :)
const Car = function(loc) {
this.loc = loc;
};
Car.prototype.move = function() {
this.loc++;
};
const Truck = function(loc) {
/** Code here **/
};
let audi = new Car(5);
audi.move();
let lorry = new Truck(2);
lorry.move();
lorry.load();
Superclass를 Truck이라는 Subclass에서 어떻게 연관 짓는지 확인하기 전, 쉽게 잘 못 생각할 수 있는 오답을 먼저 확인해보겠습니다.
const Truck = function(loc) {
this.loc = loc;
};
let lorry = new Truck(2);
lorry.move();
lorry.load();
위와 같이 단순하게 Car class와 동일하게 this keyword만 사용하려 할 수 있습니다. 히자만 이것은 이상적이지 못 합니다. Car 함수의 내용이 중복될 뿐더러, Car class와는 연관성이 없어 Car.prototype객체를 참조할 수도 없습니다.
그리고, Truck 함수 안에서 Superclasss 함수를 호출하는 경우도 있을 수 있습니다. 이 경우도 this keyword가 어디를 가리키는지 인지를 하고 있으면 정상적으로 작동하지 않는다는 것을 알 수 있습니다.
const Truck = function(loc) {
/* 이것은 작동하지 않습니다. */
new Car(loc);
// --> Car를 호출함으로써 새 Context가 생기고 그 안에서 this는 Car의 instance를 가리킵니다.
/* 이것도 정상적으로 작동하지 않고, 이상적이지 않습니다. */
this = new Car(loc);
// --> new Truck으로 함수를 호출했기에, 함수 호출 시 this는 Truck의 instance이고, 그 위에 다시 Car의 instance객체를 덮어 씌우게 됩니다.
};
let lorry = new Truck(2);
lorry.move();
lorry.load();
잘못된 접근 방식을 확인해 봤습니다. 그러면, 어떻게 Truck class를 호출해서 Car를 Superclass로 이용할 수 있을까요?
Function method를 이용하면 Car 호출 시 this 값을 지정해 줄 수 있습니다. Call method를 이용해 Car 호출 시 this 값을 Truck의 instance 객체가 되도록 지정해 주었습니다.
const Truck = function(loc) {
// this = Object.create(Truck.prototype) ==> new keyword가 하는 일
Car.call(this, loc);
};
Call method를 사용한 지금까지 전체 코드는 아래와 같습니다.
const Car = function(loc) {
this.loc = loc;
};
Car.prototype.move = function() {
this.loc++;
};
const Truck = function(loc) {
Car.call(this, loc);
};
let lorry = new Truck(2);
lorry.move();
위의 코드를 실행하면 lorry 객체가 move 메소드를 사용할 수 있을까요? 자바스크립트가 lorry.move() 코드를 만나면 에러가 발생합니다. 위에서 lorry 객체가 검색필드를 Car.prototype 객체에 위임하지 않았습니다. 때문에, lorry 객체의 prototype chain에서 move 메서드를 찾을 수 없습니다. 현재 lorry 객체는 검색필드를 Object.prototype에 위임을 하고 있습니다.
lorry객체에서 Car.prototype의 속성에 접근을 하기 위해서 subclass Truck의 prototype과 superclass Car의 prototype을 연관 지어야 합니다. 우리는 위에서 Object.create 메소드를 이용해 특정 객체를 prototype으로 갖는 객체를 생성하는 방법을 배웠습니다. 이 메소드를 이용해 Truck의 instance 객체의 검색필드를 Car.prototype에 위임할 수 있습니다.
const Truck = function(loc) {
Car.call(this, loc);
};
Truck.prototype = Object.create(Car.prototype);
let lorry = new Truck(2);
lorry.move();
위의 작업을 하며 가장 많이 하는 실수는 아래와 같이 Car 생성자 함수의 instance 객체를 할당하는 것입니다.
const Truck = function(loc) {
Car.call(this, loc);
};
Truck.prototype = new Car();
이 방법은 잘못된 방법이지만, 과거 몇 년간 일반적으로 사용되어 지금도 이러한 코드를 어딘가에서 확인할 수 있습니다. 하지만 이 코드에는 문제점이 있습니다. Car 함수를 실행해야만 반환되는 객체를 얻을 수 있습니다. 그리고 Superclass 함수인 Car는 함수 실행 시 arguments를 전달 받아야만 하는 경우가 있습니다. arguments가 undefined일 경우 에러가 발생할 여지가 있습니다.
이제 우리는 아래와 같이 Pseudoclassical pattern으로 Superclass와 Subclass를 사용하는 방법에 대해 거의 다 학습했습니다. 하지만 한 가지 아직 처리하지 못 한 부분이 남아있습니다.
const Truck = function(loc) {
Car.call(this, loc);
};
Truck.prototype = Object.create(Car.prototype);
Truck.prototype.load = function() {
/*do something*/
};
let lorry = new Truck(2);
lorry.move();
lorry.load();
우리는 Truck 생성자 함수에 자동으로 할당된 prototype 객체를 Object.create(Car.prototype)가 반환한 객체로 덮어 씌었습니다. 앞에서 함수에 할당되는 prototype 객체는 constructor 속성이 포함돼 있다고 했습니다.
위의 코드에서 lorry.constructor 속성을 확인하면, Car 함수가 나옵니다. Object.create 메소드로 반환 받은 객체는 그 객체 자신의 constructor 속성이 없습니다. 그리고 그 위의 객체인 Car.prototype의 속성에서 constructor 속성값을 확인 후 반환합니다.
따라서, 우리는 Truck.prototype 객체가 본래 가지고 있던 이 속성을 명시적으로 지정해 주어야 합니다.
const Truck = function(loc) {
Car.call(this, loc);
};
Truck.prototype = Object.create(Car.prototype);
Truck.prototype.constructor = Truck;
Truck.prototype.load = function() {
/*do something*/
};
let lorry = new Truck(2);
lorry.move();
lorry.load();
자 지금까지 여러 방법의 코드 재사용 방법에 관래 확인했습니다. 이 포스팅을 정리하며 저도 객체 지향 프로그래밍의 기본을 다질 수 있었습니다. 누가 이 긴 글을 읽으려나 생각이 들지만, 혹시 보게 된다면 조금이나마 도움이 되었으면 좋겠네요. :)