Mar 6

小议:thisless的Javascript编程

Lrdcq , 2021/03/06 21:14 , 程序 , 閱讀(1224) , Via 本站原創
目前业界普遍认可的认知上,对于Javascript这门语言来说,最大的包袱或者隐患就是this与其动态绑定问题。this的动态绑定问题带来的不止是写出bug的风险,还在代码可维护性,性能与语言发展上设置了无数障碍,同时也是典型的反直觉产物。因此业界一直在讨论,能否在javascript开发最佳实践中摒弃this,即实现thisless的Javascript。

摒弃this后Javascript是否还可以实现如今的所有功能

首先需要判断的是摒弃this后Javascript是否还完备。

1. 首先它图灵完备么?当然,如果摒弃this,js至少也是一门功能完备的面向过程的语言,模拟单带图灵机不太可能有影响,因此它是图灵完备的。

2. 没有this后有没有方法可以替代面向对象的class。当然是可以的,对象可以用类似于这样的方式模拟:
//真实类AAA
class AAA {
  a;
  alert() {
    console.log(this.a);
  }
}
//模拟类AAB
let AAB = function() {
  let a = 0;
  let alert = function() {
    console.log(a);
  }
  return {
    set a(d) {
      a = d;
    },
    get a() {
      return a;
    },
    alert
  };
}

替换逻辑为:a. this上的属性通过闭包持有。b. 属性的暴露通过对象get/set实现。c. 方法互调和熟悉同理,在闭包内解决。简单来说,通过闭包替代this持有状态来实现摒弃this,至少完全保证了。

这个思路很类似于react的函数式组件的思路,即无对象即无状态,而如果真的要状态,通过hook实质是持有闭包去解决。另外如果使用过rxjs这样的函数响应式编程框架,理论上rxjs的操作对象都是无状态的,但是部分操作符的实现其实是有状态的,比如combineLatest会合并发送上一次发送的对象,即是在闭包中将上一个next的返回值持有住了。

3. 没有this后依赖函数调用者的代码如何改变?在dom事件中,常用的一个特性是利用call event function的caller即this是dom元素这个特性。即可以通过this找到函数的caller。如果是一些老旧的代码,还有还有可能还在通过callee.caller。

不过实际上无论如何都不建议通过获取函数caller,包括this的方式来获取caller(“函数的this”本身语义上不符合逻辑)做任何正式的业务逻辑。如果是dom事件的话,通过event对象就能找到caller与target了。

这样做的局限性

我们知道现代js引起对class的调用做过大量的优化,如果我们摒弃this形式class,而是如上使用object的方式,调用性能是否会有影响呢?

1. 调用性能:我们一般首要关注的是对对象的方法与属性调用的时候的耗时,这决定了在高频率对象业务调用时实际业务代码性能。不过理论上现代js引擎中对函数调用链条上不管是原型链上还是对象中,都进行了命中优化,一般不会存在性能差距,不过还是进行了一下测量,大概的数据是:

对class实例与object(array with object properties)进行空方法调用,for循环1亿次,在我的电脑chrome中分别是73.49 ms与1089.54ms,object中的函数调用明显还是慢很多。原因是:class原型链方法调用在ie时代之后都做了命中缓存,在ie的时候原型链可能反而更慢,但是现在来看显然会快很多,并且基于v8编译优化,class实例的方法调用会被直接编译为函数调用。而相反object的方法调用虽然有做map命中优化,说到底还是是基于字符串对比的,显然会慢一些。

而对于get/set,编写了obj.value = obj.value + 1这样的读写调用,同样是循环1亿次,在我的电脑chrome中分别是275.167ms与19090 ms,差距过大。这是因为class变异后的属性set/get在没有定义setter/getter的情况下,是和native一样的直接内存结构取值赋值,性能当然比方法调用快很多了。

因此在性能上目前用object代替class来摒弃this陷入没有优势。未来可能的优化上,如果对于少写多读的object也在编译时构成固定结构式内存结构来布局所有的属性与函数,也能提升访问效率。

2. 内存占用:如果object访问性能有劣势,但是在内存占用上有优势,也是可以接受的,但是拥有原型链与接近native结构体的数据储存区的class内存占用当然不会大。

实际测试后,如上的AAAclass与AAB输出对象持有十万份,内存占用在1:4到1:5之间,显然是完败。具体展开内存占用情况,比如我们的AAAclass的实例在内存中如下:
點擊在新視窗中瀏覽此圖片

一个class实例在内存中是包括属性结构(未显示),与原型链指针与类型map,当然原理上还存在一个class指针指向内部class定义的描述,在memory面板上的含义是类名即type。属性结构当然是如果定义的属性越多,它就越长的样子。

而一个object,其实是在array中的(object properties)类型的对象,在内存中看起来更像是一个map,包含key-value对,内存中有每一个属性与方法的名字。
點擊在新視窗中瀏覽此圖片

同时还注意到,源码中getter/setter与定义的一个方法均对内部对象a进行了访问,那么每一个方法会形成一个闭包,而每一个实例都会形成这样的闭包。
點擊在新視窗中瀏覽此圖片

在加上每个闭包实际上持有的a,object的内存占用包括了:object本身,object持有的闭包context,闭包context持有的对象即作为class属性的对象。这一套内存占用下来,显然比class占得太多了。

从本质上看,object目前比class占用资源多这么多,核心点在于:class的设计为定义不可变对象(immutable),而object主要是面向可变对象(mutable),因此在进行类似重复性性能测试的时候不可变对象占优。更具体的描述的话,同一个class的所有实例的结构定义是一致的就是如上类型map中描述的那样,如果我为一个实例动态添加一个属性,这个属性会被定义到同一个DescriptorArray中,并且派生出新的类型描述map,而属性会利用类似于AssociatedObject的方式挂载上去。可以观察如下代码的4个对象内存cap占用情况进行验证:
class AAA {
  a1 = 0;
  a2 = 0;
  a3 = 0;
  a4 = 0;
  a5 = 0;
  alert() {
  }
}//a1-a5会生成在class原始结构体中,并存在在类型描述map中

let map = [];
for(let i = 1; i < 5; i++) {
  let t = new AAA();
  t.a1 = i;
  if (i >= 3) {
    t.a6 = i;//运行到此处,会在类型描述中新增出a6a7,并且对于实例会随着赋值在结构之外挂载动态添加的属性。
    t.a7 = i;
  }
  map[i] = t;
}


结论上,我们现在该怎么践行thisless

既然上文结论上,目前class实现的优越性对应着目前我们无法大胆在工程中使用面向对象的替代方案,那现阶段我们能否并怎么践行thisless呢?

前几天正好讨论过react函数组件的问题,显然,函数组件正是react团队践行thisless或者classless的方式。而业界近几年一直比较火的函数响应式编程,rxjs这样的框架,也可以让面向对象抽象而变得混沌的业务逻辑更线性与可读。虽然根据react函数组件讨论时的结论,现在函数组件的角度,同样也是业界对函数响应式编程的态度,无状态编程并不能解决100%的业务问题,但是确实是一种值得铺开的尝试。

而在class的问题上,除了上文这个object替代class的思路,还有一些其他可能更不自然但是有更优良性能的方式值得尝试。另一个视角看,对object进行可变不可变的优化是各个js引擎未来可能存在的方向,这也有可能给我们带来和class地位一致的object与更多的可能性。
关键词:thisless , javascript
logo