Skip to content

Latest commit

 

History

History
489 lines (357 loc) · 21 KB

프로토타입.md

File metadata and controls

489 lines (357 loc) · 21 KB

프로토타입

  • 자바스크립트는 프로토타입 기반 언어이다.
  • 프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻음
✅ 즉, 데이터 자신에게는 메서드들이 없지만, 생성자함수의 prototype 프로퍼티에 있는 것 [[Prototype]]라는 연결 통로에 의해 마치 자신의 것처럼 메서드들을 쓸 수 있다.

프로토타입의 개념 이해

1) constructor, prototype, instance

image

image

var instance = new Constructor();
  • 6-2의 윗변(실선)의 왼쪽 꼭짓점에는 Constructor(생성자 함수)를, 오른쪽 꼭짓점에는 Constructor.prototype이라는 프로퍼티가 위치하고 있다.
  • 왼쪽 꼭짓점으로부터 아래를 향한 화살표 new가 있고, 화살표 종점에는 instance가 있다.
  • 오른쪽 꼭짓점으로부터 대각선 아래로 향하는 화살표의 종점에는 instance.proto이라는 프로퍼티가 위치하고 있다.

Array 프로토타 예시)
RubberDuck

RubberDuck

  • 왼쪽에 있는 new Array를 오른쪽의 그림 처럼 도식화 하였다.

프로토타입 흐름

  • 어떤 생성자 함수 된 내용을 바탕으로 새로운 **인스턴스(instance)**가 생성된다.
  • 이때 instance에는 proto라는 프로퍼티가 자동으로 부여되는데,
  • 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다.

prototype과 proto

  • prototype은 객체이고, 이를 참조하는 proto도 객체이다.
  • prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장함
  • 그러면 인스턴스도 숨겨진 프로퍼티인 proto를 통해 이 메서드들에 접근할 수 있게된다.
📢 참고 [[Prototype]] 와 __proto__는 같다. 또한, __proto__보다는 **Object.getPrototypeOf를 써라**

Person.prototype - Person 생성자 함수의 prototype에 getName메서드를 지정한 예시 )

var Person = function (name) {
	this._name = name;
};
Person.prototype.getName = function() {
	return this._name;
};

var suzi = new Person('Suzi');
suzi.__proto__.getName(); // undefined
  • Person의 인스턴스는 proto 프로퍼티를 통해 getName을 호출할 수 있다.
  • 왜냐하면 instance의 proto가 Constructor의 prototype 프로퍼티를 참조하므로 결국 둘은 같은 객체를 바라본다.

Person.prototype === suzi.proto  **//**true
  • 위 코드 메서드 호출 결과로 undefined가 나왔다.
    • 어떤 변수를 실행해 undefined가 나왔다는 것은 이 변수가 ‘호출할 수 있는 함수’에 해당한다는 것을 의미한다.
    • 만약 실행할 수 없는 즉, 함수가 아닌 다른 데이터 타입이었다면 TypeError가 발생했을 것이다.
    • 어떤 함수를 ‘메서드로서’ 호출할 때는 메서드명이 앞의 객체가 곧 this가 된다.
      • 예시) thomas.proto.getName();에서 getName함수 내부에서의 this는 thomas이 아니라 thomas.proto라는 객체가 되는 것이다.
  • 위 코드 객체 내부에는 name 프로퍼티가 없으므로 ‘찾고자 하는 식별자가 정의돼 있지 않을 때는 Error 대신 undefined를 한다’라는 자바스크립트 규약에 의해 undefined가 반환됨

proto 객체에 name 프로퍼티 있을때 예시 )

var Person = function (name) {
  this.name = name;
};
Person.prototype.getName = function () {
  return this.name;
};
var suzi = new Person("Suzi");
suzi.__proto__.name = "SUZI__proto__";
suzi.__proto__.getName(); // SUZI__proto__
  • SUZIproto가 출력된다.

proto없이 객체에서 곧바로 메서드를 사용 예시 )

var Person = function (name) {
  this.name = name;
};
Person.prototype.getName = function () {
  return this.name;
};

