개발공부/자바스크립트

[JavaScript] 스터디 11일차_ 클래스(2) 상속(ES6)

햄❤️ 2021. 7. 15. 00:28
728x90

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

 - 클래스(prototype)가 구체적인 데이터를 지니지 않게 하는 방법
   => 프로퍼티를 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 한다.

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

왼쪽에서 Square의 prototype은 height와 width가 있지만, 오른쪽에서 width와 height가 삭제되었다. 

✨Object.freeze()

    - 객체를 동결한다. 동결된 객체는 더 이상 변경될 수 없다. 즉 동결된 객체는 새로운 속성을 추가하거나 존재하는 속성을 제거하는 것을 방지하며, 존재하는 속성의 값이 변경되는 것도 방지한다. 또한 동결 객체는 그 프로토타입이 변경되는 것도 방지한다. (출처: mdn)

  예시) obj객체를 동결했으므로, 내부에 존재하는 속성의 값인 prop을 33으로 변경 시도했으나, 42를 유지한다. 

const obj = {
  prop: 42
};

Object.freeze(obj);

obj.prop = 33;
// Throws an error in strict mode

console.log(obj.prop);
// expected output: 42

 

1.3.2.1 클래스 상속 및 추상화 방법 - 인스턴스 생성 후 프로퍼티 제거_예제 1번

- 추상화? 객체지향 프로그래밍에서의 추상화란 결국 클래스를 정의하는 것이다. ( = 객체를 정의하는 것!!) 

 상위 클래스의 인스턴스로 하위 클래스의 프로토타입을 할당한다. 하위 클래스에서는 상위 클래스의 메서드나 프로퍼티에 접근 가능하다. 하위 클래스의 프로토타입들을 삭제(delete) 후 하위 클래스의 프로토타입 객체를 freeze()로 동결하여 변경 방지 

  🍄Square 생성자 = SubClass(하위 클래스), Rectangle 생성자 = superClass (상위 클래스), Square 생성자는 상속받은 Rectangle 생성자의 메서드와 프로퍼티가 삭제되어 없다

(좌) 프로퍼티 제거 전 Square.prototype / (우) 프로퍼티 제거 후 Square.prototype 

 

var extendClass1 = function(SuperClass, SubClass, subMethods) {
	SubClass.prototype = new SuperClass();
    for(var prop in SubClass.prototype){
    	if(SubClass.prototype.hasOwnProperty(prop)){
        //SubClass.prototype이 prop이라는 프로퍼티가 있는지 true/false로 반환, 있다면 해당 프로퍼티 삭제
        	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);
});

 

1.3.2.2 클래스 상속 및 추상화 방법 - 빈 함수를 활용_예제 2번

  - 다른 방법으로는, SubClass(하위 클래스)에 직접 SuperClass(상위 클래스)의 인스턴스를 할당하는 대신, 아무런 프로퍼티를 생성하지 않는 빈 생성자 함수(=Bridge)를 하나 더 만들어서 그 prototype이 SuperClass(상위클래스)의 prototype을 바라보게끔 한 다음, SubClass(하위 클래스) prototype에는 Bridge의 인스턴스를 할당한다.

 - Bridge의 프로토타입이 Rectangle의 prototype을 참조한다. 그리고 Bridge 생성자의 인스턴스로 Square의 프로토타입을 할당함으로, Square의 프로토타입은 Bridge의 프로토타입을 참조한다( = Rectangle의 프로토타입)

 - Rectangle의 자리는 Bridge로 대체되고, 인스턴스를 제외한 프로토타입 체인 경로상에는 더는 구체적인 데이터가 남아있지 않다. (width, height, getArea이 없어진다) 

(위) 프로퍼티 제거 전 Square.prototype / (아래) 프로퍼티 제거 후 Square.prototype 

 

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

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

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

var Bridge = function(){};
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);

 

 

1.3.2.3 클래스 상속 및 추상화 방법 - 빈 함수를 활용_예제 3번

   - 즉시실행함수 내부에서 Bridge를 선언해서 클로저로 활용하며 메모리에 불필요한 함수 선언을 줄였다.(?) subMethods에는 SubClass의 prototype에 담길 메서드들을 객체로 전달했다.  마찬가지로 Bridge의 인스턴스로 하위 클래스의 프로토타입이 정의되어 Square.prototype은 1.3.2.2 처럼 빈 배열을 가리킨다. 

var extendClass2 = (function(){
	var Bridge = function(){};
    //return 밖에 있는 Bridge 함수를 참조한다. = 클로저 
    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;
    };

})();

 

1.3.2.1 클래스 상속 및 추상화 방법 - Object.create ()활용_예제 4번

   - 이 방법은 SubClass의 prototype의 __proto__가 SuperClass의 prototype을 바라보되, SuperClass의 인스턴스가 되지 않는다. new 없이 객체를 생성가능하다. 

