Js- class
in Javascript
참고
[1] https://ko.javascript.info/class
[2] https://ko.javascript.info/static-properties-methods
[3] https://ko.javascript.info/private-protected-properties-methods
[4] https://ko.javascript.info/extend-natives
[5] https://ko.javascript.info/mixins
- 자바스크립트에서 클래스는 사실 함수
- 클래스와 함수의 차이점
- 클래스 표현식
- getter와 setter
- 대괄호를 통한 method 등록
- 클래스 필드
- 클래스 필드로 바인딩 된 메서드 만들기
- 클래스 상속
- 조건마다 다른 클래스르 상속하는 클래스 만들기
- constructor 오버라이딩
- 클래스 필드의 오버라이딩
- static method in class
- static property
- static mothod와 static property의 상속
- 객체를 상속하는 클래스
- private field와 method
- 읽기만 가능한 property
- 내장 클래스 확장하기 및 Symbol.species
- 내장 클래스와 static method 상속
- instanceof
- instanceof 의 구현
- 메소드 믹스인
자바스크립트에서 클래스는 사실 함수
- 자바스크립트에서 클래스는 새롭게 창안한 entitiy가 아니며, 함수의 한 종류이다.
- 아래의 class 코드를 실행하면 자바스크립트는 User라는 이름을 가진 함수를 만든다. 함수의 본문은 constructor 함수에서 가져온다. constructor가 없다면 body가 비워진 함수로 만들어진다. 그래서 new 연산자를 사용할 수 있는 거구만..
- sayHi 같은 클래스 내에서 정의한 메서드를 User.prototype에 저장한다. field는 prototype에 저장하지 않는다. 메소드만 저장한다고 여겨도 되겟다.
class User{ constructor(name){this.name = name;} sayHi(){alert(this.name);} } alert(typeof User); // function alert(User == User.prototype.constructor); // true alert(User.prototype.sayHi); // alert(this.name) alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
클래스와 함수의 차이점
- 클래스와 함수와는 같아 보이지만 약간의 차이가 있다.
- class로 만든 함수엔 특수 내부 property인 IsClassConstructor: true 가 이름표처럼 붙는다.
- 클래스도 함수지만 일반 함수와 다르게 new 연산자 없이 사용할 수 없는데, 그 기반으로 여기는 것이 IsClassConstructor이다.
- 클래스에 정의된 method는 열거할 수 없다(non-enumerable). 클래스의 prototype property에 추가된 method의 enumerable flag는 false이다.
class User { constructor() {} } User(); // TypeError: Class constructor User cannot be invoked without 'new'
클래스 표현식
- 함수처럼 클래스에도 다양한 표현방식이 있다.
// Named Class Expression let User = class MyClass { sayHi() { alert(MyClass); // MyClass라는 이름은 오직 클래스 안에서만 사용할 수 있습니다. } }; new User().sayHi(); // 원하는대로 MyClass의 정의를 보여줍니다. alert(MyClass); // ReferenceError: MyClass is not defined, MyClass는 클래스 밖에서 사용할 수 없습니다.// 함수처럼 클래스를 반환할 수도 있다. function makeClass(phrase){ return class{ sayHi(){ alert(phrase); } }; } let User = makeClass("Hi~"); new User().sayHi(); // Hi~
getter와 setter
- literal Object처럼 클래스도 getter나 setter를 지원한다.
- 당연히 method이므로 User.prototype에 정의된다.
class User{ constructor(name){ this.name = name; } get name(){ return this._name; // 언더바가 없으면 오류가 난다. 이유는 잘 모르겟다. } set name(value){ this._name = value; } } let user = new User("보라"); console.log(user.name); // call get name()
대괄호를 통한 method 등록
- literal Object처럼 대괄호를 사용해 계산을 통해 메소드 이름을 등록할 수 있다.
class User{ ['say' + 'Hi'](){ alert("Hello"); } } new User().sayHi();
클래스 필드
- 클래스의 field도 클래스의 property의 한 종류이다.
- 다른 property랑 다르게 User.prototype에 저장되지 않고 개별 객체에 설정된다.
class User{ name = "종연"; // class field } let user = new User(); console.log(user.name); // 종연 consoler.log(User.prototype.name); // undefined
클래스 필드로 바인딩 된 메서드 만들기
- 자바스크립트에서 this는 동적으로 결정된다.
- 객체 메서드를 여기저기 복사 전달하여 다른 컨택스트에서 호출하게 되면 this는 정의된 객체를 참조하지 않는다.
- this의 컨택스트를 알 수 없게 되는 문제를 losing this라고 한다.
class Button{ constructor(value){ this.value = value; } click(){ console.log(this.value); } } let button = new Button("안녕하세요."); setTimeout(button.click, 1000); // undefined- 위의 코드를 해결하기 위해서는 setTimeout(function(){button.click();}, 1000); 을 사용해도 된다.
- 클래스 필드를 이용해서 해결할 수도 있다.
- 클래스 필드를 이용해서 화살표 함수를 만들어주면 그 안에서 this는 항상 객체로 바인딩된다. (일반함수면 안된다) 이유는 모르겠다. 어쩌면 클래스 필드의 고유한 특징일지도….?
class Button{ constructor(value){ this.value = value; } click = () => { // class field // 화살표 함수로 만들어줘야만 this가 객체로 바인딩된다. console.log(this.value); } click = function(){ console.log(this.value); } } let button = new Button("안녕하세요."); setTimeout(button.click, 1000); // 안녕하세요.function Button(value){ this.value = value; } Button.click = () => { console.log(this.value); } Button.click(); // undefinedlet button = { value: 3, click: () => { console.log(this.vaule); } } button.click(); // undefinedlet button2 = { value: 3, click: function() { console.log(this.value); } } button2.click(); // 3
클래스 상속
- 자바스크립트는 프로토타입 기반의 언어이기 때문에 클래스르 만든다고 하더라도 내부적으로는 프로토타입 기반으로 구현된다.
- 일반 클래스의 [[prototype]]은 Function이고 상속받은 클래스의 [[prototype]]은 부모 클래스(함수)이다.
- 아래의 코드는 Animal 클래스를 만들고 Animal 클래스를 상속하는 Rabbit 클래스를 만드는 코드이다.
- Rabbit 클래스는 constructor 함수를 작성하지 않았다. 그렇다면 Animal 클래스에서 constructor를 불러와서 쓴다.
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed = speed; } stop() { this.speed = 0; } } class Rabbit extends Animal { hide() { alert(`${this.name} 이/가 숨었습니다!`); } } let animal = new Animal("동물"); let rabbit = new Rabbit("흰 토끼"); rabbit.run(5); rabbit.hide();
조건마다 다른 클래스르 상속하는 클래스 만들기
- 자바스크립트의 클래스는 사실상 함수이기 때문에 함수의 리턴문으로 사용할 수 있다
- 그렇기 때문에 extends class 구문에 class 대신 함수의 리턴문을 넣을 수도 있다.
- 조건에 따라 다양한 클래스를 넣기 위해서 사용하기에 좋다.
// 각기 다른 sayHi 함수를 가진 클래스를 상속하기 위한 것 function f(phrase){ return class{ sayHi(){ console.log(phrase); } } } class User extends f(phrase) {} new User().sayHi();
constructor 오버라이딩
- 클래스의 constructor가 비어있다면 디폴트로 constructor 함수를 만들고 부모 클래스의 constructor 함수를 실행시킨다.
- 상속을 한 클래스에서는 반드시 부모 클래스의 constructor 함수를 먼저 실행시켜주어야 한다. 안하면 오류남
- 자바스크립트에서는 상속받은 클래스의 constructor 함수에서는 특수 내부 property인 [[constructor]]: “derived” 가 존재한다.
- 일반 클래스 constructor와 상속받은 클래스의 constructor에는 new 연산자와 함께 차이가 난다.
- 일반 클래스의 constructor가 new와 함께 실행되면, 빈 객체가 만들어지고 this에 이 객체를 할당한다.
- 상속받은 클래스의 constructor가 new와 함께 실행되면, 빈 객체가 만들어지고 this에는 아무런 객체를 할당하지 않는다. 그래서 오류가 걸린다.
- 따라서 부모 클래스(일반 클래스)의 constructor를 실행시켜주어 this에 객체를 할당시켜줘야 한다.
class Animal{ constructor(name){this.name = name;} } class Rabbit extends Animal{ constructor(name, age){ this.name = name; this.age = age;} // 오류 constructor(name, age){ super(name); this.age = age;} // 정상 } rabbit = new Rabbit("jy", 20);
클래스 필드의 오버라이딩
- 클래스는 메소드 뿐만 아니라 내부 필드도 오버라이딩 할 수 있다.
- 필드의 초기화 순서가 일반 클래스와 상속받은 클래스가 다르기 때문에 주의해서 사용할 필요가 있다.
- 일반 클래스의 필드는 constructor 실행 이전에 초기화 된다.
- 상속받은 클래스의 필드는 constructor 실행 이후에 초기화 된다.
- 상속받은 클래스의 constructor에서는 사용하지 말라는 소리
class Animal{ name = 'animal' constructor(){ alert(this.name); } showfield(){ alert(this.name); } } class Rabbit extends Animal{ name = 'rabbit'; } animal = new Aminal(); // animal rabbit = new Rabbit(); // animal rabbit.showfield(); // rabbit
static method in class
- 클래스의 필드처럼 클래스의 prototype이 아닌 클래스 함수 자체에 method를 설정할 수도 있다
- 이런 method를 static method라고 부른다
- static method는 클래스의 method를 직접적으로 할당하는 것과 동일하다
- static method의 this는 클래스 함수(객체) 자체가 된다.
- 클래스로 만든 객체에서는 static method에 접근할 수 없다.
class Article{ constructor(title, date){ this.title = title; this.date = date; } static compare(articleA, articleB){ return articleA.date - articleB.date; } } let articles = [ new Article("HTML", new Date(2019, 1, 1)), new Article("CSS", new Date(2019, 0, 1)), new Article("JS", new Date(2019, 11, 1)) ]; console.log(articles.sort(Article.compare));// 클래스 뿐만 아니라 일반 객체에서도 성립 function Article(){ this.title = arguments[0]; this.date = arguments[1]; } Article.compare = function(articleA, articleB){ return articleA.date - articleB.date; } let articles = [ new Article("HTML", new Date(2019, 1, 1)), new Article("CSS", new Date(2019, 0, 1)), new Article("JS", new Date(2019, 11, 1)) ]; console.log(articles.sort(Article.compare));
static property
- static method처럼 static property도 존재한다.
- static method의 특징과 같다.
- field랑 문법이 비슷해보이니 헷갈리지 않도록 주의하자.
class Article{ static publisher = "Ilya Kantor"; } console.log(Article.publisher);
static mothod와 static property의 상속
- static method와 static property는 일반 mothod와 property처럼 상속이 가능하다.
- 상속 받은 클래스의 [[prototype]]이 상속한 클래스를 가리키고 있기 때문이다. (클래스안에 static method, property 들이 있으니 말이다.)
class Animal{ static planet = "지구"; } class Rabbit extends Animal {} console.log(Rabbit.planet); // "지구"
객체를 상속하는 클래스
- 클래스는 실제로는 함수(객체)이기 때문에 함수(객체)를 상속할 수도 있다.
- 아래의 두 코드들의 차이점을 살펴보자
class Rabbit { constructor(name){ this.name = name; } } let rabbit = new Rabbit("Rab"); console.log(rabbit.hasOwnProperty('name')); // rabbit이 hasOwnProperty method를 가지고 있음에 주의 console.log(Rabbit.prototype.__proto__ === Object.prototype); // true console.log(Rabbit.__proto__ === Object); // false console.log(Rabbit.__proto__ === Function.prototype); // trueclass Rabbit extends Object{ constructor(name){ super(); // Object() constructor가 실행된다. this.name; } } let rabbit = new Rabbit("Rab"); console.log(rabbit.hasOwnProperty('name')); console.log(Rabbit.prototype.__proto__ === Object.prototype); // true console.log(Rabbit.__proto__ === Object); // true
private field와 method
- 자바스크립트에는 private, public 두개의 접근 제한 인터페이스를 제공한다.
- 디폴트로 사용하는 것이 public이고 이름 앞에 #을 붙이면 private로 설정해서 클래스 내에서만 접근할 수 있도록 한다.
- private 와 public는 상충하지 않기 때문에 두가지 모두 만들 수 있다.
- private property는 field를 통해서만 만든다.
class Test{ constructor(name){ this.name = name; } #name = "JY" #show_name(){ console.log(this.#name); } show_name(){ console.log(this.name); } show() { this.#show_name(); } } let test = Test("jongyeon"); test.show(); // "JY" test.show_name(); // "jongyeon"
읽기만 가능한 property
- setter 메소드를 사용하지 않고 private field와 getter 메소드만을 사용한다면 읽기만 가능한 property를 만들 수 있다.
class CoffeeMachine{ #name = "" constructor(name){ this.#name = name; } get name(){ return this.#name; } } let coffeemachine = CoffeeMachine("jongyeon"); console.log(coffeemachine.name); // "jongyeon" coffeenachine.name = "test" // error.. no setter
내장 클래스 확장하기 및 Symbol.species
- 자바스크립트에서 기본으로 제공하는 내장 클래스들도 상속이 가능하다
- 아래의 코드에서 내장 클래스로부터 상속받은 메소드(filter, map)같은 것을 사용할 때 Array 함수의 메소드임에도 Array 객체가 아닌 PowerArray 객체를 리턴한다.
- 이것은 해당 메소드들이 리턴할 때 사용하는 객체 constructor를 [Symbol.species]로 부터 얻는데 디폴트 constructor가 자기자신(PowerArray)이기 때문이다.
- Symbol.species를 다른 Constructor로 수정하면 다른 결과를 볼 수 있다.
class PowerArray extends Array { isEmpty(){ return this.length === 0;} } let arr = new PowerArray(1, 2, 5, 10, 50); console.log(arr.isEmpty()); // false let filteredArr = arr.filter(item => item >= 10); console.log(filteredArr); // 10, 50 console.log(filteredArr.isEmpty()); // false ==> filteredArr가 PowerArray의 객체인 것을 알 수 있음.class PowerArray extends Array { isEmpty(){ return this.length === 0;} static get [Symbol.species](){ return Array; } } let arr = new PowerArray(1, 2, 5, 10, 50); console.log(arr.isEmpty()); // false let filteredArr = arr.filter(item => item >= 10); // constructor = arr[Symbol.species] console.log(filteredArr); // 10, 50 console.log(filteredArr.isEmpty()); // error : isEmpty() 존재하지 않는다.
내장 클래스와 static method 상속
- 일반 클래스를 상속할 때는 static method와 property 모두 상속이 가능했다.
- 하지만 내장 클래스를 상속하는 경우 static method와 property 모두 상속하지 못한다.
- 클래스의 [[prototype]] property가 상속하는 클래스를 가리키지 못하기 때문이다. 일반 클래스의 상속의 경우 [[prototype]] property는 상속하는 클래스를 가리킨다.
instanceof
- instanceof 연산자를 사용하면 객체가 특정 클래스에 속하는 지 확인할 수 있다.
- 해당 클래스뿐만 아니라 parent 클래스 모두 훑어본다.
- 클래스 뿐만 아니라 생성자 함수에도 사용할 수 있다.
function A(){} class B extends A {} class C extends B {} c = new C(); console.log(c instanceof A); // True
instanceof 의 구현
- 각각의 함수 내부에는 정적 메소드 Symbol.hasInstance가 구현되어 있다. obj instanceof Class 문이 실행될 때, Class[Symbol.hasInstance](obj) 가 호출된다.
class Animal{ static [Symbol.hasinstance](obj){ if (obj.canEat) return true; } } let obj = { canEat : true }; console.log(obj isinstanceof Animal); // true- 하지만 대부분의 클래스에는 Symbol.hasinstance가 구현되어 있지 않다. 따라서 [[prototype]] 객체를 확인하는 일반적인 로직을 사용한다.
- 이 로직은 obj instanceof Class를 Class.prototype.isPrototypeof(obj)와 동일하게 작동한다.
// __proto__가 null이 나오거나 Object가 나올때까지 loop를 돌며 true가 존재하면 true를 리턴한다. obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype?function Rabbit(){} console.log(new Rabbit() instanceof Rabbit); // true Rabbit.prototype = {}; console.log(new Rabbit() instanceof Rabbit); // falsefunction A() {} function B() {} A.prototype = B.prototype = {}; let a = new A(); console.log(a instanceof B); // true
메소드 믹스인
- 특정 클래스를 상속하지 않고 클래스에 있는 method, property들만을 복사해서 사용하고 싶을 때가 있을 수 있다.
- Object.assign(obj1, obj2); 명령어를 사용하면 obj2에 있는 method, property들을 obj1에 복사해서 사용할 수 있다.
- 아래 코드에서 super에 주의해서 보자.
let sayMixin = { say(phrase) { console.log(phrase); } }; let sayHiMixin = { __proto__: sayMixin, task: "say hello", sayHi() { super.say(`Hello ${this.name}`) }, sayBye() { super.say(`Bye ${this.name}`) } }; class User{ constructor(name){ this.name = name; } } Object.assign(User.prototype, sayHiMixin); // sasyHiMixin에 있는 메소드들을 User.prototype 객체에 복사한다. new User("Dude").sayHi();




