深入理解es6的Class(类)

152 阅读7分钟

深入理解es6的Class(类)


概述

ES6提供了更接近传统语言的写法,引入class类作为对象的模板,class可以看作只是一个语法糖,它的绝大部分功能es5都能做到,class写法只是让对象原型的写法更加清晰,更像面向对象编程的语法而已

class Point {
    constructor(x, y) {   // 构造方法
        this.x = x;
        this.y = y;
    }           // 方法之间不需要逗号分隔
    toString() {   // 定义类的方法时,前面不需要加function
        return `(${this.x},${this.y})`;
    }
}

es6的类,完全可以看作构造函数的另一种写法
类 的数据类型就是函数,类本身就指向构造函数 使用的时候也是new,和构造函数用法一致

var point = new Point(1, 2);
point.toString()

构造函数的prototype属性,在es6的“类”上面继续存在,事实上类的所有方法都定义在类的prototype属性上面

class Exam {
    constructor() { }
    toString() { }
    toValue() { }
}
// 等同于  
Point.prototype = {
    constructor() { },
    toString() { },
    toVlaue() { },
}

因为类的方法都定义在prototype对象上面,所以【类的新方法】可以添加在prototype对象上

Object.assign方法可以很方便地一次向类添加多个方法

Object.assign(Point.prototype, {
    stop() { },
    pass() { }
})

constructor方法

constructor方法是类的默认方法,new生成对象实例时自动调用该方法。

一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加

constructor方法默认返回实例对象(即this) 完全可以指定返回另外一个对象

class Foo {
    constructor() {
        return Object.create(null);
    }
}
new Foo() instanceof Foo  // false  

constrcutor函数返回一个全新的对象

另外,实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层

class IncreasingCounter {
    // constructor() {
    //     this._count = 0;
    // }
    _count = 0;    // 实例对象自身的属性 定义在类的头部
    get value() {
        return this._count;
    }
    increment() {
        this._count++;
    }
}

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,可读性比较好

类的实例

实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return this.x + this.y;
    }
}
var point = new Point(1, 2);
point.toString()  // 3
point.hasOwnProperty('x')   // true
point.hasOwnProperty('toString')     //  false
point.__proto__.hasOwnProperty('toString')   //  true

x和y都是实例对象point的自身属性(因为定义在this变量上),而toString是原型对象的属性

另外,与es5一样,类的所有实例对象共享一个原型对象 这也意味着可以通过实例的__proto__属性为类添加方法

var point2 = new Point(2, 3);
point.__proto__ === point2.__proto__   // true

类内部get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为

class MyClass {
    constructor() {
        // ...
    }
    get prop() {
        return 'getter';
    }
    set prop(value) {
        console.log('setter:' + value);
    }
}
var inst = new MyClass();
inst.prop  // getter
inst.prop = 123  // setter:123  123

这段代码中 prop属性有对应的存值函数和取值函数,因此【赋值和读取行为 都被自定义了】

存值和取值函数是设置在属性的Descriptor对象上的

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

上面代码说明,存值和取值函数定义在html属性的描述对象上

属性表达式
let methodName = 'getArea';
class Square {
    constructor() { }
    [methodName]() { }  // 该类的方法名getArea是从表达式得到的
}
var test = new Square();
test.__proto__.hasOwnProperty('getArea')   // true
Class表达式

与函数一样,类也可以使用表达式的形式定义

var TheClass = class Me {     // 使用表达式定义一个叫做 Me 的类
    getClassName() {
        return Me.name;
    }
};

但是表达式定义的类只能在Class内部使用,指代当前类。在Class外部,这个类只能用TheClass引用

var inst = new TheClass();
inst.getClassName()   //  Me

采用class表达式,可以写出立即执行的class

let person = new class {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}('zyyy');
person.sayName();   // zyyy  person是一个立即执行的类的实例

注意点:

