《深入理解ES6》第九章 JS的类

第九章 JS的类

《深入理解ES6》—— Nicholas C. Zakas

JS从创建之初就不支持类,
也没有把类继承作为定义相似对象以及关联对象的主要方式。

很多库都创建了一些工具,让JS显得貌似能支持类,导致ES6最终引入了类。

ES6的类并不与其他语言的类完全相同,
所具备的独特性正配合了JS的动态本质。

1. ES5中的仿类结构

ES6之前不存在类,但与类最接近的是:
创建一个构造器,然后将方法指派到该构造器的原型上。

这种方式通常被称为创建一个自定义类型,如:

function PersonType( name ) {
    this.name = name;
}
PersonType.prototype.sayName = function () {
    console.info( this.name );
}
let person = new PersonType( "吴钦飞" );
person.sayName(); //=> 吴钦飞

person instanceof PersonType; //=> true
person instanceof Object; //=> true

使用 new 运算符创建了 PersonType 的一个新实例 person
此对象会被认为是一个通过原型继承了 PersonTypeObject 的实例。

这种基本模式在许多对类进行模拟的JS库中都存在,而这也是ES6类的出发点。

2. 类的声明

2.1. 基本的类声明

格式:class 类名 对象字面量的简写形式。

class PersonClass {
    // <==> PersonType的构造器
    constructor( name ) {
        this.name = name;
    }

    // <==> PersonType.prototype.sayName
    sayName() {
        console.info( this.name );
    }
}
let person = new PersonClass( "吴钦飞" );
person.sayName();

typeof PersonClass; //=> "function"

PersonClass 实际上创建了一个拥有 constructor 方法及其行为的函数。

自有属性:own properties,该属性出现在实例上而不是原型上,
只能在类的构造器或方法内部进行创建。
建议在构造器函数内创建所有可能出现的自有属性,
这样在类中声明变量就会被限制在单一位置(有助于代码检查)。

相对于已有的自定义类型声明方式来说,
类声明仅仅是以它为基础的一个语法糖。

2.2. 为何要使用类的语法

类与自定义类型的区别:

  • 类声明不会被提升。在程序的执行到达声明处之前,类都存在暂时性死区。
  • 类声明中的所有代码都自动运行在严格模式下
  • 类的所有方法都不可枚举
  • 类的所有方法内部都没有[[Construct]],因此使用new调用会报错
  • 调用类构造器时不使用new,会抛异常。
  • 视图在类的方法内部重写类名,会抛出错误.

等价于一下未使用类语法的代码:

let PersonType = (function() {
    "use strict";

    const PersonType = function( name ) {
        if ( typeof new.target === "undefined" ) {
            throw new Error( "Constructor must be called with new." );
        }
        this.name = name;
    };

    Object.defineProperty( PersonType.prototype, "sayName", {
        value: function () {
            if ( typeof new.target !== "undefined" ) {
                throw new Error("Method cannot be called with new.");
            }
            console.info( this.name );
        },
        enumerable: false,
        writable: true,
        configurable: true
    } );

    return PersonType;
})();

上例说明了尽管不使用新语法也能实现类的任何特性,但类语法显著简化了所有功能的代码。

3. 类表达式

类和函数都有两种形式:声明和表达式。

类表达式被设计用于变量声明、或可作为参数传递给函数。

3.1. 基本的类表达式

let Person = class {
    constructor( name ) {
        this.name = name;
    }
    sayName() {
        console.info( this.name );
    }
};

3.2. 具名类表达式

let Person = class Person2 {
    constructor( name ) {
        this.name = name;
    }
    sayName() {
        console.info( this.name );
    }
};
typeof Person2; //=> "undefined"

Person2 只能在类的内部使用,在外部使用会报语法错误。

4. 作为一级公民的类

在编程中,能被当做值来使用的就称为** 一级公民(first-class citizen)**。

它能 作为参数传递给函数、作为函数的返回值、能给变量赋值。

JS的函数就是一级公民(又称为一级函数)。
ES6的类也是一级公民。

类作为参数传入函数:

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

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

obj.sayHi();

创建单例:

let person = new class{
    constructor( name ) {
        this.name = name;
    }
    sayName() {
        console.info( this.name );
    }
}( "吴钦飞" );

person.sayName();

5. 访问器属性

在类构造器中可以创建 自有属性,
可以使用关键字set/get在原型上定义 访问器属性。

class CustomHTMLElement {
    constructor( element ) {
        this.element = element;
    }
    get html() {
        return this.element.innerHTML;
    }
    set html(content) {
        this.element.innerHTML = content;
    }
}

