[코어 자바스크립트] 7장 클래스

Dochaki·2024년 10월 9일
0

코어자바스크립트

목록 보기
7/7
post-thumbnail

자바스크립트는 프로토타입 기반 언어라 상속이 존재 하지 않음.
클래스와 비슷한 여러 기법이 나타 났고 ES6에 클래스 문법이 추가 되었음.
But, ES6의 클래스에서도 일정 부분은 프로토타입을 활용하고 있기 때문에 ES5 체제에서 클래스 구현방식을 학습해야함

7-1 클래스와 인스턴스의 개념 이해

상위클래스 - superclass
하위클래스 - subclass

  • 어떤 클래스의 속성을 지니는 실존하는 개체를 일컬어 인스턴스(instance) 라고 함
  • 현실 세계에서는 개체들이 이미 존재하는 상태에서 이들을 구분짓기 위해 클래스를 도입하는데, 그래서 하나의 개체가 같은 레벨에 있는 서로 다른 여러 클래스의 인스턴스일 수 있음
  • 반면 프로그래밍 언어상에서는 접근 방식이 정반대임
    • 사용자가 직접 여러 가지 클래스를 정의해야 하며, 클래스를 바탕으로 인스턴스를 만들 때 비로소 어떤 개체가 클래스의 속성을 지니게 됨.
    • 또한 한 인스턴스는 하나의 클래스만을 바탕으로 만들어짐
    • 어떤 인스턴스가 다양한 클래스에 속할 수는 있지만 이 클래스들은 모두 인스턴스 입장에서는 직계존속

프로그래밍 언어에서의 클래스는 현실세계의 클래스와 마찬가지로 '공통 요소를 지니는 집단을 분류하기 위한 개념' 이라는 측면에서는 일치하지만, 클래스가 먼저 정의돼야만 그로부터 공통적인 요소를 지니는 개체들을 생성할 수 있다는 차이가 있음. 프로그래밍 언어에서의 클래스는 사용하기에 따라 추상적인 대상일 수도 있고 구체적인 개체가 될 수도 있음

7-2 자바스크립트의 클래스

  • JS는 프로토타입 기반 언어로 클래스의 개념이 존재 하지 않음 -> 프로토타입을 활용해서 클래스와 비슷한 기능 구현
  • 생성자 함수 Array를 new 연산자와 함께 호출하면 인스턴스가 생성 됨
    • Array를 클래스라고 하면 Array의 prototype 객체 내부 요소들이 인스턴스에 상속된다고 볼 수 있음
    • Array내부 프로퍼티들 중 prototype 프로퍼티를 제외한 나머지는 인스턴스에 상속되지 않음
  • 인스턴스에 상속(참조)되는지에 따라서 스태틱 멤버(static member)와 인스턴스 멤버(instance member)로 나뉨
    • 클래스 입장에서 사용 대상에 따라 구분한 것
    • 클래스 기반 언어와 달리 js는 인스턴스에서도 직접 메서드를 정의할 수 있어서 인스턴스 메서드라는 명칭이 프로토타입에 정의한 메서드를 지칭하는 것인지 인스턴스에 정의한 메서드를 지칭하는 지 헷갈림
    • 그래서 프로토타입 메서드라고 부르는게 좋음
// 생성자 함수
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

// (프로토타입) 메서드
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};

// 스태틱 메서드
Rectangle.isRectangle = function (instance) {
  return (
    instance instanceof Rectangle && instance.width > 0 && instance.height > 0
  );
};

var rect1 = new Rectangle(3, 4);
console.log(rect1.getArea()); // 정상 작동, 출력: 12
// 잘못된 호출: rect1은 스태틱 메서드를 호출할 수 없음
console.log(rect1.isRectangle(rect1)); // TypeError
// 올바른 호출: 스태틱 메서드는 클래스 이름을 통해 호출해야 함
console.log(Rectangle.isRectangle(rect1)); // true
  • 인스턴스에서 직접 접근할 수 없는 메서드를 스태틱 메서드라고 함
  • 스태틱 메서드는 생성자 함수를 this로 해야만 호출할 수 있음
  • 구체적인 인스턴스가 사용할 메서드를 정의한 '틀'의 역할을 담당하는 목적을 가질 때의 클래스는 추상적인 개념이지만, 클래스 자체를 this로 해서 직접 접근해야 되는 스태틱 메서드를 호출할 때의 클래스는 그 자체가 하나의 개체로서 취급됨

