1. 先想清楚:用ES5实现ES6里的“类”到底在实现什么
很多前端同学第一次看到“用ES5实现ES6里的类”这个需求,第一反应是:“这不就是Babel的活吗?直接上Babel不就完了?”话是这么说,但真到了实际操作中——老项目没法上构建、面试要手写、或者你正在追查一个奇奇怪怪的线上Bug,你会发现完全不了解ES6类背后的细节,根本无从下手。而且写ES5实现并不是为了替代Babel,而是为了彻底搞懂class语法糖背后的机制。
ES6的class本质上不是一个新东西,它是在函数构造器+原型链这套老机制上做的“语法糖”。比如:
javascript复制class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
static info() {
return 'animal class';
}
}
这段代码做的事情,往下拆解就是三件事:一个构造函数Animal,一个挂在Animal.prototype上的speak方法,以及一个挂在Animal自身上的静态方法info。此外还隐藏了几个细节:类声明不允许被提升(暂时性死区)、类方法不可枚举、类体内的代码强制严格模式、只能通过new调用不能当普通函数调用。
如果你平时只写ES6,可能从来没注意过这些细节点,因为浏览器帮你处理好了。但一旦需要“手工翻译”成ES5,这些地方全是坑。另一个容易被忽略的点:ES6里有一个new.target,可以在函数体内判断是被new调用还是被普通调用,这在模拟类的时候也很有用。
所以这篇文章就是围绕这条主线:把ES6类逐项拆开,然后用ES5的能力把它重新拼装回去。适合这样几类人看:一是老项目还在用ES5语法、想引入一点“类”的组织方式的;二是准备面试,被问到“ES6 class原理”时需要当场手写的;三是看Babel编译产物一头雾水,想搞懂那些helper函数到底是干嘛的。
提示:以下所有ES5代码都是基于严格模式写作的,建议在你的源码顶部也加上'use strict'。因为ES6类内部本身是严格模式,模拟行为时保持严格模式会避免很多诡异问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实现前必备的基础:构造函数、原型链和new的机制
2.1 构造函数和new的完整行为
在ES5里,最接近类的概念就是“构造函数”。一个普通函数,只要搭配new关键字使用,就能创建出一个新对象。很多人写过这样的代码:
javascript复制function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log('Hi, ' + this.name);
};
但问起new到底做了什么,很多人只记得“创建了一个新对象、this指向它、返回它”。其实完整步骤如下:
- 创建一个全新的对象;
- 把这个对象的原型(
__proto__)指向构造函数的prototype属性; - 以这个新对象为
this执行构造函数体内的代码; - 如果构造函数显式返回一个对象,则返回该对象;否则返回步骤1创建的新对象。
这里的第2步,严格来说在ES5里调用Object.create(Person.prototype)就能完成。但如果环境太老连Object.create都没有,就要手动搞一个临时构造函数来打通原型链。
这跟ES6里的类有什么关系?ES6的构造函数本质就是这类函数,只是多了“必须用new调用”的限制。用ES5模拟时,就得在函数体入口手动拦截这种非法调用。
2.2 原型继承的经典演进
模拟类继承的时候,最常被提起的是“组合寄生式继承”。为什么不是“原型链继承”或者简单的“构造函数继承”?因为单独用某一种都有致命缺陷。
原型链继承,问题是子类实例会共享父类实例上的引用属性。如果父类构造函数里有一个数组属性,两个子类实例会操作同一个数组,这基本就是灾难。
构造函数继承,问题是虽然避免了实例属性共享,但父类原型上的方法子类拿不到,还得额外手动把Child.prototype指向Parent.prototype。
组合寄生式继承的做法是:
javascript复制function Parent(name) {
this.name = name;
this.list = [];
}
Parent.prototype.say = function() { console.log(this.name); };
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() { console.log(this.age); };
关键的Object.create(Parent.prototype)就是寄生的一环。它的目的在于只继承原型方法,而不是把父类的prototype直接赋值给子类,否则修改子类原型会污染父类原型。`const