*Object.create(새로 만든 객체의 prototype이어야 할 객체) 메소드는 새롭게 생성 된 객체의 프로토타입과 같은 기존의 객체를 사용하여 새로운 객체를 생성한다.  

 - Object.create()는 상위 클래스의 프로토타입을 상속하는 하위 클래스 프로토타입을 바로 만들 수 있는 장점이 있으나, 하위 프로토타입의 constructor가 정의되지 않는다는 단점이 있다. 이를 해결하기 위해 1.3.3.3 에서 해결한다. 

// (...생략)
Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);
//(...생략..)

 

Object.create(); 의 mdn 예제를 보자... 

 - 여기서 핵심은 Rectangle.prototye.constructor = Rectangle; 전과 후인데, 이 문장을 쓰기 전 Rectangle.prototype의 구조를 보면 constructor가 없다. Object.create()로 만들어진 하위 클래스는 constructor가 없기 때문에 Rectangle.prototye.constructor = Rectangle; 를 할당해주어야 한다.

 

Rectangle.prototye.constructor = Rectangle; 

 

Rectangle.prototye.constructor = Rectangle;

// Shape - 상위클래스
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 상위클래스 메서드
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 하위클래스
function Rectangle() {
  Shape.call(this); // super 생성자 호출.
}

// 하위클래스는 상위클래스를 확장
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'

1.3.3 constructor 복구하기

 - 위의 3가지 방법으로 기본적인 상속은 가능하지만, 하위 클래스 인스턴스의 constructor은 상위 클래스를 가리킨다. 엄밀히는 하위클래스 인스턴스에는 constructor가 없고, SubClass.prototype에도 없다.
 따라서 위 코드들의 SubClass.prototype.constructor가 원래의 SubClass를 바라보도록 해주어야 한다.

1.3.3.1 클래스 상속 및 추상화 방법 - 인스턴스 생성 후 프로퍼티 제거 _ 예제 1번

var extendClass1 = function(SuperClass, SubClass, subMethods) {
	SubClass.prototype = new SuperClass();
    for(var prop in SubClass.prototype){
    	if(SubClass.prototype.hasOwnProperty(prop)){
        //SubClass.prototype이 prop이라는 프로퍼티가 있는지 true/false로 반환, 있다면 해당 프로퍼티 삭제
        	delete SubClass.prototype[prop];
        }
    }
    //✨추가된 것
    SubClass.prototye.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);
});

 

1.3.3.2 클래스 상속 및 추상화 방법 - 빈 함수를 활용_ 예제 2번

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

})();

 

1.3.3.3 클래스 상속 및 추상화 방법 - Object.create_ 예제 3번

var extendClass3 = function(SuperClass, SubClass, subMethods){
	SubClass.prototype = Object.create(SuperClass.prototype);
    
    //✨추가된 것
    SubClass.prototype.constructor = SubClass;
    if(subMethods){
    	for(var method in subMethods){
        	SubClass.prototype[method] = subMethods[method];
        }
    }
    Object.freeze(SubClass.prototype);
    return SubClass;
};

 

1.3.4 상위 클래스에의 접근 수단 제공 🍄🍄..(skip 합니다... 주륵) 

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.apply(self, arguments)
        }
    }
    var prop = SuperClass.prototype[propName];
    if(typeof prop !== 'function') return prop;
    return function(){
    	return prop.apply(self,argument);
    }
    if(subMethods){
    	for(var method in subMethods){
        	SubClass.prototype[method] = subMethods[method];
        }
    }
    Object.freeze(SubClass.prototype);
    return SubClass;
};

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

var Square = extendClass(
	Rectangle,
    function(width){
    	this.super()(width, width); //super 사용 (1)
    }, {
    	getArea: function(){
        	console.log('size is: ', this.super('getArea')()); //super 사용 (2)
        }
    }
);

var sq = new Square(10);
sq.getArea();// size is : 100
console.log(sq.super('getArea')()); //100

 

1.4  ES6의 클래스 및 클래스 상속

 - 우리의 구세주 ES6의 클래스를 통해 여태까지 한 ES5에서의 클래스(프로토타입)을 다뤘던 방식과 비교하자 ! 

 - class 명령어 바로 뒤에 {} 가 등장. 중괄호 묶음 내부가 클래스 본문 영역

   - Constructor:  class로 키워드로 선언된 클래스 내부에 constructor 키워드로 생성자를 정의한다. 클래스 본문에서는 function 키워드 없어도 메서드로 인식한다. 클래스에서 constructor 이름을 갖는 메소드는 하나여야 한다. 만약 클래스에 여러 개의 constructor 메서드가 존재하면 SyntaxError 가 발생한다. 

 - 메서드와 메서드 사이는 , 콤마로 구분하지 않는다. 

   - 스태틱 메서드: static이라는 키워드와 함께 선언되는 메서드로  클래스의 인스턴스에서 static 메소드를 호출 할 수 없고, 생성자 함수(클래스) 자신만이 호출할 수 있다. 

  - prototype 메서드: 클래스 내에 function 키워드를 생략한 함수는 모두 메서드로 인식. 자동으로 prototype 내부에 할당된다. 인스턴스가 프로토타입 체이닝을 통해 자기 것처럼 메서드를 호출 가능하다.

 

