【译】《Understanding ECMAScript6》- 第五章-Class

目录

  • ES5中的拟Class结构
  • Class声明
  • Class表达式
  • 囤器属性
  • 静态成员
  • 派生类
  • new.target
  • 总结

自JavaScript面世以来,许多开发者疑惑呢何JavaScript没有Class。大多数面向目标语言都支持Class以及Class继承,尽管一些开发者认为JavaScript语言并不需要Class,但其实很多叔着库通过工具方法来拟Class。

ES6正式引入了Class规范。为了保险JavaScript语言的动态性,ES6的Class规范和其他面向对象语言的Class并无完全相同。

ES5中的拟Class结构

于详细讲述Class之前,我们率先了解一下Class的内层机制。ES5还是又早的版中,在没Class的环境下,最相仿Class的模式是开创一个构造函数并且扩张其的prototype方法。这种模式通常为号称由定义类型。如下:

function PersonType(name) {
    this.name = name;
}
PersonType.prototype.sayName = function() {
    console.log(this.name);
};
let person = new PersonType("Nicholas");
person.sayName();   // outputs "Nicholas"
console.log(person instanceof PersonType);  // true
console.log(person instanceof Object);      // true

上述代码中,PersonType是一个构造函数,它创建了一个name属性。sayName()方法是prototype的扩充方法,它好为PersonType的有所实例使用。随后,通过new创建了PersonType的一个实例person对象,根据原型链继承原理,person同时也是Object的实例。

这种体制是各种拟Class模式之理论功底,也是ES6备受Class规范的功底。

Class声明

Class的宣示语法与外语言类,采用class关键字+类名的语法。Class内部的语法与Object字面量方法的简练语法类似,只不过方中不必下逗号隔开。将上例改写为Class如下:

class PersonClass {
    // 等价于构造函数PersonType
    constructor(name) {
        this.name = name;
    }
    // 等价于PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
}
let person = new PersonClass("Nicholas");
person.sayName();   // outputs "Nicholas"
console.log(person instanceof PersonClass);     // true
console.log(person instanceof Object);          // true
console.log(typeof PersonClass);                    // "function"
console.log(typeof PersonClass.prototype.sayName);  // "function"

上述代码的PersonClass与前例中之PersonType作用类似。Class声明中使用constructor关键字定义构造函数。方法的概念可以使简单语法,不必下function关键字。除constructor以外的措施名好因产品需要自由定义。

村办属性只能当Class的构造函数内声明。比如本例中之name属性便是私有属性,属性值与实例声明时之传参有关。笔者强烈推荐所有的个人属性都于构造函数内创建,以便统一保管

翻译注:私有属性指的凡一直给该目标的性,不需要从原型链上进行检索的习性

实际,ES6挨的Class只是在语法更加语义化,本质上仍然是根据prototype原理。比如本例中之PersonClass本质上是一个构造函数,typeof
PersonClass的运转结果吗”function”。sayName()同前例的PersonType.prototype.sayName()一样,是PersonClass.prototype的恢弘方法。

不过Class与常规的构造函数并无完全相同,再以Class时需留意以下几点区别

  1. Class不会被声称提升。与let声明类似,Class在宣称语句执行前是休可知于访的;
  2. Class声明语句内部的代码全部周转于从严模式下;
  3. Class的有所方还是不可枚举的。而常规的自定义类型需要采取Object.defineProperty()来定义不枚举属性;
  4. 务必采取new调用Class构造函数,否则会报错;
  5. Class不能被自身之章程函数重命名。

冲以上标准,前例中之PersonClass等价于以下代码:

// 等价于PersonClass
let PersonType2 = (function() {

    "use strict";

    const PersonType2 = function(name) {

        // 确保只能被new调用
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new.");
        }

        this.name = name;
    }

    Object.defineProperty(PersonType2.prototype, "sayName", {
        value: function() {
            console.log(this.name);
        },
        enumerable: false,
        writable: true,
        configurable: true
    });

    return PersonType2;
}());

虽然非以Class也得以兑现平等的效益,但是Class的语法更加简洁易读。

常量类名

Class的类名与const类似,在该内部是一个不可变的常量。也就是说,Class不能被我之办法函数重命名,但是得当表进行重新命名。如下:

class Foo {
   constructor() {
       Foo = "bar";    // throws an error when executed
   }
}

// but this is okay
Foo = "baz";

上述代码中的,Foo在该里面代码和外表代码中之行完全不同。在里面,Foo类名是一个休能够于重复写的常量,尝试再次写会丢来荒谬;在表,Foo是一个近似let声明的变量,可以让随意重写。

Class表达式

Class与function都起少数栽声明方式:字面量声明和表达式声明。字面量声明即重点字(class/function)+类名/函数曰。函数的表达式声明语法可以看略函数称作,类似的,Class的表达式声明语法也可简简单单类名:

// class expressions do not require identifiers after "class"
let PersonClass = class {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
};

let person = new PersonClass("Nicholas");
person.sayName();   // outputs "Nicholas"

console.log(person instanceof PersonClass);     // true
console.log(person instanceof Object);          // true

console.log(typeof PersonClass);                    // "function"
console.log(typeof PersonClass.prototype.sayName);  // "function"

Class的字面量声明与表达式声明是意等价格的。class关键字后的类名可以让简单,也得不略,如下:

let PersonClass = class PersonClass2 {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
};

console.log(PersonClass === PersonClass2);  // true

上述代码中之PersonClass和PersonClass2是和一个class的援,两者是全等价格的。

Class表达式还有一部分另特别有趣的利用状况。比如可以看作参数传入函数:

function createObject(classDef) {
    return new classDef();
}

let obj = createObject(class {

    sayHi() {
        console.log("Hi!");
    }
});

obj.sayHi();        // "Hi!"

上述代码中,匿名class表达式作为createObject()的参数使用,在函数内部用new创建并回到了一个class实例。

Class表达式还足以经过这实施构造函数来创造单例。这种模式下,必须运用new调用class表达式,并且class表达式的末尾需要圆括号传回参数。如下:

let person = new class {

    constructor(name) {
        this.name = name;
    }

    sayName() {
        console.log(this.name);
    }

}("Nicholas");

person.sayName();       // "Nicholas"

上述代码中,匿名class表达式被创造时及时执行构造函数。这种模式可应用class语法创建单例,而毋庸遗留class的援。

Class声明与class表达式只在语法上设有出入,两者可以相互替换。与函数声明/表达式不同之是,class声明/表达式并无见面吃声称提升。

积存器属性

尽管私有属性应该以class的构造函数内创建,class允许在构造函数以外的区域定义其原型的积存器属性,语法类似Object字面量。创建getter的语法是get关键字+空格+方法名;创建setter的语法是set关键字+空格+方法名。如下:

class CustomHTMLElement {

    constructor(element) {
        this.element = element;
    }

    get html() {
        return this.element.innerHTML;
    }

    set html(value) {
        this.element.innerHTML = value;
    }
}

var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype,"html");
console.log("get" in descriptor);   // true
console.log("set" in descriptor);   // true
console.log(descriptor.enumerable); // false

翻译注:Object.getOwnPropertyDescriptor()
返回指定对象上一个自有属性对应之属性描述符,包括value、writable、get、set、configurable、enumerable。

上述代码中,CustomHTMLElement类是对准点名DOM一文山会海操作的简短封装。html的setter和getter方法是原生innerHTML方法的轩然大波代理。存储器属性归属于CustomHTMLElement.prototype,并且是不可枚举的。上述代码改写为常规函数模式如下:

// direct equivalent to previous example
let CustomHTMLElement = (function() {

    "use strict";

    const CustomHTMLElement = function(element) {

        // make sure the function was called with new
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new.");
        }

        this.element = element;
    }

    Object.defineProperty(CustomHTMLElement.prototype, "html", {
        enumerable: false,
        configurable: true,
        get: function() {
            return this.element.innerHTML;
        },
        set: function(value) {
            this.element.innerHTML = value;
        }
    });

    return CustomHTMLElement;
}());

及前例的class语法相比,上述代码要麻烦很多。

翻译注:请留心眼前例class语法中的getter和setter方法的名称是千篇一律之,因为两岸都是CustomHTMLElement.prototype.html的积存器属性。这一点便于生出困惑,本例中Object.defineProperty()则一目了然。

静态成员