7-3 클래스 상속

7-3-1 기본 구현

  • 클래스 상속은 객체지향에서 가장 중요한 요소 중 하나
  • JS에서 클래스 상속을 구현했다는 것은 결국 프로토타입 체이닝을 잘 연결한 것
  • 클래스에 있는 값이 인스턴스의 동작에 영향을 줘서는 안됨
  • 클래스의 추상성을 해치지 않도록 인스턴스와의 관계에서는 구체적인 데이터를 지니지 않고 오직 인스턴스가 사용할 메서드만을 지니는 '틀'로서만 작용하게끔 작성해야 됨

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

  • 클래스가 구체적인 데이터를 지니지 않게 하는 방법은 여러 가지가 있음
  • 그 중 가장 쉬운 방법은 일단 만들고 나서 프로퍼티들을 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 것
  • 클래스 상속 및 추상화 방법(1) - 인스턴스 생성 후 프로퍼티 제거
// 생성자 함수
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

// 프로토타입 메서드 정의
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};

// 인스턴스 생성
var rect1 = new Rectangle(3, 4);

// 인스턴스의 구체적인 데이터 제거
delete rect1.width;
delete rect1.height;

// 새로운 프로퍼티 추가 방지
Object.freeze(rect1);

console.log(rect1.getArea()); // NaN (width와 height가 없기 때문)
  • 클래스 상속 및 추상화 방법(2) - 빈 함수를 활용
// 빈 생성자 함수
var Rectangle = function () {};

// 프로토타입 메서드 정의
Rectangle.prototype.init = function (width, height) {
  this.width = width;
  this.height = height;
};

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

// 인스턴스 생성 및 초기화
var rect1 = new Rectangle();
rect1.init(3, 4);

console.log(rect1.getArea()); // 출력: 12
  • 클래스 상속 및 추상화 방법(3) - Object.create 활용
// 상위 클래스 정의
var Shape = {
  init: function (type) {
    this.type = type;
  },
  getType: function () {
    return this.type;
  },
};

// 하위 클래스 생성
var Rectangle = Object.create(Shape);
Rectangle.init = function (width, height) {
  Shape.init.call(this, "Rectangle");
  this.width = width;
  this.height = height;
};
Rectangle.getArea = function () {
  return this.width * this.height;
};

// 인스턴스 생성
var rect1 = Object.create(Rectangle);
rect1.init(3, 4);

console.log(rect1.getType()); // 출력: "Rectangle"
console.log(rect1.getArea()); // 출력: 12

7-3-3 constructor 복구하기

  • 클래스 상속 및 추상화 방법 - 완성본(1) - 인스턴스 생성 후 프로퍼티 제거
// SuperClass 정의
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

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

// SubClass 정의
var Square = function (width) {
  Rectangle.call(this, width, width); // SuperClass 생성자 호출
};

// 프로토타입 상속 및 constructor 수정
Square.prototype = Object.create(Rectangle.prototype);
Square.prototype.constructor = Square; // constructor 설정
Object.freeze(Square.prototype); // 프로토타입 동결

// 인스턴스 생성 및 사용
var sq = new Square(5);
console.log(sq.getArea()); // 출력: 25
console.log(sq.constructor === Square); // 출력: true (constructor가 올바르게 설정됨)
  • 클래스 상속 및 추상화 방법 - 완성본(2) - 빈 함수를 활용
var extendClass2 = (function () {
  var Bridge = function () {};

  return function (SuperClass, SubClass, subMethods) {
    Bridge.prototype = SuperClass.prototype;
    SubClass.prototype = new Bridge();
    SubClass.prototype.constructor = SubClass; // constructor 설정

    // SubClass의 메서드 추가
    if (subMethods) {
      for (var method in subMethods) {
        if (subMethods.hasOwnProperty(method)) {
          SubClass.prototype[method] = subMethods[method];
        }
      }
    }

    Object.freeze(SubClass.prototype); // 프로토타입 동결
    return SubClass;
  };
})();

