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

Donghun Seol·2022년 11월 27일
0

코어자바스크립트

목록 보기
6/7

자바스크립트는 생소한 프로토타입 기반 언어다. 클래스 기반에서는 '상속'을 활용하지만 프로토타입 기반 언어는 특정 객체를 프로토타입으로 삼고, 이를 복제나 참조함으로써 상속과 비슷한 효과를 구현한다. 자바스크립트를 이해하기 위해서는 프로토타입이란 녀석을 잘 알고 넘어가야 한다.

프로토타입의 개념 이해

Constructor, Prototype, instance, __proto__


위의 그림은 아래의 코드를 도식화한 것이다.

var instance = new Constructor();
  1. 특정 생성자 함수를 new와 함께 호출하면
  2. Constructor()에서 정의된 내용처럼 새로운 인스턴스가 생성된다.
  3. 이때 생성된 인스턴스에는 __proto__라는 프로퍼티가 자동으로 부여된다.
  4. 해당 프로퍼티는 Constuctor의 prototype이라는 프로퍼티를 참조한다.

아래 코드를 통해서 구체적으로 알아보자
kim이라는 인스턴스는 Person의 prototype에 정의된 getName() 메서드를 바로 활용가능하다. 직관적이지만 구체적으로 왜 이렇게 활용가능할 수 있는지는 아직 알 수 없다.

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

var kim = new Person('김밥맨');
console.log(kim.getName()); // '김밥맨'

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

그런데 kim.__proto__.getName()으로 호출시 왜 undefined가 나올까? '김밥맨'이 나와야 하지 않나? 사실 undefined가 출력되는 이유는 앞서 언급했다.4. __proto__가 Constructor의 prototype을 참조하기 때문이다. 방금의 실행결과를 순차적으로 되짚어보면 다음과 같다.

  1. undefined가 출력되었다는 것은 객체의 식별자가 정의되어 있지 않은 것이다.
  2. Person.prototype === kim.__proto__; //true 이므로 둘은 같은 객체를 가리킨다.
  3. 그러므로 메서드의 this 바인딩이 예상과는 달리 구현되었음을 의심해 볼 수 있다.
  4. 아하 getName()의 호출시점에서 this는 Person.prototype이므로 해당 객체안에 _name의 식별자가 존재하지 않는다.
  5. 결국 식별자가 정의되지 않은 속성을 호출하여 JS엔진은 undefined를 반환했다.

따라서 아래와 같이 Person의 prototype에 해당 식별자를 만들어주면 해당 값이 출력된다.

// (생략...)
Person.prototype._name = '굥';
// (생략...)
kim.__proto__.getName(); // 굥

그렇다면 getName() 메서드에 올바르게 this를 바인딩 하는 방법은? 그냥 직관적으로 메서드형식으로 사용하는 것이다. __proto__를 생략했는데 올바르게 메서드가 호출되고, this도 직관적으로 바인딩 된 것은 자바스크립트의 설계상 그렇기 때문이다. __proto__는 언어의 설계시점부터 생략가능한 프로퍼티로 정해졌기 때문이다. 브랜던 아이크가 자바스크립트를 만들 때 선택한 것이므로 자바스크립트는 원래 그렇다.는 정도로 넘어가자

__proto__를 생략하고 컨스트럭터의 프로토타입에 정의된 메서드를 인스턴스에서 호출하면 this가 직관적으로 바인딩된 메서드를 호출할 수 있다. 이건 설계상 그렇게 정의된 것이다. 그렇게 알고 넘어가면 된다.

아래에서는 생략가능한 속성인 __proto__를 생략하고 메서드를 호출하여 this바인딩이 의도적으로 잘 되었을 확인가능하다.

var suzi = new Person('suzi');
var iu = new Person('jieun');

suzi.getName(); // suzi
iu.getName(); // jieun

iu.getThis(); // iu instance
iu.__proto__.getThis(); // Person Constructor

내장 생성자인 Array를 살펴보면 프로토타입 방식이 어떻게 작동하는지 더 잘 이해할 수 있다. 스태틱 메서드처럼 작동하는 Array.isArray(), Array.from()의 메서드는 Array 생성자의 prototype에 정의되어 있지 않으므로, Array의 인스턴스에서는 사용할 수 없다. pop(), push(), indexOf(), forEach()등은 prototype에 정의되어 있어 인스턴스에서 메서드로 자유롭게 호출 가능하다.

constructor 프로퍼티

생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있습니다. 이 프로퍼티는 생성자함수를 참조한다. 인스턴스로부터 그 원형이 무엇인지를 알 수 있는 수단으로 작동한다. 인스턴스도 해당 프로퍼티를 가지고 있으므로 인스턴스에서 해당 프로퍼티를 호출함으로써 새로운 인스턴스를 만들 수 있다. 일단 마지막 문단처럼 객체의 constructor 값은 변경가능함을 알아두자.

var arr = [1, 2];
Array.prototype.constructor === Array // true
arr.__proto__.constructor === Array // true
arr.constructor === Array

var arr2 = new arr.constructor(3, 4); // This Works!!
console.log(arr2);

