第九章、JavaScript中的类
1、类的声明
1、基本的类声明语法
不需要在类的个元素之间使用逗号隔开。类除了constructor外没有其他保留的方法名。
class PersonClass {
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
let person = new PersonClass("zhangsan");
person.sayName();
自有属性是实例中的属性,不会出现在原型上,且只能在类的构造函数或方法中创建。建议在构造函数中创建自有属性,可以方便的控制。
类属性不可被赋予新值。
类声明仅仅是基于已有自定义类型声明的语法糖。
2、为何使用类语法
类声明与自定义类型的区别:
1、函数声明可以被提升,类声明与let声明不可以;真正执行声明语句之后,它们会一直存在于临界死区。
2、类声明中的所有代码自动运行在严格模式下。
3、类中所有方法都是不可枚举的;自定义类型中,需要通过Object.defineProperty()手动指定某个方法不可枚举。
4、每个类都有一个[[constructor]]的内部方法,通过关键字new调用那些不含[[constructor]]的方法会报错。
5、使用除new关键字以外的方式调用类的构造函数会报错。
6、在类中修改类名会报错。
类的名称在类中你能修改是因为,类的名称在类中相当于常量const声明的;在类外可以修改是因为,类的名称在类外相当于变量let生命的。
2、类表达式
类和函数类似,也存在表达式形式
1、基本的类表达式语法
let PersonClass = class {
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
let person = new PersonClass("zhangsan");
person.sayName();
类表达式不需要标识符在类后,类表达式在功能上等价于类声明。
类表达式也不存在变量提升。
2、命名类表达式
声明时,在关键字class后添加一个标识符即可定义为命名类表达式。
3、作为一等公民的类
一等公民是指一个可以传入函数,可以从函数返回,可以赋值给变量的值。
JavaScript中函数和类都是一等公民。
将类作为参数传入函数中:
function createObj(classDef){
return new classDef();
}
let obj = createObj(class {
sayHi(){
console.log("Hi");
}
});
obj.sayHi(); //"Hi"
类表达式的另一种使用方式:通过立即调用类构造函数创建单例。
let person = new class {
constructor(name){
this.name = name;
}
sayHi(){
console.log(this.name);
}
}("lisi");
person.sayHi(); //"Hi"
4、访问器属性
类支持在原型上定义访问器属性
访问器属性getter和setter
class CustomHTMLElement {
constructor(element){
this.element = element;
}
get html(){
return this.element.innerHTML;
}
set html(value){
this.element.innerHTML = value;
}
}
let descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype,"html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false
5、可计算成员名称
类方法支持使用可计算名称。
let methodName = "sayName";
class PersonClass {
constructor(name){
this.name = name;
}
[methodName](){
console.log(this.name);
}
}
let me = new PersonClass("wangwu");
me.sayName(); // "wangwu"
访问器属性也支持使用可计算名称。
let propertyName = "html";
class CustomHTMLElement {
constructor(element){
this.element = element;
}
get [propertyName](){
return this.element.innerHTML;
}
set [propertyName](value){
this.element.innerHTML = value;
}
}
6、生成器方法
可以将任何方法定义成生成器,包括类中的方法。
如果一个类是用来表示值得集合的,就应该为它定义一个默认迭代器。
使用Symbol.iterator定义生成器方法即可为类定义默认迭代器。
class Collection {
constructor(){
this.items = [];
}
*[Symbol.iterator](){
yield this.items.values();
}
}
let collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let v of collection){
console.log(v);
}
7、静态成员
创建静态成员:在方法或访问器属性名前使用正式的静态关键字static
class PersonClass {
constructor(name){
this.name = name;
}
static create(name){
return new PersonClass(name);
}
}
let person = PersonClass.create("shuhe");
类中的所有方法和访问器属性都可以用static关键字定义,除了构造函数constructor
备注:不可在类实例中访问类的静态成员,只能通过类直接访问。
8、继承与派生类
使用extends关键字实现指定类的继承,原型会自动调整,通过调用super()方法即可访问基类的构造函数。
class Rectangle {
constructor(length,width){
this.length = length;
this.width = width;
}
getArea(){
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length){
super(length,length);
}
}
let square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
继承自其他类的类被称为派生类,如果在派生类中指定了构造函数则必须调用super(),否则程序会报错;
如果在派生类中不使用构造函数,则当创建派生类的实例时,系统会自动调用super()并传入参数。
备注:
①、只可在派生类的构造函数中使用super(),如果尝试在非派生类中或函数中使用会报错。
②、在派生类的构造函数中访问this之前一定要调用super(),它负责初始化this,如果在调用super()之前访问this会报错。
③、如果不想调用super(),则唯一的方法是让类的构造函数返回一个对象。
1、类方法遮蔽
派生类中的方法总会覆盖基类中的同名方法。
在派生类中的方法中可以通过super调用基类中的同名方法。
class Square extends Rectangle {
constructor(length){
super(length,length);
}
getArea(){
return super.getArea();
}
}
2、静态成员继承
如果基类中有静态成员,那么这些静态成员在派生类中也可用。
3、派生自表达式的类
只要表达式可以被解析为一个函数并且具有[[constructor]]属性和原型,那么就可以用extends进行派生。
extends强大的功能使得类可以继承自任意类型的表达式。
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 getBase() {
constructor(length){
super(length,length);
}
}
let x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
由于可以动态的确定使用哪个基类,因此可以创建不同的继承方法。
let SerializableMixin = {
serialize(){
return JSON.stringify(this); }
};
let AreaMixin = {
getArea(){
return this.length * this.length;
}
};
function mixin(...mixins){
let base = function(){};
Object.assign(base.prototype,...mixins);
return base;
}
class Square extends mixin(AreaMixin,SerializableMixin){
constructor(length){
super();
this.length = length;
this.width = length;
}
}
let x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3, "width":3}"
备注:由于使用了extends关键字,派生类构造函数必须调用super();
如果多个mixin对象具有相同属性,那么是有最后一个被添加的属性被保留。
4、内建对象的继承
在ES5的传统继承中,先又派生类型创建this的值,然后调用基类型的构造函数。即this的值开始指向的是派生类的实例。
ES6中的继承恰好相反,先由基类创建this的值,然后派生类的构造函数再来修改这个值。
基于类生成特殊数组的例子:
clas MyArray extends Array {
// ...
}
let colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
5、Symbol.species属性
内建对象的一个实用之处:原本在内建对象中返回实例自身的方法将自动返回派生类的实例。
clas MyArray extends Array {
// ...
}
let items = new MyArray(1,2,3,4);
let subItems = items.slice(1,3);
console.log(items instanceof Array); // true
console.log(subItems instanceof Array); // true
这种情况是通过Symbol.species属性实现的。
Symbol.species被用于定义返回函数的静态访问属性。
被返回的函数是一个构造函数(即this),每当要在实例的方法中创建类的实例时必须使用这个构造函数。
已经定义Symbol.species属性的内建类型有:
Array、ArrayBuffer、Map、Promise、RegExp、Set、Typed arrays
通过Symbol.species可以定义当派生类的方法返回实例时,应该返回的值的类型。
clas MyArray extends Array {
static get [Symbol.species](){
return Array;
}
}
let items = new MyArray(1,2,3,4);
let subItems = items.slice(1,3);
console.log(items instanceof Array); // true
console.log(subItems instanceof Array); // true
console.log(subItems instanceof MyArray); // false
一般来说,只要想在类方法中调用this.constructor,就应该使用Symbol.species属性,从而让派生类重写返回类型。
9、在类的构造函数中使用new.target
new.target的值是根据函数被调用的方式改变的。
在类的构造函数中也可以通过new.target来确定类是如何被调用的。
简单情况下,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的值是Square
let obj = new Square(3); // false
可以用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;
}
}
let x = new Shape(); // 抛出错误
let y = new Rectangle(3,4); // 没有抛出错误
console.log(y instanceof Shape); // true
备注:类必须通过关键字才能调用,所以在类的构造函数中,new.target值永远不会是undefined。