资讯动态

ES6 class与ES5构造函数对比:原型链、继承与严格模式深度解析

发布时间:2026/10/14 4:53:48 来源:尧图企业网站定制
说实话前端圈里聊ES6的class最常听到的一句话就是class就是语法糖本质还是构造函数。这话没错但也就对了一半。真正被问到底的时候——比如class和ES5构造函数到底差在哪、继承的实现逻辑为什么不同、为什么class里有个方法就是不可枚举——能讲清楚的人其实不多。这篇文章我就从头捋一遍把class到底做了什么、ES5构造函数又做了什么逐行拆开看顺便把容易踩的坑和面试里容易栽跟头的地方都指出来。1. 从一段看起来熟悉又陌生的代码说起我估计不少人第一次见class的时候心理活动是这样的这不就是把构造函数换了个写法吗先别急着下结论我们先把两段代码并排放仔细对比。1.1 一个最小可运行的class示例先写一个非常基础的类class Person { constructor(name, age) { this.name name; this.age age; } sayHello() { console.log(我是${this.name}今年${this.age}岁); } static create(name, age) { return new Person(name, age); } } const p new Person(小张, 18); p.sayHello(); // 我是小张今年18岁 const p2 Person.create(小李, 20); console.log(p2 instanceof Person); // true这段代码有什么组成要素class声明、constructor构造函数、实例方法sayHello、静态方法create。日常开发里这些基本都够用了。1.2 手写ES5等价版本先拼出一模一样的效果如果我们用ES5构造函数来模拟同样的效果通常会写出这样function Person(name, age) { this.name name; this.age age; } Person.prototype.sayHello function () { console.log(我是 this.name 今年 this.age 岁); }; Person.create function (name, age) { return new Person(name, age); }; var p new Person(小张, 18); p.sayHello(); // 我是小张今年18岁 var p2 Person.create(小李, 20); console.log(p2 instanceof Person); // true表面上看起来class版本就是用constructor替代了function Person把方法挪到了类体内部静态方法用static标注。功能上确实对得上但如果你以为这就是全部那后面那些差异就能打得你措手不及。提个醒ES5构造函数里如果你漏了new函数照样会执行——于是this就绑定到window上你的name和age就变成全局变量了。class没有这个问题后面细说。2. class背后的原型本质它到底是不是语法糖把语法糖三个字挂在嘴边很容易但真正要理解class就得先弄清楚它的运行时行为。JS没有真正的类class底层仍然是函数加原型链。2.1 方法都挂在prototype上不是全部class体里写的方法本质上确实是挂到Person.prototype上的。用代码验证一下console.log(typeof Person); // function console.log(Person.prototype.sayHello); // [Function: sayHello] console.log(Object.getOwnPropertyNames(Person.prototype)); // [constructor, sayHello]从上面可以看到class声明之后的Person类型是function而且它的prototype上确实有sayHello方法。但这里有个细节Object.getOwnPropertyNames列出的属性顺序是constructor、sayHello而且你就看不到什么class的内部标记——说明class就是构建在原型机制之上的。再检查一下console.log(Person.prototype.constructor Person); // true实例之间共享方法内存里只有一份这跟ES5构造函数在prototype上挂方法的行为如出一辙。所以从这一个维度来说class确实没有引入什么新的面向对象模型底子还是原型链。2.2 逐项核对class做法与手写函数的行为差异光看方法挂原型还不够我们得把行为和属性一项项对比。下面这张表是我实际测试过之后整理的差别一目了然。对比项ES5构造函数ES6 class类型functionfunction实例方法存放位置手动挂到prototype类体内定义自动挂到prototype方法可枚举性默认可枚举默认不可枚举调用方式既可new也可直接调用只能new直接调用直接报错是否严格模式取决于代码上下文类内部一律严格模式声明提升有提升没有提升有暂时性死区静态方法手动挂在构造函数上static关键字定义继承写法手写原型链或借用构造函数extends super访问当前构造目标变量或argument追踪new.target这里稍微解释一下方法可枚举性这个很多人没意识到的差异点。ES5写法里你用Person.prototype.sayHello function这种赋值方式默认情况下Object.keys(Person.prototype)是可以枚举到sayHello的。而class体里定义的方法它的enumerable属性是被设为false的。// ES5 Object.keys(Person.prototype); // [sayHello]如果手动挂载默认可以枚举出来 // ES6 Object.keys(Person.prototype); // []class方法默认不可枚举也就是说在for...in遍历实例属性的时候class里的方法不会被遍历出来而ES5手动挂上去的方法会被遍历出来。这个差异在你做对象遍历、序列化、或者一些判断对象自身属性的工具函数时可能会让你踩一个意想不到的坑。2.3 更贴切的称呼严格化的语法糖从上面的表格可以看出单纯说class是语法糖并不完整。它确实是基于原型链的封装但本质上是把一套约定俗成、手动实现容易出错的写法固定成了语言层面的标准行为同时顺手修正了ES5时代一些不太合理的设计。比如说直接用function当构造函数它同时也是一个普通函数能调用、能apply、能被bind——这在设计上其实是过于自由了。而class从语言层面限制了这种自由让你的代码意图更加清晰。我的观点是class是带约束的语法糖。它不是另起炉灶的类机制也不是纯粹的写法替换而是把原型链编程中常见的最佳实践固化成了语法。补充一个偏底层的事实class里的方法其实和Object.defineProperty设置出来的属性描述符一致——writable为true、configurable为true、enumerable为false。你如果实在想用ES5手搓一个效果完全一致的版本那得用defineProperty逐个定义而不是简单的赋值语法。3. 容易被忽略的硬差异严格模式、提升、枚举性这一节我们专门聊那些写起来不太起眼、但运行时行为差异很大的点。这些也是我在团队内做代码评审时经常看到有人搞错的地方。3.1 类声明不提升跟函数声明有本质区别函数声明是会提升的这意味着你在定义之前调用它也不会报错foo(); // 能执行因为函数声明提升了 function foo() {}但class声明不提升而且存在暂时性死区const p new Person(); // ReferenceError: Cannot access Person before initialization class Person {}这个问题比较隐蔽尤其是把class写在文件后部、调用代码写在前面的时候很容易出现为什么这里报错的困惑。类表达式也是一样const Person class {};虽然const本身也有暂时性死区但这里要区分的是即便你使用var来声明一个类表达式它依然保持访问时的错误行为。原因在于class在进入作用域到声明被执行的这段区间内任何对它的访问都不允许。3.2 class内部强制严格模式单看这一点在开发上影响不大——绝大多数现代项目可能整个文件都在严格模式下但如果你在维护一些老代码、混写ES5和ES6风格的模块就要注意了。class声明本身就会把它所在的整个类体变成严格模式包括构造器、静态方法、实例方法内部。也就是说即使你整个文件没有写use strictclass内部也不能用with、不能给只读属性赋值、不能有八进制字面量等。class Foo { constructor() { undeclaredVariable 1; // 在class内部这个赋值会直接抛ReferenceError // 但在ES5构造函数里如果不开启严格模式这个赋值会创建一个全局变量 } }这个差异带来的好处是class内的错误往往能被更早地暴露出来而不是悄悄污染全局。坏处当然也有——如果你把一个ES5时代的构造函数原样搬进class里其中有些依赖非严格模式的技巧可能会直接失效。3.3 方法的enumerable差异实际影响这个点前面提过这里举一个具体例子。假设我们有个实例对象需要做深拷贝如果用{...p}这种展开语法ES5模式下方法可能会出现在拷贝结果里而class模式下不会。function ES5Person(name) { this.name name; } ES5Person.prototype.sayHi function () { console.log(hi); }; class ClassPerson { constructor(name) { this.name name; } sayHi() { console.log(hi); } } const a new ES5Person(a); const b new ClassPerson(b); console.log({ ...a }); // ES5的情况结果里可能带sayHi如果没写不可枚举 // class的情况结果里只有{ name: b }在实际开发中这会影响一些通用工具函数比如合并对象、浅拷贝、基于key的序列化等。class的这种设计实际上更符合直觉——实例对象上不应当附带方法名字段。3.4 类的不可直接调用硬约束class被设计为只能用new调用直接用会把类型错误扔你脸上class Foo {} Foo(); // TypeError: Class constructor Foo cannot be invoked without new这一点真的是个好设计。ES5构造函数时代我们往往需要自己加一层判断来防止别人忘了newfunction Foo() { if (!(this instanceof Foo)) { throw new Error(请使用new关键字调用Foo); } // 或者用 new.target 判断 }不管用哪种方式都需要程序员自觉加保护逻辑而class直接把这种保护变成了语言内置行为。从维护的角度看class对API的误用有更强的防御性。3.5 new.target追踪当前调用目标class里可以通过new.target来读取当前实际被new调用的构造函数。如果函数是通过new调用new.target指向这个构造函数如果只是普通调用new.target为undefined。这个特性在class和ES5构造函数里行为一致function Person() { console.log(new.target Person); } new Person(); // true Person(); // false class Animal {} console.log(new Animal().constructor); // Animal对于想要创建抽象基类的场景你可以用这个来判断类是否被直接实例化class Base { constructor() { if (new.target Base) { throw new Error(Base不能直接实例化请继承后使用); } } }这个做法在ES5时代也可以模拟但class让这一切更直接。不过这属于进阶用法普通业务代码里不需要天天写。4. 继承的完整对比从手写原型链到extends如果说class和ES5构造函数在基本定义上的差距算温和那继承这块就是真正的分水岭。ES5的继承写法尤其要做到正确且无副作用是需要手动处理不少细节的。4.1 ES5时代最常用的寄生组合式继承先看一段最简单的ES5继承function Animal(name) { this.name name; } Animal.prototype.sayName function () { console.log(this.name); }; function Dog(name, breed) { Animal.call(this, name); // 借用构造函数继承实例属性 this.breed breed; } Dog.prototype Object.create(Animal.prototype); Dog.prototype.constructor Dog; Dog.prototype.bark function () { console.log(汪汪); };这里有几个关键步骤不能少Animal.call(this, name)把父类构造函数里的实例属性复制到子类实例上。Object.create(Animal.prototype)让Dog.prototype继承Animal.prototype上的方法。Dog.prototype.constructor Dog修复constructor指向。如果你忘记第三行那new Dog().constructor会指向Animal很多依赖constructor判断类型的代码就会出问题。如果你忘了第一行name根本不会初始化。这种bug在ES5里很容易出现且不容易一眼看出来。我见过不少老项目写继承写得五花八门——有的直接用Dog.prototype Animal.prototype导致子类和父类共享原型对象改个子类方法父类方法也跟着变有的用new Animal()来设置原型结果子类实例多了一堆父类的实例属性。这些写法在class里就不太可能出现了。4.2 extends关键字展开后做了什么class Animal { constructor(name) { this.name name; } sayName() { console.log(this.name); } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed breed; } bark() { console.log(汪汪); } }这里extends做的事情等效于把刚才ES5那段继承代码里最核心的几个步骤封装起来设置Dog.prototype的原型为Animal.prototype同时把Dog的自身原型也就是Object.getPrototypeOf(Dog)指向Animal。这意味着静态方法在子类上也能继承到class Animal { static create() { return new this(); } } class Dog extends Animal {} console.log(Dog.create() instanceof Dog); // true这一点是ES5手写继承很容易忽略的地方——因为Dog.create如果没有手动拷贝Animal上的静态方法那Dog上根本不存在create。而class的extends连静态方法继承一起搞定了。4.3 super()的调用时机和this初始化顺序很多刚学class的开发者在继承时写这个都会报错class Dog extends Animal { constructor(name, breed) { this.breed breed; // ReferenceError: Must call super constructor in derived class before accessing this super(name); } }报错信息很直白在子类里使用this之前必须先调用super()。原因是继承时子类的this对象是逐步构建出来的——先由父类构造函数完成部分初始化再交由子类构造函数继续处理。这项约束在ES5构造函数里是没有的因为继承是通过函数调用手动完成的没有这种强制顺序。所以正确顺序永远是class Dog extends Animal { constructor(name, breed) { super(name); this.breed breed; // 先super再使用this } }在子类constructor里this在super()执行之前是不可用的。所谓super()之前不可访问this本质上是出于初始化顺序的考虑。4.4 继承中的一些边界行为差异这里再补充两个容易被忽略的行为差异。第一个父类和子类实例的prototype链判断。class和ES5的写法在instanceof的表现上其实是相同的但如果你用Object.getPrototypeOf去追会发现class子类的__proto__不再是Function.prototype而是指向父类本身class A {} class B extends A {} console.log(Object.getPrototypeOf(B) A); // trueclass静态继承会这样而ES5手写继承做不到这个效果——因为B.__proto__依然是Function.prototype。有些依赖这个链做反射判断的库在class环境下和ES5环境下表现会不同。第二个继承中父类构造函数的返回。ES5时代如果构造函数显式返回一个对象那new出来的结果会替换掉刚生成的this。class的constructor也存在这个行为class Foo { constructor() { return Object.create(null); } } const f new Foo(); console.log(f instanceof Foo); // false // f是一个新对象而且不是Foo的实例但这有个限制如果constructor返回的是基本类型class会忽略它仍然使用this。ES5里构造函数返回字符串这种基本类型时new出来的结果也会被忽略返回this。两边行为基本一致但class的语义更严格一些。5. 实际开发中怎么选结合几个真实场景说了这么多原理最终还是要落地到代码决策上。class是不是一定比构造函数好有没有什么时候你还得用ES5的写法这一节分享下我实际项目里的取舍。5.1 新项目定调class优先还是构造函数优先如果是新写的代码没有历史包袱我会毫不犹豫用class。原因有三个代码可读性更好。构造器在哪、方法有哪些、静态方法是什么扫一眼类体就清楚了。继承的深度和复杂度可控尤其多层继承时ES5手写原型链的心智负担很大。团队协作时class设定的边界更明确不容易写出构造函数和普通函数混用的代码。不过如果你的运行环境是必须兼容IE11这种老浏览器而且项目没有Babel或TypeScript编译这一步那你就要慎重。class在IE11里就是没得跑的语法直接不识别。这种时候ES5构造函数虽然写起来烦但至少能把功能跑起来。5.2 老代码维护、Babel编译、库开发的取舍再说一个老代码维护的场景。有些存量代码里已经有了一套基于构造函数的继承体系你接手之后大动干戈全改成class风险其实不小。尤其是里面依赖了非标准技巧的地方比如直接把父类原型的方法拷贝到子类上、或者用一些很古怪的call方式来调用方法class的严格模式会让这些代码直接抛错。我的建议是老代码先用着新功能模块用class写通过顶层设计逐步迁移。这种渐进式改造比一次推倒重来要稳得多。如果你在开发一个对外发布的库比如组件库、工具库需要考虑到使用方的运行环境和打包方式。很多库作者仍然会使用ES5风格的构造函数目的就是为了不需要编译、直接能在老环境里跑。如果你的库已经通过Babel编译那class就不存在兼容性问题放心用即可。5.3 面试与考察class和ES5构造函数的常见考点这个问题在面试中出现的概率极高。我简单梳理下面试官在这个话题上喜欢挖的坑考点常见陷阱class的本质只说语法糖不展开讲不清原型链细节继承顺序子类constructor里在super之前用this报错原因讲不准确方法枚举不知道class方法默认不可枚举调用方式不知道class不能直接调用声明提升以为class和函数一样会提升静态继承忽略extend对静态方法的传递new.target不知道如何判断抽象基类如果你能把上面这些点都讲明白面试官基本认为你对class的理解是有深度的。关于ES5的继承写法至少要把Object.create设置原型链和constructor修复这一步解释清楚。很多人能写出代码但说不出为什么需要Object.create(Animal.prototype)而不是直接用Animal.prototype这里稍微深入一下// 错误示范共享同一个原型对象 Dog.prototype Animal.prototype; // 这样Dog.prototype上再添加bark方法时Animal.prototype也会被污染 // 正确示范创建一个新对象其原型指向Animal.prototype Dog.prototype Object.create(Animal.prototype);这种差异在class里被extends完全隐藏了但底层的原型对象共享会造成污染的问题依然存在只是你不会直接撞上而已。最后再说一个我踩过的坑我自己在写继承代码的时候踩过一次挺深的坑。当时有一个需求是要做一个多层继承的组件体系基类、中间类、具体类三层。基类里有属性初始化中间类里复写了基类的方法。一开始我用ES5构造函数写中间层的prototype忘了用Object.create设置结果中间类的方法全都覆盖到了基类的原型上导致所有基础组件都出现了同一个异常行为。当时排查了好一阵子才定位到问题后来凡是涉及多层继承的场景我都建议优先换算成class。还有一次遇到的是Babel编译环境下的class继承问题。ES6的class经过Babel编译后很多行为需要通过辅助函数来模拟尤其在继承链比较长、或者父类是箭头函数环境的时候会出现_inherits这类辅助代码把prototype链强制拼起来的情况。这种时候如果你直接用instanceof去判断实例类型结果通常是正确的但如果你查看原型链内部的构造器指向可能会发现一些和原生class不太一致的地方。所以在使用编译产物做类型判断时尽量别依赖构造器名优先用instanceof或Symbol.hasInstance相关的方案。还有个实际开发中常用的判断方法如果你要检查一个对象是否是某类的实例并且在跨库跨框架的场景下instanceof有可能会因为存在两个不同副本的类而失效。这种时候可以考虑用Object.prototype.toString配合自定义标签来判断但class环境下你甚至可以增加一个静态方法专门做校验比如class User { static isUser(obj) { return obj instanceof this; } }这比在外面散落一堆判断逻辑要清晰得多。总的来说class不是那个更高级的替代品而是更规范的原型链写法。读懂class的规则能让你少踩很多ES5时代的坑也能在理论上明白JS的面向对象到底是怎么运作的。你不需要把每种写法都背下来只需要理解原型链和构造调用这两个核心机制class那层语法会自动变得通透。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