arr.constructor = String
var isString = new arr.constructor('hello world');
console.log(isString); // [String: 'hello world']

배운 내용을 활용하면 하나의 생성자 함수를 다음과 같이 다른 형식으로 호출가능함을 알 수 있다

var Person = function (name) {
  this.name = name;
}
var p1 = new Person('사람1');
var p1Proto = Object.getPrototypeOf(p1);

var p2 = new Person.prototype.constructor('사람2');
var p3 = new p1Proto.constructor('사람3');
var p4 = new p1.__proto__.constructor('사람4');
var p5 = new p1.constructor('사람5');

프로토타입 체인

프로토타입의 특성을 이해했으니, 객체지향 언어의 상속과 메서드 오버라이드가 프로토타입을 통해 어떻게 구현되는지 알아보자.

메서드 오버라이드

아래와 같은 경우 인스턴스에서 정의된 메서드가 스코프체인상의 탐색 우선순위에 위치하므로 오버라이드되는 것과 비슷하게 작동한다. 생성자 함수에서 정의된 메서드는 사라지는 것이 아니라 instance.__proto__.getName()으로 접근 가능함을 유의하자.

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()); // 바로지금 
console.log(iu.__proto__.getName()); // undefinded due to this binding
console.log(iu.__proto__.getName.call(iu)); // 지금 after explicit this binding

프로토타입 체이닝

상속과 같은 개념아닌가?

저자의 말에 의하면 상속과는 다르다. 상속으로 이해하고 혼란스러워하는 개발자들이 많이 있다고 한다. 나는 왜, 어떻게 다른지는 아직 잘 모르겠다. 7장의 클래스를 공부한다면 이해할 수 있을까? 🤔

아래의 코드블록에서 최상위에 Object가 위치하고, Array.__proto__는 Object.prototype를 참조하고 Array의 인스턴스인 arr.__proto__는 Array.prototype을 참조함을 알 수 있다.

또한 앞서 다뤘듯이 기존 내장객체의 메서드를 오버라이드 하거나=사용자 정의 메서드를 추가할 수도 있다. 아래의 코드블록에서 확인 가능하다.

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

Array.prototype.newToString = function () {
  return 'New ' + Array.prototype.toString.call(this);
}

newArrInstance1 = new Array(1, 2, 3);
console.log(newArrInstance1);
console.log(newArrInstance1.toString()); // 1,2,3
console.log(newArrInstance1.newToString());// New 1,2,3

Array.prototype.toString = function () {
  return 'haha I hacked toString()'
}

hackedArr = new Array(1,2,3);
console.log(hackedArr.toString()) // haha I hacked  toString()

다중 프로토타입 체인

자바스크립트의 내장 데이터 타입들은 프로토타입이 1단계거나 2단계로 끝나지만, 사용자가 정의하는 데이터타입은 그 이상도 가능하다. 대각선의 __proto__들을 연결해나가면 무한대로 체인관계를 구현 가능해진다.

교재에 나온 코드샘플대로 아래와 같이 정의해 주면 Grade의 인스턴스인 g에서 배열메서드를 사용가능하다.

var Grade = function () {
  var args = Array.from(arguments)
  for (var i = 0; i < args.length; ++i) {
    this[i] = args[i];
  }
  this.length = args.length;
  this.__proto__ = []
}
var g = new Grade(100, 80);
g.pop();

아 공부하다보니 너무 양이 많아지는데..😅😅 일단 이것만 정리하고 마무리 하자....

먼저 ES5 문법에서 생성자 함수에 한번에 prototype을 정의할 수는 없다.

아래의 내용을 참고해서 막혔던 부분을 해결했다. 내가 하려던 문법이 안되는 거였다.

뭘 하려고 했느냐?

  1. 생성자 A와 B를 정의하고, 각각의 protoype에 고유한 메서드를 정의한다.
  2. 생성자 함수 C 내부에서 prototype을 참조하는 로직을 작성한다.
  3. new로 생성한 C의 인스턴스의 __proto__에서 동시에 참조하고 있는 A, B의 메서드가 잘 작동하는지 확인해본다.

😎 아래의 코드에서 구현하려한 내용이 정상적으로 작동함을 확인했다. 야후~

var A = function (name) {
  this.name = name;
}
A.prototype.printA = () => console.log('A');
A.prototype.sayHo = () => console.log('Ho', 'A');
A.prototype.sayName = function () {
  console.log(this.name);
}
var B = function (name) {
  this.name = name;
}
B.prototype.printB = () => console.log('B');
B.prototype.sayHo = () => console.log('Ho', 'B');

var C = function (name) {
  this.name = name;
  this.__proto__ = { ...A.prototype, ...B.prototype };
}
// this also works!  C.prototype = {...A.prototype, ...B.prototype};

cInstance = new C('씨에요');
console.log(cInstance.name); // 씨에요
cInstance.printA(); // A
cInstance.printB(); // B
cInstance.sayHo(); // Ho B
cInstance.sayName(); // 씨에요
profile
I'm going from failure to failure without losing enthusiasm

0개의 댓글