ES6引入的类语法让JavaScript的面向对象写法更清晰,但本质上它只是传统基于原型继承的语法糖。很多场景下我们需要把ES6类转换成更早版本支持的普通函数构造器,比如需要兼容不支持ES6的旧运行环境,或者需要深入理解原型继承的底层逻辑。转换过程并不是简单的语法替换,需要严格对应类的各个组成部分,才能保证转换后的构造器拥有和原类完全一致的行为,尤其是原型链继承的部分不能出现偏差。

ES6类与函数构造器的核心对应关系
要完成正确的转换,首先得明确ES6类各个部分在传统构造器模式中对应的实现位置。ES6类的constructor方法本质上就是函数构造器的函数体本身,当使用new关键字调用类的时候,执行的其实就是这个constructor里的逻辑。比如一个定义实例属性的constructor,转换后就是构造器函数内部直接给this挂载属性的逻辑,这部分是一一对应的,没有额外的语法差异。
ES6类中定义在类体里的方法,默认都是挂载到类的prototype属性上的,对应传统构造器就是给构造器的prototype对象添加方法。需要注意的是,ES6类里的方法默认是不可枚举的,而传统给prototype直接赋值的方法默认是可枚举的,如果需要完全对齐行为,还需要用Object.defineProperty来定义方法,设置enumerable为false。另外ES6类里的静态方法和静态属性,是挂载到类本身上的,对应传统构造器就是直接给构造器函数对象添加属性,和原型对象没有关系。
我们可以通过一个简单的ES6类来看对应关系,比如下面这个定义:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, I am ${this.name}, ${this.age} years old`);
}
static createDefault() {
return new Person("Default", 0);
}
}
Person.type = "Human";
这个类中,constructor里的name和age是实例属性,sayHello是原型方法,createDefault和type是静态属性。转换的时候就要分别对应到构造器函数体、构造器prototype、构造器函数对象本身这三个位置,不能混淆。
基础ES6类转换为函数构造器的完整步骤
第一步先定义构造器函数,把原constructor的逻辑直接放到函数体中。如果原类有继承,还需要在构造器里调用父类的构造器,对应ES6类里的super()调用,传统写法里就是用父类构造器.call(this, 参数)的方式来实现,确保父类的实例属性被正确初始化到当前实例上。比如上面的Person类,构造器函数就可以写成:
function Person(name, age) {
this.name = name;
this.age = age;
}
第二步给构造器的prototype添加原型方法,对应ES6类里定义的非静态方法。因为ES6类的方法默认不可枚举,所以这里用Object.defineProperty来定义更严谨,如果需要兼容极老的环境不支持这个API,也可以直接赋值,只是枚举性会有差异。上面的sayHello方法转换后就是:
Object.defineProperty(Person.prototype, "sayHello", {
value: function() {
console.log("Hello, I am " + this.name + ", " + this.age + " years old");
},
enumerable: false,
writable: true,
configurable: true
});
第三步添加静态属性和方法,直接挂载到构造器函数上即可,和ES6类的静态成员对应。上面的静态createDefault方法和type属性转换后就是:
Person.createDefault = function() {
return new Person("Default", 0);
};
Person.type = "Human";
到这里基础的无继承ES6类就转换完成了,我们可以测试一下转换后的构造器行为是否和原类一致:
var p = new Person("Tom", 18);
p.sayHello(); // 输出 Hello, I am Tom, 18 years old
console.log(Person.createDefault()); // 输出 Person { name: 'Default', age: 0 }
console.log(Person.type); // 输出 Human
console.log(p instanceof Person); // 输出 true
</script>
可以看到实例的行为、静态成员的访问、instanceof的判断都和原ES6类完全一致,说明基础转换是成功的。
带继承的ES6类转换时保持原型链的关键要点
当ES6类存在继承关系时,转换的复杂度会提升,核心是要正确设置原型链,保证instanceof判断正确,子类的原型方法能正确访问到父类的原型方法。ES6的extends关键字本质上做了两件事:一是把子类的原型对象的原型指向父类的原型对象,二是把子类的__proto__指向父类本身,对应传统写法里需要手动设置这两个指向。
假设我们有一个继承Person的Student类,ES6写法如下:
class Student extends Person {
constructor(name, age, grade) {
super(name, age);
this.grade = grade;
}
study() {
console.log(`${this.name} is studying in grade ${this.grade}`);
}
}
转换这个子类的时候,首先要定义子类的构造器函数,在构造器里用Person.call(this, name, age)来调用父类构造器,对应原类里的super()调用,这一步不能省略,否则父类的实例属性不会被初始化到子类实例上。然后需要设置子类的原型链,传统写法里不能直接把Student.prototype赋值成Person.prototype,否则修改子类原型会影响父类,正确的做法是创建一个以Person.prototype为原型的对象,赋值给Student.prototype,可以用Object.create(Person.prototype)来实现。
另外还需要把子类的prototype.constructor指回子类本身,因为用Object.create创建的对象,其constructor会继承自Person.prototype,指向Person,这会导致instanceof判断或者constructor访问出现错误。最后还要设置子类的__proto__指向父类,对应ES6类里子类本身的__proto__指向父类的行为,这一步是为了保证子类的静态成员继承正确。完整的转换代码如下:
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
// 设置子类原型链,原型对象的原型指向父类原型
Student.prototype = Object.create(Person.prototype);
// 修正constructor指向
Student.prototype.constructor = Student;
// 设置子类的__proto__指向父类,对应extends的静态继承
Object.setPrototypeOf(Student, Person);
// 添加子类的原型方法
Object.defineProperty(Student.prototype, "study", {
value: function() {
console.log(this.name + " is studying in grade " + this.grade);
},
enumerable: false,
writable: true,
configurable: true
});
转换完成后我们测试继承相关的行为:
var s = new Student("Jerry", 16, 10);
s.sayHello(); // 输出 Hello, I am Jerry, 16 years old,调用父类原型方法
s.study(); // 输出 Jerry is studying in grade 10,调用子类原型方法
console.log(s instanceof Student); // 输出 true
console.log(s instanceof Person); // 输出 true,原型链正确
console.log(Student.createDefault()); // 输出 Person { name: 'Default', age: 0 },静态方法继承正确
可以看到子类的实例既能访问父类的原型方法,也能访问自己的原型方法,instanceof对父类和子类的判断都正确,静态方法也正确继承了父类的静态方法,说明带继承的转换也完全保留了原有的原型链逻辑。如果省略了设置原型链或者constructor指向的步骤,就会出现s.sayHello不存在,或者s instanceof Person返回false的问题,这就是转换时最容易出错的地方。