// SuperClass 정의
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

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

// SubClass 정의 및 상속
var Square = function (width) {
  Rectangle.call(this, width, width);
};

extendClass2(Rectangle, Square);

// 인스턴스 생성 및 사용
var sq = new Square(5);
console.log(sq.getArea()); // 25
console.log(sq.constructor === Square); // true
  • 클래스 상속 및 추상화 방법 - 완성본(3) - Object.create 활용
var extendClass3 = function (SuperClass, SubClass, subMethods) {
  // SuperClass의 프로토타입을 SubClass의 프로토타입으로 설정
  SubClass.prototype = Object.create(SuperClass.prototype);

  // SubClass의 constructor를 올바르게 설정
  SubClass.prototype.constructor = SubClass;

  // SubClass의 메서드 추가
  if (subMethods) {
    for (var method in subMethods) {
      if (subMethods.hasOwnProperty(method)) {
        SubClass.prototype[method] = subMethods[method];
      }
    }
  }

  Object.freeze(SubClass.prototype); // 프로토타입 동결
  return SubClass;
};

// SuperClass 정의
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

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

// SubClass 정의 및 상속
var Square = function (width) {
  Rectangle.call(this, width, width);
};

extendClass3(Rectangle, Square);

// 인스턴스 생성 및 사용
var sq = new Square(5);
console.log(sq.getArea()); // 25
console.log(sq.constructor === Square); // 출력: true

7-3-4 상위 클래스에의 접근 수단 제공

  • 상위 클래스 접근 수단인 super 메서드 추가
// 상위 클래스와 하위 클래스를 확장하는 함수 정의
var extendClass = function (SuperClass, SubClass, subMethods) {
  // SuperClass의 프로토타입을 SubClass의 프로토타입으로 설정
  SubClass.prototype = Object.create(SuperClass.prototype);

  // SubClass의 constructor를 올바르게 설정
  SubClass.prototype.constructor = SubClass;

  // super 메서드 정의
  SubClass.prototype.super = function (propName) {
    var self = this;
    if (!propName) {
      // 인자가 비어있을 경우 상위 클래스의 생성자 함수 호출
      return function () {
        SuperClass.apply(self, arguments);
      };
    }
    var prop = SuperClass.prototype[propName];
    if (typeof prop !== "function") {
      // propName이 함수가 아닌 경우 해당 값을 그대로 반환
      return prop;
    }
    // propName이 함수인 경우 클로저를 활용하여 메서드에 접근
    return function () {
      return prop.apply(self, arguments);
    };
  };

  // 하위 클래스의 추가 메서드 정의
  if (subMethods) {
    for (var method in subMethods) {
      if (subMethods.hasOwnProperty(method)) {
        SubClass.prototype[method] = subMethods[method];
      }
    }
  }

  // SubClass의 프로토타입을 동결
  Object.freeze(SubClass.prototype);
  return SubClass;
};

// SuperClass 정의
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

// SuperClass의 프로토타입 메서드 정의
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};

// 하위 클래스 정의 및 상속
var Square = extendClass(
  Rectangle,
  function (width) {
    // super 사용하여 상위 클래스의 생성자 호출
    this.super()(width, width);
  },
  {
    // 하위 클래스의 메서드 정의
    getArea: function () {
      // super 사용하여 상위 클래스의 메서드 호출
      console.log("size is :", this.super("getArea")());
    },
  }
);

// 인스턴스 생성 및 사용
var sq = new Square(10);
sq.getArea(); // size is : 100
console.log(sq.super("getArea")()); // 100

7-4 ES6의 클래스 및 클래스 상속

  • ES5
// ES5 클래스 정의
var ES5 = function (name) {
  this.name = name;
};

// 정적 메서드 추가
ES5.staticMethod = function () {
  return this.name + " staticMethod";
};

// 프로토타입 메서드 추가
ES5.prototype.method = function () {
  return this.name + " method";
};

