본문으로 건너뛰기

class_ES6 노트 10

무료2016-07-30#JS#es6 class#js class#javascript class#js类#javascript类

JS 가 Java 로 변하는 것인가!?상쾌한 타입 정의 문법, keep it simple

一.JS 가 Java 로 변하는 것인가!?

ES6 은 class, constructor, static, extends, super 등의 키워드를 활성화하여 클래스 정의를 지원합니다. 마치 Java 로 변하려는 것 같아서, 드디어 prototype 을 신경 쓰지 않아도 되는 것일까요?

그렇지 않습니다. 이 일련의 키워드를 활성화하는 것은 단순히 "문법 노이즈"를 줄이고, 클래스 구조를 정의할 때의 작업량을 줄이기 위함입니다. JS 의 프로토타입 기반 (prototype-based) 오브젝트 시스템을 클래스 기반 (class-based) 으로 쉽게 변경할 수는 없습니다. 이는 JS 언어 설계자의 선택이며, 불가능합니다. 기존의 오브젝트 시스템을 뿌리째 뽑아내고, Java 의那一套을 꿰매어 넣을 수는 없습니다.

P.S. "문법 노이즈"는 老趙에서 빌려온 단어입니다. Python 과 Java 로 같은 기능을 구현하면, diff 결과가 "문법 노이즈"가 됩니다. 예를 들어 JS 중의 function, prototype 등 입력하기 괴로운 것들

또한, 프로토타입 기반의 오브젝트 시스템은 비교적 유연합니다. 예를 들어 JS 는 런타임에서 동적으로 클래스 상속 트리를 수정할 수 있습니다. 따라서 빈 클래스 상속 트리를 미리 정의해 두고, 필요할 때 클래스 멤버를 추가할 수 있습니다. 예를 들어:

// 빈 상속 트리
var SuperType = function() {};
var SubType = function() {};
SubType.prototype = new SuperType();
// 동적 강화
void function() {
    //...
    SuperType.prototype.cl = console.log.bind(console);
    new SubType().cl('invoked by subType'); // invoked by subType
}();

이 느낌은 매우 기묘합니다. Java 와 JS 가 함께 그림을 그립니다. Java 는 정교한 조각도로 캔버스 위에 일사불란하게 청명상하도를 완성하고, 장첩하여 벽에 걸어 사방을 놀라게 합니다. JS 는 수성펜으로 2 개의 구불구불한 가로선을 그려 "그림을 그렸다. 이것이 청명상하도다"라고 말합니다. 관객은 우롱당했다고 느끼지만, 어쩔 수 없이 JS 의 작품도 장첩하여 Java 의 대작 옆에 겁니다. Java 는束手而立하여 분曉를 기다립니다.这时 JS 가 바쁘게 움직이기 시작해, 펜을 들고 진지하게 액자 유리면 위에一模一样的 작품을 완성합니다. 관객은 불가사의하게 느낍니다. 갑자기 人群中에서 하나의 소리가 전달됩니다. "왼쪽에서 세 번째 집은 뾰족한 지붕이어야 한다.你们都画错了". Java 의 얼굴은 견딜 수 없게 되어, 급히 다른 선지를 펼쳐서,那个碍眼的 오류를 고쳐重新 그리려고 합니다. JS 는 왼쪽 위 모서리를 가리켜 "여기인가?我已经改好了"라고 말합니다

JS 는 명령형 언어, 동적 언어, 함수형 언어의 결합체입니다. 체계 전체而言, 오브젝트 시스템은 프로토타입 구현에 의한 이유연성이 필요합니다. Java 의 정彫細刻한 클래스 기반 오브젝트 시스템을 부러워할 필요는 없습니다. 완전히 맞지 않습니다. ES 규범 설계자는 Java 를 그대로 베낄 수 없고, 이유도 없습니다.至此, JavaScript 와 Java 는 여전히毫도 관계없습니다. 20 년 전과 마찬가지로

