和函数定义相似,类的定义有类的声明和类表达式,但需要使用到 class 关键字, 类的本质也是通过原型链等继承,将面向对象带入了 js 编程语言
class 受块作用域限制
// 函数受函数作用域限制,而类受块作用域限制
{
function Fn() {}
class ClassDeclaration {}
}
log(Fn); // log: [Function: Fn] (node 环境)
log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
类构造函数
js 中类的构造函数统一 为 constructor, constructor 会告诉解释器在使用 new 操作符创建类的新实例时调用这个函数,即 new 操作符 会调用构造函数实例化一个对象
new 操作符调用类构造函数执行如下事情
- 在内存中创建一个新对象
- 这个新对象内部的 [[Prototype]] 指针指向 构造函数的 prototype 属性
- 将这个新对象作为构造函数的 this 指向(通过 call、apply 等方法)
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象则返回该对象,否则返回新创建的对象
构造函数的返回&传参
默认情况下,类构造函数会在执行之后返回 this 对象,构造函数返回的对象会被用作实例化对象,如果返回的不是 this 对象,而是其他对象,那么这个对象不会通过 instanceof 操作符检测出跟类有相关性,因为这个对象的原型指针并没有被修改
class Person {
constructor(name, isReturnNewObj = false) {
this.name;
// 如果 isReturnNewObj 为 true 则返回一个新对象,否则返回默认 this
if (isReturnNewObj) {
return {
name,
age: 23,
};
}
}
}
const p1 = new Person("jinmu", true);
log(p1 instanceof Person); // log: false, 因为 Person 构造函数返回的对象的原型指针没有被修改
const p2 = new Person("kj");
log(p2 instanceof Person); // log: true
类的特性
类的类型
调用类构造函数必须使用 new 操作符(没有使用 new 则会报错),而普通函数构造函数如果不使用 new 调用,那么就会以全局 this(浏览器时 window)作为内部对象。
ECMAScript 没有 “class” 这个类型,类只是一个特殊的函数,因此类也可以当成参数传递给函数
class Person {
constructor(name) {
this.name = name;
}
}
const p = new Person("k1");
log(p.name); // log: k1
// 使用实例上的 constructor 创建一个新实例
const p2 = new p.constructor("k2");
log(p2.name); // log: k2
log(typeof Person); // log: function ; 标明 类是一个函数
实例成员&类原型成员&类的方法
每次通过 new 调用类时,执行类的构造函数,在这个函数内部会为新实例(this)添加“自有”属性。至于添加什么样的属性,则没有限制,在构造函数执行完毕后,仍然可以给实例继续添加新成员。
每个实例都对应一个唯一的成员对象,所有的成员都不会在原型上共享;类块中箭头函数是定义在实例上的,属于实例成员;函数简写是定义在类原型上的(所有实例共享);函数和箭头函数可以直接定义在类块中;
静态方法通常用于执行不特定于实例的操作,也不要求存在于类的实例,与原型成员类似,静态成员每个类上只有一个,使用关键字 static定义静态方法
class Person {
constructor() {
this.name = new String("default_name");
this.sayName = () => log(this.name);
this.nicknames = ["k1", "k2"];
}
// 类块中简写函数声明是定义在类的原型上的
protoFn() {
log("prototype_function");
}
// 箭头函数是定义在实例上的
instanceFn = () => log("instance_fn");
// 静态方法只能通过类名直接调用
static staticFn() {
log("专属于类的方法,通过类名调用");
}
}
Person.prototype.protoFn(); // log: prototype_function
// Person.prototype.instanceFn(); // TypeError: Person.prototype.instanceFn is not a function
const p1 = new Person();
const p2 = new Person();
Person.staticFn(); // log: 专属于类的方法,通过类名调用
// Person.prototype.staticFn() // TypeError: Person.prototype.staticFn is not a function
// p1.staticFn(); // TypeError: p1.staticFn is not a function
p2.instanceFn(); // log: prototype_function
// 可以给创建出来的实例添加新属性
p1.age = 15;
p2.age = 23;
p1.sayName(); // log: [String: 'default_name']
p2.sayName(); // log: [String: 'default_name']
log(p1.name === p2.name); // log: false
log(p1.sayName === p2.sayName); // log: false
log(p1.nicknames === p2.nicknames); // log: false
log(p1.protoFn === p2.protoFn); // log: true, 因为 protoFn 是挂在到类原型上的
类中的 getter 和 setter
类定义也支持获取和设置访问器,语法与行为跟普通对象一样; 获取的时候使用的是 setter 或者 getter 的函数名!
class Person {
set name(name) {
log("setter_name");
this._name = name;
}
get name() {
log("getter_name");
return this._name;
}
}
const p = new Person();
p.name = "jinmu"; // log: setter_name
log(p.name); // log: getter_name 然后在 log: jinmu
非函数原型和类成员
类定义并不支持在原型或类上添加成员函数,但在类外部可以手动添加类成员;值得注意的是,类中如果显式添加数据成员这是一种反模式,因为共享目标(原型和类)上添加可变数据是不合理的
class Person {
sayName() {
log(`${Person.say} ${this.name}`);
}
}
// 在类上定义数据成员
Person.say = "Helo";
// 在原型上定义数据成员
Person.prototype.name = "1516";
const p = new Person();
p.sayName(); // log: Helo 1516
迭代器与生成器方法
class Person {
constructor() {
this.friends = ["Yohan", "Wender", "Zhuli"];
}
// 实例生成器(定义在类的原型上的),所有实例共享而各子实例独立
*protoGen() {
yield* Object.entries(this.friends);
}
// 静态生成器,类直接调用的
static *saticGen() {
yield* ["k1", "k2", "k3"];
}
// 默认迭代器,可以将实例变成可迭代对象
*[Symbol.iterator]() {
yield { name: "default1" };
yield { name: "default2" };
}
}
const p1 = new Person();
// 因为 Person 类实现了默认迭代器,因此可以直接遍历实例获取默认迭代器的内容
for (const obj of p1) {
log(JSON.stringify(obj)); // 依此换行打印: {"name":"default1"}、{"name":"default2"}
}
// 调用实例的生成器获得生成器对象
const p1GenObj = p1.protoGen();
for (const [k, v] of p1GenObj) {
log(`${k}-${v}`); // 一次换行打印: 0-Yohan、1-Wender、2-Zhuli
}
const staticGenObj = Person.saticGen();
log(staticGenObj.next()); // log: { value: 'k1', done: false }
log(staticGenObj.next()); // log: { value: 'k2', done: false }
log(staticGenObj.next()); // log: { value: 'k3', done: false }
log(staticGenObj.next()); // log: { value: undefined, done: false }
类原生支持继承
虽然类继承使用的是新语法,但背后依旧使用的是原型链,因此可以继承一个类,也可以继承普通的构造函数(保持向后兼容);
注意: extends 关键字也可以在类表达式中使用,因此 let Sub = class extends Base {...} 是有效的
继承基础
ES6 类支持单继承,使用 extends 关键字,就可以继续任何拥有 [[Construtor]] 和原型的对象。派生类会通过原型链访问到类和原型上定义的方法。派生类仅可以在类构造函数、静态方法内部访问 super,super 关键字 引用他们的原型,在派生类的 constructor 中,super 等效于父类构造函数
class Base {
constructor(name) {
this.name = name;
}
baseProtoFn() {
log("base_proto_fn, instance_can_call");
}
static baseStaticFn() {
return "static_base_fn";
}
}
// 单继承、关键字 extends
class Sub extends Base {
constructor(name, age) {
super(name); // super 是 父类的构造函数
this.age = age;
// 只能在子类构造函数和子类静态方法中功能型使用,不能单独使用,不然会报错
// log(super) // SyntaxError: 'super' keyword unexpected here
}
subProtoFn() {
log("sub_proto_fn, sub_instance_can_call");
}
static subStaticFn() {
// super 在派生类的的静态方法中就是 基类
return super.baseStaticFn() + "&sub_static_fn";
}
}
const sub = new Sub("sub_name", 23);
// 因为是继承,所以可以调用继承的方法,派生类 Sub 会通过原型链访问到类和原型上定义的方法
sub.baseProtoFn(); // log: base_proto_fn, instance_can_call
sub.subProtoFn(); // log: sub_proto_fn, sub_instance_can_call
log(Base.baseStaticFn()); // log: static_base_fn
log(Sub.baseStaticFn()); // log: static_base_fn
log(Sub.subStaticFn()); // log: static_base_fn&sub_static_fn
super 关键字的特点
- 只能在派生类中使用,且只能在派生类中的 constructor 和静态方法使用,而且不能单独使用 super 关键字,要么用它构造函数,要么用它引用静态方法;比如你 console.log(super) 会报错
- 派生类中使用 super 前,不能引用 this,否则会抛出 ReferenceError
- super() 会调用父类构造函数,并将返回的实例赋值给 this, 需要给父类构造函数传参,通过向 super(arguemtns) 传递即可
- 如果派生类没有定义构造函数,在实例化派生类时会调用 super(), 而且会像器传入 所有传给派生类的参数
- 如果在派生类中显示定义了构造函数,则必须在其内部调用 super() ,要么在其内部返回一个对象。
继承内置类型
可以借助继承来扩展内置类型,为内置类型扩展功能
// ExtendArray 继承于 Array, 因此可以调用任何 Array 原型上的方法
class ExtendArray extends Array {
// 洗牌算法
shuffle() {
for (let i = 0; i < this.length; i++) {
const randomeIdx = Math.floor(Math.random() * (i + 1));
[this[i], this[randomeIdx]] = [this[randomeIdx], this[i]];
}
}
}
const ea = new ExtendArray(1, 2, 3, 4, 5);
log(ea instanceof Array); // true
log(ea instanceof ExtendArray); // true
log(ea); // log: ExtendArray(5) [ 1, 2, 3, 4, 5 ]
ea.shuffle();
log(ea); // log: ExtendArray(5) [ 5, 1, 3, 4, 2 ]
log(ea.filter((item) => item > 2)); // log: ExtendArray(3) [ 5, 4, 3 ]
抽象基类
如果需要定义一个可供其他类继承,但本身不会被实例化的类(抽象类),虽然 ECMAScript 没有专门支持这种类的语法,但通过 new.target 来阻止对抽象类的实例化;new.target 保存通过 new 关键字调用的类或函数
如果要求派生类必须定义某个方法,可以在抽象基类构造函数中进行检查,因为原型方法在调用构造函数之前已经存在了,所以可以通过 this 关键字来检查相应方法。
// 抽象基类
class AbstractBase {
constructor(value) {
log(new.target);
if (new.target === AbstractBase) {
throw new Error("Abstract class cannot be directly instantiated");
}
// 必须要求派生必须实现 say 方法,不然直接报错、阻止程序进行(阻止实例化)
if (!this.say) {
throw new Error("Inheriting [AbstractBase] must define say function");
}
// 到这里说明没有报错阻断程序,可以进行实例化了
this.value = value;
}
}
// 派生类
class Sub extends AbstractBase {
constructor(value, other) {
super(value);
this.other = other;
}
// 注释 say 方法,实例化 Sub 将会报错
say() {
log("sub_proto_say_fn");
}
}
const sub = new Sub("val15", "other_val"); // log: [class Sub extends AbstractBase] (来源于基类中构造函数的 打印)
log(sub.value); // val15
// 尝试直接去实例化抽象基类,将会报错
const ab = new AbstractBase(); // 先 log: [class AbstractBase], 然后抛出错误终止程序(Error: Abstract class cannot be directly instantiated)
类的混入
Object.assign() 方法是为了混入对象行为而设计的,如果需要混入多个对象的属性,那么使用 Object.assign 即可(或者扩展运算符);只有需要混入类的行为时才有必要自己实现混入表达式
虽然 ES6 没有显式支持多类继承,但是可以把现有特性模拟出多类继承效果;比如 Son 类需要继承于 A、B、C 三个类,那么可以让 B 继承 A,C 在 继承 B,最终 Son 在继承 C,这样 Son 就算是继承了 A、B、C;可以借助类表达式来实现( 不推荐该设计模式),因为 【组合胜过继承 composition over inheritance】
class A {
aMethod() {
log("a_method");
return this;
}
}
const BMixin = (SuperClass) =>
class extends SuperClass {
bMethod() {
log("b_method");
// 实现链式调用
return this;
}
};
const CMixin = (SuperClass) =>
class extends SuperClass {
cMethod() {
log("c_method");
return this;
}
};
// BMixin(A) 就是 继承 A 后的 B, CMinx(xx) 就是 C 继承 xx
class Son extends CMixin(BMixin(A)) {
sonMethod() {
log("son_method");
}
}
const son = new Son();
son.aMethod().bMethod().cMethod(); // 依此换行输出 a_method, b_method, c_method
son.sonMethod(); // log: son_method
// 为了防止 Mixin 数量太多导致需要不停手动改动(违背开闭原则),可以实现多个混合
const mixAll = (BaseClass, ...Mixins) => {
// 返回的是混入后的类
return Mixins.reduce((Cls, Mixin) => Mixin(Cls), BaseClass);
};
class Son2 extends mixAll(A, BMixin, CMixin) {
son2Method() {
log("son2_method");
}
}
const son2 = new Son2();
son2.aMethod().bMethod().cMethod(); // 依此换行输出 a_method, b_method, c_method
son2.son2Method(); // log: son2_method