也构造函数添加额外的方法来效仿静态成员是JavaScript中常用之模式有。如下:

function PersonType(name) {
    this.name = name;
}

// static method
PersonType.create = function(name) {
    return new PersonType(name);
};

// instance method
PersonType.prototype.sayName = function() {
    console.log(this.name);
};

var person = PersonType.create("Nicholas");

以其它编程语言中,工厂方法PersonType.create()被叫作静态方法,因为它们跟PersonType的实例无关。

Class简化了静态方法的创进程,在术名或存储器属性之前用static修饰即可。前例中之代码可以改写为以下形式:

class PersonClass {

    // 等价于构造函数PersonType
    constructor(name) {
        this.name = name;
    }

    // 等价于PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }

    // 等价于PersonType.create
    static create(name) {
        return new PersonClass(name);
    }
}

let person = PersonClass.create("Nicholas");

PersonClass使用static修饰符定义了一个静态方法create()。

static修饰符可以用来除constructor外边的任何class方法和储存器属性。

跟class的另外成员一致,静态成员默认不可枚举。

派生类

ES6之前实现持续需要非常麻烦的逻辑,比如:

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

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

function Square(length) {
    Rectangle.call(this, length, length);
}

Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        value:Square,
        enumerable: true,
        writable: true,
        configurable: true
    }
});

var square = new Square(3);

console.log(square.getArea());              // 9
console.log(square instanceof Square);      // true
console.log(square instanceof Rectangle);   // true

上述代码中,Square继承自Rectangle。首先,以Rectangle.prototype为原型创建Square.prototype;其次,Square函数内部用用call()函数调用Rectangle。实现连续的逻辑太过繁琐,不仅仅让新手望而却步,即使是经验丰富的开发者也会于此跌跟头。

ES6标准并简化了贯彻持续的法子,使用extends关键字就足以指定派生类的父类。派生类中可以采用super()调用父类的主意。基于这规范,前例的代码可以简化为以下形式:

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }

    getArea() {
        return this.length * this.width;
    }
}

class Square extends Rectangle {
    constructor(length) {

        // 等同于前例的Rectangle.call(this, length, length)
        super(length, length);
    }
}

var square = new Square(3);

console.log(square.getArea());              // 9
console.log(square instanceof Square);      // true
console.log(square instanceof Rectangle);   // true

Square类使用extends关键字继承自Rectangle。Square的构造函数内使super()调用Rectangle的构造函数并传指定参数。需要专注的是,Rectangle只在派生类声明时,即extends之后用,这是跟ES5异的地方。

翻译注:最后一句话可如此敞亮,派生类里调用父类全部采用super(),而休用直白利用类名来调用父类。

一旦派生类内显式定义了构造函数,那么构造函数内部必须用super()调用父类,否则会出错误。如果构造函数没有给显式定义,class会默认隐式定义一个构造函数,并且构造函数内部以super()调用父类,同时传入生成class实例时之享有参数。例如,以下简单独class是截然等价格的:

class Square extends Rectangle {
    //constructor没有被显式定义
}

// 等价于
class Square extends Rectangle {
    constructor(...args) {
        super(...args);
    }
}

上述代码中的亚栽写法表示的是构造函数未被显式定义时的所作所为。所有的参数按梯次为传父类的构造函数。笔者建议始终显式定义构造函数,以保险参数的不错。

利用super()是索要留意以下几点

  1. super()只能当派生类中采用,否则会生错误;
  2. super()必须于操作this之前用。因为super()的企图就是初始化this的针对性,如果在super()之前操作this会时有发生错误;
  3. 构造函数中未使用super()的绝无仅有场景是回到一个Object。

Class方法

派生类中定义之方法会覆盖父类中的同名方法。例如,派生类Square中定义了getArea()方法:

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }

    // override and shadow Rectangle.prototype.getArea()
    getArea() {
        return this.length * this.length;
    }
}

上述代码中,派生类Square的概念了措施getArea(),Square的实例便不再调用Rectangle.prototype.getArea()。当然,你依旧可应用super.getArea()间接调用父类的办法,如下:

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }

    // override, shadow, and call Rectangle.prototype.getArea()
    getArea() {
        return super.getArea();
    }
}

Class方法无中间属性[[Construct]],不能被new调用。如下:

