资讯专栏INFORMATION COLUMN

ES6系列---类

huayeluoliuhen / 2284人阅读

摘要:原型会自动调整,通过调用方法即可访问基类的构造函数。在简单情况下,等于类的构造函数的值是输出这段代码展示了当调用时等于。

大多数面向对象编程语言都支持类和类继承的特性,而JavaScript只能通过各种特定方式模仿并关联多个相似的对象。这个情况一直持续到ES5。由于类似的库层出不穷,最终ES6引入了类特性,统一了类和类继承的标准。

ES5模仿类

先看一段ES5中模仿类的代码:

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

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

var person = new PersonType("Nicholas");
person.sayName();

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

这段代码中的PersonType是一个构造函数,执行后创建一个名为name的属性;给PersonType的原型添加一个sayName()方法,所以PersonType对象的所有实例共享这个方法。然后使用new操作符创建一个PersonType的实例person,并最终证实了person对象确实是PersonType的实例。

ES6的类

ES6有一种与其他语言中类似的类特性:类声明。

类声明语法
class PersonType {
    // 等价于PersonType构造函数
    constructor(name) {
        this.name = name;
    }
    
    // 等价于PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
}

let person = new PersonType("Nicholas");
person.sayName();

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

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

通过类声明语法定义PersonType的行为与之前创建PersonType构造函数的过程相似,只是这里直接通过特殊的constructor方法名来定义构造函数。

访问器属性

尽管应该在类构造函数中创建自己的属性,但是类也支持直接在原型上定义访问器属性。创建getter时,需要在关键字get后紧跟一个空格和相应的标识符;创建setter时,只需把关键字get替换为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

这段代码中的CustomHTMLElement类是一个针对现有DOM元素的包装器,并通过getter和setter方法将这个元素的innerHTML方法委托给html属性,这个访问器属性是在CustomHTMLElement.prototype上创建的。

可计算成员名称

类和对象字面量还有更多相似之处,类方法和访问器属性也支持使用可计算名称:

let methodName = "sayName";

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

let me = new PersonType("Nicholas");
me.sayName();

通过相同的方式可以在访问器属性中应用可计算名称:

let propertyName = "html";

class CustomHTMLElement {
    constructor(element) {
        this.element = element;
    }
    
    get [propertyName]() {
        return this.element.innerHTML;
    }
    
    set [propertyName](value) {
        this.element.innerHTML = value;
    }
}
生成器方法

关于生成器和迭代器的知识点,可以参考ES6系列---生成器和迭代器。

在对象字面量中,可以通过在方法名前附加一个星号(*)的方式来定义生成器,在类中亦是如此:

class MyClass {
    *createIterator() {
        yield 1;
        yield 2;
        yield 3;
    }
}

let instance = new MyClass();
let iterator = instance.createIterator();

如果用对象来表示集合,又希望通过简单的方法迭代集合中的值,那么生成器方法就派上用场了。数组、Set集合及Map集合为开发者们提供了多个生成器方法来与集合中的元素交互。
尽管生成器方法很实用,但如果你的类是用来表示值的集合的,那么定义一个默认迭代器会更有用。通过Symbol.iterator定义生成器方法即可为类定义默认迭代器:

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

ES6简化了创建静态成员的过程,在方法或访问器属性名前使用正式的静态注释即可:

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

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

静态成员或方法,不可在实例中访问,必须要直接在类上访问。

继承与派生类

在ES6之前,实现继承与自定义类型是个不小的工作:

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

Square继承自Rectangle,为了这样做,必须用一个创建自Rectangle.prototype的新对象重写Square.prototype并调用Rectangle.call()方法。

ES6中实现继承

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

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

这一次,Square类通过extends关键字继承Rectangle类,在Square构造函数中通过super()调用Rectangle构造函数并传入相应参数。

类方法重写

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

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }
    
    // 重写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);
    }
    
    // 重写后调用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

在这段代码中,新的静态方法create()被添加到Rectangle类中,继承后的Square.create()与Rectangle.create()行为一致。

