코어 자바스크립트 06프로토타입 & 07클래스

dabin *.◟(ˊᗨˋ)◞.*·2021년 8월 9일
0

Javascript

목록 보기
24/25
post-thumbnail

프로토타입의 개념

자바스크립트는 프로토타입 기반 언어다. 클래스 기반 언어에서는 '상속'을 사용하지만, 프로토타입 기반 언어에서는 어떤 객체는 prototype으로 삼고 이를 참조하여 상속과 비슷한 효과를 얻는다.

constructor, prototype, instance

var instance = new Constructor;

이 코드를 도식화하면 아래와 같다.

  • 어떤 생성자 함수 constructor을 new연산자와 함께 호출하면, constructor에 정의된 내용을 바탕으로 새로운 instance가 생성되고, instance에는 __proto__프로퍼티가 자동으로 부여되며, 이 프로퍼티는 constructorprototype 프로퍼티를 참조한다.
  • prototype은 객체고, 따라서 이를 참조하는 __proto__도 객체다. prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장하는데, 인스턴스는 숨겨진 프로퍼티인 __proto__를 통해 이 메서드들에 접근할 수 있다.

ES5 환경에서는 __proto__가 아닌 Object.getPrototypeOf()/Reflect.getPrototypeOf(instance)를 통해서만 접근할 수 있도록 했다. 호환성 문제가 있을 수 있으니 실무에서는 __proto__사용을 지양.

  • __proto__는 생략가능한 프로퍼티이다. 생정자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해상 메서드나 프로퍼티에 접근할 수 있다. 대표적인 내장 성성자 함수인 Array를 살펴보자.

    [1, 2] 인스턴스의 __proto__는 생략 가능하기 때문에 인스턴스가 push, pop 등의 메서드를 자신의 것처럼 호출할 수 있다. 하지만 Array의 prototype 프로퍼티 내부에 있지 않은 from, of 등은 Array 생성자 함수에서 직접 접근해야 실행이 가능하다.

constructor property

  • prototype 객체 내부에는 원래의 생성자 함수를 참조하는 constructor이라는 프로퍼티가 있다. 이를 참조하는 __proto_도 마찬가지다. 이를 통해 원형이 무엇인지 알 수 있다.
  • constructor은 읽기 전용 속성이 부여된 예외적인 경우(number, string, boolean)를 제외하고는 값을 바꿀 수 있다. 원형이나 데이터 타입이 변하는 것은 아니고, 참조하는 대상이 변경되는 것이다. 따라서 생성자 정보를 알아내기 위해 constructor 프로퍼티를 이용하는 것이 항상 안전하지는 않다.
  • 다양한 constructor 접근 방법
