본문 바로가기

07. 클래스

본 내용은 책 코어 자바스크립트(2019, 정재남)를 세 번 정독 후, 글쓴이가 이해한 내용과 실제 업무에서 겪은 경험을 기술하였습니다. 다소 형식적이지 못합니다.

ES6에 클래스 문법이 추가됐지만 클래스 문법은 Syntax Sugar일 뿐, 원래 자바스크립트는 프로토타입 기반 언어로써 상속을 제공하고 있지 않으므로 기존에는 어떻게 구현하고 있는지 알 필요가 있다.

 

1. 객체 지향 프로그래밍(Object Oriented Programming)

책에서는 클래스와 인스턴스에 대해 현실세계의 개념을 예시로 들어 설명하고 있지만 클래스 뿐만 아니라 객체 지향 프로그래밍을 전반적으로 정리해보기로 하였다.

 

객체 지향 프로그래밍(이하 OOP)란, 현실세계의 일련의 사건들을 객체로 모델링하여 이들 간의 상호작용을  객체의 메소드와 속성으로 표현하는 프로그래밍 방식이다.

 

OOP의 대표적인 특징으로는 추상화, 캡슐화, 상속, 다형성이 있다.

1.1 추상화

추상화란, 공통의 성질을 갖는 대상들을 같은 이름으로 명명하는 것이다.

 

OOP에서는 클래스를 정의하는 것이 곧 추상화이며, 자바스크립트에서 객체를 정의하는 것에 해당한다.

 

자바스크립트에서 객체를 생성하는 방법은 총 4가지가 있다.

  • 리터럴 함수
  • 팩토리 함수
  • 생성자 함수
  • Object.create()

각각의 자세한 설명은 여기를 참고한다.

1.2 캡슐화

캡슐화의 목적은 코드를 수정없이 재활용하는 것이다. 클래스라는 캡슐을 정의하면, 이 캡슐 안에 기능과 속성을 담아서 특정 목적으로 묶는 것이다. 캡슐화 덕분에 외부에 노출되면 안되는 데이터는 해당 데이터의 직접적인 접근이 아닌, 함수를 통해서 접근할 수 있도록 하는 은닉화가 가능해졌다.

 

자바스크립트에서는 변수 은닉화를 클로저를 통해 구현할 수 있다.

1.3 상속

상속이란 객체 간의 관계를 표현하기 위해 부모의 메소드와 속성을 자식에게 상속하여 자식은 부모의 데이터를 사용할 수 있는 동시에 자기자신만의 고유한 메소드와 속성을 지닐 수 있다.

 

2022-02-20 추가

OOP에서 상속을 사용할 때, "동일한 메시지에 대해 다르게 행동할 수 있는 다형성"을 구현하기 위한 타입 계층을 표현하고자 할 때 사용해야 한다. 단순히 코드 재사용만을 위해서 사용하지 말자!

 

자바스크립트에서는 상속을 프로토타입 체인을 통해 구현할 수 있다.

1.4 다형성

다형성이란, 같은 형태의 코드가 여러 기능을 할 수 있는 것을 말한다. 상속 혹은 위임을 통해 기능을 확장 및 변경하는 것을 용이하게 해주고, 코드의 길이도 줄일 수 있다.

 

자바스크립트에서는 이 다형성을 프로토타입의 메소드와 같은 이름으로 인스턴스에서 정의하면 프로토타입 체인 상에서 가장 가까운 함수를 호촐하므로 오버라이딩을 구현할 수 있다.

 

2. 프로토타입 메소드와 스테틱 메소드

let arr = new Array();

arr.push(1);
arr.push(2);

Array.isArray(arr);

위의 Array 객체를 new 키워드를 통해 새 인스턴스 arr을 생성하면, arr__proto__가 Array.prototype을 참조하여 arr은 Array의 프로토타입 메소드를 사용할 수 있다. 반면, Array의 내부 메소드로 정의된 isArray는 prototype 객체에 정의되어 있지 않으므로, 원형 함수의 메소드로만 사용할 수 있고, 인스턴스의 메소드로는 사용할 수 없다.

이처럼 인스턴스에서 직접 호출할 수 있는 메서드가 바로 프로토타입 메서드입니다. 반면, 인스턴스에서 직접 접근할 수 없는 메서드를 스테틱 메서드라고 합니다.

 

3. 클래스 상속

프로토타입 체인을 통해 클래스 상속을 구현하는 법을 알아보자.

책에서는 3가지 방법을 제시하고 있지만, 이 중 가장 안전한 Object.create()로 상속을 구현해보았다.

