JS 类实例属性写法的区别

今天(2024-09-29)排查的一个编译问题,关键词: esbuild、decorators、esnext、mobx

如下代码被 esbuild 编译时

1
2
3
4
5
6
7
8
9
import { observable } from 'mobx'

class MyStore {
@observable
data = []
}

const instance = new MyStore()
instance.data.toJS();

Case1: 当 target: esnext 时被编译为,此时 data.toJS() 会抛错 ❌,data.toJS is not a function

1
2
3
4
5
6
7
8
9
10
11
import { observable } from 'mobx'

class MyStore {
+ data = [];
}
__decorateClass([
observable
], MyStore.prototype, "data", 2);

const instance = new MyStore()
instance.data.toJS();

Case2: 当 target: es6 时被编译为,代码正常运行 ✅

1
2
3
4
5
6
7
8
9
10
11
12
13
import { observable } from 'mobx'

class MyStore {
+ constructor() {
+ this.data = [];
+ }
}
__decorateClass([
observable
], MyStore.prototype, "data", 2);

const instance = new MyStore()
instance.data.toJS();

这两种写法在 ChatGPT 看来是几乎等价的,Case1 是新语法,Case2 是旧语法,那么造成 Case1 代码运行异常的原因是什么 ?

两者的共同点是使用 __decorateClass 函数中的 Object.defineProperty 给原形链对象 MyStore.prototype 的 key 设置拦截函数

1
2
3
4
5
6
7
8
9
10
11
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result)
__defProp(target, key, result);
return result;
};

不同点是

  • Case2: new MyStore() 实例化时,首先运行构造函数的 this.data = []; 赋值语句,这行代码非常具有迷惑性,初一看会认为是给实例化的对象 instance 设置实例属性,实际上由于原形链对象上有了 data 属性,这行代码会进入到上一步 Object.defineProperty 的拦截函数中,所以此时设置后的 data 属性仍然是 __decorateClass 中包裹的 Mobx 对象,具有 toJS 等属性
  • Case1: new MyStore() 实例化时,data 会被直接设置为实例化的对象 instance 的实例属性,而不会像 Case1 一样进入到原形链对象 Object.defineProperty 的拦截函数中。至此 instance 的实例属性 data 是普通的数组没有 toJS 属性,instance 的原型链属性 data 是 Mobx 对象具有 toJS 等属性,而当 JS 中某个对象实例属性与原型链属性同名时,返回的是实例属性的值,故该代码抛错 ❌

结论: Case2 会触发原型链上的拦截函数 setter,Case1 不会触发

image