派生自表达式的类

ES6最强大的一面或许是从表达式导出类的功能了。只要表达式可以被解析为一个函数并且具有[[Construct]]属性和原型,那么就可以用extends进行派生:

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]]属性和原型,因此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 Squre extends getBase() {
    constructor(length) {
        super(length, length);
    }
}

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

getBase()函数是类声明的一部分,直接调用后返回Rectangle,此示例实现的功能与之前的示例等价。由于可以动态确定使用哪个基类,因而可以创建不同的继承方法。例如,可以这样创建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函数代替传统的继承方法,它可以接受任意数量的mixin对象作为参数。首先创建一个函数base,再将每一个mixin对象的属性值赋值给base的原型,最后mixin函数返回这个base函数,所以Square类就可以基于这个返回的函数用extends进行扩展。
Square的实例拥有来自AreaMixin对象的getArea()方法和来自SerializableMixin对象的serialize方法,这都是通过原型继承实现的,mixin()函数会用所有mixin对象的自有属性动态填充新函数的原型。

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

在类的构造函数中也可以通过new.target来确定类是如何被调用。在简单情况下,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

这段代码展示了当调用new Rectangle(3, 4)时new.target等于Rectangle。
继承情况下,有所不同:

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调用Rectangle的构造函数,所以当调用发生时new.target等于Square。据此,我们可以创建一个抽象基类(不能被实例化的类),就像这样:

// 抽象基类
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.target是Shape时构造函数总会抛出错误,这相当于调用new Shape()时总会出错。但是,仍可用Shape作为基类派生其他类。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91873.html

相关文章

  • ES6 系列之 Babel 是如何编译 Class 的(上)

    摘要:前言在了解是如何编译前,我们先看看的和的构造函数是如何对应的。这是它跟普通构造函数的一个主要区别,后者不用也可以执行。该函数的作用就是将函数数组中的方法添加到构造函数或者构造函数的原型中,最后返回这个构造函数。 前言 在了解 Babel 是如何编译 class 前,我们先看看 ES6 的 class 和 ES5 的构造函数是如何对应的。毕竟,ES6 的 class 可以看作一个语法糖,...

    shadajin 评论0 收藏0
  • [js高手之路] es6系列教程 - 新的语法实战选项卡

    摘要:其实的面向对象很多原理和机制还是的,只不过把语法改成类似和老牌后端语言中的面向对象语法一用封装一个基本的类是不是很向和中的类其实本质还是原型链,我们往下看就知道了首先说下语法规则中的就是类名,可以自定义就是构造函数,这个是关键字,当实例化对 其实es6的面向对象很多原理和机制还是ES5的,只不过把语法改成类似php和java老牌后端语言中的面向对象语法. 一、用es6封装一个基本的类 ...

    yintaolaowanzi 评论0 收藏0
  • 揭秘babel的魔法之class继承的处理2

    摘要:并且用验证了中一系列的实质就是魔法糖的本质。抽丝剥茧我们首先看的编译结果这是一个自执行函数,它接受一个参数就是他要继承的父类,返回一个构造函数。 如果你已经看过第一篇揭秘babel的魔法之class魔法处理,这篇将会是一个延伸;如果你还没看过,并且也不想现在就去读一下,单独看这篇也没有关系,并不存在理解上的障碍。 上一篇针对Babel对ES6里面基础class的编译进行了分析。这一篇将...

    BlackHole1 评论0 收藏0
  • ES6 系列之私有变量的实现

    摘要:前言在阅读入门的时候,零散的看到有私有变量的实现,所以在此总结一篇。构造函数应该只做对象初始化的事情,现在为了实现私有变量,必须包含部分方法的实现,代码组织上略不清晰。 前言 在阅读 《ECMAScript 6 入门》的时候,零散的看到有私有变量的实现,所以在此总结一篇。 1. 约定 实现 class Example { constructor() { this...

    lentoo 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<