// throws an error
var x = new Square.prototype.getArea();

好在由class方法不可让new调用,减少了受误使用导致的不测状况。

同Object字面量类似,class方法名好使方括哀号动态运算。如下:

let methodName = "getArea";
class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }

    // override, shadow, and call Rectangle.prototype.getArea()
    [methodName]() {
        return super.getArea();
    }
}

上述代码和前例等价格。唯一的区分就是是getArea()的不二法门名是由此方括号运算得到的。

静态成员

派生类中仍然可以动用那父类的静态成员。如下:

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    getArea() {
        return this.length * this.width;
    }
    static create(length, width) {
        return new Rectangle(length, width);
    }
}

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }
}

var rect = Square.create(3, 4);

console.log(rect instanceof Rectangle);     // true
console.log(rect.getArea());                // 12
console.log(rect instanceof Square);        // false

上述代码中,Rectangle有一个静态方法create()。派生类可以调用Square.create(),但是意义等价于Rectangle.create()

动态派生类

派生近似强大的功用之一即是可以经过表达式动态生成派生类。extends可以用于其它表达式,倘表达式可以生成一个享[[Construct]]与prototype属性的函数,就可以非常成一个派遣生类。例如:

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

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

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }
}

var x = new Square(3);
console.log(x.getArea());               // 9
console.log(x instanceof Rectangle);    // true

上述代码中的Rectangle是ES5业内之常规函数,而Square是一个接近。由于Rectangle具备[[Construct]]同prototype属性,Square类可以直接接轨其。

extends语法的动态性可以啊众多劲的意义提供理论功底。比如动态变化继承对象:

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

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

function getBase() {
    return Rectangle;
}

class Square extends getBase() {
    constructor(length) {
        super(length, length);
    }
}

var x = new Square(3);
console.log(x.getArea());               // 9
console.log(x instanceof Rectangle);    // true

上述代码功能跟前例等价格。getBase()函数在class声明语句被吃实践。开发者可以继续增强getBase()函数的动态性,以出不同的受持续对象。比如,我们得使用mixin模式:

let SerializableMixin = {
    serialize() {
        return JSON.stringify(this);
    }
};

let AreaMixin = {
    getArea() {
        return this.length * this.width;
    }
};

function mixin(...mixins) {
    var base = function() {};
    Object.assign(base.prototype, ...mixins);
    return base;
}

class Square extends mixin(AreaMixin, SerializableMixin) {
    constructor(length) {
        super();
        this.length = length;
        this.width = length;
    }
}

var x = new Square(3);
console.log(x.getArea());               // 9
console.log(x.serialize());             // "{"length":3,"width":3}"

上述代码中之mixin()函数接受任意数目的参数,将这些参数作为扩大属性赋值给base.prototype,并回到base函数以要extends语法生效。需要小心的是,你仍然要重显式定义的构造函数内调用super()。

Square的实例x同时所有AreaMixin的getArea()方法以及SerializableMixin的serialize方法。

虽然extends可以用来任意的表达式,但毫无有的表达式都能出一个官方的class。以下表达式会出错误:

  • null
  • 生成器表达式(第八回会详细讲述)

如上表达式生成的class不能被创造实例,否则会扔来左。

坐对象的继续

直白以来,开发者都盼能够延续JavaScript数组并且由定义特殊之数组类型。然而以ES5及其早期版本被连无支持这种求:

// 内置数组对象的行为
var colors = [];
colors[0] = "red";
console.log(colors.length);         // 1

colors.length = 0;
console.log(colors[0]);             // undefined

//ES5环境中尝试继承内置数组对象
function MyArray() {
    Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        writable: true,
        configurable: true,
        enumerable: true
    }
});

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length);         // 0

colors.length = 0;
console.log(colors[0]);             // "red"

上述代码是JavaScript实现连续的藏方式,但是最终赢得的结果没达标预期。length属性以及枚举属性的所作所为跟坐数组对象的一言一行并不相同,这是出于无是Array.apply(),还是通过扩大prototype,派生类型的习性修改并未投射到基础项目。

翻译注:
也就是说,修改colors.length并未改观内置数组类型的length。实际上,本例中的MyArray并非数组,而是一个类似于arguments的仿佛数组对象ECMAScript

