class
“ES6 的
class本质上是基于原型继承的语法糖。它不仅让对象原型的写法更加清晰、更像面向对象编程,还引入了extends关键字,其底层实现就是经典的寄生组合式继承。”
ES6 class
在定义一个类时,除了常规的实例属性和方法,ES6+ 还提供了以下强大的内置特性:
class Animal {
// 1. 实例属性的新写法(ES2022):直接在类顶层定义,等同于写在 constructor 的 this 上
type = "未知生物";
// 2. 私有属性和方法(ES2022):使用 # 开头,外部和子类都无法访问
#weight = 100;
#getWeight() {
return this.#weight;
}
constructor(name) {
this.name = name;
}
// 3. 静态属性和方法:使用 static 关键字,直接挂载在类本身(Animal),而不是原型上
static parentName = "Animal";
static getParentName() {
// 静态方法中的 this 指向类本身(Animal),而不是实例
console.log(this.parentName);
return this.parentName;
}
// 4. 静态初始化块(ES2022):在类初始化时仅执行一次,常用于复杂的静态属性赋值
static {
console.log("Animal 类被加载了!");
this.parentName = "Animal_Init";
}
// 5. 访问器属性(Getter/Setter):拦截属性的读取和设置,挂载在原型上
get weightInfo() {
return `体重是:${this.#getWeight()}kg`;
}
set weightInfo(val) {
if (val < 0) throw new Error("体重不能为负数");
this.#weight = val;
}
// 6. 普通实例方法:挂载在类的原型对象(Animal.prototype)上
getName() {
console.log(this.name);
return this.name;
}
}
Class 的继承
class Cat extends Animal {
constructor(name, age) {
// 必须在使用 this 之前调用 super()
// 相当于 Animal.prototype.constructor.call(this, name)
// 作用是:调用父类构造函数,塑造子类自己的 this 对象
super(name);
this.age = age;
}
getItsName() {
// 在普通方法中,super 指向父类的原型对象(Animal.prototype)
// 在静态方法中,super 指向父类本身(Animal)
super.getName();
}
}
Babel 是如何编译 Class 的?
function createClass(Constructor, protoProps, staticProps) {
if (protoProps) {
Object.defineProperties(Constructor.prototype, protoProps);
}
if (staticProps) {
Object.defineProperties(Constructor, staticProps);
}
return Constructor;
}
function Animal(name) {
// 实例属性赋值
this.name = name;
}
// 在构造函数的原型对象上挂载实例方法
createClass(
Animal,
{
getName: {
value: function getName() {
console.log("调用实例方法,获取实例属性", this.name);
return this.name;
},
},
},
{
getParentName: {
// 在构造函数上挂载静态属性和方法
value: function getParentName() {
console.log("调用静态方法,获取静态属性", this.parentName);
return this.parentName;
},
},
parentName: {
value: "Animal",
},
},
);
let animal1 = new Animal("animal1");
// 调用实例方法
animal1.getName();
// 调用静态方法
Animal.getParentName();
在创建对象实例时 let animal1 = new Animal('animal1'),主要会经历几个步骤
// 1.新建空对象
let newObj = new Object();
// 2.指向构造函数的原型对象,共享实例方法
newObj.__proto__ = Animal.prototype;
// 3.以新对象为上下文执行构造函数
let result = Animal.call(newObj, "animal1");
// 4.返回一个新对象
// 如果构造函数返回一个对象或函数,对象实例就是该对象或函数;否则返回一个新对象
return result instanceof Object ? result : newObj;
继承
下面通过 extends 关键字实现对 Animal 的继承
class Animal {
constructor(name) {
// 实例属性赋值,这里面的this会指向实例
this.name = name;
}
static parentName = "Animal"; // 静态属性
getName() {
console.log("调用实例方法,获取实例属性", this.name);
return this.name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
}
}
let cat = new Cat("jack");
cat.getName();
console.log(Cat.parentName);
// 可以发现,静态属性和方法也被子类继承了
经典继承方式对比
在 ES6 class 出现之前,开发者们探索了多种基于原型的继承方式
1. 原型链继承
直接将子类的原型指向父类的实例。
function Parent() {
this.colors = ["red"];
}
function Child() {}
Child.prototype = new Parent();
- 优点:父类方法可以复用。
- 缺点:
- 所有子类实例共享同一个父类引用属性(如果一个子类修改了
colors数组,其他子类的colors也会跟着变)。 - 创建子类实例时,无法向父类构造函数传参。
- 所有子类实例共享同一个父类引用属性(如果一个子类修改了
2. 借用构造函数继承
在子类构造函数内部,通过 call 或 apply 调用父类构造函数。
function Parent(name) {
this.name = name;
this.colors = ["red"];
}
function Child(name) {
Parent.call(this, name);
}
- 优点:解决了引用类型被共享的问题,且可以向父类传参。
- 缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法,无法实现函数复用。而且子类也拿不到父类原型(
Parent.prototype)上的方法。
3. 组合继承
结合前两者的优点:使用原型链继承原型上的属性和方法,使用借用构造函数继承实例属性。
function Parent(name) {
this.name = name;
this.colors = ["red"];
}
Parent.prototype.getName = function () {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 第二次调用 Parent
this.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child;
- 优点:JS 中最常用的继承模式,解决了共享问题和传参问题,方法也可以复用。
- 缺点:调用了两次父类构造函数,导致子类的原型上多了不必要的父类实例属性(被子类实例属性屏蔽了),存在内存浪费。
4. 寄生组合式继承
为了解决组合继承调用两次父类的缺点,我们只需要父类原型的副本,而不需要实例化父类。
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function () {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 只调用一次
this.age = age;
}
// 使用 Object.create 创建一个原型指向 Parent.prototype 的空对象
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
- 优点:只调用了一次父类构造函数,避免了在子类原型上创建不必要的属性。原型链保持不变,能正常使用
instanceof和isPrototypeOf。这是ES6 出现之前最完美的继承范式。
实现 extends
ES6 的 extends 关键字,在底层的编译产物其实就是基于寄生式组合继承来实现的。
- new 一个实例时,调用父类构造函数,继承实例属性
- 通过原型链继承父类实例方法
- 设置子类的 proto = 父类,让子类可以调用父类静态属性和方法
// 已知 Animal 这个父类
function _inherits(child, parent) {
let proto = Object.create(parent.prototype);
proto.constructor = child;
child.prototype = proto;
}
function Cat(name) {
// 调用父类的构造函数,继承实例属性
Animal.call(this, name);
}
// 继承父类实例方法
_inherits(Cat, Animal);
// 设置子类的 __proto__ = 父类,让子类可以调用父类静态属性和方法
Object.setPrototypeOf(Cat, Animal);
let cat = new Cat("jack");
cat.getName();
console.log(Cat.parentName);