class_ES6笔记10

一.JS要变Java了!?

ES6启用了classconstructorstaticextendssuper等关键字来支持类定义,感觉是马上要变Java了,终于不用管prototype了吗?

不是这样的。启用这一套关键字仅仅是为了减少“语法噪音”,减少定义类结构时的工作量,JS基于原型的(prototype-based)对象系统无法轻易改成基于类的(class-based),这是JS语言设计者的选择,不可能把现有的对象系统连根拔起,再把Java那一套塞进来缝合好。

P.S.“语法噪音”是从老赵那搬过来的词,用Python和Java实现同样的功能,diff结果就是“语法噪音”,比如JS中的functionprototype等等输入起来很难受的东西

而且,基于原型的对象系统相对灵活,比如JS可以在运行时动态修改类继承树,因此我们可以只预先定义好空的类继承树,需要时才去增添类成员,例如:

// 空的继承树
var SuperType = function() {};
var SubType = function() {};
SubType.prototype = new SuperType();
// 动态增强
void function() {
    //...
    SuperType.prototype.cl = console.log.bind(console);
    new SubType().cl('invoked by subType'); // invoked by subType
}();

这种感觉很奇妙,Java和JS一起画画,Java用精细的刻刀在画布上一丝不苟地完成了清明上河图,装裱好挂在墙上,四下惊艳。JS拿起水笔画了两条弯弯扭扭的横线,说“我画完了,这就是清明上河图”,观众感到被愚弄了,但也勉强把JS的作品装裱好,挂在Java的大作旁边。Java束手而立静待分晓,这时JS才开始忙活,拿起笔认真地在相框玻璃面上完成了一模一样的作品,观众感到不可思议。突然人群里传出一个声音,“左边第三个房子应该是尖顶的,你们都画错了”,Java脸上挂不住了,急匆匆地摊开另一张宣纸,想要重新画一幅改掉那个碍眼的错误。JS指着左上角说“是这里吗?我已经改好了”

JS是命令式语言、动态语言和函数式语言的结合体,就整个体系而言,对象系统需要基于原型实现带来的这种灵活性,不用羡慕Java精雕细刻的基于类的对象系统,完全不合身。ES规范设计者不会也没有理由去照搬Java,至此,JavaScript与Java仍然毫不相干,像20年前一样

P.S.JS和笔者都是1995年诞生的,至于Netscape与JavaScript和Java Applet的丝丝缕缕,请自行查找

二.class带来的新特性

ES6之前的“class”可能是这样的:

function Circle(radius) {
    if (typeof radius !== 'number') {
        throw new TypeError('radius: a number expected');
    }
    // 实例属性
    this.radius = radius;
    // 静态属性
    Circle.count++;
}
Circle.count = 0;
// 原型属性
Circle.prototype.getArea = function() {
    return Math.PI * this.radius * this.radius;
};

new Circle得到的每个实例都具有两个属性,实例属性radius和原型属性getArea

偶尔能看到用ES5特性实现的更严谨的“class”:

// 更严谨的,定义getter/setter
function CircleEx(radius) {
    this.radius = radius;
    CircleEx.count++;
}
// 定义静态属性
Object.defineProperty(CircleEx, 'count', {
    get: function() {
        // this指向CircleEx
        // console.log(this);
        // CircleEx.count++先get返回0
        // 再set给CircleEx添上_count属性并赋值为1
        return !this._count ? 0 : this._count;
    },
    set: function(val) {
        this._count = val;
    }
});
CircleEx.prototype.getArea = function() {
    return Math.PI * this.radius * this.radius;
};
// 定义原型属性
// 实际上是为实例属性定义getter/setter
Object.defineProperty(CircleEx.prototype, 'radius', {
    get: function() {
        //!!! this指向CircleEx实例
        // 而不是其原型对象
        // console.log(this);
        // 构造函数中this.radius = radius;
        // 查找radius属性,触发get,返回this._radius
        // 赋值为radius
        return this._radius;
    },
    set: function(val) {
        if (typeof val !== 'number') {
            throw new TypeError('radius: a number expected');
        }
        this._radius = val;
    }
});

new CircleEx得到的每个实例都具有两个属性,实例属性_radius和原型属性getArea,至于radius,则是定义在原型对象上的访问器(getter/setter),不可枚举

之前说“偶尔”能看到这样的“class”定义,就是因为太麻烦了,大家都懒得用。ES6 class部分的目标就是改变现状,提供更方便的类成员定义方式

简化了对象的定义方式

可以直接在对象字面量中定义getter/setter,简化了函数类型属性定义方式,包括一般函数、生成器和动态函数名,例如:

// getter/setter
var obj = {
    // getter
    //!!! getter无参,无法传参
    get attr() {
        console.log('getter');
        return this._attr || 0;
    },
    // setter
    //!!! setter至少接受一个参数
    set attr(val) {
        console.log('setter');
        this._attr = val;
    },
    // 预计算属性(用[]语法添加的函数属性,动态函数名)
    [(function() {return 'print';})()](arg) {
        console.log(arg);
    },

    // 一般方法
    fn(arg) {
        console.log(`arg = ${arg}`);
    },
    // 生成器
    *gen(i) {
        while(true) {
            yield i++;
        }
    }
}

