[코어 자바스크립트 24.06.10-CLEAR] 클래스

변진상·2024년 6월 10일
0

학습 기록

목록 보기
27/31

자바스크립트는 프로토타입 기반 언어라서 '상속' 개념이 존재하지 않는다.
그래서 클래스를 흉내내는 기법들이 탄생했고 ES6에는 클래스 문법이 추가되었다.
클래스 문법에서도 프로토타입을 활용하기에 ES5 체제 하에 클래스를 구현하는 방식을 학습하는 것이 의미가 있다.

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

  • 프로그래밍 언어에서의 클래스와 현실세계에서의 클래스 비교
    - 공통점: 공통요소를 지니는 집단을 분류하기 위한 개념
    - 차이점: 현실세계는 존재하는 인스턴스들의 공통점으로 클래스 정의, 프로그래밍 언어에서는 클래스를 정의해야만 공통적인 요소를 지니는 개체를 생성할 수 있다.
  • 클래스는 추상적일 수도 있고 구체적인 개체가 될 수도 있다. → 이후에 설명

2. 자바스크립트의 클래스

자바스크립트는 프로토타입 기반 언어이므로 클래스 개념이 존재하지 않지만 비슷하게 해석할 요소가 있다.

  • 생성자 함수 Array를 new 연산자와 호출하면 인스턴스가 생성된다.
  • 이때 Array를 클래스라고 하면 Array의 prototype 객체 내부 요소들이 인스턴스에 상속된다고 볼 수 있다(엄밀히는 상속이 아닌 프로토타입 체이닝에 의한 참조이다). Array 내부 프로퍼티는 prototype을 제외한 나머지는 인스턴스에 상속되지 않는다.
  • 인스턴스에 상속되는지 여부에 따라 스태틱 멤버(static member)와 인스턴스 맴버(instance member)로 나눈다. 이는 클래스 기반 언어의 정의를 차용한 것이다.
  • 자바스크립트에서는 인스턴스에서도 직접 메서드를 정의할 수 있기 때문에 인스턴스 메서드라는 명칭은 프로토 타입에 정의 한 메서드를 지칭하는 것인지 인스턴스에 정의한 메서드를 지칭하는 것인지 혼란을 야기한다. 그래서 프로토타입 메서드라고 부르는 편이 좋다(실제로 커뮤니티에서는 후자를 더 많이 사용한다.).
	var Person = function(name){
    	this._name = name;
    }
    
    Person.prototype.getName = function() {
      return this._name;
    } // 프로토 타입에 정의한 메서드 (프로토타입 메서드)

	Person.isPerson = function(inst){
    	return inst instanceof Person;
    } // 스태틱 메서드

	var	p1 = new Person('kim');

	p1.getName = function(){
		return this._name + '입니다.';
    } // 인스턴스에서 정의한 메서드

클래스는 추상적일 수도 있고 구체적인 개체가 될 수도 있다.
- 인스턴스를 생성할 때의 틀 역할 담당하는 목적을 가질 때의 클래스는 추상적 개념
- 클래스 자체를 this로 해서 직접 접근해야하는 스태틱 메서드를 호출할 때의 클래스는 구체적 개체

3. 클래스 상속

7-3-1. 기본 구현

  • ES5까지의 자바스크립트 커뮤니티에서는 클래스 상속을 다른 클래스 기반 언어를 흉내내는 것이 주요 관심사였다.

  • 아래 코드가 클래스 상속의 핵심이다. Grade ↔ Array ↔ Object간의 Superclass, Subclass관계를 가진다.

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;
}

Grade.prototype = [];
var g = new Grade(100, 80);

클래스 상속과 프로토타입 체인의 관계도

  • length 프로퍼티가 configurable(삭제 가능)하다는 점과, Grade.prototype에 빈 배열을 참조 시켰다는 점에서 완벽하게 클래스를 구현했다기에는 무리가 있다. 아래 코드는 그 예를 보여주는 코드이다.
g.push(90);
cosole.log(g); // Grade { 0: 100, 1:80, 2:90, length: 3}