1.4.1 ES5와 ES6의 클래스 문법 비교 _ 예제 1번

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

ES5.staticMethod = function(){
	return this.name + 'staticMethod';
};

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

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

var ES6 = class {
	constructor(name){
    	this.name = name;
    }
    static staticMethod(){
    	return this.name + 'staticMethod';
    }
    method (){
    	return this.name + 'method';
    }
};

const es6Instance = new ES6('es6');
console.log(ES6.staticMethod()); //es6 staticMethod
console.log(es6Instance.method()); //es6 method

 

1.4.2 ES6의 클래스 상속_ 예제 2번

   - extends : class를 다른 class의 자식으로 만들기 위해 사용.  
        상속받고 싶은 클래스 앞에 extends키워드를 선언하면 된다.

class 자식클래스명 extends 부모클래스명 {}

 

  - 아래 예제에서 Square가 Rectangle로부터 extends 키워드로 상속받았다. prototype이 Rectangle 을 가리킨다.

- super:  constructor 내부에서는 super라는 키워드를 함수처럼 사용할 수 있는데, 이 함수는 SuperClass의 constructor을 실행한다. constructor 메서드를 제외한 다른 메서드에서는 super 키워드를 마치 객체처럼 사용할 수 있고, 이때 객체는 SuperClass.prototype을 바라본다. 호출한 메서드의 this는 'super'가 아닌 원래 this를 따른다. 
 (Square 생성자로 만든 인스턴스(sq)의 this는 Square이다. Rectangle이 아님)

 

 

쉽게 말하면, super 키워드는 부모 class를 참조하거나 부모 class의 constructor을 호출할 때 사용한다. constructor 안에 부모에게 상속받을 변수를 super()을 사용해서 불러온다. 예제에서는 Rectangle로부터 width 변수를 super() 키워드로 불러왔다. (!?) * 메서드 오버라이딩을 기억하시는지! 자식 클래스는 부모 클래스와 이름이 같은 메소드를 만들 수 있는 것!
   그리하여, 자바스크립트가 호출된 객체 안에서 메서드를 바로 찾을 수 있으면 자식 클래스의 메서드가 실행된다. 
 but super 키워드를 이용하면, 자식클래스의 메서드가 아닌 부모 클래스의 메서드를 이용할 수 있다.

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

//🍄버섯이 추가한 내용임
let nemo = new Rectangle(10,20);
console.log(nemo.getArea());
//200 출력


var Square = class extends Rectangle {
	constructor (width) {
    	super(width, width);
    }
    getArea(){
    	console.log('size is:', super.getArea()); //부모 클래스인 Rectangle의 메서드인 getArea가 실행된다
    }
};

 

let nemo = new Rectangle(10,20);
console.log(nemo);

 

let sq = new Square(10);
sq.getArea();
// size is: 100 출력

 

super를 이용한 조금 더 쉬운 예를 들어보겠습니다... 🔥 T_T

 - super 사용 전

class diet {
	eat() {
    	return "banana"
    }
}

class godanback extends diet {
	eat() {
    	return "chicken breast";
    };
};

const sikdan = new godanback();
sikdan.eat();
//"chicken breast" 출력

 

 - super 사용 후

class diet {
	eat() {
    	return "banana"
    }
}

class godanback extends diet {
	eat() {
    	//super을 이용해 부모의 메서드를 훔쳐올 것이다
    	return super.eat();
    };
};

const sikdan = new godanback();
sikdan.eat();
// banana 출력

 

정리

✨ 클래스 상속을 흉내내기 위한 3가지 방법. 첫번째 SubClass.prototype에 SuperClass의 인스턴스를 할당하고, 다음 프로퍼티를 삭제하는 법이고 두번째는 빈 함수(Bridge)를 활용하는 방법, 세번째는 Object.create를 이용하는 방법이다. but 이 세가지 모두 constructor 프로퍼티가 원래의 생성자 함수를 바라봐야한다. 

✨ ES6에서 상위 클래스에 접근할 수 있는 수단인 super 키워드가 있다. 

 

출처

정재남, 『코어자바스크립트』, 위키북스(2019)

https://typeof-undefined.tistory.com/7 

https://geunee92.tistory.com/14

728x90