ES6:类 Class

255 阅读6分钟

前言

ES5 及早期版本中没有类的概念,接近的办法就是创建自定义类型:首先创建构造函数,然后定义另一方法并赋值给构造函数的原型。ES6 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。

类声明

定义一个类的一种方法:使用类声明,使用 class 关键字。

class MyClass {
    //  类的构造函数
    constructor (name) {
        this.name = name;
    }
    
    getName () {
        return this.name;
    }
}

let myClass = new MyClass("111");
console.log(myClass.getName());

//  自定义类型,等价于上面 MyClass
let MyClass = (function () {
    "use strict";
    const MyClass = function (name) {
        if (typeof new.target === "undefined") {
            throw new Error("请使用new调用构造函数");
        }
        this.name = name;
    };
    Object.defineProperty(MyClass.prototype, "getName", {
        value: function () {
            if (typeof new.target !== "undefined") {
                throw new Error("不可以通过new来调用");
            }
            return this.name;
        },
        writable: true,
        enumerable: false,
        configurable: true
    });
    return MyClass;
})();

上面代码一种通过class声明的,一种通过 ES5 语法实现等价类的特性。
class 类语法要注意几点:

  • 类声明无法提升,存在临死死区;
  • 类声明代码会自动运行在严格模式之下,无法改变;
  • 必须使用 new 方式调用类的构造函数;
  • 在类中修改类名会报错。

类表达式

类和函数都存在两种形式:声明形式和表达形式。类声明形式上面已经相关的代码列举了,下面介绍类表达式:

//  第一种:匿名类表达
let MyClass = class {
    constructor () {
        //  代码
    },
    //  其他属性、方法
}

//  第二种:命名类表达
let MyClass1 = class MyClass2 {
    constructor () {
        //  代码
    },
    //  其他属性、方法
}
console.log(typeof MyClass1);   //  function 
console.log(typeof MyClass2);   //  undefined

第二种方式定义,MyClass2 类名在类中是可以使用的,而类外部是不存在的。

类的属性和方法

访问器属性

类在构造函数创建自己属性,同时类也支持直接在原型上定义访问器属性。

var defaultInfo = { name: "ES6" };
class MyClass {
    constructor (info = defaultInfo ) {
        this.info = info;
    }
    get name () {
        return this.info.name;
    }
    set name (value) {
        this.info.name = value;
    }
}
var descriptor = Object.getOwnPropertyDescriptor(MyClass.prototype, "name");
console.log(descriptor);        //  Object { get: name(), set: name(), enumerable: false, configurable: true }

var example = new MyClass();
console.log(example.name);      //  "ES6"
example.name = "JS"
console.log(example.name);      //  "JS"

可计算成员名称

var name = "sayName";
class MyClass {
    constructor (name) {
        this.name = name;
    }
    [name] () {
        console.log(this.name);
    }
}
var example = new MyClass("ES6");
example[name]();
example.sayName();

静态成员

ES5 及早期版本中,直接将方法添加到构造函数下来模拟静态成员。ES6 类语法简化了创建静态成员的过程。

//  ES6 类语法
class MyClass {
    constructor (name) {
        this.name = name;
    }
    //  静态方法
    static getNumber () {
        return 11111;
    }
    //  实例方法
    sayName () {
        console.log(this.name);
    }
}
console.log(MyClass.getNumber());   //  11111

//  ES5 版本
function Fun (name) {
    this.name = name;
}
//  Fun实例方法
Fun.prototype.sayName = function () {
    this.name;
}
//  Fun静态方法
Fun.getNumber = function () {
    return 11111;
}
console.log(Fun.getNumber());       //  11111

静态成员不能在实例中访问。

继承与派生类

简单的继承

//  ES6 前实现继承
function Animal (weight) {
    this.weight = weight;
}
Animal.prototype.action = function () {
    console.log("跑");
}

function Dog (weight) {
    return Animal.call(this, weight);
}
Dog.prototype = Object.create(Animal.prototype,{
    constructor: {
        value: Dog,
        enumerable: true,
        writable: true,
        configurable: true
    }
});

let dog = new Dog();
console.log(dog instanceof Dog);        //  true
console.log(dog instanceof Animal);     //  true

上面示例是基于 ES5 来实现继承与自定义类型的工作,看起来十分复杂:Dog 构造函数中调用 Animal.call() 方法、创建来自 Animal.prototype 的新对象并且修改该对象 [Constructor] 来赋值给 Dog 的原型。
ES6 类的出现使继承工作更容易完成。

class Animal {
    constructor (weight) {
        this.weight = weight;
    }
    action () {
        console.log("跑");
    }
}

class Dog extends Animal {
    constructor (weight) {
        super(weight);
    }
}

let dog = new Dog();
console.log(dog instanceof Dog);        //  true
console.log(dog instanceof Animal);     //  true

使用 extends 轻松实现继承,super() 访问基类的构造函数。

成员继承

派生类方法中与基类方法同名时,派生类会覆盖继承的基类方法。

//  基类
class SuperClass {
    //  静态方法
    static name () {
        console.log("Super");
    }
    
    //  实例方法
    sayName () {
        console.log("SuperClass");
    }
}

