ES6提供的class语法糖h2
es6中,相比于之前es5的工厂模式和构造函数方式,提供了面向对象语言都该有的一种语法糖class类,使用constructor方法定义构造函数,使用this关键字定义实例属性,使用extends关键字实现继承。
回忆构造函数的写法:
function createPerson(name, age) { this.name = name this.age = age this.running = function () { console.log('running') }}回顾一下,在es5的构造函数中,this在“函数执行阶段”发生了什么?
当我们写new createPerson('小王', 18)时,js内部会创建一个新对象{},接着把this指向这个对象,逐行执行函数体里的代码,返回这个对象。
所以,this.running = function () {}是运行时赋值,每 new 一次就创建一个新函数(性能一般)
开始尝试使用class来定义类h3
我们来尝试的一种低级错误的写法:
class Person1{ constructor(name,age){ this.name = name; this.age = age; } this.running(){ console.log('running'); }}代码根本跑不起来,会直接报错SyntaxError: Unexpected token '.',因为class 体里不能直接写 this.xxx
// class的花括号内,只允许写“方法定义”,不允许写“执行语句”// class 体里允许的东西:
class A { constructor() {} foo() {} static bar() {} get value() {} set value(v) {}
this.x = 1 // ❌ console.log() // ❌ if (true) {} // ❌ running: function(){console.log('running');} // ❌}所以我们得铭记,这样的写法比如this.running = ...,js会认为你在类定义阶段执行代码,这是不被允许的
有以下两种正确写法:
// 这最贴近我们学es5的理解,this在此处明确指向新创建的Person1实例,赋值行为是给实例挂载属性或方法,完全符合es6类的语法规则。class Person1 { constructor(name, age) { this.name = name this.age = age // this.running = function(){} 写在 constructor 内,执行时机是:当用 new Person1() 创建实例时(运行时),而非 类定义阶段(解析时); this.running = function () { console.log('running') } }}这种写法等价于之前写的es5构造函数写法。
或者
class Person2 { constructor(name, age) { this.name = name this.age = age } running() { console.log('running') }}这样写,来分析一下发生了什么:首先constructor,相当于实例初始化逻辑(运行时),而running()是定义在Person.prototype上,等价于es5中:
Person.prototype.running = function () { console.log('running')}
function Person(name, age) { this.name = name this.age = age}
Person.prototype.running = function () { console.log('running')}既然说类体的核心规则是:顶层只能写方法定义,如running(){},但写在constructor内部是“实例初始化逻辑区”,允许任意赋值或者执行代码,那么就可以说明写在constructor内部的这种running是实例的自有属性,而非原型属性,并且每个实例的running应该是独立的函数(因为每个实例的running应该是一个函数,而不是同一个函数)
我们来验证:
const p1 = new Person1('小王', 18)const p2 = new Person2('小李', 17)
// 1. running 是实例的“自有属性”(而非原型属性)console.log(p1.hasOwnProperty('running')) // trueconsole.log(Person1.prototype.running) // undefined
// 2. 每个实例的 running 是独立函数console.log(p1.running === p2.running) // false从性能角度思考h3
现在我们从内存性能角度思考,不难发现,这个写法的核心代价是内存占用更高:若创建100个Person1实例,就会生成100个独立的running函数(每个函数占用独立内存空间);
对比原型方法(Person2的写法):所有实例共享原型上的1个running函数,内存效率远高于此。
难道把方法挂载到实例上一无是处h3
虽然内存效率低,不过,并不能说Person1这种写法一无是处,什么时候什么场景下这个写法不可替代呢?
需要每个实例的方法有 “独立状态 或者 逻辑”:
// 示例:每个实例的 running 记录自己的调用次数(独立状态)class Person1 { constructor(name, age) { this.name = name this.age = age let callCount = 0 // 实例独有的闭包变量 this.running = function () { callCount++ console.log(`${this.name} 跑步中,已调用 ${callCount} 次`) } }}
const p1 = new Person1('张三', 20)const p2 = new Person1('李四', 21)p1.running() // 张三 跑步中,已调用 1 次p1.running() // 张三 跑步中,已调用 2 次p2.running() // 李四 跑步中,已调用 1 次(不受 p1 影响)这个场景中,方法内部依赖“实例独有的闭包变量”(如上例的 callCount),需要重写某个实例的running方法,且不影响其他实例;方法逻辑需要和具体实例强绑定(比如每个实例的方法有不同的参数默认值)。
如何理解呢?为什么let声明的变量,在函数闭包内能记住上一个调用的值?
让我们一步一步拆解,首先先搞懂“每次 new Person1 时发生了什么”
当你执行 new Person1('张三', 20) 时,js会做这些事:
- 创建一个空的新对象(比如
{}),这个对象就是p1; - 执行
Person1的constructor函数,此时constructor的执行上下文(可以理解为 “临时的运行环境”)被激活; - 在
constructor的执行上下文中:- 给
this绑定到新对象p1,所以this.name = '张三'就是给p1加属性; - 声明
let callCount = 0:这个变量属于 “本次constructor执行上下文” 的私有变量,不是p1的属性,只在这次constructor执行时存在; - 创建
running函数,并且这个函数引用了当前上下文的callCount变量; - 把
running函数赋值给p1.running(相当于把这个函数挂载到p1上);
- 给
constructor执行完毕,但因为p1.running函数还引用着callCount,所以 js 不会回收这个callCount变量 —— 这就是闭包的核心:内部函数引用外部函数的变量,外部函数执行完后,变量不会被销毁,而是被内部函数 保留。
其次,搞懂“为什么调用 p1.running() 能记住 callCount 的值”?
- 第一次调用
p1.running():函数内部找到它引用的callCount(初始值 0),执行callCount++变成 1,然后打印; - 第二次调用
p1.running():函数还是找它绑定的那个callCount(现在是 1),再++变成 2,打印; - 而
p2 = new Person1('李四', 21)时,会重新执行一次constructor,生成全新的执行上下文,里面有全新的callCount = 0,以及全新的running函数(引用这个新的callCount)—— 所以p2.running()操作的是自己的callCount,和p1完全无关。
用比喻理解更直观:
把每个 new Person1() 比作 “开一个独立的小房间”:
constructor 执行等于 装修这个房间;callCount = 0等于 房间里放了一个只有你能看到的计数器(不是挂在门上的,别人看不到);running 函数等于 房间里的一个按钮,按一下计数器就+ 1,并且按钮和这个房间的计数器绑定死了;p1等于 这个房间的钥匙,你拿着钥匙(调用p1.running())按按钮,只会改这个房间的计数器;p2等于 另一个新房间,有自己的计数器和按钮,按p2的按钮,不会影响p1的房间。
小结h3
如下这张表格将实例独有方法和原型方法做一个对比:
| 维度 | Person1(实例独有方法) | Person2(原型方法) |
|---|---|---|
| 挂载位置 | 实例自身 | 类的 prototype 上 |
| 内存占用 | 高(每个实例独立函数) | 低(所有实例共享) |
| 独立状态 | 支持(闭包 / 独立变量) | 不支持(共享逻辑) |
| 适用场景 | 实例独有逻辑 | 通用共享逻辑 |
构造函数和类的核心差异h2
es5中,构造函数本质上就是普通函数,如果忘了写new,它会作为一个普通的函数执行,this可能会意外指向全局对象window,导致严重污染。
但是在es6中class,这种行为完全被禁止了,为了安全,类必须使用new调用,否则会抛出错误。
// ES5function Person(name) { this.name = name}const p = Person('小王') // 不报错,但 window.name 变成了 '小王',p 是 undefined
// ES6class Person { constructor(name) { this.name = name }}const p = Person('小王')// ❌ TypeError: Class constructor Person cannot be invoked without 'new'第二个差异就是es6不存在变量提升,es5中的函数声明会被提升到作用域顶部,这就意味着你可以“先使用,后定义”
但是es6的class和let、const一样,你必须先定义,先定义类,才能实例化它,这有助于让代码逻辑更清晰
// ES5:可行const p = new Person('小王');function Person(name) { ... }
// ES6:报错const p = new Person('小王'); // ❌ ReferenceErrorclass Person { ... }还有一个一个非常底层但重要的细节,在es5中,我们在原型上定义的方法,默认是可枚举的,这就意味着如果我们使用for...in遍历实例,这些方法会被遍历出来
而在es6中,定义在类体内的所有方法(除了constructor),默认都是不可枚举的。这更符合我们要把数据(属性)和行为(方法)分离的思维,通常我们遍历对象只想拿数据,不想拿到一堆方法。
// ES5function Person5() {}Person5.prototype.running = function () {}
// ES6class Person6 { running() {}}
const p5 = new Person5()const p6 = new Person6()
// 验证console.log(Object.keys(Person5.prototype)) // ['running'] -> 可枚举console.log(Object.keys(Person6.prototype)) // [] -> 不可枚举!类继承构造函数可行吗h2
// ES5 构造函数function createPerson1(name, age) { this.name = name this.age = age this.running = function () { console.log('running') }}
// ES6 类继承 ES5 构造函数class Student extends createPerson1 {}
// 实例化并调用方法const student1 = new Student('小王', 18)student1.running() // 输出:running这段代码能正常运行,为什么可行?
es6的class extends并非只能继承es6类,而是可以继承“任意能通过new调用的构造函数”,核心原因有两点:
- 类的默认构造函数自动调用
super:当class Student没有显式定义constructor时,js会自动生成默认构造函数
class Student extends createPerson1 { constructor(...args) { super(...args) // 关键:调用父类构造函数 createPerson1 }}super(...args)等价于createPerson1.call(student1实例, '小王', 18),此时 createPerson1 内部的 this 会绑定到 Student 的实例,因此 this.name/this.age/this.running 都会挂载到 student1 上,最终 student1.running() 能正常执行。
- es5构造函数满足
extends的继承条件:extends对被继承的对象只有一个核心要求:能通过new调用(即符合构造函数的规范)。createPerson1作为 es5 构造函数,只要通过new调用,就能为子类实例挂载属性或方法,因此能被class继承。
但这是语法兼容带来的结果,并非推荐的写法,它存在内存效率低、语法风格割裂等隐藏问题。
Comments