어떤 객체를 원형으로 삼고, 이를 복제(참조)함으로 상속과 비슷한 효과를 얻는 언어
01. 프로토타입의 개념 이해
6-1-1. constructor , prototype, instance
<<< 그림 첨부 >>>
해당 이미지는 아래의 코드를 추상화한 것이다.
var instance = new Constructor();
- 생성자 함수를 new 연산자와 함께 호출
- Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스 생성
- 인스턴에 __proto__ 라는 프로퍼티 자동 부여
- __proto__는 Constructor의 prototype이라는 프로퍼티를 참조
Constructor에는 prototype이라는 객체가 존재하는데, 이는 내부에 인스턴스가 사용할 메서드를 저장한다. Constructor를 new와 함께 호출해 instance를 생성하면 자동으로 __proto__ 라는 프로퍼티가 내부에 생성되는데 , 이는 Constructor의 prototype 객체를 참조한다.
__proto__는 객체이며 , 인스턴스가 prototype에서 정의된 메서드에 접근할 수 있게 해주는 역할을 한다.
*__proto__ 를 읽을 때는 'dunder proto', '던더 프로토'
* 참조
ES5.1에는 __proto__가 아닌 [[prototype]]이라는 명칭으로 정의되어있다.
__proto__ 프로퍼티는 브라우저가 [[prototype]]을 구현한 대상에 지나지 않는다.
본래 instance.__proto__ 를 통해 직접적으로 접근하는 방식은 제한되었고, 그 대신에 Object.getPrototypeOf(instance)/Refelect.getPrototypeOf(instance)를 통해서 접근할 수 있도록 정의했었다.
차후 브라우저들이 __proto__에 직접 접근 방식을 포기하지 않아, ES6부터는 레거시코드에 대한 호환성 유지 차원에서 정식적으로 인정했다.
하지만 호환성을 위한 지원일뿐, 권장되는 방법은 아니다.
되도록이면 Object.getPrototypeOf()/Object.create() 등의 접근방식을 이용하자.
예제를 통해 살펴보자.
6-1
var Person = function(name) {
this._name = name;
};
Person.prototype.getName = function() {
return this._name;
};
Person이라는 생성자 함수를 정의하고, prototype에 getName이라는 메서드를 지정했다.
이제 Person을 통해 만든 인스턴스는 __proto__ 프로퍼티를 통해 getName에 접근이 가능하다.
var suzi = new Person('suzi');
suzi.__proto__.getName(); // undefined
인스턴스의 __proto__가 생성자의 prototype을 참조해 결국 둘은 같은 객체를 바라보기 때문이다.
Person.prototype === suzi.__proto__ //true
위 코드에서 메서드 호출 결과가 'undefined'로 나온 점을 주목하자. 에러코드가 뜨지않았다는 것은 호출이 가능한 함수라는 뜻이다. 호출할 수 없는 함수가 실행되었다면 TypeError가 발생했을 것이다. 'undefined'는 getName 함수가 잘 실행된 결과 값이다.
결론부터 말하면 해당 코드의 'undefined'의 원인은 this에 바인딩된 대상이 잘못 지정되었기 때문이다. Person 생성자는 this.name 값을 리턴하도록 되어있는데, 해당 메서드를 호출하고 있는 메서드 바로 앞의 객체가 __proto__이다. 따라서 this는 suzi가 아니라 suzi.__proto__가 객체가 된다.
__proto__ 에 찾고자하는 식별자가 정의되어있지 않아 자바스크립트 규약에 의해 undefined가 반환된 것이다.
만약 __proto__ 객체에 name 값이 존재한다면?
var suzi = new Person('Suzi');
suzi.__proto__.name ='Suzi_proto_';
suzi.__proto__.getName(); //Suzi_proto_
결과 값이 잘 출력될 것이다. 따라서 this 바인딩이 중요하다. this에 인스턴스를 바인딩 하고싶다면 인스턴스에서 바로 메서드를 사용하면 된다.
var suzi = new Person('Suzi', 28);
suzi.getName(); // Suzi
var iu = new Person('Jieun', 28);
iu.getName(); // Jieun
이것이 가능한 이유는 __proto__이 생략 가능한 프로퍼티이기 때문이다!
suzi.__proto__.getName
-> suzi(.__proto__).getName
-> suzi.getName
<< 이미지 첨부 >>
new 연산자로 Constructor를 호출하면 instance가 만들어지고, 이 instance의 생략가능한 __proto__ 프로퍼티는 Constructor의 prototype을 참조한다.
>> 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있다!
크롬 개발자 도구의 콘솔 탭에서 출력 결과를 살펴보자.
6-2
var Constructor = function(name) {
this.name = name ;
}
Constructor.prototype.method1 = function() {};
Constructor.prototype.property1 = 'Constructor Prototype Property';
var instance = new Constructor('instance');
console.dir(Constructor);
console.dir(instance);
*참조
콘솔 창에서 색상 차이는 { enumerable : false } 속성이 부여된 프로퍼티인지 여부를 따른다.
짙은 색 : enumerable , 열거 가능한 프로퍼티
옅은 색 : innumerable , 열거할 수 없는 프로퍼티
for in 등으로 객체의 프로퍼티에 접근하고자 할때 접근 가능 여부에 따라 색상이 달라진다.
instance의 구조 출력시 결과로 Constructor가 나온다.
생성자 함수의 인스턴스는 해당 생성자 함수의 이름을 표기함으로써 해당 함수의 인스턴스임을 표기하고 있다.
콘솔창을 통해 instance의 __proto__가 Constructor의 prototype과 동일한 내부를 가지고 있는 걸 확인할 수 있다.
대표적인 내장 생성자 함수 Array를 바탕으로 다시 한 번 살펴보자.
var arr = [1,2]
console.dir(arr);
console.dir(Array);
var arr = [1,2];
arr.forEach(function () {}); // (0)
Array.isArray(Arr); // (0) true
arr.isArray(); // (X) TypeError : arr.isArray is not a function
6-1-2 constructor 프로퍼티
6-2 프로토타입 체인
6-2-1 메서드 오버라이드
6-2-2 프로토타입 체인
객체의 내부구조를 살펴보자.
console.log({ a : 1});
첫줄은 해당 객체가 Object 객체의 인스턴스임을 표시하고, 둘째줄에는 객체의 프로퍼티인 a:1 보인다.
__proto__ 내부의 constructor는 생성자 함수 Object를 가리키고 있고, 그 밑으로 익숙한 메서드들이 나열되어있는 것이 확인된다.
다음으론 배열의 구조를 살펴보자.
condole.dir([1,2]);
배열의 __proto__ 내부에는 pop, push 등 익숙한 배열 메서드와 constructor가 있고, 추가로 __proto__가 다시 등장한다.
두번째 __proto__는 위에서 언급한 객체의 __proto__와 같은 형태를 가진다.
왜냐하면 prototype 객체가 '객체'이기 때문이다!
__proto__는 prototype을 참조하고, prototype은 객체이기 때문!
모든 __proto__ 에는 Object.prototype이 연결되고, prototype도 객체이기 때문에 예외가 없다.
__proto__는 생략이 가능해서 배열이 Array.prototype 내부의 메서드를 자기 자신의 것처럼 실행가능하다. 같은 원리로 Object.prototype 내부의 메서드도 자신의 것처럼 실행가능하다. __proto__ 내부의 __proto__ 또한 생략이 가능하기 때문이다.
var arr = [1,2];
arr(.__proto__).push(3); // 배열의 prototype
arr(.__proto__)(.__proto__).hasOwnProperty(2); // true , 배열이 참조하는 객체의 prototype
어떤 데이터의 __proto__ 프로퍼티 내부에서 다시 __proto__ 프로퍼티가 연쇄적으로 이어지는 거을 프로토타입 체인(prototype chain)이라고 하고, 이를 따라가며 검색하는 것을 프로토타입 체이닝(prototype chaining)이라고 한다 .
체이닝은 오버라이드와 유사하게 작동한다.
어떤 메서드를 호출시 자신의 프로퍼티 내에서 검색하고 없으면 __proto__를 검색하고, 또 없으면 그 다음 __proto__를 검색해서 실행한다.
var arr = [1,2]
Array.prototype.toString.call(arr) // 1,2
Object.prototype.toString.call(arr) // [object Array]
arr.toString();
arr.toString = function(){
return this.join('_');
};
arr.toString(); // 1,2
위 예제에서 arr 변수는 배열이기 때문에 arr.__proto__는 Array.prototype을 참조하고, Array.prototype은 객체이기 때문에 Array.prototype.__proto__는 Object.prototype을 참조하게 된다.
배열과 객체 prototype에는 각각 toString 메서드가 존재하는데, 어떤 메서드를 호출할때 가장 가까운 곳에 정의된 메서드를 사용하여 arr.toString()을 호출하면 Array.prototype.toString.call(arr)와 같은 값을 낸다.
arr 내부에 직접 toString을 오버라이드하면, 자기 자신의 메서드가 수행되어 다른 값을 나타내게 된다.
자바스크립트의 모든 데이터는 Object.prototype을 최상위에 둔다.
* 참조하면 되는 부분
instance 중심으로 __proto__를 따라가는 루트뿐만 아니라, 접근 가능한 모든 경우를 표기한다면?
1. 생성자 함수는 모두 함수라서 Function 생성자 함수의 prototype과 연결.
2. Function 생성자 함수 역시함수라서 Function 생성자함수의 prototype과 연결. (여기서부터 재귀적으로 끝없이 반복)
메모리상에서 실제 데이터를 무한대로 돌고있는건 아니고, 사용자가 접근했을 경우에만 정보를 얻을 수 있다.
어떤 인스턴스의 생성자 함수를 알고싶을때가 아니면 접근도가 낮으니, 인스턴스와 직접적 연결이 있는 그림에만 주목하자.
6-2-3 객체 전용 메서드의 예외사항
어떤 생성자 함수던 prototype은 반드시 객체이다. 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다. 따라서 객체 안에서만 사용할 프로퍼티나 메서드는 prototype 객체 안에 정의하면 안된다.
prototype의 내부에 정의한 순간, 모든 데이터 타입들이 사용할 수 있게 되기 때문이다.
Object.prototype.getEntries = function() { //객체에서만 사용할 의도로 만든 function
var res = [];
for (var prop in this) {
if (this.hasOwnProperty(prop){
res.push([prop, this[prop]])
}
}
return res;
}
var data = [
['Object', {a:1,b:2,c:3}] // [["a":1],["b":2],["c":3]],
['name' , 345 ] // []
['string' , 'abc' ] //[["0":"a"], ["1":"b"], ["2":"c"]]
['boolean' , false ] // []
['func' , finction() {}] //[]
['array' , [1,2,3,4,5] ] // Array Iterator {}
data.forEach(function (datum) {
console.log(datum[i].getEntries());
})
]
객체에서만 사용하고자 함수를 하나 만들었다. Object.prototype 내부에 정의를 했더니, 객체 뿐만 아니라 아래의 모든 데이터에서 오류가 뜨지 않고 작동한다.
모든 데이터 타입들이 프로토타입 체이닝을 통해 마지막엔 객체의 prototype에 접근하기 때문이다!
객체에서만 사용할 수 있는 전용 메서드를 만드려면 prototype 내부가 아닌 Object 자체에 static method로 부여해야한다.
생성자 함수인 Object와 객체 리터럴 사이는 this를 통한 연결이 불가능하다. ( __proto__에 정의되어있지 않기 때문에?)
this를 사용하는 대신, 대상 인스턴스를 인자로 전달해야한다.
이것은 모두 객체가 모든 데이터들이 __proto__를 반복해 접근할 수 있는 최상위 존재이기 때문이다.
이와 같은 이유로 모든 데이터에서 사용가능한 메서드들만 객체의 prototype에 정의되어있다.
정리하자면,
1. 객체 전용 메서드는 객체의 prototype이 아닌 객체 자체에 정의해야한다 (__proto__로 접근이 안되게)
2. 객체의 prototype에는 모든 데이터타입에서 사용가능한 범용적 메서드들만 정의되어있다.
*참조
예외적으로 Object.create를 이용하면 Object.prototype의 메서드에 접근이 불가능한 객체를 생성이 가능하다.
Object.create(null)은 __proto__가 없는 객체를 만든다.
이 방식으로 만든 객체는 내장메서드 및 프로퍼티가 제거되어, 기본기능 활용이 안되는 대신 객체 자체의 무게가 아주 가벼워진다.
6-2-4 다중 프로토타입 체인
__proto__를 연결해나가면 무한대로 체인 관계를 만들 수 있다. __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 = arg.length;
};
var g = new Grade(100,80);
Grade의 인스턴스는 유사배열 객체이다. 유사배열객체에 배열 메서드를 적용하는 방법으론 call/apply가 있지만 인스턴스가 직접 배열 메서드를 사용할 수 있도록 하기 위해선 g.__prototype, 즉 Grade.prototype이 배열 인스턴스를 바라보게 만들면 된다.
Grade.prototype = []
위의 명령으로 아래와 같은 결과가 나오고, g에서 배열 메서드를 사용할 수 있게된다.
<<그림>>
console.log(g); //Grade(2) {100,80}
g.pop();
console.log(g); // Grade(1) {100}
g.push(90);
console.log(g); // Grade(2) {100, 90}
g는 프로토타입 체인에 의해 자기자신, Grade의 prototype, Array의 prototype, Ojbect의 prototype에 접근할 수 있게 되었다.
'프로그래밍 > javascript&typescript' 카테고리의 다른 글
Typescript (0) | 2021.09.09 |
---|---|
프론트엔트 개발환경이 복잡해진 이유 (0) | 2021.09.08 |
INTRO (0) | 2021.09.06 |
데이터 타입 (0) | 2020.03.26 |
07. 클래스 (0) | 2020.03.10 |