var suzi = new Person("Suzi", 28);
suzi.getName(); // 'Suzi'
var iu = new Person("Jieun", 28);
iu.getName(); // 'Jieun'
  • proto를 빼면 this는 instance가 된다. 그 이유는 바로 proto생략 가능한 프로퍼티 이기 떄문이다.
    • 이유가 어찌됐던 proto가 생략 가능하다는 점을 기억하면 된다.

proto생략

suzi.__proto__.getName
-> suzi(.__proto__).getName
-> suzi.getName
  • proto를 생략하지 않으면 this는 suzi.proto를 가리키지만, 이를 생략하면 suzi을 가리킨다.
    • suzi.__proto에 있는 메서드인 getName을 실행하지만 this는 suzi을 바라보게 된다.

image

  • 함수에 자동으로 객체인 prototype 프로퍼티를 생성해 놓는데, 해당 함수를 생성자 함수로써 사용할 경우, 즉 new 연산자와 함께 함수를 호출할 경우, 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 proto가 자동으로 생성되며, 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다.
  • proto 프로퍼티는 생략 가능하도록 구현돼 있기 때문에 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 된다.

prototype과 proto 예시)

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);

Constructor의 디렉터리 구조 결과)

RubberDuck

  • Constructor의 디렉터리 구조 출력 결과의 첫 줄에는 함수를 의미하는 f와 함수 이름인 Constructor, 인자 name이 있다.
  • 그 내부에는 옅은 색의 argumnets, caller, length, name, prototype, proto등의 프로퍼티들이 있다.
  • prototype을 열면
    • 짙은 색 - method1, property1이 있고,
    • 옅은 색 - constructor, proto이 있다.
  • 짙은색 : enumerable, 열거 가능한 프로퍼티를 의미함
  • 옅은색 : innumerable, 열거할 수 없는 프로퍼티를 의미함
    • for in 등으로 프로퍼티 전체에 접근하고자 할 때 접근 가능 여부를 색상으로 구분 지어 표기하는 것이다.

2) constructor 프로퍼티

  • 생성자 함수의 프로퍼티인 prototype 객체 내부에 constructor라는 프로퍼티가 있다. 인스턴스의 proto도 마찬가지이다.
  • constructor 프로퍼티는 원래의 생성자 함수(자기 자신)을 참조한다.
    • 자신을 참조하는 이유는 인스턴스로부터 그 원형이 무엇인지 알 수 있는 수단이다.

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]
  • 인스턴스의 proto가 생성자 함수의 prototype 프로퍼티를 참조하며 proto가 생략 가능하기 때문에 인스턴스에서 직접 constructor에 접근할 수 있는 수단이 생기는 것이다.

다양한 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의 인스턴스이다.

image

  1. 각 줄은 모두 동일한 대상을 가리키는 코드이다.
[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor
  1. 모두 동일한 객체(prototype) 접근 가능함
[Constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])

프로토타입 체인

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()); // 바로 지금
  • 위 코드는 iu.proto.getName이 아닌 iu변수가 가리키는 Person객체 내 getName메서드를 호출되었다.

    • 위는 메서드 오버라이드가 발생하였다.
  • 메서드 오버라이드에서는 ‘교체’와 ‘얹는’ 것을 구분해야함

    • 교체하는 형태 : 원본에 접근할 수 없는 형태가 됨
    • 얹는 형태 : 원본이 아래에 유지되고 있으니 원본에 접근할 수 있음
  • 그렇다면 메서드 오버라이딩이 이뤄져 있는 상황에서 prototype에 있는 메서드에 접근하려면 어떻게 해야할까?

console.log(iu.__proto__.getName()); // undefined
  • iuproto.getName()을 호출했더니 undefined가 출력됨
    • this가 prototype 객체(iu.proto)를 가리키는데 prototype 상에는 name 프로퍼티가 없기 때문이다.

ⓑ prototype에 name 프로퍼티가 있다면 예시)

var Person = function(name) {
	this.name = name;
};
Person.prototype.getName = function (){
	return this.name;
};

