자바스크립트는 프로토타입 기반 언어이다. 클래스 기반 언어에서는 '상속'을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻는다.
프로토타입의 개념 이해
먼저, 아래 프로토타입 도식을 통해 개념을 이해해보자.
- 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출하면
- Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스(instance)가 생성된다.
- 이때 instance에는 __proto__ 라는 프로퍼티가 자동으로 부여되는데,
- 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다.
- prototype은 객체이며, 이를 참조하는 __proto__ 역시 객체이다.
즉, 인스턴스에서도 숨겨진 프로퍼티인 __proto__를 통해 메서드들에 접근할 수 있게 된다.
var Person = function(name){
this._name = name;
};
Person.prototype.getName = function() {
return this._name;
}
var suzi = new Person('Suzi');
suzi.__proto__.getName(); // undefined
Person이라는 생성자 함수의 prototype에 getName이라는 메서드를 지정했다. 이제 Person의 인스턴스는 __proto__ 프로퍼티를 통해 getName을 호출할 수 있다.
위 코드에서 'Suzi'라는 값이 나오지 않은 것보다 에러가 발생하지 않았다는 점에 주목해본다면, getName이라는 함수가 실제로 실행되었음을 알 수 있다. 여기서 'Suzi'라는 값이 나오지 않은 이유는 this 바인딩과 관련 있다. 어떤 함수를 '메서드로서' 호출할 때는 메서드명 바로 앞의 객체가 곧 this가 된다. 즉, getName 함수 내부에서의 this는 _proto__라는 객체가 되는 것이다. 이 객체 내부에 name 프로퍼티가 없으므로 에러 대신에 undefined를 반환한 것이다.
그렇다면 만약 __ proto__ 객체에 프로퍼티가 있다면 어떨까?
var suzi = new Person('Suzi');
suzi.__proto__.name = 'SUZI__proto__'
suzi.__proto__.getName(); // SUZI__proto__
예상대로 값이 잘 출력된다. this를 인스턴스로 하기 위해서는 간단하게 __proto__를 제거하면 된다. __proto__는 원래부터 생략이 가능한 프로퍼티이기 때문이다.
이번에는 대표적인 내장 생성자 함수인 Array를 콘솔에 찍어보자
var arr = [1, 2];
console.dir(arr);
console.dir(Array);
왼쪽은 arr 변수를 출력한 결과이고, 오른쪽은 생성자 함수인 Array를 출력한 결과다. arr은 Array라는 생성자 함수를 원형으로 삼아 생성됐고, length가 2임을 알 수 있다. __proto__([[Prototype]]) 에는 옅은 색상의 다양한 배열 메서드들이 존재한다. 이는 __proto__이 Array.prototype을 참조하는데, __proto__가 생략이 가능하도록 설계돼 있기 때문에 인스턴스가 다양한 배열 메서드를 마치 자신의 것처럼 호출할 수 있다.
한편 Array의 prototype프로퍼티 내부에 있지 않은 from, isArray 등의 메서드는 인스턴스가 직접 호출할 수 없다.
색상의 차이는 { enumerable: false } 속성이 부여된 프로퍼티인지 여부에 따라 다르다. 짙은색은 enumerable 즉 열거 가능한 프로퍼티를 의미하고, 옅은색은 innumerable, 즉 열거할 수 없는 프로퍼티를 의미한다. for in 등으로 객체의 프로퍼티 전체에 접근하고자 할 때 접근 가능 여부를 나타낸다.
Constructor의 프로퍼티
생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있다. 인스턴스의 __proto__객체 내부에도 마찬가지다. 이 프로퍼티는 단어 그대로 원래의 생성자 함수(자기 자신)를 참조한다. 이는 인스턴스로부터 그 원형이 무엇인지 알 수 있는 수단이 된다.
한편 constructor는 읽기 전용 속성이 부여된 예외적인 경우(기본형 리터럴 변수 - number, string, boolean)를 제외하고는 값을 바꿀 수 있다.
또한, constructor에 접근하는 다양한 방법들도 있다.
var Person = function (name){
this.name = name;
}
var p1 = new Person('사람1'); // {name: '사람1'} true
var p1Proto = Object.getPrototypeOf(p1);
var p2 = new Person,prototype.constructor('사람2'); // {name: '사람2'} true
var p3 = new p1Proto.constructor('사람3'); // {name: '사람3'} true
var p4 = new p1.__proto__.constructor('사람4'); // {name: '사람4'} true
var p5 = new p1.constructor('사람5'); // {name: '사람5'} true
[p1, p2, p3, p4, p5].forEach(function(p) {
console.log(p, p instanceof Person);
});
p1부터 p5까지는 모두 Person의 인스턴스이다.
프로토타입 체인
메서드 오버라이드
prototype 객체를 참조하는 __proto__를 생략하면 인스턴스는 prototype에 정의된 프로퍼티나 메서드를 마치 자신의 것처럼 사용할 수 있다고 했다. 만약 인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가지고 있는 상황이라면 메서드 오버라이드, 즉 메서드 위에 메서드가 덮어씌워진다.
var Person = function (name){
this.name = name;
};
Person.prototype.getName = function (){
return this.name;
};
var iu = new Person('지금');
ui.getName = funtion() {
return '바로' + this.name;
};
console.log(iu.getName()) // 바로 지금
iu.__proto__.getName이 아닌 iu객체에 있는 getName 메서드가 호출됐다. 이런 현상을 바로 메서드 오버라이드라고 한다. 이는 원본을 제거하고 다른 대상으로 교체하는 것이 아닌 원본 위에 다른 대상을 그 위에 얹는 것이라 생각하면 된다.
'교체'가 아닌 '얹는' 것이기 때문에 메서드 오버라이딩 상태에서도 원본에 접근할 수도 있다.
console.log(iu.__proto__.getName()) // undefined
Person.prototype.name = '이지금';
console.log(iu.__proto__.getName()) // 이지금
console.log(iu.__proto__.getName.call(iu)) // 지금
- 1번째 줄에 iu.__proto__.getName을 호출했더니 undfined가 출력됐다. this가 prototype 객체를 가리키는데 prototype 상에는 name 프로퍼티가 없기 때문이다.
- 따라서 3번째 줄에 prototype.name에 값을 할당한다.
- 원하는 메서드(prototype에 있는 getName)이 잘 호출되고 있다.
- 다만 this가 prototype을 바라보고 있는데 이제 이걸 인스턴스를 바라보도록 call이나 apply를 활용한다.
프로토타입 체인
콘솔에 각각 객체와 배열을 출력했다. 배열 리터럴의 __proto__에는 여러 배열 메서드와 함께 또다시 __proto__가 존재한다. (가장 오른쪽 사진) 바로 prototype 객체가 '객체'이기 때문이다. 기본적으로 모든 객체의 __proto__에는 Object.prototype이 연결된다. 따라서 아래와 같은 도식이 나오게 된다.
어떤 데이터의 __proto__ 프로퍼티 내부에서 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라 하고, 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝이라고 한다. 이는 메서드 오버라이드와 동일한 맥락으로, 어떤 메서드를 호출하면 자바스크립트 엔진은 데이터 자신의 프로퍼티들을 검색해서 원하는 메서드가 있으면 그 메서드를 실행하고, 없으면 __proto__를 검색해서 있으면 그 메서드를 실행한다. 없으면 다시 __proto__를 검색해서 실행하는 식으로 진행된다.
이는 실제 메모리상에서 데이터를 무한대의 구조 전체를 들고 있는 것이 아니고, 사용자가 이런 루트를 통해 접근하고자 할 때 비로소 해당 정보를 얻는 것이므로 메모리가 낭비되진 않는다.
객체 전용 메서드의 예외사항
어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다. 따라서 객체에서만 사용할 메서드는 다른 여느 데이터 타입처럼 프로토타입 객체 안에 정의할 수가 없다. 객체에서만 사용할 메서드를 Object.prototype 내부에 정의한다면 다른 데이터 타입도 해당 메서드를 사용할 수 있기 때문이다.
Object.prototype.getEntries = function() {
let res = [];
for (let prop in this) {
if (this.hasOwnProperty(prop)) {
res.push([prop, this[prop]]);
}
}
return res;
};
let data = [
['object', { a: 1, b: 2, c: 3 }], //[["a",1], ["b",2],["c",3]]
['number', 345], // []
['string', 'abc'], //[["0","a"], ["1","b"], ["2","c"]]
['boolean', false], //[]
['func', function () {}], //[]
['array', [1, 2, 3]]
// [["0", 1], ["1", 2], ["2", 3]]
];
data.forEach(function(datum) {
console.log(datum[1].getEntries())
});
위 예제는 1번째 줄에서 객체에만 사용할 의도로 getEntries라는 메서드를 만들었다. 그러나 각 데이터마다 getEntries를 실행해 보니, 모든 데이터가 오류 없이 결과를 반환한다. 원래 의도대로라면 객체가 아닌 다른 데이터 타입에 대해서는 오류를 던져야하지만 어느 데이터 타입이건 무조건 프로토타입 체이닝을 통해 getEntries메서드에 접근할 수 있기 때문에 의도대로 동작하지 않는 것이다.
따라서 객체만을 대상으로 동작하는 객체 전용 메서드들은 Object.prototype이 아닌 Object에 있는 static 메서드로 부여할 수 밖에 없다.
또한 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 대상 인스턴스를 인자로 직접 주입해야 하는 방식으로 구현돼 있다.
Object.freeze(instance);
Object.getPrototypeOf(instance);
같은 이유에서 Object.prototype에는 어떤 데이터에서도 활용할 수 있는 범용적인 메서드들만 있다. toString, hasOwnProperty, valueOf, isPrototypeOf 등은 모든 변수가 마치 자신의 메서드인 것처럼 호출할 수 있다.
Object.create를 이용하면 Object.prototype의 메서드에 접근할 수 없다. Object.create(null)은 __proto__가 없는 객체를 생성한다.
let _proto = Object.create(null);
_proto.getValue = function(key) {
return this[key];
};
let obj = Object.create(_proto);
obj.a = 1;
console.log(obj.getValue('a')); // 1
console.dir(obj);
_proto에는 __proto__프로퍼티가 없는 객체를 할당했다. obj를 출력하면 __proto__에는 오직 getValue 메서드만 존재한다. 이 방식으로 만든 객체는 일반적인 데이터에서 반드시 존재하던 내장 메서드 및 프로퍼티들을 제거함으로써 기본 기능에 제약이 생겼지만 대신, 객체 자체의 무게가 가벼워짐으로써 성능상 이점이 있다.
'개발 > 프론트엔드' 카테고리의 다른 글
[JavaScript] 코어 자바스크립트 week7 - 클래스 (0) | 2024.12.03 |
---|---|
[JavaScript] 코어 자바스크립트 week5 - 클로저 (1) | 2024.11.18 |
[JavaScript] 코어 자바스크립트 week4 - 콜백함수 (0) | 2024.11.11 |
[JavaScript] 코어 자바스크립트 week3 -this (0) | 2024.11.05 |
[JavaScript] 코어 자바스크립트 week2 -실행 컨텍스트 (0) | 2024.10.28 |