ECMAScript5中的近类结构

  1. function PersonType(name) {
  2. this.name = name;
  3. }
  4. PersonType.prototype.sayName = function () {
  5. console.log(this.name);
  6. }
  7. var person = new PersonType("abc");
  8. person.sayName(); //abc
  9. console.log(person instanceof PersonType); //true
  10. console.log(person instanceof Object); //true

类的声明

基本的类声明语法

    class PersonClass {

        // 等价于PersonType
        constructor(name) {
            this.name = name;
        }

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

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

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

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

类与自定义构造函数的区别

· 函数声明可以被提升,而类声明与let声明类似,不能被提升;真正执行声明语句之前,它们会一直存在于临时死区中。
· 类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式执行。
· 在自定义类型中,需要通过Object.defineProperty()方法手工指定某个方法为不可枚举;而在类中,所有方法都是不可枚举的。
· 每个类都有一个名为[[Construct]]的内部方法,通过关键字new调用那些不含[[Construct]]的方法会导致程序抛出错误。
· 使用除关键字new以外的方式调用类的构造函数会导致程序抛出错误。
· 在类中修改类名会导致程序报错。
**

类表达式

基本的类表达式语法

    let PersonClass = class {

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

    let person = new PersonClass("abc");

    person.sayName();

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

    console.log(typeof PersonClass);
    console.log(typeof PersonClass.prototype.sayName);

**

命名类表达式

    let PersonClass = class PersonClass2 {

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


    console.log(typeof PersonClass); //function
    console.log(typeof PersonClass2); //undefined

类表达式被命名为PersonClass2,由于标识符PersonClass2只存在于类定义中,PersonClass2只能在类的内部使用

作为一等公民的类

在程序中,一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。JavaScript函数是一等公民(也被称作头等函数),这也正是JavaScript中的一个独特之处。
ECMAScript 6延续了这个传统,将类也设计为一等公民,允许通过多种方式使用类的特性。例如,

可以将类作为参数传入函数中:

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

    let obj = createObject(class {
        sayHi() {
            console.log("Hi!");
        }
    });

    obj.sayHi();


类表达式还有另一种使用方式,通过立即调用类构造函数可以创建单例。用new调用类表达式,紧接着通过一对小括号调用这个表达式,例如:**

    let person = new class {
        constructor(name) {
            this.name = name;
        }
        sayName() {
            console.log(this.name);
        }
    }("abc");

    person.sayName(); //"abc"

访问器属性

    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);
    console.log("set" in descriptor);
    console.log(descriptor.enumerable);

可计算成员名称

    let methodName = "sayName";

    class PersonClass {
        constructor(name) {
            this.name = name;
        }
        [methodName]() {
            console.log(this.name);
        }
    };

    let me = new PersonClass("abc");
    me.sayName();

生成器方法

    class MyClass {

        * createIterator() {
            yield 1;
            yield 2;
            yield 3;
        }
    }
    let instance = new MyClass();
    let iterator = instance.createIterator();

    for (let i of iterator) {
        console.log(i);  // 1 2 3
    }

定义一个默认迭代器会更有用

    class Collection {
        constructor() {
            this.items = [];
        }
        *[Symbol.iterator]() {
            yield* this.items.values();
        }
    }
    var collection = new Collection();
    collection.items.push(1);
    collection.items.push(2);
    collection.items.push(3);

    for (let x of collection) {
        console.log(x); // 1 2 3
    }

静态成员

如果不介意在对象的实例中出现添加的方法和访问器属性,则可以将它们添加到类的原型中;如果你希望它们只出现在类中,那么需要使用静态成员。

es5写法**

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

    //静态方法
    PersonType.create = function (name) {
        return new PersonType(name);
    }
    // 实例方法
    PersonType.prototype.sayName = function () {
        console.log(this.name);
    };

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

    person.sayName();

es6写法

    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("abc");

继承与派生类

es5实现继承与自定义类型是一个不小的工作。严格意义上的继承需要多个步骤实现。

    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());
    console.log(square instanceof Square);
    console.log(square instanceof Rectangle);

类的出现让我们可以更轻松地实现继承功能,使用熟悉的extends关键字可以指定类继承的函数。原型会自动调整,通过调用super()方法即可访问基类的构造函数。这段代码是之前示例的ECMAScript6等价版本:

    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());
    console.log(square instanceof Square);
    console.log(square instanceof Rectangle);

Square类通过extends关键字继承Rectangle类,在Square构造函数中通过super()调用Rectangle构造函数并传入相应参数。请注意,与ECMAScript 5版本代码不同的是,标识符Rectangle只用于类声明(extends之后)。
**

继承自其他类的类被称作**派生类如果在派生类中指定了构造函数则必须要调用super(),如果不这样做程序就会报错。如果选择不使用构造函数,则当创建新的类实例时会自动调用super()并传入所有参数**。举个例子,以下两个类完全相同:

    class Square extends Rectangle {

    }

    //等价于

    class Square extends Rectangle {
        constructor(...args) {
            super(...args);
        }
    }