**var iu = new Person('지금');
iu.getName = function() {
	return '바로' + this.name;
};**
Person.prototype.name = '이지금';
console.log(iu.__proto__.getName()); //이지금
  • prototype에 name 프로퍼티가 있다면 객체 내 prototype.getName의 값이 출력된다.
  • ⓐ와 ⓑ예시는 상반된 설명과 결과이다.
  • 위 코드에서는 this가 prototype을 바라보고 있는데 이걸 인스턴스를 바라보도록 바꿔줄 수 도 있다.

⇒ call, apply를 활용하는 것이다.

call, apply를 활용 예시)

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.__proto__.getName.call(iu)); // 지금
  • 위 코드는 call, apply를 활용해서 iu객체 내 getName메서드를 사용하였음
    • 오버라이드된 경우 자신에 가장 가까운 메서드에만 접근할 수 있지만,
    • 그 다음으로 가까운 proto의 메서드도 우회적인 방법을 통해 접근 가능하다.

2) 프로토타입 체인

  • 프로토타입 체인 : 어떤 데이터의 **proto** 프로퍼티 내부에 다시 __**proto__** 프로퍼티가 연쇄적으로 이어진 것
  • 프로토타입 체이닝 : 프로토타입 체인을 따라가며 검색하는 것
    • 진행방법) 어떤 메서드를 호출하면 자바스크립트 엔진은 데이터 자신의 프로퍼티들을 검색해서 원하는 메서드가 있으면 그 메서드를 실행하고, 없으면 proto를 검색해서 실행한다

배열의 내부 구조 예시)
RubberDuck

RubberDuck

  • 배열 리터럴의 proto에는 pop, push 등의 배열 메서드가 있고, constructor도 있다.
  • proto 안에는 또다시 proto가 존재한다. 이는 객체의 proto와 동일한 내용을 가진다. 왜냐하면 prototype 객체가 ‘객체’이기 때문이다.
    • 모든 객체의 proto에는 Object.prototype이 연결된다.

위 예제 도식화)

image

  • Array.prototype 내부 메서드를 마치 자신의 것처럼 실행할 수 있고, Object.prototype 내부의 메서드도 자신의 것처럼 실행할 수 있다.
  • 생략 가능한 proto를 따라가면 Object.prototype을 참조할 수 있기 때문이다.

배열에서 배열 메서드 및 객체 메서드 실행 예시)

var arr= [1,2]
arr(.__proto__).push(3); // [1,2,3]
arr(.__proto__)(.__proto__).hasOwnProperty(2); //true

// var arr= [1,2];
// arr.push(3);
// arr.hasOwnProperty(2);
  • 어떠한 데이터 proto 프로퍼티 내부에서 다시 proto 프로퍼티가 연쇄적로 이어진것을 프로토 타입 체인이라고 하고, 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝 이라고 함
    • 프로토타입 체이닝 : 어떤 메서드를 호출하면 자바스크립트 엔진은 데이터 자신의 프로퍼티들을 검색해서 원하는 메서드가 있으면 그 메서드를 실행하고, 없으면 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
  • arr 배열 변수이고, arr.proto는 Array.prototype을 참조하고, Array.prototype는 객체이므로 Array.prototype.proto는 Object.prototype을 참조한다.
  • toString 메서드는 Array.prototype뿐 아니라 Object.prototype에도 있다.
  • ⓐⓑ 둘 중 어떤 값이 출력되는지 확인하기 위해서 Array, Object의 각 프로토타입에 있는 toString 메서드를 arr에 적용하였다. 그 결과 Array.prototype.toString을 적용한 것과 동일한 결과가 나왔다.
  • ⓒ는 Array.prototype.toString이 아닌 arr.toString이 실행되었다.

데이터 타입별 프로토타입 체인 도식화 예시)

image

  • 자바스크립트 데이터는 모두 도식화 그림처럼 동일한 형태의 프로토타입 체인 구조를 지닌다.

3) 객체 전용 메서드의 예외사항

image

  • 어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재함

  • 따라서 객체에서만 사용할 메서드는 다른 여느 데이터 타입처럼 프로토타입 객체 안에 정의할 수 없다.

    • 객체에서만 사용할 메서드를 Object.prototype 내부에 정의한다면 다른 데이터 타입도 해당 메서드를 사용할 수 있게 된다.

