ECMAScript【译】《Understanding ECMAScript6》- 第陆章-Class

目录

自JavaScript面世以来,许多开采者困惑为啥JavaScript未有Class。大大多面向对象语言都援助Class以及Class承袭,固然部分开荒者认为JavaScript语言并不要求Class,但其实许多第3方库通过工具方法来模拟Class。

ES六正式引进了Class规范。为了有限协理JavaScript语言的动态性,ES陆的Class规范与别的面向对象语言的Class并不完全同样。

ES5中的拟Class结构

在事无巨细描述Class在此以前,大家率先了然一下Class的内层机制。ES五竟然更早的本子中,在未曾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是贰个构造函数,它创造了1个name属性。sayName()方法是prototype的恢弘方法,它能够被PersonType的装有实例使用。随后,通过new创制了PersonType的1个实例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属性正是个体属性,属性值与实例声明时的传参有关。作者强烈推荐全体的私人住房属性均在构造函数内创设,以便统壹保管

翻译注:私有属性指的是直接给予该指标的属性,不须要从原型链上进行搜寻的性质

实在,ES陆中的Class只是在语法特别语义化,本质上还是是依照prototype原理。比如本例中的PersonClass本质上是1个构造函数,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是3个近似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和PersonClass二是同3个class的引用,两者是截然等价的。

Class表明式还有一部分别样很有趣的利用情况。比如能够用作参数字传送入函数:

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

let obj = createObject(class {

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

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

上述代码中,匿名class表明式作为createObject()的参数使用,在函数内部使用new成立并回到了1个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类是对点名DOM1多元操作的简练封装。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的此外成员壹致,静态成员暗中认可举不胜举。

派生类

ES陆在此以前实现接二连三供给极度麻烦的逻辑,比如:

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。完结持续的逻辑太过繁琐,不仅仅令菜鸟望而却步,固然是经验丰盛的开拓者也会在此跌跟头。

ES六正式并简化了贯彻一连的点子,使用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之后选拔,这是与ES伍分化的地方。

翻译注:最终一句话能够这么通晓,派生类内部调用父类全体利用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有1个静态方法create()。派生类能够调用Square.create(),然则作用等价于Rectangle.create()

动态派生类

派生类庞大的意义之1正是能够透过表明式动态生成派生类。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是ES伍正式的健康函数,而Square是贰个类。由于Rectangle具备[[Construct]]和prototype属性,Square类能够1直接轨它。

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数组并且自定义特殊的数组类型。然则在ES伍及其早期版本中并不帮助那种必要:

// 内置数组对象的行为
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的类数组对象

ES6引进Class的对象之一,就是支撑内置对象的承继。class的持续模型与ES5经文一连模型有以下几点差异:

  1. ES5精粹延续模型中,this的由派生类型(如本例的MyArray)起初化,然后经过Array.apply()调用基础项目(Array)的构造函数。也便是说,this最初是MyArray的一个实例,随后被授予了基础项目Array的性质。
  2. ES陆的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构造函数Nelly用,用来决断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,
四)时,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不能够被实例化,不过能够当作基类由派生类承接。

总结

ES陆制定了class的行业内部规范,使JavaScript语言的编程理念越发切近其余面向对象语言。Class并不只是ES伍卓绝一而再情势的语法规范,还扩展了壹密密麻麻有力的新职能。

Class机制建立在原型承接的基本功上,非静态方法被给予构造函数的prototype,静态方法直接予以构造函数自己。Class的富有办法都以不可胜数的,这点与内置对象的习性行为是平等的。别的,class只好作为构造函数使用,也正是只好被new调用,而不能够作为常规函数实行。

Class承袭机制允许从class、函数,甚至表达式生成派生类。那种机制得以提供各类路径和格局来创制多个新的class。并且,传承机制一样适用于内置对象(比如Array)。

Class被实施的不二等秘书诀分歧,class构造函数内的new.target的取值也比不上,利用那些机制得以满意壹些卓越的要求。比如创制一个无法被实例化然则足以被持续的抽象类。

简来说之,class是JavaScript语言分外首要的模块,它提供了特别功效化的建制以及越发简洁的语法,使自定义类型的创办进度越是安全统1。