一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情
引子
还记得以前读红宝书(JavaScript高级程序设计)的时候,js中要实现继承,居然那么麻烦,光是这些方法的名字就难记住,原型链继承、盗用构造函数、组合继承、原型式继承、
寄生式继承、寄生式组合继承。java和php只需要一个extends就能完成,到这里只能用头很晕来形容。
终于ES6推出了重磅特性class
关键字,前端程序员仿佛看到了曙光,以后实现继承再也不用那么麻烦了。可随着时间的推移,慢慢发现好像class
关键字,并没有想象中的那么受欢迎。首先看看尤大对class
关键字的评价:
本文一起来看看js中的
class
存在哪些争议点。
1.容易出错的this指向
一般来说类方法内部如果含有this
,它默认指向类的实例。但是实际上根本没这么简单。
先来看一个一般的例子,此时一切正常:
class Kunkun {
//唱
sing() {
console.log('唱歌', this)
}
}
const kun = new Kunkun()
kun.sing() // 唱歌 Kunkun {}
1.1单独使用提取出来的方法this
容易出错
但是如果我们把方法从对象中提取出来,尤其是当我们把对象方法作为回调函数时很容易出错,this会指向方法运行时所在的环境,因为找不到sing
方法从而导致报错。
class Kunkun {
//唱
sing() {
console.log('唱歌', this)
//边唱边跳
this.jump();
}
//跳
jump() {
console.log('跳舞', this)
}
}
const kun = new Kunkun();
const { sing } = kun;
sing(); // TypeError: Cannot read properties of undefined (reading 'jump')
有三种解决方案,第一种是在构造方法中用bind
硬绑定this
,这样就不会找不到sing
方法了。
class Kunkun {
constructor() {
//手动绑定this
this.sing = this.sing.bind(this);
}
// ...
}
第二种方案是使用箭头函数
class Kunkun {
//唱
sing = () =>{
console.log('唱歌', this)
//边唱边跳
this.jump();
}
//跳
jump = ()=> {
console.log('跳舞', this)
}
}
const kun = new Kunkun();
const { sing } = kun;
sing();
//此时就不会报错了
//唱歌 Kunkun {sing: ƒ, jump: ƒ}
//跳舞 Kunkun {sing: ƒ, jump: ƒ}
第三种方法,通过proxy自动绑定this
class Kunkun{
sing(){
this.jump();
}
jump(){
console.log('可以跳吗?');
}
}
let kun= new Kunkun();
function classP1(target){
let m= new WeakMap();
const handle={
get(target,key){
//得到p1实例
let val= Reflect.get(target,key);
if(typeof(val)!=='function'){
return val;
}
if(!m.has(val)){
m.set(val,val.bind(target));
}
return m.get(val);
}
}
let proxy= new Proxy(target,handle)
return proxy;
}
let {sing}=classP1(kun);
sing();
1.2静态方法中的this指向类而不是对象实例
静态方法是可以和普通方法同名的,下面的例子有两个jump
方法,一个非静态方法,一个静态方法,从下面代码可以看出静态方法里的this指向类本身而不是对象实例。
class Kunkun {
static sing() {
this.jump();
}
static jump() {
console.log('静态方法jump');
}
jump() {
console.log('普通方法jump');
}
}
Kunkun.sing() // 静态方法jump
1.3小总结
class
中的this
的指向尽管可以bind
、箭头函数
、proxy
来修复,但总归是比较麻烦且比较臃肿的的,而且静态方法里面的this
不指向对象指向类本身(当然这很容易理解), 让本身就比较复杂this
变得更加的麻烦了。
2.class
只是语法糖,要想真正用好它,还是要认真学习原型继承的知识
有一个经典的困惑,为什么一开始class
关键字推出的时候,不支持私有属性呢?私有属性不应该很重要吗?没有私有属性,如何实现封装。尽管可以使用 闭包,Proxy
,Symbol
,WeakMap
等方法曲线实现私有属性,但它们各有各的缺点,闭包的缺点是可以实现外部不能访问,但是有内部方法也不能访问,Proxy
的缺点是要多套一层Proxy比较麻烦,Symbol
的缺点是仍然可以通过getOwnPropertySymbols
来访问,WeakMap
的缺点是还要把保存私有变量的WeakMap变量值定义到class
外面。直到proposal-class-fields提案在2021年7月进入stage4阶段才可以使用#
原生的实现私有属性。这距离ES6发布已经过去了6年。
其原因是js中的class
不是真正的class
,是穿了一层衣服的语法糖,其本质还是函数。
class Kunkun {
//唱
sing() {
console.log('唱歌', this)
}
//跳
jump() {
console.log('跳舞', this)
}
}
console.log(typeof(Kunkun));//打印出 function
所以要想真正用好class
,首先要学好原型继承那一套,否则很多困惑和坑让人很难想通。
3class
性能不如Prototypes
具体测试过程请参考这篇文章,在某些情况下class
性能居然比Prototypes
差了近20倍
总结
class
关键字是语法糖,是披着羊皮的狼,class
能解决的问题ES5语法也能解决,而且class
让js变得更复杂,还引入了新的问题,或许class
关键字的推出本身就是为了迎合面向对象语言程序员,它是一个妥协的产物,目前看来是不够成功的。作为一个前端程序员还是要认真理解原型对象继承,否则很难用好class
,也无法理解为什么class
一开始并不支持私有属性这个特性,以及一些其他奇怪的地方。