// ES5 인스턴스 생성과 메서드 호출
var es5Instance = new ES5("es5");
console.log(ES5.staticMethod()); // 출력: es5 staticMethod
console.log(es5Instance.method()); // 출력: es5 method
  • ES5에서는 클래스를 정의하기 위해 함수와 프로토타입을 사용
  • 정적 메서드는 생성자 함수에 직접 추가하고, 인스턴스 메서드는 프로토타입에 추가

  • ES6
// ES6 클래스 정의
var ES6 = class {
  constructor(name) {
    this.name = name;
  }

  // 정적 메서드 정의
  static staticMethod() {
    return this.name + " staticMethod";
  }

  // 프로토타입 메서드 정의
  method() {
    return this.name + " method";
  }
};

// ES6 인스턴스 생성과 메서드 호출
var es6Instance = new ES6("es6");
console.log(ES6.staticMethod()); // 출력: es6 staticMethod (정적 메서드는 클래스 이름이 반환됨)
console.log(es6Instance.method()); // 출력: es6 method
  • ES6에서는 class 키워드를 사용하여 클래스를 정의
  • 정적 메서드는 static 키워드를 사용해 정의하며, 인스턴스 메서드는 클래스 내부에 메서드로 직접 정의할 수 있음

  • ES6의 클래스 상속
var Rectangle = class {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  // 인스턴스 메서드 정의
  getArea() {
    return this.width * this.height;
  }
};

// Square 클래스가 Rectangle 클래스를 상속받음
var Square = class extends Rectangle {
  constructor(width) {
    // 상위 클래스의 생성자를 호출하여 width, height를 초기화
    super(width, width);
  }

  // 인스턴스 메서드 재정의 (오버라이딩)
  getArea() {
    // 상위 클래스의 getArea 메서드를 호출
    console.log("size is:", super.getArea());
  }
};

// 인스턴스 생성과 메서드 호출
var squareInstance = new Square(5);
squareInstance.getArea(); // 출력: size is: 25
  • ES6에서는 extends 키워드를 사용하여 클래스 상속을 구현할 수 있음
  • super 키워드를 사용하여 상위 클래스의 메서드를 호출할 수 있음

ES5, ES6 차이점

클래스 정의
ES5: 생성자 함수와 프로토타입을 사용하여 클래스를 정의
ES6: class 키워드를 사용하여 클래스를 정의

정적 메서드
ES5: 정적 메서드는 생성자 함수에 직접 추가
ES6: static 키워드를 사용하여 클래스 내부에서 정의

상속
ES5: 프로토타입 체인을 통해 상속을 구현
ES6: extends와 super 키워드를 사용하여 쉽게 상속을 구현

가독성
ES5: 코드가 다소 복잡하고 장황함
ES6: 코드가 간결하고 가독성이 높음

7-5 정리

  • 자바스크립트는 프로토타입 기반 언어라서 클래스 및 클래스의 상속 개념은 없지만 클래스 기반 언어와 비슷하게 동작하게끔 하는 다양한 기법들이 도입 되어 옴

  • 클래스는 어떤 사물의 공통 속성을 모아 정의한 추상적 개념이고, 인스턴스는 클래스의 속성을 지니는 구체적인 사례

  • 상위 클래스(superclass)의 조건을 충족하면서 더 구체적인 조건이 추가된 것을 하위 클래스(subclass)

  • 클래스의 prototype 내부에 정의된 메서드를 프로토타입 메서드라고 하며 이 메서드들은 인스턴스에서 자신의 것처럼 호출할 수 있음

  • 클래스(생성자 함수)에 직접 정의한 메서드를 스태틱 메서드라고 하며 이 메서드들은 인스턴스가 직접 호출할 수 없고 클래스(생성자 함수)에 의해서만 호출됨

  • 클래스 상속을 흉내내기 위한 3가지 방법

    • SubClass.prototype에 SuperClass의 인스턴스를 할당한 다음 프로퍼티를 모두 삭제하는 방법
    • 빈 함수(Bridge)를 활용하는 방법
    • Object.create를 이용하는 방법
    • 위 3가지 방법 모두 constructor 프로퍼티가 원래의 생성자 함수를 바라보도록 조정해야 함

0개의 댓글