[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor

프로토타입 체인

메서드 오버라이드

만약 인스턴스에 동일한 이름의 프로퍼티나 메서드가 있다면? 메서드 오버라이드는 메서드 위에 메서드를 덮어씌웠다는 뜻이다. 이 경우 자신으로부터 가장 가까운 메서드에만 접근할 수 있다.

var Person = function() {
  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__의 메서드에도 접근이 가능하긴 하다.

//prototype메서드에 접근
Person.prototype.name = '이지금';
console.log(iu.__proto__.getName()); //"이지금"

//this가 인스턴스를 바라보도록 수정
console.log(iu.__proto__.getName.call(iu)); //"지금"

프로토타입 체인

프로토타입 체인은 어떤 데이터의 __proto__ 프로퍼티 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것이다. 이 체인을 따라가며 검색하는 것은 프로토타입 체이닝이다. 어떤 메서드를 호출하면 자신의 프로퍼티들을 검색해서 메서드를 실행하고, 없으면 __proto__를 검색해서 실행하고, 또 없으면 다시 __proto__를 검색해서 실행한다. 배열뿐 아니라, 자바스크립트 데이터는 모두 동일한 형태의 프로토타입 체인 구조를 가진다.

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

Object.prototype은 언제나 프로토타입 최상단에 존재하게 되기 때문에, 객체 한정 메서드는 프로토타입 객체 안에 정의할 수가 없고, Object에 스태틱 메서드로 부여해야 한다. Object.prototype이 다른 데이터 타입이 __proto__로 반복 접근해서 도달할 수 있는 최상위 존재이기 때문이다. 반대로, Object.prototype에는 어떤 데이터에서도 활용할 수 있는 메서드(toString, valueOf, hasOwnProperty 등)들만 있다.

예외 Object.create

  • 이 방식은 __proto__가 없는 객체를 생성한다.
  • Object.prototype메서드에 접근할 수 없다.
  • 내장 메서드 및 프로퍼티가 제거되어 객체 자체 무게가 가벼워져 성능상 이점이 있다.

다중 프로토타입 체인

기본 내장 데이터 타입들은 체인이 1단계(객체) 혹은 2단계(나머지 타입)로 끝났지만, 사용자가 새롭게 만드는 경우에는 대각선의 __proto__를 연결해 무한대로 체인 관계를 이어나갈 수 있다.

대각선의 __proto__를 연결하려면 __proto__가가리키는 대상, 즉 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게 하면 된다.

위의 그림에서 Grade는 배열의 메서드를 사용할 수 없는 유사배열객체이며, g는 Grade의 인스턴스를 바라본다. 인스턴스에서 배열 메서드를 직접 쓸 수 있게 하려면 g.__proto__, 즉 Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.

이렇게 되면 g는 객체 자신, Grade.prototype, Array.prototype, Object.prototype에 있는 멤버에까지 접근할 수 있다.

클래스와 인스턴스의 개념 이해

클래스는 공통 요소를 지니는 집단을 분류하기 위한 개념이고, 인스턴스는 클래스의 속성을 지니는 구체적인 사례다. 상위클래스(superclass)의 조건을 충족하면서 더 구체적인 조건이 추가된 것을 하위클래스(subclass)라고 한다. 클래스가 먼저 정의돼야만 그로부터 공통적인 요소를 지니는 개체들을 생성할 수 있으며, 한 인스턴스는 하나의 클래스만을 바탕으로 만들어진다.

자바스크립트의 클래스

자바스크립트에는 클래스의 개념이 존재하지 않는다. 프로토타입에 클래스 개념을 적용해보면 아래와 같다.

인스턴스에서 직접 호출할 수 있는 메서드가 프로토타입 메서드고, 인스턴스에서 직접 접근할 수 없는 메서드가 스태틱 메서드다. 스태틱 메서드는 인스턴스가 직접 호출할 수 없고 클래스(생성자 함수)에 의해서만 호출할 수 있다.

클래스 상속

자바스크립트에서 클래스 상속을 구현했다는 것은 프로토타입 체이닝을 잘 연결한 것으로 이해하면 된다. 위에서 본 다중 프로토타입 체이닝의 Grade에서와 같이, 하위 클래스로 삼을 생성자 함수의 prototype에 상위 클래스의 인스턴스를 부여하는 것으로 기본적인 메서드 상속은 가능하다. 하지만 이는 다양한 문제를 가지며 구조적 안정성이 떨어진다. Grade에서는 length 프로퍼티가 삭제 가능하고 Grade.prototype에 빈 배열을 참조시켰다는 점이 문제가 된다. 즉, 클래스에 있는 값이 인스턴스에 영향을 주는 구조라는 문제를 지닌다. 이를 해결하는 여러가지 방법을 알아보자.

클래스가 구체적인 데이터를 지니지 않게 하는 방법

일단 만들고 나서 프로퍼티들을 일일이 지우고(delete) 새로운 프로퍼티를 추가할 수 없게(freeze) 만든다.

delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(Square.prototype);

Subclass의 prototype에 직접 SuperClass의 인스턴스를 할당하는 대신, 프로퍼티를 생성하지 않는 빈 생성자 함수 Bridge를 만들어 그 prototype이 SuperClass의 prototype을 바라보게 한 다음, SubClass의 prototype에는 Bridge의 인스턴스를 할당하게 하는 방법도 있다. 이렇게 되면 인스턴스를 제외한 프로토타입 체인 경로상에 구체적인 데이터가 남아있지 않게 된다.

var extendClass2 = (function () {
  var Bridge = function () {};
  return function (SuperClass, SubClass, subMethods) {
    Bridge.prototype = SuperClass.prototype;
    Subclass.prototype = new Bridge();
    if (subMethods) {
      for (var method in subMethods) {
        SubClass.prototype[method] = subMethods[method];
      }
    }
    Object.freeze(subclass.prototype);
    return SubClass;
  };
})();

이보다 간단하게 하려면 Object.create를 이용하자.

Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);

위의 방법들은 기본적인 상속에는 성공했지만, SubClass 인스턴스의 constructor는 여전히 SuperClass를 가리킨다. 원래의 Subclass를 바라보도록 만들어보자.

var extendClass2 = (function () {
  var Bridge = function () {};
  return function (SuperClass, SubClass, subMethods) {
    Bridge.prototype = SuperClass.prototype;
    Subclass.prototype = new Bridge();
    Subclass.prototype.constructor = SubClass; //여기!!!! 여기!!!여기!!
    if (subMethods) {
      for (var method in subMethods) {
        SubClass.prototype[method] = subMethods[method];
      }
    }
    Object.freeze(subclass.prototype);
    return SubClass;
  };
})();

위에서 언급한 다른 방법을 사용했더라도 Subclass.prototype.constructor = SubClass; 를 통해 복구해주면 된다.

ES6의 클래스 및 클래스 상속

ES6에서는 본격적으로 클래스 문법이 도입됐다.

//class 명령어 뒤 바로 {}, 내부가 클래스 본문 영역.
//class 본문에서 function 생략해도 모두 메서드로 인식.
//constuctor는 생성자 함수와 동일한 역할
//method와 method 사이는 콤마로 구분하지 않음.
var Rectangle = class {
  constructor (width, height) {
    this.width = width;
    this.height = height;
  }
  getArea () {
    return this.width * this.height;
  }
};
//Square을 Rectangle 클래스를 상속받는 SubClass로
//extends Rectangle : 이거 하나로 상속 관계 설정 끝!
var Square = class extends Rectangle {
  constructor (width) {
    super(width, width); 
  } //super을 함수처럼 사용, SuperClass의 constructor 실행
  getArea () {
    console.log(super.getArea())'
  }
};
/*
constructor 메서드를 제외한 다른 메서드에서 super 키워드를 마치 객체처럼 사용할 수 있음.
이때 객체는 SuperClass.prototype을 바라보는데, 
호출한 메서드의 this는 'super'이 아닌 원래의 this를 따름
*/
profile
모르는것투성이

0개의 댓글