개발/Javascript

[You don't know JS] Part3 - 부록 (feat. ES6 class)

lanace 2020. 10. 24. 20:11

드디어 마지막 You don't know JS 2권의 마지막인 부록이다.

정리에 앞서 목차를 정리하자면 다음과 같다.

  • ES6 Class
  • asynquence 라이브러리
  • 고급 비동기 패턴

asynquence 라이브러리 와 고급 비동기 패턴의 경우 asynquence 라이브러리의 설명 정도인데, 이 라이브러리가 현재 많이 쓰이는지 잘 모르겠고, 개인적으로 필요해졌을때 보아도 늦지 않을거라고 생각한다.

커뮤니티 자체가 죽은건 아니지만 활발하게 이루어지고 있진 않았고, 최근엔 많은 대안들이 있으므로 선택적인 내용인듯 하다.

따라서 나는 따로 정리하지 않는다.

한번 보고싶으신 분들은 아래 링크를 확인하면 좋을것같다.

https://github.com/getify/asynquence

 

APPENDIX A: ES6. Class


Javascript에서의 Class는 어디까지나 선택적이고, 코드 디자인 패턴일 뿐 [prototype] 중심적인 언어에서 클래스를 구현하는건 어울리지 않다.

  • 상위 수준 메서드를 참조할때 억지로 다형적 레퍼런스를 쓰기 시작하면서 코드 곳곳이 .prototype으로 가득차고, 결국 명시적 의사다형성 형태의 나납한 구문이 됨
  • .constructor 역시 생성되는걸로 오해받고, 정의가 불분명함
  • [prototype]은 복사되는게 아니라 위임되는것임

A.1 Class

기본적인 class 예제를 보자

class Widget {
    constructor(width, height) {
        this.width = width;
        this.height = height;
        this.$elem = null;
    }

    render($where) {
        if (this.$elem) {
            this.$elem.css({
                width: this.width,
                height: this.height
            }).appendTo($where);
        }
    }
}

class Button extends Widget {
    constructor(width, height, label) {
        super(width, height);
        this.label = label || 'Default';

        this.$elem = $('<button>').text(this.label);
    }

    render($where) {
        super($where);

        this.$elem.click(this.onClick.bind(this));
    }

    onClick() {
        console.log('Clicked!');
    }
}

Class 의 도입으로 인한 장점은 다음과 같다.

  1. 더는 .prototype 레퍼런스를 남발할 일이 없다.
  2. Button이 Widget을 extends로 상속을 선언할 수 있다.
    ⇒ Object.create()와 Object.setPrototypeOf() 를 사용하지 않아도 된다.
  3. super()라는 메서드를 통해 상위에 있는 동일 명칭의 메서드에 접근 가능하다.
  4. class 리터럴 구문에서 프로퍼티를 꼭 지정할 필요 없다.
  5. extends는 Array나 RegExp 같은 내장 객체의 서브타입도 확장이 가능하게 해준다.

A.2 Class의 함정

명심해야 하는것은 Class는 기존 [prototype] 체계에 기반을 둔 간편 구문일 뿐이다.

동적으로 부모가 바뀌는 문제

class는 다른 class지향 언어처럼 선언 시점에 정적으로 정의된 내용을 복사하지 않는다.

즉, 부모 클래스 메서드를 수정하면 자식에게도 영향을 미친다.

class C {
    constructor() {
        this.num = Math.random();
    }

    rend() {
        console.log(`난수: ${this.num}`);
    }
}

var c1 = new C();
c1.rand();

C.prototype.rand = function() {
    console.log(`난수: ${this.num * 1000}`);
}

var c2 = new C();
c2.rand();
c1.rand();

위의 예제에서처럼 c1의 rand 까지도 중간에 바뀌어버린다.

이는 복사가 아닌 위임되기 때문이다.

공유 인스턴스 문제

또한 class구문에서 클래스 멤버 프로퍼티를 선언할 수 없다.

따라서 인스턴스끼리 공유하는 변수를 만들기 위해선 prototype구문을 사용할수밖에 없다.

그치만 .prototype을 노출하면서 class 구문의 원칙을 저버리게 된다.

또한 공유 상태를 업데이트하려고 this.count++를 하면 모두 가려진 .count 프로퍼티를 암시적으로 생성한다.

가려짐 문제

class C {
    constructor(id) {
        this.id = id;
    }

    id() {
        console.log('id');
    }
}

var c1 = new C();
c1.id(); // error

위와 같이 가려짐 문제도 여전히 있다.

super의 문제

super의 작동 방식도 문제가 있는데, super가 정적바인딩 된다는 것이다.

다른것들은 this 바인딩과 같이 동적으로 될것같지만 정적으로 동작한다.

깊숙한 곳에서 매번 재바인딩 하면서 생기는 사이드이펙트를 유발할 수 있으므로 꽤 큰 문제이다.

class P {
    foo() {
        console.log("P");
    }
}

class C extends P {
    foo() {
        super();
    }
}

var D = {
    foo: function {
        console.log("D");
    }
}

var E = {
    foo: C.prototype.foo
}

Object.setPropertypeOf(E, D);

E.foo();  // P

위의 예제에선 E에 D를 바인딩 했지만 여전히 super를 통해 P가 출력됨을 볼 수 있다.

이러한 문제를 해결하지 못하는 이유는 현실적인 성능 이슈때문에 this 처럼 늦은 바인딩이 되지 않고 호출 시점에 바인딩 된다.