const div = document.createElement('div');
const customDiv = new CustomHTMLElement(div);

customDiv.html = 123;
customDiv.html; //=> "123"

CustomHTMLElement类用于包装DOM元素,
它的html属性拥有getter与setter,委托了元素自身的 innerHTML 方法,
它创建在原型上,并且不可枚举。

等价非类代码实现 需要写大量代码:

let CustomHTMLElement = ( function() {
    "use strict";
    const CustomHTMLElement = function( element ) {
        if ( typeof new.target === "undefined" ) {
            throw new Error( "构造器必须通过 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;
} )();

6. 需计算的成员名

类方法、类访问器的名称可通过 [表达式] 计算获得。

let methodName = "sayName";
let propertyName = "html";

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

    get [propertyName]() {}
    set [propertyName]() {}
}
new PersonClass( "吴钦飞" ).sayName();

7. 生成器方法

在表示集合的自定义类中定义一个默认迭代器。

class Collection {
    constructor() {
        this.items = [];
    }
    *[Symbol.iterator]() {
        // 委托到该数组的values()迭代器
        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.info( x );
}

8. 静态成员

ES6之前,直接在构造器上添加额外的方法来模拟静态成员。

ES6的类简化了静态成员的创建,
只要在方法与访问器属性的名称前加上 static 关键字即可。

class PersonClass {
    constructor( name ) {
        this.name = name;
    }
    sayName() {
        console.info( this.name );
    }

    static create( name ) {
        return new PersonClass( name );
    }
}
PersonClass.create( "吴钦飞" ).sayName();

9. 继承

9.1. 基础

ES6之前的继承

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

function Square( width ) {
    Rectangle.call( this, width, width );
}
Square.prototype = Object.create( Rectangle.prototype, {
    constructor: {
        value: Square,
        enumerable: true,
        writable: true,
        configurable: true
    }
} );
var square = new Square( 3 );
console.info( square.getArea() );   //=> 9
console.info( square instanceof Square );   //=> true
console.info( square instanceof Rectangle );//=> true

Square 继承 Rectangle

  • 重写 Square.prototype,等于 Rectangle.prototype 创建的对象
  • 在构造函数里先调用 Rectangle 的构造函数进行初始化 this

ES6类的继承

class Rectangle {
    constructor( width, height ) {
        this.width = width;
        this.height = height;
    }
    getArea() {
        return this.width * this.height;
    }
}

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

var square = new Square( 3 );
console.info( square.getArea() );   //=> 9
console.info( square instanceof Square );   //=> true
console.info( square instanceof Rectangle );//=> true

Square 继承 Rectangle

  • 使用 extends 关键字

  • 使用 super() 初始化this

  • 必须在访问 this 之前调用 super()

    • 如果派生类指定了构造器,则需要显式使用 super()
    • 如果派生类未指定构造器,super() 会自动调用
    class Square extends Rectangle {
        // 未指定构造器
    }
    // 等价于:(添加默认构造器)
    class Square extends Rectangle {
        constructor( ...args ) {
            super( ...args );
        }
    }
    

9.2. 屏蔽类方法

派生类中方法会屏蔽掉基类的同名方法,
可使用 super.methodA() 访问基类的方法。

9.3. 继承静态成员

派生类可继承基类的静态成员。

9.4. 从表达式中派生类

class ClassName extends 表达式 {

}

该表达式返回一个具有 [[Construct]] 属性的函数。

9.5. 继承内置对象

ES6中的类,允许从内置对象上进行继承。

class MyArray extends Array {

}

ES5中传统的继承,this 值先被派生类(MyArray)创建,
随后调用基类构造器(Array.apply())。
这意味着 this 一开始就是 MyArray 的实例,
之后才使用了 Array 的附加属性对其进行了装饰。

ES6基于类的继承中,this 的值会先被基类(Array)创建,
随后才被派生类的构造器(MyArray)所修改。
结果是this初始就拥有作为基类的内置对象的所有功能,
并能正确接收与之关联的所有功能。

9.6. Symbol.species 属性

继承内置对象的特点:任意能返回内置对象实例的方法,在派生类上却自动返回派生类的实例。

如,调用 MyArray 实例的 slice() 方法会返回 MyArray 的实例。

Symbol.species 属性在后台造成了这种变化。

10. 在类构造器中使用 new.target

当被派生类调用构造器时,new.target 不等于基类。

这可以创建不可实例化的基类。

class Shape {
    constructor() {
        if ( new.target === Shape ) {
            throw new Error( "基类不能实例化" );
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值