ES
里没有像其他语言一样的 Class
和继承, 在 ES5
里开发者通常需要利用构造函数和原型模拟出这一特性, 但比较繁琐。而ES6
通过 class
和 extends
关键字简化了这一操作,可以快速便捷地创建出类。虽然它是个语法糖,但大大提高了开发的体验和效率。
在 ES5
, 这样声明类:
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function () {
console.log(this.name);
};
var person = new PersonType("Nicholas");
person.sayName(); // outputs "Nicholas"
console.log(person instanceof PersonType); // true
console.log(person instanceof Object); // true
而 ES6
可以这样:
class PersonClass {
// equivalent of the PersonType constructor
constructor(name) {
this.name = name;
}
// equivalent of PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("Nicholas");
person.sayName(); // outputs "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
用 关键字 class
声明一个类,里面的语法类似对象字面量,但是各属性之间不需要逗号,
,方法声明也不用funtion
(用了反而会报错)。看起来简洁多了。
另外,类的实例属性,只能诞生在 constructor
或其他方法里。
还有一点,类的原型,如上面的 PersonClass.prototype
是只读的,不能像 ES5
那样被修改。
class
声明不存在变量提升class
声明里的代码默认是严格模式class
的方法都是不可枚举的,不能被new
操作- 直接调用而不是
new
class
会报错 - 在
class
方法里修改class
名字会报错
用 ES5
来描述这些特定就是:
// direct equivalent of PersonClass
let PersonType2 = (function () {
"use strict";
const PersonType2 = function (name) {
// make sure the function was called with new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(PersonType2.prototype, "sayName", {
value: function () {
// make sure the method wasn't called with new
if (typeof new.target !== "undefined") {
throw new Error("Method cannot be called with new.");
}
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});
return PersonType2;
}());
函数有函数声明和函数表达式两种创建方式,class
也一样,除了上面的 class
声明,也有 class
表达式。
需要注意的是用表达式声明类,类名的变化。
这个比较简单,这里就不浪费大家的时间了。
编程语言里的一等公民,指的是一个值既能当参数传给函数,也能作为函数的返回值,并且可以赋值给其他变量。ES
里函数是一等公民,而 class
也是。
一般存储器属性是针对某个属性而设置的, 而 class
也可以直接设置存储器属性:
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
和对象一样,类也是支持计算属性的:
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
};
let me = new PersonClass("Nicholas");
me.sayName(); // "Nicholas"
这点,对存取器属性也不例外:
let propertyName = "html";
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get [propertyName]() {
return this.element.innerHTML;
}
set [propertyName](value) {
this.element.innerHTML = value;
}
}
与 generator
章节对象的 generator
方法类似,class
也可以设置:
class MyClass {
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}
let instance = new MyClass();
let iterator = instance.createIterator();
既然有 iterator
, 那最好个默认的,方便在 for-of
之类的场景下使用:
class Collection {
constructor() {
this.items = [];
}
*[Symbol.iterator]() {
yield* this.items;
}
}
var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
console.log(x);
}
// Output:
// 1
// 2
// 3
ES5
可以直接在构造函数上添加属性来创建静态成员:
function PersonType(name) {
this.name = name;
}
// static method
PersonType.create = function (name) {
return new PersonType(name);
};
// instance method
PersonType.prototype.sayName = function () {
console.log(this.name);
};
var person = PersonType.create("Nicholas");
之所以叫静态成员,是因为它不依赖实例的数据
而 ES6
通过在方法或存取器属性名前加 static
关键字,就可以实现相同的功能:
class PersonClass {
// equivalent of the PersonType constructor
constructor(name) {
this.name = name;
}
// equivalent of PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// equivalent of PersonType.create
static create(name) {
return new PersonClass(name);
}
static get hobbies () {
return ['coding', 'reading']
}
}
let person = PersonClass.create("Nicholas");
console.log(PersonClass.hobbies)
// ['coding', 'reading']
static
不能用在constructor
前
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
ES6
用 extends
简化了这一做法:
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// equivalent of Rectangle.call(this, length, length)
super(length, length);
}
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
constructor
里必须要调用 super
,并且是在使用 this
之前。 唯一避免使用 super
的方法,就是在 constructor
里返回一个对象。
当派生类没写 constructor
时, ES
会自动调用它, 并传入实例化时的所有参数:
class Square extends Rectangle {
// no constructor
}
// is equivalent to
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
子类要覆盖父类的方法,在子类里重写一个就可以了,跟 ES5
一样。
需要留意的是,父类的静态成员也会被继承下来:
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) {
// equivalent of Rectangle.call(this, length, length)
super(length, length);
}
}
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle);
console.log(rect.getArea());
console.log(rect instanceof Square);
// true
// 12
// false
extends
后面可以跟任何表达式,只要这个表示最后的结果是个函数,函数有 [[Constuctor]]
和原型就可以。
如下,clsss
可以继承 ES5
里的类
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());
console.log(x instanceof Rectangle);
// 9
// true
凭借这个特性,class
还可以 extends
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());
console.log(x.serialize());
// 9
// "{"length":3,"width":3}"
如果多个 mixin
有同样的属性的话,后者会覆盖前者。
ES5
里,继承内置对象是存在问题的,如下面自定义数组的操作:
// built-in array behavior
var colors = [];
colors[0] = "red";
console.log(colors.length);
colors.length = 0;
console.log(colors[0]);
// 1
// undefined
// trying to inherit from array in ES5
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length);
colors.length = 0;
console.log(colors[0]);
// 0
// "red"
可以看出,MyArray
并没有真正继承 Array
, 因为 MyArray
对下标的长度的操作,与原生数组比是无效的。
这是因为在 ES5
的继承里,派生类 MyArray
先创建出 this
, 然后 Array
才实例化。这意味着,是用内置的Array
去覆盖 MyArray
实例。而正确的做法应该是用 MyArray
实例去修改 Array
实例。
ES6
正是这么做的:
class MyArray extends Array {
// empty
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length);
colors.length = 0;
console.log(colors[0]);
// 1
// undefined
如上面用 ES6
方式继承类的实例,上述操作与原生数组一样,是有效的继承。
至于为什么
ES6
可以实现这个功能,其实是与Symbol.species
密切相关,感兴趣的同学可以继续深究下
在函数章节里,我们提到过 new.target
可以用来判断函数是被用作普通函数,还是构造函数。如果是用作构造函数,那 new.target
就等于调用它的构造函数。凭借它,我们可以对 class
更多控制
如我们限定一个 class
是抽象类,只能用来被继承的话,可以这么写:
// abstract base class
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("This class cannot be instantiated directly.")
}
}
}
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);
// throws an error
// no error
// true
其实在继承中,父类的 new.target
指向子类:
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 is Square
var obj = new Square(3);
// outputs false
ES6
提供了 class
和 extends
是创建类和实现继承更方便了。而且还可以继承原生对象,可以继承 mixin
, 提高了开发效率。
但记住 class
只是 ES5
的语法糖,理解 ES5
的继承方法,对理解 class
是有一定帮助的。