ES6唤起入Class的目标有,便是永葆内置对象的持续。class的持续模型与ES5经典延续模型产生以下几点区别:

  1. ES5经典延续模型中,this的出于派生类型(如本例的MyArray)初始化,然后经过Array.apply()调用基础项目(Array)的构造函数。也就是说,this最初是MyArray的一个实例,随后于授予了根基项目Array的性。
  2. ES6的class继承模型中,this由基础类(Array)初始化,然后为派生类(MyArray)的构造函数修正。也就是说,this拥有基础类的备属性与作用。

以下的class继承可以实现从定义数组类型的需求:

class MyArray extends Array {
    // ...
}

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length);         // 1

colors.length = 0;
console.log(colors[0]);             // undefined

上述代码中之MyArray继承自置数组对象Array,与Array的行为完全一致。枚举属性与length属性互相影响,改变length属性的同时,枚举属性被更新。

另外,MyArray为延续了Array的静态成员,可以一直运用:

class MyArray extends Array {
    // ...
}

var colors = MyArray.of(["red", "green", "blue"]);
console.log(colors instanceof MyArray);     // true

上述代码中的静态方法MyArray.of()与Array.of()的行为同,它创了一个MyArray的实例而无是Array的实例。这是坐对象的静态方法与正规对象静态方法的不同之处。

翻译注:请留意放对象及健康对象的派生类吃,静态成员见的分。

JavaScript的具有坐对象都支持class继承,并且派生类的所作所为和坐对象完全一致。

new.target

仲回里介绍了new.target与函数调用方式的涉。new.target也堪当class构造函数内动,用来判定class的执行方式。这种光景下,new.target相当给class的构造函数,如下:

class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width;
    }
}

// new.target is Rectangle
var obj = new Rectangle(3, 4);      // outputs true

翻译注:要知道“new.target相当给class的构造函数”这词话,首先使清楚class本质上是一个构造函数。根据第二回的讲诉,使用new调用构造函数时,new.target的取值是构造函数的函数称为。

上述代码中,执行new Rectangle(3,
4)时,new.target等于Rectangle。Class本质上是一个新鲜之构造函数,它只能为new调用,所以new.target始终在class的构造函数内给定义。不同的光景下,new.target的取值也不比:

class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width;
    }
}

class Square extends Rectangle {
    constructor(length) {
        super(length, length)
    }
}

// new.target等于Square
var obj = new Square(3);      // 输出false

上述代码中创造Square实例时,Square类调用Rectangle的构造函数,所以Rectangle构造函数内之new.target等于Square。这种机制可以支持构造函数根据调用方式的例外,改变我之行为模式。比如,用new.target的做事原理可以创造抽象类(即无可知为直实例化的类似):

// 抽象类
class Shape {
    constructor() {
        if (new.target === Shape) {
            throw new Error("This class cannot be instantiated directly.")
        }
    }
}

class Rectangle extends Shape {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

var x = new Shape();                // throws error

var y = new Rectangle(3, 4);        // no error
console.log(y instanceof Shape);    // true

上述代码中,new
Shape()会丢弃来荒唐,因为Shape类的构造函数不允new.target等于Shape。抽象类Shape不可知让实例化,但是足以作为基类由派生类继承。

总结

ES6制定了class的正统规范,使JavaScript语言的编程思想进一步接近其他面向对象语言。Class并不仅仅是ES5经延续模式的语法规范,还多了同一文山会海有力的新职能。

Class机制建立以原型继承的基础及,非静态方法被与构造函数的prototype,静态方法直接给予构造函数本身。Class的保有术都是不可枚举的,这一点暨坐对象的性质行为是一律的。另外,class只能作为构造函数使用,也便是不得不吃new调用,而不能够作常规函数执行。

Class继承机制允许打class、函数,甚至表达式生成派生类。这种体制好提供多路子和模式来创造一个新的class。并且,继承机制同适用于坐对象(比如Array)。

Class被实践的措施不同,class构造函数内之new.target的取值也差,利用这机制得以满足一些非正规之急需。比如创建一个非能够叫实例化但是可以叫接续的抽象类。

总而言之,class是JavaScript语言非常主要之模块,它提供了越来越功能化的编制和愈发从简之语法,使打定义类型的缔造过程更安全统一。