[코어 자바스크립트] 06 프로토타입

임승민·2023년 1월 2일
0
post-thumbnail

💡 JS는 프로토타입 기반 언어이다. 클래스 기반 언어는 상속을 사용하지만 프로토타입 기반 언어는 객체를 원형으로 삼아 복제해서 상속과 비슷한 효과를 얻는다.

01 프로토타입의 개념 이해

6-1-1 constructor, prototype, instance

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

prototype객체에는 인스턴스가 사용할 메서드를 저장해서 인스턴스에서도 숨겨진 프로퍼티인 __proto__를 통해 메서드들에 접근할 수 있다.

Person 생성자 함수의 prototype에 getName 메서드를 추가했다.

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

그럼 Person의 인스턴스는 __proto__ 프로퍼티를 통해 메서드를 호출할 수 있다.
왜냐하면 __proto__ 가 prototype을 참조하므로 둘은 같은 객체를 바라보기 때문이다.

var tom = new Person('tom');
tom.__proto__ .getName(); //undefined
Person.prototype === tom.__proto__  //true

하지만 결과는 undefined가 나왔다. 하지만 에러가 안나온걸 보아선 getName이 실행됐고 함수라는 것도 입증됐다. 그렇다면 왜 tom이 출력되지 않았을까?

메서드로서 호출 시 메서드명 앞의 객체가 this가 된다. 따라서 getName 내부의 this는 tom이 아닌 tom.__proto__ 객체가 된다. 따라서 __proto__ 없이 인스턴스에서 메서드를 사용하면 된다.

var tom = new Person('tom');
tom.getName(); //tom

__proto__ 를 제거해서 this는 tom이 됐는데 어째서 정삭적으로 출력될까? 이유는 __proto__생략 가능한 프로퍼티이다. (창시자 브렌든 아이크의 아이디어로 이해의 영역이 아닌 그냥 받아들이면 된다.)

요약하자면

  1. 생성자 함수로서 함수를 호출하면
  2. 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__가 자동 생성되며,
  3. 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다.
  4. __proto__ 프로퍼티는 생략이 가능하다.

따라서 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 된다.

개발자 도구에서 배열 리터럴의 __proto__를 열어보면 배열에 사용 가능한 메서드들이 나온다.

Array의 prototype을 열어보면 배열 리터럴의 __proto__와 동일한 내용이다.

let arr = [1, 2];
arr.forEach(function() {}); // (0)
Array.isArray(arr); // (0) true
arr.isArray(); // (x) TypeError: arr.isArray is not a function

하지만 Array의 prototype 프로퍼티 내부에 없는 from, isArray 등의 메서드들은 인스턴스가 직접 호출할 수 없어서 Array에서 직접 접근해야 실행 가능하다.

6-1-2 constructor 프로퍼티

생성자 함수 prototype 안에는 constructor 프로퍼티가 있고 __proto__ 내부에도 있다.

이 프로퍼티는 생성자 함수를 참조한다. 존재 이유는 인스턴스로부터 원형을 알기 위해서 이다.

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]

따라서 인스턴스에서 constructor에 접근할 수 있다.

constructor는 읽기 전용 속성이 부여된 예외적인 경우(기본형 리터럴 변수 - number, string, boolean) 외엔 값을 변경할 수 있다.

let NewConstructor = function() {
  console.log('this is new constructor!');
};
let dataTypes = [
  1,              // Number & false
  'test',         // String & false
  true,           // Boolean & false
  {},             // NewConstructor & false
  [],             // NewConstructor & false
  function () {}, // NewConstructor & false
  /test/,         // NewConstructor & false
  new Number(),   // NewConstructor & false
  new String(),   // NewConstructor & false
  new Boolean,    // NewConstructor & false
  new Object(),   // NewConstructor & false
  new Array(),    // NewConstructor & false
  new Function(), // NewConstructor & false
  new RegExp(),   // NewConstructor & false
  new Date(),     // NewConstructor & false
  new Error()     // NewConstructor & false
];

dataTypes.forEach(function(d) {
  d.constructor = NewConstructor;
  console.log(d.constructor.name, '&', d instanceof NewConstructor);
});

모든 데이터가 d instanceof NewConstructor 결과로 false를 반환했다. 따라서 constructor를 변경해도 참조하는 대상이 변경될 뿐 원형이나 데이터 타입이 변하는 것은 아니다.

인스턴스의 생성자 정보를 알아내기 위해 constructor 프로퍼티에 의존하는 것이 항상 안전한 것은 아니다.

정리하자면

🔽 모두 동일한 대상을 가리킨다.

[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor

🔽 모두 동일한 객체에 접근할 수 있다.

[constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])

02 프로토타입 체인

