一.JS要变Java了!?
ES6启用了class
、constructor
、static
、extends
、super
等关键字来支持类定义,感觉是马上要变Java了,终于不用管prototype
了吗?
不是这样的。启用这一套关键字仅仅是为了减少“语法噪音”,减少定义类结构时的工作量,JS基于原型的(prototype-based)对象系统无法轻易改成基于类的(class-based),这是JS语言设计者的选择,不可能把现有的对象系统连根拔起,再把Java那一套塞进来缝合好。
P.S.“语法噪音”是从老赵那搬过来的词,用Python和Java实现同样的功能,diff结果就是“语法噪音”,比如JS中的function
、prototype
等等输入起来很难受的东西
而且,基于原型的对象系统相对灵活,比如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语言精髓与编程实践》