//  派生类
class SubClass extends SuperClass {
    //  静态方法    
    static name () {
        super.name();
        console.log("Sub");
    }
    
    //  实例方法
    sayName () {
        super.sayName();
        console.log("SubClass");
    }
}

SubClass.name();        //  打印:Super    Sub

var myClass = new SubClass();
myClass.sayName();      //  打印:SuperClass    SubClass

派生自表达式的类

只要继承的表达式可以被解析为一个函数并且具有[Constructor]属性和原型,那么就可以用 extends 关键字来继承。

//  第一种
function fun (name) {
    this.name = name;
}

class MyClass extends fun {
    constructor (name) {
        super(name);
    }
    sayName () {
        console.log(this.name);
    }
}

var myclass = new MyClass("ES6");
myclass.sayName();      //  "ES6"


//  第二种
let NameMixin = {
    getName () {
        return this.name;
    }
}

function mixin () {
    let fun = function () {};
    Object.assign(fun.prototype, ...arguments);
    return fun;
}

class MyClass extends mixin(NameMixin) {
    constructor (name) {
        super();
        this.name = name;
    }
}

let myclass = new MyClass("ES6");
console.log(myclass.getName());         //  "ES6"

第一种方式:fun 是 ES5 风格的构造函数,所以 fun 具有[Constructor]与原型,所以 MyClass 可以继承 fun。
第二种方式:mixin 来代替传统函数,接受任意数量的对象参数。首先创建 fun 函数,并且将对象参数的属性赋值给 fun 的原型上,最后返回 fun 这个函数。因此 MyClass 继承 mixin 函数返回的函数,这样做的好处:动态地确定基类上的方法。

内建对象的继承

ES5 版本中,开发者想通过继承数组方式来创建自定义的特殊数组,用传统的继承方式来实现这个操作几乎是不可能的。

//  间接调用 Array 构造函数
function newArray () {
    Array.apply(this, arguments);
}
//  修改 newArray 的原型
newArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: newArray,
        enumerable: true,
        writable: true,
        configurable: true
    }
});
var myArray = new newArray();
myArray[5] = 1;
console.log(myArray.length);    //  0

上面代码 myArray 行为表现与内建数组的不一致,这是因为通过传统 JS 继承形式实现的数组继承没有从 Array.apply() 或原型赋值中继承相关功能。
ES6 类语法支持内建对象继承,示例:

class MyArray extends Array {
}

var myarray = new MyArray();
myarray[5] = 1;
console.log(myarray.length);    //  6

MyArray 与 Array 行为相似,操作数值型属性会更新 length 属性,操作 length 属性也会更新数值型数据。
上面两个代码示例体现 ES5 与 ES6 继承方式不同:

  1. ES5 先由派生类 newArray 创建 this 值,然后通过 apply() 调用基类 Array 的构造函数。这意味着 this 一开始就指向 newArray 的实例,然后被 Array 属性所修饰。
  2. ES6 先由基类 Array 创建 this 值,再由派生类构造函数 MyArray 修改 this 值。所以一开始 this 就可以访问基类 Array 的内建功能,再接收与之相关的功能。

Symbol.species

Symbol.species 是内部 Symbol 中的一个,它被用于定义返回函数的静态访问器属性,返回的函数是一个构造函数,用于实例方法中创建类的实例时必须使用这个构造函数。

//  基类
class SuperClass {
    constructor () {}
    
    static get [Symbol.species] () {
        return this;
    }
    
    newObject () {
        return new this.constructor[Symbol.species]();
    }
}

//派生类
class SubClass1 extends SuperClass {
    
}

class SubClass2 extends SuperClass {
    static get [Symbol.species] () {
        return SuperClass;
    }
}

let sub1 = new SubClass1(),
    new1 = sub1.newObject(),
    sub2 = new SubClass2(),
    new2 = sub2.newObject();
    
console.log(new1 instanceof SuperClass);    //  true
console.log(new1 instanceof SubClass1);     //  true
console.log(new2 instanceof SuperClass);    //  true
console.log(new2 instanceof SubClass2);     //  false

SubClass1 继承 SuperClass 时未改变 Symbol.species 静态访问属性,由于 this.constructor[Symbol.species] 返回值是 SubClass1 ,所以 newObject() 返回 SubClass1 的实例。
SubClass2 继承 SuperClass 改变 Symbol.species 静态访问属性,由于 this.constructor[Symbol.species] 返回值是 SuperClass ,所以 newObject() 返回 SuperClass 的实例。
注意: 内建对象都有一个默认 Symbol.species 属性,该属性返回 this。

new.target

new.target 是 ES6 引入的元属性,该属性提供 new 调用信息。

class MyClass {
    constructor () {
        console.log(new.target === MyClass);
    }
}
let myClass = new MyClass();    //  true

由于 new MyClass(),创建 MyClass 实例,所以构造函数中的 new.target 为 MyClass。
可以利用 new.target 来创建抽象类,示例:

class SuperClass {
    constructor () {
        if (new.target === SuperClass) {
            throw new Error("这个类不能实例化!");
        }
    }
}

class SubClass extends SuperClass {
    constructor () {
        super();
    }
}

let myClass1 = new SubClass(); 
let myClass2 = new SuperClass();     //  抛出错误(Error: 这个类不能实例化!)