ECMAScript5中的近类结构
function PersonType(name) {this.name = name;}PersonType.prototype.sayName = function () {console.log(this.name);}var person = new PersonType("abc");person.sayName(); //abcconsole.log(person instanceof PersonType); //trueconsole.log(person instanceof Object); //true
类的声明
基本的类声明语法
class PersonClass {
// 等价于PersonType
constructor(name) {
this.name = name;
}
// 等价于PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("abc");
person.sayName(); //abc
console.log(person instanceof PersonClass); //true
console.log(person instanceof Object); //true
console.log(typeof PersonClass); //function
console.log(typeof PersonClass.prototype.sayName); //function
类与自定义构造函数的区别
· 函数声明可以被提升,而类声明与let声明类似,不能被提升;真正执行声明语句之前,它们会一直存在于临时死区中。
· 类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式执行。
· 在自定义类型中,需要通过Object.defineProperty()方法手工指定某个方法为不可枚举;而在类中,所有方法都是不可枚举的。
· 每个类都有一个名为[[Construct]]的内部方法,通过关键字new调用那些不含[[Construct]]的方法会导致程序抛出错误。
· 使用除关键字new以外的方式调用类的构造函数会导致程序抛出错误。
· 在类中修改类名会导致程序报错。
**
类表达式
基本的类表达式语法
let PersonClass = class {
// 等价于PersonType构造函数
constructor(name) {
this.name = name;
}
// 等价于PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
let person = new PersonClass("abc");
person.sayName();
console.log(person instanceof PersonClass);
console.log(person instanceof Object);
console.log(typeof PersonClass);
console.log(typeof PersonClass.prototype.sayName);
命名类表达式
let PersonClass = class PersonClass2 {
// 等价于PersonType构造函数
constructor(name) {
this.name = name;
}
// 等价于PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
console.log(typeof PersonClass); //function
console.log(typeof PersonClass2); //undefined
类表达式被命名为PersonClass2,由于标识符PersonClass2只存在于类定义中,PersonClass2只能在类的内部使用
作为一等公民的类
在程序中,一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。JavaScript函数是一等公民(也被称作头等函数),这也正是JavaScript中的一个独特之处。
ECMAScript 6延续了这个传统,将类也设计为一等公民,允许通过多种方式使用类的特性。例如,
可以将类作为参数传入函数中:
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
});
obj.sayHi();
类表达式还有另一种使用方式,通过立即调用类构造函数可以创建单例。用new调用类表达式,紧接着通过一对小括号调用这个表达式,例如:**
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("abc");
person.sayName(); //"abc"
访问器属性
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);
console.log("set" in descriptor);
console.log(descriptor.enumerable);
可计算成员名称
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
};
let me = new PersonClass("abc");
me.sayName();
生成器方法
class MyClass {
* createIterator() {
yield 1;
yield 2;
yield 3;
}
}
let instance = new MyClass();
let iterator = instance.createIterator();
for (let i of iterator) {
console.log(i); // 1 2 3
}
定义一个默认迭代器会更有用
class Collection {
constructor() {
this.items = [];
}
*[Symbol.iterator]() {
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.log(x); // 1 2 3
}
静态成员
如果不介意在对象的实例中出现添加的方法和访问器属性,则可以将它们添加到类的原型中;如果你希望它们只出现在类中,那么需要使用静态成员。
es5写法**
function PersonType(name) {
this.name = name;
}
//静态方法
PersonType.create = function (name) {
return new PersonType(name);
}
// 实例方法
PersonType.prototype.sayName = function () {
console.log(this.name);
};
var person = PersonType.create("abc");
person.sayName();
es6写法
class PersonClass {
// 等价于PersonType构造函数
constructor(name) {
this.name = name;
}
// PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// 等价于PersonType.create
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create("abc");
继承与派生类
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());
console.log(square instanceof Square);
console.log(square instanceof Rectangle);
类的出现让我们可以更轻松地实现继承功能,使用熟悉的extends关键字可以指定类继承的函数。原型会自动调整,通过调用super()方法即可访问基类的构造函数。这段代码是之前示例的ECMAScript6等价版本:
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// 等价于Rectangle.call(this,length,length)
super(length, length);
}
}
var square = new Square(3);
console.log(square.getArea());
console.log(square instanceof Square);
console.log(square instanceof Rectangle);
Square类通过extends关键字继承Rectangle类,在Square构造函数中通过super()调用Rectangle构造函数并传入相应参数。请注意,与ECMAScript 5版本代码不同的是,标识符Rectangle只用于类声明(extends之后)。
**
继承自其他类的类被称作**派生类,如果在派生类中指定了构造函数则必须要调用super(),如果不这样做程序就会报错。如果选择不使用构造函数,则当创建新的类实例时会自动调用super()并传入所有参数**。举个例子,以下两个类完全相同:
class Square extends Rectangle {
}
//等价于
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
使用super()的的注意事项
· 只可在派生类的构造函数中使用super(),如果尝试在非派生类(不是用extends声明的类)或函数中使用则会导致程序抛出错误。
· 在构造函数中访问this之前一定要调用super(),它负责初始化this,如果在调用super()之前尝试访问this会导致程序出错。
· 如果不想调用super(),则唯一的方法是让类的构造函数返回一个对象
**
类方法遮蔽
派生类中的方法总会覆盖基类中的同名方法。
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// 覆盖并遮蔽Rectangle.prototype.getArea()方法
getArea() {
return this.length * this.length;
}
}
如果想调用基类中的同名方法,则可以调用super.getArea()方法,就像这样:
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
// 覆盖遮蔽后调用Rectangle.prototype.getArea()
getArea() {
return super.getArea();
}
}
静态成员继承
如果基类有静态成员,那么这些静态成员在派生类中也可用
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) {
// 等价于Rectangle.call(this,length,length)
super(length, length);
}
}
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); //true
console.log(rect.getArea()); //12
console.log(rect instanceof Square); //false
派生自表达式的类
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()); //9
console.log(x instanceof Rectangle); //true
动态地确定类的继承目标
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function () {
return this.length * this.width;
};
function getBase() {
return Rectangle;
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var x = new Square(3);
console.log(x.getArea());
console.log(x instanceof Rectangle);
getBase()函数是类声明的一部分,直接调用后返回Rectangle
创建不同的继承方法**
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;
}
}
内建对象的继承
基于类生成特殊数组
class MyArray extends Array {
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors[0]);
console.log(colors.length);
colors.length = 0;
console.log(colors[0]);
console.log(colors.length);
Symbol.species属性
class MyArray extends Array {
}
let items = new Array(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); //true
console.log(subitems instanceof MyArray); //true
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {
}
class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1("foo"),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2("bar"),
clone2 = instance2.clone();
console.log(clone1 instanceof MyClass); //true
console.log(clone1 instanceof MyDerivedClass1); //true
console.log(clone2 instanceof MyClass); //true
console.log(clone2 instanceof MyDerivedClass2); //false
这段代码重写了MyDerivedClass2继承自MyClass的Symbol.species属性,所有返回数组的继承方法现在将使用MyClass的实例而不使用MyDerivedClass2的实例。
在类的构造函数中使用new.target
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
// new.target的值是Rectangle
var obj = new Rectangle(3, 4); //输出true
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
// new.target的值是Rectangle
// var obj = new Rectangle(3, 4); //输出true
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
// new.target的值是Square
var obj = new Square(3); //false
Square调用Rectangle的构造函数,所以当调用发生时new.target等于Square。这一点非常重要,因为每个构造函数都可以根据自身被调用的方式改变自己的行为。例如,可以用new.target创建一个抽象基类(不能被直接实例化的类),就像这样:
// 抽象基类
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("这个类不能被直接实例化");
}
}
}
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); //true
因为类必须通过new关键字才能调用,所以在类的构造函数中,new.target属性永远不会是undefined。
**
小结
ECMAScript 6的类语法让JavaScript中的继承更易于使用,所以你无须摒弃先前对于其他语言中继承语法的理解。ECMAScript 6的类语法首先是作为ECMAScript 5传统继承模型的语法糖出现,但是添加了几个能够降低风险的特性。ECMAScript 6的类语法通过在类的原型上定义非静态方法来与原型继承协同工作,而静态方法最终放在构造函数上。所有方法都不可枚举,从而可以更好地匹配内建对象的行为,因为那些方法通常是不可枚举的。此外,类构造函数必须通过new关键字调用,以确保不会意外将类作为函数去调用。基于类的继承支持从其他类、函数或表达式派生类,可以通过函数调用来确定最终要继承哪一个类,可以通过mixin对象和其他不同的组合模式来创建新的类,也可以继承诸如Array的内建对象并且像预期的那样运行。在类的构造函数中,可以通过new.target来随着类被调用的多种方式而做出不同的对应。最常见的用法是创建一个抽象基类,如果直接实例化这个类会抛出错误,但是可以通过其他的类去实例化它。总之,类是JavaScript新特性的一个重要组成部分,这一特性提供了一种更简洁的语法和更好的功能,可以让你通过一个安全、一致的方式来自定义对象类型。