冗长的function关键字从类型定义中彻底消失了,甚至比箭头函数都简洁。又感受到了初学JS时的欣喜——给变量添上一对圆括号就是函数调用,怎么这么简单粗暴

用ES6增强版对象字面量重写之前的CircleEx类,将会是这样:

// 重写CircleEx
CircleEx.prototype = {
    getArea() {
        return Math.PI * this.radius * this.radius;
    },
    get radius() {
        return this._radius;
    },
    set radius(val) {
        if (typeof val !== 'number') {
            throw new TypeError('radius: a number expected');
        }
        this._radius = val;
    }
};

注意,与之前不同的是,此时访问器radius可枚举的,并且作为一个原型属性出现。但同样的,访问器自身不会被暴露出来,针对radius的所有操作都是对访问器返回值的操作,而不是访问器本身,因此:

typeof new CircleEx(1).radius === 'number'  // true

启用了class定义

P.S.之所以说“启用”而不是“引入”,是因为ES6 class相关的所有关键字本来就是保留字,只是现在被赋予了明确的含义

class定义中,constructor表示构造函数,static关键字用来区分一般函数和特殊函数(含义同Java、C++)

constructor可选,默认提供空构造函数(constructor() {}),constructor必须是字面量形式,不能是动态函数名,否则将得到名为constructor的一般方法,而不是构造函数

class语法重写之前的Circle类,如下:

// 重写CircleEx
class MyCircle {
    // 构造函数
    constructor(radius) {
        this.radius = radius;
        MyCircle.count++;
    }

    // 实例属性的getter/setter
    get radius() {
        return this._radius;
    }
    set radius(val) {
        if (typeof val !== 'number') {
            throw new TypeError('radius: a number expected');
        }
        this._radius = val;
    }

    // 原型属性
    getArea() {
        return Math.PI * this.radius * this.radius;
    }

    // 静态属性
    static get count() {
        return this._count || 0;
    }
    static set count(val) {
        this._count = val;
    }
}

无论实际效果如何,但至少看起来清晰多了

类型保护

内置了类型保护,用class语法定义的类型,必须通过new操作符来调用,例如:

// 当作一般函数调用,会报错
MyCircle();
// Uncaught TypeError: Class constructors cannot be invoked without 'new'

这种内置保护能够避免一些问题,但确实也限制了灵活性,比如某些类库提供的API中含有既能作为一般函数调用,也能作为构造函数使用的函数,用class语法就无法实现

此外,类定义中引用的类名不会被外部力量改变,例如:

class T {
    static get val() {
        return 'val';
    }

    get() {
        return T.val;
    }
}
// test
var t = new T();
console.log(t.get());   // val
// 外力破坏
T = null;
console.log(t.get());   // val
//! 报错Uncaught TypeError: T is not a constructor
// new T();

在ES6之前是没有这种保护的,用function重写,遭到外力破坏后必定出错

支持类表达式(匿名类)

// 匿名类
var circle = new class {
    constructor(radius) {
        this.radius = radius;
    }
}(3);
console.log(circle.radius); // 3

匿名类好像没什么用,因为一般来说,类是对象模板,通过类能实现对象的量产,Java用匿名类来创建不需要量产的临时对象,而JS有N种方法可以创建对象,用匿名类可能是最傻的方式

类中定义的方法可配置不可枚举

比如MyClass.prototype(通过class语法定义的)所有属性都不可枚举,类中定义的方法也不可枚举

这样做似乎是在刻意掩盖对象系统基于原型的事实,比如for...in无法枚举之前的MyCircle.prototype,但可以通过Object.getOwnPropertyNames()发现一些痕迹,例如:

console.log(Object.getOwnPropertyNames(MyCircle.prototype));
// log print:
// ["constructor", "radius", "getArea"]

这些属性都真实存在于原型对象上,但默认不可枚举掩盖了这个事实,可能是不希望我们在使用class语法的同时,手动操作prototype对象,破坏既有规则

可能总觉得这种掩盖prototype的做法欠妥,但又说不上哪里不对,好,考虑下原型继承的经典问题:原型引用属性会在实例间共享,例如:

function Type() {}
Type.prototype.issue = [1, 2];
// test
var t1 = new Type();
var t2 = new Type();
t1.issue.push(3);
console.log(t2.issue);  // [1, 2, 3]

class语法存在这个问题吗?首先我们得用class语法在Type的原型上定义一个数组类型属性,沮丧地发现根本做不到,除非直接访问prototype,所以不用担心这种“掩盖”会引发隐秘的问题,ES6设计者比我们考虑的多得多

三.总结

清爽的类型定义语法,keep it simple

ES正在逐渐剔除语法噪音:

箭头函数 + `class`剔掉了`function`

默认参数 + 不定参数剔掉了`arguments`

`class` + `extends` + `super`剔掉了`prototype`

从函数式语言的角度来看,这些变化是极好的,数学家追求的正是极致简洁的美

参考资料

  • 《ES6 in Depth》:InfoQ中文站提供的免费电子书

  • 《JavaScript语言精髓与编程实践》

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code