使用super()的的注意事项
· 只可在派生类的构造函数中使用super(),如果尝试在非派生类(不是用extends声明的类)或函数中使用则会导致程序抛出错误。
· 在构造函数中访问this之前一定要调用super(),它负责初始化this,如果在调用super()之前尝试访问this会导致程序出错。
· 如果不想调用super(),则唯一的方法是让类的构造函数返回一个对象
**

类方法遮蔽

派生类中的方法总会覆盖基类中的同名方法

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


        // 覆盖并遮蔽Rectangle.prototype.getArea()方法

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

如果想调用基类中的同名方法,则可以调用super.getArea()方法,就像这样:

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

        // 覆盖遮蔽后调用Rectangle.prototype.getArea()
        getArea() {
            return super.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) {

            // 等价于Rectangle.call(this,length,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

派生自表达式的类

    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

动态地确定类的继承目标

    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 Rectangle {
        constructor(length) {
            super(length, length);
        }
    }

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

getBase()函数是类声明的一部分,直接调用后返回Rectangle

创建不同的继承方法**

    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;
        }
    }

内建对象的继承

基于类生成特殊数组

    class MyArray extends Array {

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

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

Symbol.species属性

    class MyArray extends Array {

    }

    let items = new Array(1, 2, 3, 4),
        subitems = items.slice(1, 3);

    console.log(items instanceof MyArray); //true
    console.log(subitems instanceof MyArray); //true
    class MyClass {
        static get [Symbol.species]() {
            return this;
        }
        constructor(value) {
            this.value = value;
        }
        clone() {
            return new this.constructor[Symbol.species](this.value);
        }
    }

    class MyDerivedClass1 extends MyClass {

    }

    class MyDerivedClass2 extends MyClass {
        static get [Symbol.species]() {
            return MyClass;
        }
    }

    let instance1 = new MyDerivedClass1("foo"),
        clone1 = instance1.clone(),
        instance2 = new MyDerivedClass2("bar"),
        clone2 = instance2.clone();

    console.log(clone1 instanceof MyClass); //true
    console.log(clone1 instanceof MyDerivedClass1); //true
    console.log(clone2 instanceof MyClass); //true
    console.log(clone2 instanceof MyDerivedClass2); //false

这段代码重写了MyDerivedClass2继承自MyClass的Symbol.species属性,所有返回数组的继承方法现在将使用MyClass的实例而不使用MyDerivedClass2的实例。

在类的构造函数中使用new.target

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

    // new.target的值是Rectangle
    var obj = new Rectangle(3, 4); //输出true
    class Rectangle {
        constructor(length, width) {
            console.log(new.target === Rectangle);
            this.length = length;
            this.width = width;
        }
    }

    // new.target的值是Rectangle
    // var obj = new Rectangle(3, 4); //输出true
    class Square extends Rectangle {
        constructor(length) {
            super(length, length);
        }
    }

    // new.target的值是Square
    var obj = new Square(3); //false

Square调用Rectangle的构造函数,所以当调用发生时new.target等于Square。这一点非常重要,因为每个构造函数都可以根据自身被调用的方式改变自己的行为。例如,可以用new.target创建一个抽象基类(不能被直接实例化的类),就像这样:

    // 抽象基类
    class Shape {
        constructor() {
            if (new.target === Shape) {
                throw new Error("这个类不能被直接实例化");
            }
        }
    }
    class Rectangle extends Shape {
        constructor(length, width) {
            super();
            this.length = length;
            this.width = width;
        }
    }

    // var x = new Shape(); //报错

    var y = new Rectangle(3, 4); //不报错

    console.log(y instanceof Shape); //true

因为类必须通过new关键字才能调用,所以在类的构造函数中,new.target属性永远不会是undefined。
**

小结

ECMAScript 6的类语法让JavaScript中的继承更易于使用,所以你无须摒弃先前对于其他语言中继承语法的理解。ECMAScript 6的类语法首先是作为ECMAScript 5传统继承模型的语法糖出现,但是添加了几个能够降低风险的特性。ECMAScript 6的类语法通过在类的原型上定义非静态方法来与原型继承协同工作,而静态方法最终放在构造函数上。所有方法都不可枚举,从而可以更好地匹配内建对象的行为,因为那些方法通常是不可枚举的。此外,类构造函数必须通过new关键字调用,以确保不会意外将类作为函数去调用。基于类的继承支持从其他类、函数或表达式派生类,可以通过函数调用来确定最终要继承哪一个类,可以通过mixin对象和其他不同的组合模式来创建新的类,也可以继承诸如Array的内建对象并且像预期的那样运行。在类的构造函数中,可以通过new.target来随着类被调用的多种方式而做出不同的对应。最常见的用法是创建一个抽象基类,如果直接实例化这个类会抛出错误,但是可以通过其他的类去实例化它。总之,类是JavaScript新特性的一个重要组成部分,这一特性提供了一种更简洁的语法和更好的功能,可以让你通过一个安全、一致的方式来自定义对象类型。