再看es6的class,有一点尴尬

181 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

引子

还记得以前读红宝书(JavaScript高级程序设计)的时候,js中要实现继承,居然那么麻烦,光是这些方法的名字就难记住,原型链继承、盗用构造函数、组合继承、原型式继承、 寄生式继承、寄生式组合继承。java和php只需要一个extends就能完成,到这里只能用头很晕来形容。
终于ES6推出了重磅特性class关键字,前端程序员仿佛看到了曙光,以后实现继承再也不用那么麻烦了。可随着时间的推移,慢慢发现好像class关键字,并没有想象中的那么受欢迎。首先看看尤大对class关键字的评价:

image.png 本文一起来看看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倍

image.png

总结

class关键字是语法糖,是披着羊皮的狼,class能解决的问题ES5语法也能解决,而且class让js变得更复杂,还引入了新的问题,或许class关键字的推出本身就是为了迎合面向对象语言程序员,它是一个妥协的产物,目前看来是不够成功的。作为一个前端程序员还是要认真理解原型对象继承,否则很难用好class,也无法理解为什么class一开始并不支持私有属性这个特性,以及一些其他奇怪的地方。