delete g.length;
g.push(70);
console.log(g); // Grade { 0: 70, 1:80, 2:90, length: 1}
  • 첫 번째 출력은 결과가 잘 출력되었다.

  • length 프로퍼티를 삭제한 후, 두번 째 출력에서는 0번 인덱스에 70이라는 값이 들어갔고 length가 1이 되었다.

  • 이 이유는 g.length 프로퍼티가 없기 때문에 프로토타입 체이닝을 통해 따라가다가 빈 배열의 인스턴스를 가리키고 있는 g.__proto__의 length: 0을 읽어온다. 그래서 0번 인덱스에 70을 push하고 length를 1로 늘리게되어 두 번째 출력과 같은 결과가 나온다.

  • 이렇게 클래스에 위치한 값이 인스턴스의 동작에 영향을 주면 클래스의 추상성을 해치는 것이다. 인스턴스와의 관계에서는 구체적인 데이터를 지니지 않고 인스턴스가 사용할 메서드만을 지니는 틀로 작용하도록 구현해야한다.

❗️ 사용자 구현 클래스에서의 상속관계 구현

  • Before 클래스 상속 구현 전
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

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

var rect = new Rectangle(3, 4);
console.log(rect.getArea()); // 12

var Square = function (width) {
  this.width = width;
};

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

var sq = new Square(5);
console.log(sq.getArea()); // 25
  • Before 클래스 상속 구현 후
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

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

var rect = new Rectangle(3, 4);
console.log(rect.getArea()); // 12

var Square = function (width) {
  Rectangle.call(this, width, width);
};

Square.prototype = new Rectangle();

var sq = new Square(5);

console.log(sq.getArea()); // 25

❗️ 문제 1. 이 코드도 클래스에 있는 값이나 인스턴스에 영향을 줄 수 있는 동일한 구조이다.
Rectangle내의 height, width에 새 값을 할당하고 Square의 width, height를 지우면 프로토타입 체이닝에 의해 이상한 결과가 나온다.

❗️ 문제 2. Square(.__proto__).constructor가 Rectangle을 가리키고 있다.

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

  1. 일단 상속 후 클래스 내부의 프로퍼티를 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 방법
delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(delete Square.prototype);
  • 위 컨셉을 확장해 범용성 있는 코드 구현
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

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

var extendClass1 = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = new SuperClass();

  for (var prop in SubClass.prototype) {
    if (SubClass.prototype.hasOwnProperty(prop)) {
      // 프로토타입들을 돌면서 프로퍼티만 삭제
      delete SubClass.prototype[prop];
    }
  }

  if (subMethods) {
    for (var method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }

  Object.freeze(SubClass.prototype);
  return SubClass;
};

var Square = extendClass1(Rectangle, function (width) {
  Rectangle.call(this, width, width);
});

var sq = new Square(5);

console.log(sq);
  1. 더글라스 크락포드가 제시한 방법
    빈 함수를 이용한다는 특징이 있다.
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

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

var Bridge = function () {};

var Square = function (width) {
  Rectangle.call(this, width, width);
};

Bridge.prototype = Rectangle.prototype;
// Bridge의 프로토타입이 Rectangle의 프로토타입을 참조
Square.prototype = new Bridge();
// Square의 프로토타입에 Bridge의 인스턴스를 할당하면 Rectangle 자리에 Bridge가 대체한다.
// 이로써 프로토타입 체인 경로상에는 구체적인 데이터가 남아있지 않게 된다.
Object.freeze(Square.prototype);

var sq = new Square(5);

console.log(sq);
  • 위 방식을 범용성 있게 구현
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

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

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;
  };
})();

var Square = extendClass2(Rectangle, function (width) { Rectangle.call(this, width, width) });

var sq = new Square(5);

console.log(sq);
  1. ES5 Object.create 활용
Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);
  • 이 방식은 SubClass의 prototype의 __proto__가 SuperCalss의 prototype을 바라보되, SuperClass의 인스턴스가 되지는 않아 안전하다.

7-3-3. constructor 복구하기

위 세가지 방법은 기본적인 상속은 성공했지만 SubClass의 constructor가 SuperClass를 가리키는 문제가 있다. 엄밀히는 SubClass 인스턴스는 constructor가 없고 SubClass.prototype에도 없는 상태이다. 체이닝 상 가장 먼저 등장하는 SuperClass.prototype.constructor가 출력되는 것이다.

1 번 방법

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

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