(1)严格模式:类和模块的内部默认就是严格模式,不需要use strict指定运行模式。es6实际上把整个语言升级到了严格模式

(2)不存在提升:类不存在变量提升,不会把类的声明提升到代码头部。必须保证子类在父类之后定义

(3)this指向:类的方法内部的this默认指向类的实例

class Logger {
    printName(name = 'zyy') {
        this.print(`Hello ${name}`);   // printName方法中的this默认指向Logger的实例(logger)
    }
    print(text) {
        console.log(text)
    }
    constructor() {
        this.printName = this.printName.bind(this);  //  在构造方法中绑定this,这样就不会找不到Print方法了
    }
}
const logger = new Logger();
const { printName } = logger;   // printName方法中的this默认指向Logger的实例(logger)

但是如果将这个方法提取出来单独使用,this会指向该方法运行时所在环境(由于class内部是严格模式,所以this实际指向undefined),从而报错

解决:在构造方法中绑定this,这样就不会找不到Print方法了 (如上)

静态方法和静态属性

类相当于实例的原型,在所有类中定义的方法,都会被实例继承。如果在一个方法前加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这是静态方法

class Foo {
    static classMethod() {
        console.log(this);  // [class Foo]
        return 'hello';
    }
}
var foo = new Foo();
foo.classMethod()   //  TypeError: foo.classMethod is not a function

注意,如果静态方法包含this关键字,这个this指向的是类,而不是实例

另外,同一个类中静态方法可以和非静态方法重名

静态属性是Class本身的属性,即 Class.propName,而不是定义在实例对象上的属性

class Foo {
    static prop = 1;
}

私有方法和私有属性

私有方法和私有属性,是只能在类内部访问的方法和属性,外部不能访问,这样有利于代码的封装

class MyClass {
    #count = 0;   // 只能在类的内部使用 (this.#count) 
    #sum() {
        return this.#count;
    }
    printSum() {
        console.log(this.#sum());
    }
}
// const counter = new MyClass()
// counter.#count  // 报错
const counter = new MyClass()
counter.printSum()

另外,私有属性和私有方法前,也可以加上static关键字,表示这是一个静态的私有属性或私有方法

Class的继承

class Point { }
class ColorPoint extends Point {     // 通过 extends 关键字继承Point类的所有属性和方法
    constructor(x, y, color) {
        super(x, y);            // 调用父类的constructor(x,y)
        this.color = color;
    }
    toString() {
        return this.color + ' ' + super.toString();   // 调用父类的toString
    }
}

super关键字在这里表示父类的构造函数,用来新建父类的this对象 子类必须在constructor中调用super方法,否则新建实例会报错,这是因为子类自己的this对象必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象

如果子类没有定义constructor方法,这个方法会被默认添加

class ColorPoint extends Point {
    constructor(...args) {
        super(...args);
    }
}

另外,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则报错,这是因为子类实例的构建基于父类实例,并且只有super方法才能调用父类实例

class Point {
    constructor(x,y) {
        this.x=x;
        this.y=y;
    }
}
class ColorPoint extends Point {
    constructor(x,y,color) {
        this.color = color;  // 报错 因为子类的constructor方法不能再调用super之前就使用this
        super(x,y);
        this.color = color;  // 正确
    }
}
let cp = new ColorPoint(25,8,'green');    // 生成子类实例

另外,父类的静态方法也会被子类继承

判断一个类是否继承了另一个类:

Object.getPrototypeOf(ColorPoint) === Point // true

类的prototype属性和__proto__属性

子类的__proto__属性表示构造函数的继承,指向它的父类

子类prototype的__proto__属性表示方法的继承,指向它父类的prototype

class A { }
class B extends A { }
B.__proto__ === A   // true
B.prototype.__proto__ === A.prototype    // true

因为B的实例继承了A的实例,B继承了A的静态方法


内容学习自阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版

我是栖夜,感谢阅读