function Rectangle (width, height) {
  this.width = width;
  this.height = height;
}

Rectangle.prototype.getSize = function () {
  return this.width * this.height;
}

function Square (width) {
  Rectangle.call(this, width, width);
}
Square.prototype = Object.create(Rectangle.prototype);
Square.prototype.constructor = Square;

const s1 = new Square(10);

s1.getSize(); // 100

위의 예제에서 s1을 console.dir로 출력하면 다음과 같다.

Square {
  height: 10,
  width: 10,
  __proto__: Rectangle { // Square.prototype
    constructor: ƒ Square(width),
    __proto__: { // Rectangle.prototype
      getSize: ƒ (),
      constructor: ƒ Rectangle(width, height),
      __proto__: Object // 가장 최상위 prototype,
    }
  }
}

3.1 상위 클래스의 메소드에 접근하기

이번 방법은 클래스의 super처럼 상위 클래스의 메소드에 접근하여 사용하는 방법을 알아본다.

 

만약, 위의 Square 생성자 함수의 prototype에 같은 이름의 getSize 함수를 새로 정의하면, Rectangle 프로토타입에 정의된 getSize에는 더이상 접근할 수 없게 된다.

 

접근할 수 있는 방법으로는 Rectangle 프로토타입 메소드에 this를 바인딩해서 호출할 수 있다.

// ... 중략

Square.prototype.getSize = function () {
  return `this is square's getSize()`
}

const s1 = new Square(10);

s1.getSize() // "this is square's getSize()"

Rectangle.prototype.getSize.apply(s1); // 100

책에서는 매번 this를 바인딩해서 불러오기 번거로우므로 super 메소드를 prototype 메소드로 정의하는 법을 소개한다.

const extendClass = function (superClass, subClass, subMethods) {
  subClass.prototype = Object.create(superClass.prototype);
  subClass.prototype.constructor = subClass;
  
  subClass.prototype.super = function (superMethod) {
    let self = this; // (1)
    if (!superMethod) return function () {
      superClass.apply(self, arguments); // (2)
    }
    
    let prop = superClass.prototype[superMethod];
    if (typeof prop !== 'function') return prop;
    
    return function () {
      return prop.apply(self, arguments); // (3)
    }
  }
  
  if (subMethods) {
    for (let method in subMethods) {
      subClass.prototype[method] = subMethods[method]
    }
  }
  Object.freeze(subClass.prototype);
  return subClass;
}

const Square = extendClass(Rectangle, function (width) {
    this.super()(width, width);
  }, {
  getSize: function () {
    console.log(`size is ${this.super('getSize')()}`);
  }
});

let s1 = new Square(10);
/*
(1) extendClass.getSize {width: 10, height: 10}
(2) Window {…}
*/

s1.getArea(); // size is 100
/*
(1) extendClass.getSize {width: 10, height: 10}
(3) Window {…}
*/

s1.super('getArea')(); // 100
/*
(1) extendClass.getSize {width: 10, height: 10}
(3) Window {…}
*/

super 메소드만 따로 떼서 분석해보았다.

subClass.prototype.super = function (superMethod) {
    let self = this; // extendClass 객체를 this로 저장
    
    // subClass에서 superClass 상속받을 때: superClass 생성자 함수에 접근
    // 예시: 인스턴스.super()(arguments)
    if (!superMethod) return function () {
      superClass.apply(self, arguments);
    }
    
    let prop = superClass.prototype[superMethod];
    if (typeof prop !== 'function') return prop;
    
    // 상위 클래스의 프로토타입 메소드를 호출할 때
    // 예시: 인스턴스.super('메소드이름')(arguments);
    return function () {
      return prop.apply(self, arguments);
    }
  }

 

4. ES6 클래스 문법

위에서 클래스를 따라서 구현하는 방법을 배웠다. 이제 ES6에서 제공하는 class 문법으로 위의 함수를 똑같이 구현해본다.

class Rectangle {
 constructor (width, height) {
   this.width = width;
   this.height = height;
 }
 
  getSize () {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor (width) {
    super(width, width);
  }
  
  getSize () {
    console.log(`size is ${super.getSize()}`);
  }
}

 

'Books > 코어 자바스크립트' 카테고리의 다른 글

06. 프로토타입  (0) 2020.04.16
05. 클로저  (0) 2020.04.12
04. 콜백 함수  (0) 2020.04.04
03. this와 객체  (0) 2020.03.05
02. 실행 컨텍스트  (0) 2019.12.31