var extendClass1 = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = new SuperClass();

  for (var prop in SubClass.prototype) {
    if (SubClass.prototype.hasOwnProperty(prop)) {
      // 프로토타입들을 돌면서 프로퍼티만 삭제
      delete SubClass.prototype[prop];
    }
  }
  SubClass.prototype.constructor = SubClass;

  if (subMethods) {
    for (var method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }

  Object.freeze(SubClass.prototype);
  return SubClass;
};

var Square = extendClass1(Rectangle, function (width) {
  Rectangle.call(this, width, width);
});

var sq = new Square(5);

console.log(sq);

2번 방법

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

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

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;
  };
})();

var Square = extendClass2(Rectangle, function (width) { Rectangle.call(this, width, width) });

var sq = new Square(5);

console.log(sq);

3번 방법

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

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

하위 클래스의 메서드에서 상위 크랠스의 메서드 실행 결과를 바탕으로 작업하고 싶을 때가 있다. 이럴 때 마다 SuperClass.prototype.method.apply(this, arguments)로 접근하는 것은 번거롭다.
→ 다른 객체지향 언어들의 클래스 묹법인 'super'를 흉내 내보고자 한다.

var extendClass = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = Object.create(SuperClass.prototype);
  SubClass.prototype.constructor = SubClass;
  
  SubClass.prototype.super = function (propName) { 	// 추가된 부분 시작
    var self = this;
    
    if(!propName) return function () { 
      // 비었을 경우 SuperClass 생성자 함수에 접근하는 것으로 간주
     	SuperClass.apply(self, arguments);
      // this가 달라지는 것을 방지하기 위해 클로저 사용
    }
    
    var prop = SuperClass.prototype[propName];
    
    if(typeof prop !== 'function') return prop; 	// propName에 해당하는 값이 함수가 아닌 경우 그 값을 반환
    
    return function() {
    	return prop.apply(self, arguments);  		// 메서드에 접근
    }
  }; 												// 추가된 부분 끝

  if (subMethods) {
    for (var method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }

  Object.freeze(SubClass.prototype);
  return SubClass;
};

var Square = extendClass (Rectangle, function(width){
	this.super()(width,width); 								// super 사용 (1) SuperClass 생성자 함수
}, {
  	getArea: function(){
     	console.log('size is :', this.super('getArea')()); // super 사용 (2) => size is : 100
    }
})

4. ES6의 클래스 및 클래스 상속

ES6에서 클래스 문법이 도입되었다.

ES5, ES6 클래스 비교

var ES5 = function (name) {
  this.name = name;
};

ES5.staticMethod = function () {
  return this.name + " static method";
};

ES5.prototype.method = function () {
  return this.name + " method";
};

var es5Instance = new ES5("es5");
console.log(ES5.staticMethod());  // 'ES5 static method'
console.log(es5Instance.method()); // 'es5 method'

var ES6 = class {
  constructor(name) {
    this.name = name;
  }

  static staticMethod() {
    return this.name + " static method";
  }

  method() {
    return this.name + " method";
  }
};

var es6Instance = new ES6("es6");

console.log(ES6.staticMethod());   // 'ES6 static method'
console.log(es6Instance.method()); // 'es6 method'

5. 정리

  • 자바스크립트는 프로토타입 기반 언어라서 클래스 및 상속 개념이 없다. 이를 비슷하게 동작하게끔 하는 다양한 방법들이 있었다.
  • 클래스는 어떤 사물의 공통 속석을 모아 정의한 추상적 개념이다.
  • 인스턴스는 클래스의 속성을 지니는 구체적 사례이다.
  • 상위 클래스(Superclass)의 조건을 충족하면서 더 구체화적인 조건이 추가된 것을 하위 클래스(Subcass)라고 한다.
  • 클래스(생성자 함수)에 직접 정의한 메서드를 스태틱 메서드, 인스턴스가 직접 호출할 수 없고 클래스에 의해서만 호출 가능하다.
  • 클래스 상속을 흉내낸 방법 3가지
    1. SubClass.prototype에 SuperClass의 인스턴스를 할당한 다음 프로퍼티를 모두 삭제하는 방법
    1. 빈 함수(Bridge)를 활용하는 방법
    2. Object.create를 이용하는 방법
      → 이 세가지 방법은 모두 constructor 프로퍼티가 원래의 생성자 함수를 바라보도록 조정해야한다.
  • super를 구현해봤다.
profile
자신을 개발하는 개발자!

0개의 댓글