P.S. JS 와 필자는 모두 1995 년에 탄생했습니다. Netscape 와 JavaScript 와 Java Applet 의丝丝缕缕에 대해서는,自行으로查找하십시오

二.class 가 가져오는 새 특성

ES6 이전의 "class"는 이런 것일 수 있습니다:

function Circle(radius) {
    if (typeof radius !== 'number') {
        throw new TypeError('radius: a number expected');
    }
    // 인스턴스 속성
    this.radius = radius;
    // 정적 속성
    Circle.count++;
}
Circle.count = 0;
// 프로토타입 속성
Circle.prototype.getArea = function() {
    return Math.PI * this.radius * this.radius;
};

new Circle 로 얻어지는 각 인스턴스는 2 개의 속성을 가집니다. 인스턴스 속성 radius 와 프로토타입 속성 getArea

가끔 ES5 특성으로 구현된 더 엄격한 "class"를 볼 수 있습니다:

// 더 엄격한, getter/setter 정의
function CircleEx(radius) {
    this.radius = radius;
    CircleEx.count++;
}
// 정적 속성 정의
Object.defineProperty(CircleEx, 'count', {
    get: function() {
        // this 는 CircleEx 를 가리킴
        // console.log(this);
        // CircleEx.count++ 는 먼저 get 으로 0 을 반환
        // 다음 set 으로 CircleEx 에_count 속성을 추가하고 1 로 할당
        return !this._count ? 0 : this._count;
    },
    set: function(val) {
        this._count = val;
    }
});
CircleEx.prototype.getArea = function() {
    return Math.PI * this.radius * this.radius;
};
// 프로토타입 속성 정의
// 실제로는 인스턴스 속성에 getter/setter 정의
Object.defineProperty(CircleEx.prototype, 'radius', {
    get: function() {
        //!!! this 는 CircleEx 인스턴스를 가리킴
        // 그 프로토타입 오브젝트가 아님
        // console.log(this);
        // 생성자 함수 중의 this.radius = radius;
        // radius 속성을 검색하고, get 을 트리거하고, this._radius 를 반환
        // radius 로 할당
        return this._radius;
    },
    set: function(val) {
        if (typeof val !== 'number') {
            throw new TypeError('radius: a number expected');
        }
        this._radius = val;
    }
});

new CircleEx 로 얻어지는 각 인스턴스는 2 개의 속성을 가집니다. 인스턴스 속성 _radius 와 프로토타입 속성 getArea. radius 에 대해서는, 프로토타입 오브젝트 위에 정의된 액세서 (getter/setter) 이며, 열거 불가입니다

이전에 "가끔" 이러한 "class" 정의를 볼 수 있다고 말한 것은, 너무 귀찮아서大家都懒得 사용하기 때문입니다. ES6 class 부분의 목표는 현상을 바꾸고, 더 편리한 클래스 멤버 정의 방식을 제공하는 것입니다

오브젝트 정의 방식을 간소화

오브젝트 리터럴 중에서 직접 getter/setter 를 정의할 수 있습니다. 함수 타입 속성 정의 방식을 간소화합니다. 일반 함수, 제네레이터, 동적 함수명을 포함합니다. 예를 들어:

// getter/setter
var obj = {
    // getter
    //!!! getter 는 인수 없음, 파라미터를 전달할 수 없음
    get attr() {
        console.log('getter');
        return this._attr || 0;
    },
    // setter
    //!!! setter 는 적어도 하나의 파라미터를 받음
    set attr(val) {
        console.log('setter');
        this._attr = val;
    },
    // 사전 계산 속성 ([] 문법으로 추가된 함수 속성, 동적 함수명)
    [(function() {return 'print';})()](arg) {
        console.log(arg);
    },

    // 일반 메서드
    fn(arg) {
        console.log(`arg = ${arg}`);
    },
    // 제네레이터
    *gen(i) {
        while(true) {
            yield i++;
        }
    }
}

