前言背景
在 JavaScript 的发展历程中,类的实现方式经历了从构造函数到 ES6 class 的演变。
很多开发者认为 class 只是构造函数的语法糖,但实际上两者在细节上存在诸多差异。
本文将从语法形式、内部机制、使用限制等多个维度,深入剖析 class 与普通构造器的区别。
从一道面试题说起
先看一道经典面试题:如何将以下 ES6 class 代码转换为等效的 ES5 实现?
javascript
class Example {
constructor(name) {
this.name = name;
}
init() {
const fun = () => { console.log(this.name) }
fun();
}
}
const e = new Example('Hello');
e.init(); // 输出:Hello
要解答这道题,我们需要先理解 class 与构造函数的本质区别。让我们从两者的基本写法开始对比。
基本写法对比
ES6 class 写法
ES6 引入了 class 关键字,使类的定义更加简洁清晰:
javascript
class Computer {
// 构造器
constructor(name, price) {
this.name = name;
this.price = price;
}
// 原型方法
showSth() {
console.log(`这是一台${this.name}电脑`);
}
// 静态方法
static comStruct() {
console.log("电脑由显示器,主机,键鼠组成");
}
}
使用方式:
javascript
const apple = new Computer("苹果", 15000);
console.log(apple.name); // 苹果
apple.showSth(); // 这是一台苹果电脑
Computer.comStruct(); // 电脑由显示器,主机,键鼠组成
ES5 构造函数写法
在 ES6 之前,我们通过构造函数模拟类的实现:
javascript
function Computer(name, price){
this.name = name;
this.price = price;
}
// 原型方法
Computer.prototype.showSth = function(){
console.log(`这是一台${this.name}电脑`);
}
// 静态方法
Computer.comStruct = function(){
console.log("电脑由显示器,主机,键鼠组成");
}
使用方式与 class 完全一致:
javascript
const apple = new Computer("苹果", 15000);
console.log(apple.name); // 苹果
apple.showSth(); // 这是一台苹果电脑
Computer.comStruct(); // 电脑由显示器,主机,键鼠组成
从表面看,两种写法实现了相同的功能,但深入细节会发现它们存在本质区别。
核心区别解析
1. 调用方式限制
普通构造函数本质是函数,既可以用new
调用,也可以直接调用:
javascript
// 普通构造函数
function Computer2() {}
// 直接调用(不会报错)
const i = Computer2();
console.log(i); // undefined
而 class 必须使用new
调用,直接调用会报错:
javascript
// ES6 class
class Computer1 {}
// 直接调用(报错)
Computer1();
// TypeError: Class constructor Computer1 cannot be invoked without 'new'
这是因为 class 内部做了调用方式检查,确保只能通过实例化方式使用。
2. 原型方法的可枚举性
普通构造函数的原型方法默认是可枚举的:
javascript
const apple = new Computer2("苹果", 15000);
for(var i in apple){
console.log(i);
}
// 输出:
// name
// price
// showSth
class 的原型方法默认是不可枚举的:
javascript
const huawei = new Computer1("华为", 12000);
for(var i in huawei){
console.log(i);
}
// 输出:
// name
// price
这种差异会影响对象遍历的结果,在某些场景下(如对象拷贝)需要特别注意。
3. 严格模式
class 内部默认运行在严格模式下,而普通构造函数默认在非严格模式下运行:
javascript
// class中定义重复参数(报错)
class Computer1 {
showSth(i,i) { // SyntaxError: Duplicate parameter name not allowed in this context
console.log(`这是一台${this.name}电脑`);
}
}
// 普通构造函数中定义重复参数(不报错)
Computer2.prototype.showSth = function(j,j){
console.log(`这是一台${this.name}电脑`);
}
严格模式带来了更多的语法限制,如禁止重复参数、禁止未声明变量赋值等,有助于编写更规范的代码。
4. 原型方法的构造器限制
普通构造函数的原型方法可以作为构造函数使用(通过 new 调用):
javascript
const apple = new Computer2("苹果", 15000);
const i = new apple.showSth(); // 可以执行
console.log(i); // {}
class 的原型方法不能作为构造函数使用:
javascript
const huawei = new Computer1("华为", 12000);
const i = new huawei.showSth();
// TypeError: huawei.showSth is not a constructor
Babel 转译揭示的本质
通过 Babel 将 class 转译为 ES5 代码,可以更清晰地看到 class 的内部实现机制:
javascript
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps)
_defineProperties(Constructor.prototype, protoProps);
if (staticProps)
_defineProperties(Constructor, staticProps);
return Constructor;
}
var Computer = /*#__PURE__*/function () {
function Computer(name, price) {
_classCallCheck(this, Computer);
this.name = name;
this.price = price;
}
_createClass(Computer, [{
key: "showSth",
value: function showSth() {
console.log(`这是一台${this.name}电脑`);
}
}], [{
key: "comStruct",
value: function comStruct() {
console.log("电脑由显示器,主机,键鼠组成");
}
}]);
return Computer;
}();
转译后的代码揭示了几个关键函数的作用:
_classCallCheck
:检查调用方式,确保只能通过 new 实例化_defineProperties
:定义属性时设置描述符(控制可枚举性等)_createClass
:将原型方法和静态方法挂载到对应位置
这些函数共同实现了 class 的特殊行为,使其与普通构造函数区分开来。
面试题解答
回到开头的面试题,将 ES6 class 转换为 ES5 的关键在于:
- 实现 class 的调用检查
- 正确处理原型方法的可枚举性
- 处理箭头函数的 this 绑定(箭头函数会捕获当前上下文的 this)
完整的 ES5 实现:
javascript
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps)
_defineProperties(Constructor.prototype, protoProps);
if (staticProps)
_defineProperties(Constructor, staticProps);
return Constructor;
}
var Example = /*#__PURE__*/function () {
function Example(name) {
_classCallCheck(this, Example);
this.name = name;
}
_createClass(Example, [{
key: "init",
value: function init() {
var _this = this; // 保存this引用,模拟箭头函数的this绑定
var fun = function fun() {
console.log(_this.name);
};
fun();
}
}]);
return Example;
}();
var e = new Example('Hello');
e.init(); // 输出:Hello
总结
class 虽然在表面上看起来是构造函数的语法糖,但实际上:
- class 有更严格的调用限制(必须使用 new)
- class 的原型方法默认不可枚举
- class 内部自动启用严格模式
- class 的原型方法不能作为构造函数使用
- class 提供了更清晰的语法结构和继承机制
理解这些差异有助于我们在实际开发中选择合适的方式,并避免因误解而产生的 bug。在现代 JavaScript 开发中,推荐使用 class 语法,它不仅使代码更具可读性,也能通过严格模式和语法限制帮助我们编写更健壮的代码。