6-2-1 메서드 오버라이드

인스턴스는 prototype의 프로퍼티, 메서드를 사용할 수 있다. 하지만 인스턴스에도 동일한 이름의 프로퍼티, 메서드가 있다면 어떻게 될까?

var Person = function(name) {
	this.name = name;
}
Person.prototype.getName = function() {
	return this.name;
}
var tom = new Person('tom');
tom.getName = function() {
	return 'hello' + this.name;
};
console.log(tom.getName()); // hello tom

tom.__proto__.getName이 아닌 tom객체의 getName이 호출됐다. 이런 상황을 메서드 오버라이드라고 한다. 그러니까 원본을 제거하고 교체하는 것이 아닌 원본 위에 다른 대상을 얹는 것이다.

메서드 오버라이드: 메서드 위에 메서드를 덮어씌웠다는 표현

JS가 getName을 찾는 방식은 자신의 프로퍼티부터 검색하고, 없으면 __proto__를 검색한다.

그렇다고 prototype의 메서드에 접근할 수 없는 것은 아니다.

console.log(tom.__proto__.getName()); //undefined

this가 prototype 객체를 가리키지만 prototype에는 name이 없다.

Person.prototype.name = '마이크';
console.log(tom.__proto__.getName()); //마이크

따라서 prototype에 name 프로퍼티를 넣어주면 정상 출력된다.

이제 원하는 메서드가 호출되고 있으니, call/ apply로 this를 prototype이 아닌 인스턴스를 바라보게 하면된다.

console.log(tom.__proto__.getName.call(tom)); //tom

6-2-2 프로토타입 체인

__proto__ 안에는 __proto__가 있다. 이유는 prototype객체가 객체이기 때문이다.

모든 객체의 __proto__에는 Object.prototype이 연결되고 prototype객체도 마찬가지이다.

__proto__는 생략이 가능해서 배열이 Array.prototype 내부의 메서드를 사용할 수 있다. 이와 같이 Object.prototype의 메서드도 사용이 가능하다. __proto__를 한번 더 따라가면 Object.prototype를 참조할 수 있기 때문이다.

var arr = [1,2]
arr(.__proto__).push(3);
arr(.__proto__)(.__proto__).hasOwnProperty(2); //true
💡 **프로토타입 체인:** 어떤 데이터의 __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

Array, Object의 각 프로토타입에 toString 메서드가 있어 arr에 각각 적용했더니 arr.toString의 출력값과 Array.prototype.toString과 동일했다.

6번째 줄에선 arr에 직접 toString 메서드를 부여해서 결과로 Array.prototype.toString이 아닌 arr.toString이 실행된다.

이처럼 배열뿐 아니라 JS 데이터는 모두 동일한 형태의 프로토타입 체인 구조를 지닌다.

6-2-3 객체 전용 메서드의 예외사항

어떤 생성자 함수든 prototype은 객체여서 프로토타입 체인 최상단에 Object.prototype이 있다.

따라서 객체에서만 사용할 메서드를 Object.prototype 내부에 정의하면 다른 데이터 타입도 사용할 수 있어서 객체에서만 사용할 메서드는 프로토타입 객체 안에 정의할 수 없다.

객체만을 대상으로 동작하는 객체 전용 메서드들은 Object.prototype이 아닌 Object에 스태틱 메서드로 부여할 수밖에 없다.

또 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능해서 대상 인스턴스를 인자로 직접 주입해야 하는 방식으로 구현돼 있다.

Object.freeze(instance) //O
instance.freeze()       //X

객체 한정 메서드들을 Object에 부여할 수밖에 없는 이유는 Object.prototype이 기본형 데이터조차 __proto__에 반복 접근해 도달할 수 있는 최상위 존재이기 때문이다.

반대로 Object.prototype에는 어떤 데이터든 활용 가능한 범용적 메서드들만 있다.

toString, hasOwnProperty, valueOf, isPrototypeOf

6-2-4 다중 프로토타입 체인

JS 데이터 타입들은 프로토타입 체인이 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] = arg[i]
	}
	this.length = args.length
};
var g = new Grade(100,90);

인스턴스에서 배열 메서드를 사용할 수 있게 하려면 g.__proto__(Grade.prototype)이 배열의 인스턴스를 바라보게 하면 된다.

Grade.prototype = [];

console.log(g); //Grade(2) [100,90]
g.pop();
console.log(g); //Grade(1) [100]
g.push(80);
console.log(g); //Grade(2) [100,80]

g 인스턴스 입장에선 프로토타입 체인에 따라 g객체, Grade의 prototype, Array.prototype, Object.prototype에 있는 멤버까지 접근 가능하다.

0개의 댓글