冗長한 function 키워드가 타입 정의에서 완전히 사라졌습니다. [애로우 함수](/articles/애로우 함수-es6 노트 6/) 보다도 간결합니다. JS 를 처음 배울 때의 欣喜를 다시 느낍니다——변수에 한 쌍의 둥근 괄호를 붙이면 함수 호출이 됩니다. 얼마나간단 粗暴한가요?

ES6 강화판 오브젝트 리터럴로 이전의 CircleEx 클래스를 다시 쓰면, 이렇습니다:

// CircleEx 다시 쓰기
CircleEx.prototype = {
    getArea() {
        return Math.PI * this.radius * this.radius;
    },
    get radius() {
        return this._radius;
    },
    set radius(val) {
        if (typeof val !== 'number') {
            throw new TypeError('radius: a number expected');
        }
        this._radius = val;
    }
};

주의, 이전과 다른 것은,此时 액세서 radius열거 가능하며, 프로토타입 속성으로 나타납니다. 그러나 마찬가지로, 액세서 자체는 노출되지 않습니다. radius 에 대한 모든 조작은 액세서의 반환값에 대한 조작이며, 액세서 자체가 아닙니다. 따라서:

typeof new CircleEx(1).radius === 'number'  // true

class 정의 활성화

P.S. "도입"이 아니라 "활성화"라고 말하는 이유는, ES6 class 관련의 모든 키워드는 원래 예약어였기 때문입니다. 다만 지금은 명확한 의미가 부여되었을 뿐입니다

class 정의 중에서, constructor 는 생성자 함수를 나타냅니다. static 키워드는 일반 함수와 특수 함수를 구분하는 데 사용됩니다 (의미는 Java, C++ 와 동일)

constructor 는 옵션이며, 디폴트로 빈 생성자 함수 (constructor() {}) 를 제공합니다. constructor반드시리터럴 형식이어야 합니다. 동적 함수명일 수 없습니다.否则 constructor 라는 이름의 일반 메서드가 얻어지며, 생성자 함수가 아닙니다

class 문법으로 이전의 Circle 클래스를 다시 쓰면, 다음과 같습니다:

// CircleEx 다시 쓰기
class MyCircle {
    // 생성자 함수
    constructor(radius) {
        this.radius = radius;
        MyCircle.count++;
    }

    // 인스턴스 속성의 getter/setter
    get radius() {
        return this._radius;
    }
    set radius(val) {
        if (typeof val !== 'number') {
            throw new TypeError('radius: a number expected');
        }
        this._radius = val;
    }

    // 프로토타입 속성
    getArea() {
        return Math.PI * this.radius * this.radius;
    }

    // 정적 속성
    static get count() {
        return this._count || 0;
    }
    static set count(val) {
        this._count = val;
    }
}

실제 효과가 어떻든, 적어도見た目は 훨씬 명확해졌습니다

타입 보호

타입 보호가 내장되어 있습니다. class 문법으로 정의된 타입은, new 연산자를 통해 호출해야 합니다. 예를 들어:

// 일반 함수로서 호출하면, 오류 발생
MyCircle();
// Uncaught TypeError: Class constructors cannot be invoked without 'new'

내장 보호는 몇 가지 문제를 회피할 수 있지만, 확실히 유연성을 제한합니다. 예를 들어 어떤 클래스 라이브러리가 제공하는 API 에는 일반 함수로서 호출할 수도 있고, 생성자 함수로서도 사용할 수 있는 함수가 포함되어 있습니다. class 문법으로는 실현할 수 없습니다

또한, 클래스 정의 중에서 인용된 클래스명은 외부의 힘에 의해 변경되지 않습니다. 예를 들어:

class T {
    static get val() {
        return 'val';
    }

