본문 바로가기
  • GDG on campus Ewha Tech Blog
4-1기 스터디/Javascript-Typescript Deep Dive

[JS Deep Dive 스터디] 6주차 - 프로토타입

by 호프 2023. 1. 25.

일시: 2023.01.08

참석자:


금주 스터디는 윤수지님께서 발표해주셨습니다.

6. 프로토타입

자바스크립트는 프로토타입 기반(prototype-based) 언어이다. Java와 같은 클래스 기반 언어에서는 ‘상속’을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형으로 삼고 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻는다.

6-1. 프로토타입의 개념 이해

6-1-1. constructor, prototype, instance

출처: 코어 자바스크립트 그림 6-2

위의 도식을 설명하면 다음과 같다.

  1. 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출하면
  2. Cosntructor에서 정의된 내용을 바탕으로 새로운 인스턴스(instance)가 생성
  3. 이때 instance에는 __proto__ 라는 프로퍼티가 자동으로 부여
  4. __proto__ 프로퍼티는 Constructorprototype이라는 프로퍼티를 참조

자바스크립트는 함수에 자동으로 객체인 prototype을 생성해 놓는데, 해당 함수를 생성자 함수로서 사용할 경우, 즉 new 연산자와 함께 함수를 호출할 경우, 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__ 가 자동으로 생성되며, 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다.

prototype 은 객체이고, 이를 참조하는 __proto__ 역시 객체이다. prototype 객체 내부에는 인스턴스가 사용할 메소드를 저장하므로 인스턴스에서도 __proto__ 를 통해 해당 메소드에 접근이 가능하다.

💡 NOTE

해당 챕터에서는 이해의 편의를 위해 __proto__ 를 계속 사용하지만, 실무에서는 가급적 __proto__ 를 사용하지 않기를 권장한다. 대신 Object.getPrototypeOf() / Object.create() 를 이용할 수 있다.

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

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

위의 예제에서 __proto__ 를 통해 Constructorprototype 프로퍼티에 접근하여 getName 이라는 메소드를 호출할 수 있는 것을 확인할 수 있다. 하지만 이때 ‘Suji’ 가 아닌 undefined가 출력되는 이유는 this에 바인딩된 대상이 잘못 지정되었기 때문이다.

어떤 함수를 메소드로서 호출할 때는 메소드명 바로 앞의 객체가 this가 된다. 즉, 위의 코드에서는 instance.__proto__ 라는 객체가 this로 지정되어 있는 것이다. 이 객체 내부에는 name 프로퍼티가 없으므로 해당 코드는 찾고자하는 식별자가 정의되어 있지 않을 때는 Error 대신 undefined를 반환한다’ 는 자바스크립트의 규칙에 따라 undefined가 반환된다.

instance.getName(); // Suji

__proto__ 는 생략 가능한 프로퍼티이다. 따라서 위와 같이 코드 작성이 가능하고 이 경우 instance 객체가 getName 메소드의 this로 바인딩되기 때문에 원하는 값을 호출할 수 있다.

즉, 생성자 함수의 prototype에 어떤 메소드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메소드나 프로퍼티에 접근할 수 있다. 하지만 생성자 함수의 프로퍼티 내부에 있지 않은 메소드 즉, 정적 메소드는 인스턴스가 직접 호출할 수 없기 때문에 생성자 함수에서 직접 접근해야 실행이 가능하다.

var arr = [1, 2];
Array.isArray(arr); // true
arr.isArray(); // TypeError: arr.isArray is not a function

6-1-2. 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]

생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 존재한다. 이는 생성자 함수(자기 자신)을 참조하여 인스턴스로부터 그 원형이 무엇인지를 알 수 있는 수단이 된다.

하지만 constructor는 읽기 전용 속성이 부여된 예외적인 경우(기본형 리터럴 변수 - number, string, boolean)을 제외하고는 값을 바꿀 수 있다. 하지만 이는 constructor가 참조하는 대상이 변경될 뿐 이미 만들어진 인스턴스의 원형이 바뀐다거나 데이터 타입이 변하는 것은 아니기 때문에 어떤 인스턴스의 생성자 정보를 알아내기 위해 constructor 프로퍼티에 의존하는 것이 항상 안전하지는 않다.

  • 다음의 코드는 모두 동일한 대상을 가리킨다.
[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor
  • 다음의 코드는 모두 동일한 객체(prototype)에 접근할 수 있다.
[Constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])

6-2. 프로토타입 체인

6-2-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()); // 바로 지금

이때, __proto__ 의 메소드도 접근이 불가능한 것은 아니다. 아래와 같이 우회적인 방법으로 호출이 가능하다.

