TIL053 코어자바스크립트 제6장 프로토타입

Somi·2021년 9월 12일
1

JavaScript

목록 보기
27/27
post-thumbnail

0. 프로토타입 preview

프로토타입을 한마디로 정의하면 뭐라고 할 것 인가? 그건 바로 유전자!!!!

function Person() {
  this.eyes = 2;
  this.nose = 1;
}
var kim  = new Person();
var park = new Person();
console.log(kim.eyes);  // => 2
console.log(kim.nose);  // => 1
console.log(park.eyes); // => 2
console.log(park.nose); // => 1

이렇게 하면 김과 박은 둘 다 눈과 코를 가지고 있는데, 메모리에는 눈과 코가 두 개씩 총 4개가 할당된다. 객체를 100개 만들면 200개의 변수가 메모리에 할당되는 magic... 이를 해결하기 위해 프로토타입 개념이 도입된다.

function Person() {}
Person.prototype.eyes = 2;
Person.prototype.nose = 1;
var kim  = new Person();
var park = new Person():
console.log(kim.eyes); // => 2
...

Person.prototype이라는 빈 Object(부모유전자)가 어딘가에 존재하고, Person 함수로부터 생성된 객체(김, 박)들은 부모 유전자(객체)에 들어있는 값을 가져다 쓸 수 있다.

여기까지의 출처

1. 프로토타입 개념 이해

그렇다면 본격적으로 프로토타입에 대해 파헤쳐보자!

자바스크립트는 프로토타입 기반의 언어이다.

클래스 기반 언어에서는 상속을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고, 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻는다.

1) constructor, prototype, instance

<코드 6-1>

var instance = new Contructor();

<그림 6-1>

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

property에서는 인스턴스가 사용할 메서드를 저장하기 때문에 인스턴스에서도 prototype을 참조하는 __proto__를 통해 메서드들에 접근할 수 있게된다!

<예제 6-1> Person.prototype

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

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

이제 Person의 인스턴스는 __proto__프로퍼티를 통해 getName을 호출할 수 있다.

var somi = new Person('somi');
somi.__proto__.getName(); //undefined

여기서 type error가 아닌 undefined가 나왔다는 것은 이 변수가 호출할 수 있는 함수라는 뜻으로, getName이 실제로 실행되었음을 알 수 있고, 이로부터 getName이 함수라는 것이 입증되었다.

즉, 실행가능한 호출할 수 있는 함수이나, this에 바인딩 된 대상이 잘못 지정되어 있다는 점 때문에 undefined가 리턴된 것이다. somi.__proto__.getName()에서 getName 함수 내부에서의 this는 somi가 아니라 somi.__proto__라는 객체가 되는 것이고, 이것에는 name이라는 프로퍼티가 없으므로 undefined가 리턴되었다.

이를 해결하기 위해서는 생략이 가능한 __proto__의 특성에 따라 이를 생략하고 somi.getName()이라고 써주면 된다.

결론: 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있다!

한편 프로퍼티 외부에 있는 메서드들은 인스턴스가 직접 호풀할 수 없기에 생성자 함수에서 직접 접근해야함을 유의하자.

2) constructor 프로퍼티

생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있다. 이는 인스턴스로부터 그 원형이 무엇인지 알 수 있는 수단으로 원래의 생성자 함수를 참고한다.

인스턴스의 __proto__가 생성자 함수의 prototype 프로퍼티를 참조하며 __proto__가 생략가능하기에 인스턴스에서 직접 constructor에 접근할 수 있는 수단이 생긴 것!

그래서 아래 <예제 6-2>의 여섯번째 줄과 같은 코드도 오류없이 작동한다.

<예제 6-2>

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

let arr2 = new arr.constructor(3, 4);
console.log(arr2); // [3, 4]

<예제 6-3>

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 프로퍼티에 의존하는 것이 항상 안전하지는 않다는 것을 알 수 있다.

2. 프로토타입 체인

1) 메서드 오버라이드

<예제 6-4>

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

let iu = new Person('지금');
iu.getName = function() {
  return '바로 ' + this.name;
};
console.log(iu.getName()); // 바로 지금

인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가지고 있는 상황이라면, 위의 예제<6-4>와 같이 iu.__proto__.getName이 아닌 iu객체의 getName이 호출된다. 이렇게 메서드 위에 메서드를 덮어 씌우는 것을 메서드 오버라이드라고 부른다.

만일 인스턴스를 바라보도록 바꿔주고 싶다면 call 이나 apply를 사용하면 된다.

console.log(iu.__proto__.getName.call(iu)); // 지금

2) 프로토타입 체인

prototype 객체는 객체이며, 기본적으로 모든 객체의 __proto__에는 Object.prototype이 연결된다. 이는 prototype 객체도 예외가 아니라서 이를 그림으로 표현하면 아래와 같다.

<예제 6-5>

let 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

즉 프로토타입 체인은 어떤 데이터의 __proto__ 프로퍼티 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 뜻한다.

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

어떤 생성자 함수이든, prototype은 객체이기때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다. 따라서 객체에서만 사용할 메서드는 다른 데이터 타입처럼 프로토타입 객체안에 정의할 수 없다. 객체에서만 사용할 메서드를 이곳에 저장하면 다른 데이터타입도 해당 메서드를 사용할 수 있기 때문!

이러한 이유로 객체만을 대상으로 동작하는 메서드들은 Object.prototype이 아닌 Objcet에 스태틱 메서드(static method)로 부여할 수 밖에 없다. 또한 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 여느 전용 메서드처럼 "메서드 명 앞의 대상이 곧 this"가 되는 방식 대신 this의 사용을 포기하고 대상 인스턴스를 인자로 직접 주입해야하는 방식으로 구현돼있다.

같은 이유에서 Object.prototype에는 어떤 데이터에서도 활용할 수 있는 범용적인 메서드들만 있다. toString, hasOwnProperty, valueOf, isPrototypeOf 등은 변수가 마치 자신의 메서드인 것처럼 호출할 수 있다.

4) 다중 프로토타입 체인

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

let Grade = function() {
  let args = Array.prototype.slice.call(arguments);
  for(let i = 0; i < args.length; i++) {
    this[i] = args[i];
  }
  this.length = args.length;
};
let g = new Grade(100, 80);

Grade의 인스턴스는 여러 개의 인자를 받아 각 순서대로 인덱싱해서 저장하고 length 프로퍼티가 존재하는 등으로 배열의 형태를 지니지만, 배열의 메서드를 사용할 수 없는 유사배열객체이다. 이 인스턴스에서 배열 메서드를 직접 쓸 수 있게 하기 위해서는 g.__proto__, 즉 Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.

Grade.prototype = [];

0개의 댓글

관련 채용 정보