Object.prototype에 추가한 메서드의 접근 예시)

ⓐ Object.prototype.getEntries = 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]]
	['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());
});
  • ⓐ는 객체에서만 사용할 의도로 getEntries라는 메서드를 만들었다.
    • ⓑ의 forEach에 따라 ⓒ부터 ⓓ까지 각 데이터마다 getEntries를 실행하니, 모든 데이터가 오류 없이 결과를 반환함
    • 원래 의도는 객체가 아닌 다른 데이터 타입에 대해서는 오류를 던지게끔 해야하는데, 어느 데이터 타입이건 거의 무조건 프로토타입 체이닝을 통해 getEntries 메서드에 접근할 수 있어서 오류를 던지지 않았다.
  • 이 같은 이유로 객체만을 대상으로 동작하는 객체 전용 메서드들은 Object.prototype이 아 Object에 스태틱 메서드(static method)로 부여해야한다.

4) 다중 프로토타입 체인

  • 자바스크립트의 기본 내장 데이터 타입들은 모두 프로토타입 체인이 1단계(객체)이거나, 2단계(나머지)로 끝나는 경우만 있었지만 사용자가 새롭게 만드는 경우에는 그 이상도 얼마든지 만들 수 있다.

    • 대각선 proto를 연결해나가면 무한대로 체인 관계를 이어나갈 수 있다.
  • 대각선의 proto를 연결하는 방법은 proto가 가리키는 대상, 즉 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게끔 해주면 된다.

    • 예제 참고

Grade 생성자 함수와 인스턴스 예시)

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,100);
  • 변수 g는 Grade의 인스턴스를 바라본다.
    • Grade의 인스턴스는 여러 개의 인자를 받아 각각 순서대로 인덱싱해서 저장하고, length 프로퍼티가 존재하는 등으로 배열의 형태를 지니지만, 배열의 메서드를 사용할 수 없는 유사배열객체가 된다.
    • 인스턴스에서 배열 메서드를 직접 쓸수 있게끔 만들어준다.
      • 그러기 위해서는 g.proto, 즉 Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.
Grade.prototype = [];
  • 이 명령에 의해 그림 6-13과 같이 서로 별개로 분리돼 있던 데이터가 연결되어 6-14와 같이 하나의 프로토타입 체인 형태를 띠게됨

image

image

  • 이제 Grade인스턴스인 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 인스턴스의 입장에서는 프로토타입 체인에 따라 g 객체 자신이 지니는 멤버, Grade의 prototype에 있는 멤버, Array.prototype에 있는 멤버, 끝으로 Object.prototype에 있는 멤버에까지 접근할 수 있게됨

정리

  • 어떤 생성자 함수를 new 연산자와 함께 호출하면 Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스가 생성되는데, 이 인스턴스에는 proto라는, Constructor의 prototype프로퍼티를 참조하는 프로퍼티가 자동으로 부여된다.

  • proto는 생략 가능한 속성이고, 인스턴스는 Constructor.prototype의 메서드를 마치 자신의 메서드인 것처럼 호출할 수 있다.

  • Constructor.prototype 에는 constructor라는 프로퍼티가 있는데, 이는 다시 생성자 함수 자신을 가리킨다. 이 프로퍼티는 인스턴스가 자신의 생성자 함수가 무엇인지를 알고자할 때 필요한 수단이다.

  • 직각삼각형의 대각선 방향, 즉 proto 방향을 계속 찾아가면 최종적으로 Object.prototype에 도달하게된다.

  • 프로토타입 체이닝 : proto안에 다시 proto를 찾아가는 과정

    • 프로토타입 체이닝을 통해 각 프로토타입 메서드를 자신의 것처럼 호출할 수 있다.
    • 이때 접근 방식은 자신으로부터 가장 가까운 대상부터 점차 먼 대상으로 나아가며, 원하는 값을 찾으면 검색을 중단한다.
  • Object.prototype에는 모든 데이터 타입에서 사용할 수 있는 범용적인 메서드만이 존재하며, 객체 전용 메서드는 여느 데이터 타입과 달리 Object 생성자 함수에 스태틱하게 담겨있다.


참고

정재남, 코어 자바스크립트