console.log(iu.__proto__getName()); // undefined
console.log(iu.__proto__getName.call(iu)); // 지금

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

https://velog.velcdn.com/images%2Fhyounglee%2Fpost%2F93a641aa-05b5-433d-9233-2c925082cc12%2Fimage.png

prototype 객체는 말 그대로 ‘객체’이고, 기본적으로 모든 객체의 __proto__ 에는 Object.prototype이 연결된다. 따라서 위의 그림처럼 배열 리터럴의 __proto__ 안에는 또다시 __proto__ 가 있으며, 이는 Object.prototype과 연결된다.

이때 __proto__ 는 생략 가능하기 때문에 Object.prototype 내부의 메소드도 자신의 것처럼 실행할 수 있다.

이처럼 어떤 데이터의 __proto__ 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인(prototype chain) 이라 하고, 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝(prototype chaining) 이라고 한다. 이는 앞선 메소드 오버라이딩과 동일한 맥락으로 가까운 __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

💡 INFO

각 생성자 함수는 모두 함수이기 때문에 Function 생성자 함수의 prototype과 연결된다. Function 생성자 함수 역시 함수이므로 다시 Function 생성자 함수의 prototype과 연결된다. 이런 식으로 재귀적으로 반복하는 루트를 끝없이 찾아갈 수 있지만, 이는 실제 메모리 상에서 무한대의 구조 전체의 데이터를 들고 있는 것이 아니고 사용자가 이런 루트를 통해 접근하고자 할 때 비로소 해당 정보를 얻을 수 있을 뿐이다.

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

어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재한다. 따라서 객체에서만 사용할 메소드는 다른 데이터 타입처럼 prototype 객체 안에 정의할 수 없다. Object.prototype 내부의 메소드는 어떤 데이터 타입에서도 접근하여 사용할 수 있기 때문이다.

그래서 객체 전용 메소드들은 정적 메소드(static method)로 생성하며, 생성자 함수은 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 대상 인스턴스를 인자로 직접 주입해야 하는 방식으로 구현되어 있다.

💡 INFO

Object.create(null) 를 이용하면 Object.prototype의 메소드에 접근할 수 없는 경우가 있다.

이 방식으로 만든 객체는 일반적인 데이터에 반드시 존재하던 내장(built-in) 메소드 및 프로퍼티들이 제거됨으로써 기본 기능에 제약이 생긴 대신, 객체 자체의 무게가 가벼워짐으로써 성능상 이점을 가진다.

var _proto = Object.create(null);
_proto.getValue = function(key) {
    return this[key];
};
var obj = Object.create(_proto);
obj.a = 1;
console.log(obj.getValue('a')); // 1
console.dir(obj); // __proto__ 에 getValue 메소드만 존재

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

자바스크립트의 기본 내장 데이터타입들은 모두 프로토타입 체인이 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] = args[i];
  }
  this.length = args.length;
};
var g = new Grade(100, 80);

변수 g는 Grade 인스턴스를 바라보고, Grade 인스턴스는 유사배열 객체이다. Grade가 배열 메서드를 직접 쓸 수 있게끔 만들고 싶다면 g.proto 즉, Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.

Grade.prototype = [];

6-3. 정리

  • 생성자 함수를 new 연산자와 함께 호출하면 Constructor에 정의된 내용을 바탕으로 새로운 인스턴스가 생성된다. 해당 인스턴스에는 __proto__ 라는, Constructor의 prototype 프로퍼티를 참조하는 프로퍼티가 자동으로 부여된다.
  • __proto__ 는 생략 가능한 속성이기 때문에, 인스턴스는 Constructor.prototype의 메소드를 마치 자신의 메소드인 것처럼 호출 가능하다.
  • Constructor.prototype에는 constructor라는 프로퍼티가 있는데, 이는 생성자 함수 자기 자신을 가리켜서 인스턴스가 자신의 생성자 함수를 알고자 할 때 사용된다.
  • __proto__ 안에 다시 __proto__ 를 찾아가는 과정을 프로토타입 체이닝이라고 하며, 이 프로토타입 체이닝을 통해 각 프로토타입 메소드를 자신의 것처럼 호출할 수 있다. 프로토타입의 최상단에는 항상 Object.prototype이 존재한다.
  • Object.prototype에는 모든 데이터 타입에서 사용할 수 있는 범용적인 메소드만이 존재하며, 객체 전용 메소드는 Object 생성자 함수에 static하게 담겨있다.
  • 프로토타입 체인은 무한대의 단계를 생성할 수 있다.

댓글