    get() {
        return T.val;
    }
}
// test
var t = new T();
console.log(t.get());   // val
// 외력 파괴
T = null;
console.log(t.get());   // val
//! 오류 Uncaught TypeError: T is not a constructor
// new T();

ES6 이전에는 이러한 보호가 없었습니다. function 으로 다시 쓰면, 외력 파괴를 당한 후 반드시 오류가 발생합니다

클래스 식 (익명 클래스) 서포트

// 익명 클래스
var circle = new class {
    constructor(radius) {
        this.radius = radius;
    }
}(3);
console.log(circle.radius); // 3

익명 클래스는별로 쓸모없는 것 같습니다.一般来说, 클래스는 오브젝트 템플릿이며, 클래스를 통해 오브젝트의 양산을 실현할 수 있습니다. Java 는 익명 클래스를 사용하여 양산할 필요가 없는 임시 오브젝트를 생성합니다. 그러나 JS 에는 N 가지 방법으로 오브젝트를 생성할 수 있습니다. 익명 클래스를 사용하는 것은 가장 어리석은 방법일 수 있습니다

클래스 중에서 정의된 메서드는 설정 가능하고 열거 불가

예를 들어 MyClass.prototype(class 문법으로 정의된) 의 모든 속성은 열거 불가입니다. 클래스 중에서 정의된 메서드도 열거 불가입니다

이렇게 하는 것은 의도적으로 오브젝트 시스템이 프로토타입에 기반한 사실을 은폐하는 것 같습니다. 예를 들어 for...in 은 이전의 MyCircle.prototype 을 열거할 수 없습니다. 그러나 Object.getOwnPropertyNames() 를 통해 몇 가지 흔적을 발견할 수 있습니다. 예를 들어:

console.log(Object.getOwnPropertyNames(MyCircle.prototype));
// log print:
// ["constructor", "radius", "getArea"]

이러한 속성은 모두 프로토타입 오브젝트 위에 실재하지만, 디폴트로 열거 불가가 이 사실을 은폐합니다. 아마도 class 문법을 사용하는 동시에, 수동으로 prototype 오브젝트를 조작하여, 기존 규칙을 파괴하지 않기를 바라는 것일 수 있습니다

아마도 이러한 prototype 을 은폐하는做法가 적절하지 않다고总觉得 하지만, 어디가 틀렸는지 말할 수 없습니다. 자, 프로토타입 상속의 고전적인 문제를 생각해 봅시다:프로토타입 참조 속성은 인스턴스 간에 공유됩니다. 예를 들어:

function Type() {}
Type.prototype.issue = [1, 2];
// test
var t1 = new Type();
var t2 = new Type();
t1.issue.push(3);
console.log(t2.issue);  // [1, 2, 3]

class 문법에는 이 문제가 있습니까?먼저 class 문법으로 Type 의 프로토타입 위에 배열 타입 속성을 정의해야 합니다. 실망스럽게도 근본적으로 할 수 없다는 것을 알게 됩니다. 직접 prototype 에 액세스하지 않는 한. 따라서걱정할 필요는 없습니다. 이러한 "은폐"가 은밀한 문제를 일으키지 않습니다. ES6 설계자는 우리보다 훨씬 많이 고려했습니다

三.정리

상쾌한 타입 정의 문법, keep it simple

ES 는 점차 문법 노이즈를 제거하고 있습니다:

애로우 함수 + `class` 는 `function` 을 제거

디폴트 파라미터 + 부정 파라미터는 `arguments` 를 제거

`class` + `extends` + `super` 는 `prototype` 을 제거

함수형 언어의 각도에서 보면, 이러한 변화는 극히 좋은 것입니다. 수학자가 추구하는 것은 바로 극치 간결의 미입니다

참고 자료

  • 《ES6 in Depth》: InfoQ 中文站이 제공하는 무료 전자책

  • 《JavaScript 언어 정수와 프로그래밍 실천》

댓글

아직 댓글이 없습니다

댓글 작성