일시: 2023.01.08
참석자:
금주 스터디는 윤수지님께서 발표해주셨습니다.
6. 프로토타입
자바스크립트는 프로토타입 기반(prototype-based) 언어이다. Java와 같은 클래스 기반 언어에서는 ‘상속’을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형으로 삼고 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻는다.
6-1. 프로토타입의 개념 이해
6-1-1. constructor, prototype, instance
위의 도식을 설명하면 다음과 같다.
- 어떤 생성자 함수(
Constructor
)를new
연산자와 함께 호출하면 Cosntructor
에서 정의된 내용을 바탕으로 새로운 인스턴스(instance
)가 생성- 이때
instance
에는__proto__
라는 프로퍼티가 자동으로 부여 __proto__
프로퍼티는Constructor
의prototype
이라는 프로퍼티를 참조
자바스크립트는 함수에 자동으로 객체인 prototype
을 생성해 놓는데, 해당 함수를 생성자 함수로서 사용할 경우, 즉 new
연산자와 함께 함수를 호출할 경우, 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__
가 자동으로 생성되며, 이 프로퍼티는 생성자 함수의 prototype
프로퍼티를 참조한다.
prototype
은 객체이고, 이를 참조하는 __proto__
역시 객체이다. prototype
객체 내부에는 인스턴스가 사용할 메소드를 저장하므로 인스턴스에서도 __proto__
를 통해 해당 메소드에 접근이 가능하다.
💡 NOTE
해당 챕터에서는 이해의 편의를 위해
__proto__
를 계속 사용하지만, 실무에서는 가급적__proto__
를 사용하지 않기를 권장한다. 대신Object.getPrototypeOf()
/Object.create()
를 이용할 수 있다.
var Person = function(name) {
this._name = name;
};
Person.prototype.getName = function() {
return this._name;
};
var instance = new Person('Suji');
instance.__proto__.getName(); // undefined
위의 예제에서 __proto__
를 통해 Constructor
의 prototype
프로퍼티에 접근하여 getName
이라는 메소드를 호출할 수 있는 것을 확인할 수 있다. 하지만 이때 ‘Suji’ 가 아닌 undefined
가 출력되는 이유는 this
에 바인딩된 대상이 잘못 지정되었기 때문이다.
어떤 함수를 메소드로서 호출할 때는 메소드명 바로 앞의 객체가 this가 된다. 즉, 위의 코드에서는 instance.__proto__
라는 객체가 this
로 지정되어 있는 것이다. 이 객체 내부에는 name
프로퍼티가 없으므로 해당 코드는 찾고자하는 식별자가 정의되어 있지 않을 때는 Error 대신 undefined
를 반환한다’ 는 자바스크립트의 규칙에 따라 undefined
가 반환된다.
instance.getName(); // Suji
__proto__
는 생략 가능한 프로퍼티이다. 따라서 위와 같이 코드 작성이 가능하고 이 경우 instance 객체가 getName
메소드의 this
로 바인딩되기 때문에 원하는 값을 호출할 수 있다.
즉, 생성자 함수의 prototype에 어떤 메소드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메소드나 프로퍼티에 접근할 수 있다. 하지만 생성자 함수의 프로퍼티 내부에 있지 않은 메소드 즉, 정적 메소드는 인스턴스가 직접 호출할 수 없기 때문에 생성자 함수에서 직접 접근해야 실행이 가능하다.
var arr = [1, 2];
Array.isArray(arr); // true
arr.isArray(); // TypeError: arr.isArray is not a function
6-1-2. constructor 프로퍼티
var arr = [1, 2];
Array.prototype.constructor === Array; // true
arr.__proto__.constructor === Array; // true
arr.constructor === Array; // true
var arr2 = new arr.constructor(3, 4);
console.log(arr2); // [3, 4]
생성자 함수의 프로퍼티인 prototype
객체 내부에는 constructor
라는 프로퍼티가 존재한다. 이는 생성자 함수(자기 자신)을 참조하여 인스턴스로부터 그 원형이 무엇인지를 알 수 있는 수단이 된다.
하지만 constructor
는 읽기 전용 속성이 부여된 예외적인 경우(기본형 리터럴 변수 - number, string, boolean)을 제외하고는 값을 바꿀 수 있다. 하지만 이는 constructor
가 참조하는 대상이 변경될 뿐 이미 만들어진 인스턴스의 원형이 바뀐다거나 데이터 타입이 변하는 것은 아니기 때문에 어떤 인스턴스의 생성자 정보를 알아내기 위해 constructor
프로퍼티에 의존하는 것이 항상 안전하지는 않다.
- 다음의 코드는 모두 동일한 대상을 가리킨다.
[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor
- 다음의 코드는 모두 동일한 객체(prototype)에 접근할 수 있다.
[Constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])
6-2. 프로토타입 체인
6-2-1. 메소드 오버라이드
인스턴스가 생성자와 동일한 이름의 프로퍼티 혹은 메소드를 가지고 있는 경우, 생성자가 아닌 인스턴스의 것이 호출된다. 이를 메소드 오버라이드라고 한다.
var Person = function(name) {
this.name = name;
};
Person.prototype.getName = function() {
return this.name;
};
var iu = new Person('지금');
iu.getName = function() {
return '바로 ' + this.name;
};
console.log(iu.getName()); // 바로 지금
이때, __proto__
의 메소드도 접근이 불가능한 것은 아니다. 아래와 같이 우회적인 방법으로 호출이 가능하다.
console.log(iu.__proto__getName()); // undefined
console.log(iu.__proto__getName.call(iu)); // 지금
6-2-2. 프로토타입 체인
prototype 객체는 말 그대로 ‘객체’이고, 기본적으로 모든 객체의 __proto__
에는 Object.prototype이 연결된다. 따라서 위의 그림처럼 배열 리터럴의 __proto__
안에는 또다시 __proto__
가 있으며, 이는 Object.prototype과 연결된다.
이때 __proto__
는 생략 가능하기 때문에 Object.prototype 내부의 메소드도 자신의 것처럼 실행할 수 있다.
이처럼 어떤 데이터의 __proto__
내부에 다시 __proto__
프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인(prototype chain) 이라 하고, 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝(prototype chaining) 이라고 한다. 이는 앞선 메소드 오버라이딩과 동일한 맥락으로 가까운 __proto__
를 먼저 검색해서 실행한다.
var arr = [1, 2];
Array.prototype.toString.call(arr); // 1,2
Object.prototype.toString.call(arr); // [object Array]
arr.toString(); // 1,2
arr.toString = function() {
return this.join('_');
};
arr.toString(); // 1_2
💡 INFO
각 생성자 함수는 모두 함수이기 때문에 Function 생성자 함수의 prototype과 연결된다. Function 생성자 함수 역시 함수이므로 다시 Function 생성자 함수의 prototype과 연결된다. 이런 식으로 재귀적으로 반복하는 루트를 끝없이 찾아갈 수 있지만, 이는 실제 메모리 상에서 무한대의 구조 전체의 데이터를 들고 있는 것이 아니고 사용자가 이런 루트를 통해 접근하고자 할 때 비로소 해당 정보를 얻을 수 있을 뿐이다.
6-2-3. 객체 전용 메소드의 예외사항
어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재한다. 따라서 객체에서만 사용할 메소드는 다른 데이터 타입처럼 prototype 객체 안에 정의할 수 없다. Object.prototype 내부의 메소드는 어떤 데이터 타입에서도 접근하여 사용할 수 있기 때문이다.
그래서 객체 전용 메소드들은 정적 메소드(static method)로 생성하며, 생성자 함수은 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 대상 인스턴스를 인자로 직접 주입해야 하는 방식으로 구현되어 있다.
💡 INFO
Object.create(null)
를 이용하면Object.prototype
의 메소드에 접근할 수 없는 경우가 있다.
이 방식으로 만든 객체는 일반적인 데이터에 반드시 존재하던 내장(built-in) 메소드 및 프로퍼티들이 제거됨으로써 기본 기능에 제약이 생긴 대신, 객체 자체의 무게가 가벼워짐으로써 성능상 이점을 가진다.
var _proto = Object.create(null);
_proto.getValue = function(key) {
return this[key];
};
var obj = Object.create(_proto);
obj.a = 1;
console.log(obj.getValue('a')); // 1
console.dir(obj); // __proto__ 에 getValue 메소드만 존재
6-2-4. 다중 프로토타입 체인
자바스크립트의 기본 내장 데이터타입들은 모두 프로토타입 체인이 1단계(객체)거나 2단계로 끝나는 경우만 있지만, 사용자가 새롭게 만드는 경우에는 대각선의 __proto__
를 연결하기만 하면 무한대로 체인 관계를 이어나갈 수 있다.
방법은 __proto__
가 가리키는 대상, 즉 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게 하면 된다.
var Grade = function() {
var args = Array.prototype.slice.call(arguments);
for (var i = 0; i < args.length; i++) {
this[i] = args[i];
}
this.length = args.length;
};
var g = new Grade(100, 80);
변수 g는 Grade 인스턴스를 바라보고, Grade 인스턴스는 유사배열 객체이다. Grade가 배열 메서드를 직접 쓸 수 있게끔 만들고 싶다면 g.proto 즉, Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.
Grade.prototype = [];
6-3. 정리
- 생성자 함수를 new 연산자와 함께 호출하면 Constructor에 정의된 내용을 바탕으로 새로운 인스턴스가 생성된다. 해당 인스턴스에는
__proto__
라는, Constructor의 prototype 프로퍼티를 참조하는 프로퍼티가 자동으로 부여된다. __proto__
는 생략 가능한 속성이기 때문에, 인스턴스는 Constructor.prototype의 메소드를 마치 자신의 메소드인 것처럼 호출 가능하다.- Constructor.prototype에는 constructor라는 프로퍼티가 있는데, 이는 생성자 함수 자기 자신을 가리켜서 인스턴스가 자신의 생성자 함수를 알고자 할 때 사용된다.
__proto__
안에 다시__proto__
를 찾아가는 과정을 프로토타입 체이닝이라고 하며, 이 프로토타입 체이닝을 통해 각 프로토타입 메소드를 자신의 것처럼 호출할 수 있다. 프로토타입의 최상단에는 항상 Object.prototype이 존재한다.- Object.prototype에는 모든 데이터 타입에서 사용할 수 있는 범용적인 메소드만이 존재하며, 객체 전용 메소드는 Object 생성자 함수에 static하게 담겨있다.
- 프로토타입 체인은 무한대의 단계를 생성할 수 있다.
'4-1기 스터디 > Javascript-Typescript Deep Dive' 카테고리의 다른 글
[JS Deep Dive 스터디] 7주차 - 클래스 (1) | 2023.01.26 |
---|---|
[JS Deep Dive 스터디] 5주차 - 클로저 (0) | 2023.01.25 |
[JS Deep Dive 스터디] 4주차 - 콜백 함수 (1) | 2023.01.25 |
[JS Deep Dive 스터디] 3주차 - this (0) | 2023.01.25 |
[JS Deep Dive 스터디] 2주차 - 실행 컨텍스트 (0) | 2022.11.06 |
댓글