传统上,开发人员在JavaScript类内部为实例中可能需要的任何数据创建了属性。 对于在构造函数内部随时可用的小数据而言,这不是问题。 但是,如果实例中一些数据需要计算了才可用,你可能不希望把计算过程提前。 例如,思考下面这个类:
class MyClass {
constructor() {
this.data = someExpensiveComputation();
}
}
此处,创建data
属性需要执行一些耗时的计算。 如果你不确定该属性是否会被使用,则提前执行该计算可能显得效率不高。 幸运的是,有几种方法可以将这些计算操作推迟到以后。
按需属性模式
优化执行耗时操作的最简单方法是等到需要数据时再进行计算。 例如,你可以使用带有getter
的访问器属性来按需计算,如下所示:
class MyClass {
get data() {
return someExpensiveComputation();
}
}
在这种情况下,只有当第一次读取data
属性时,耗时的计算才会发生,这是一种改进。 但是,每次读取data
属性都执行相同的计算,这比之前的示例(至少仅执行一次计算)差。 这不是一个好的解决方案,但是你可以在此基础上创建一个更好的解决方案。
复杂的延迟加载属性模式
只有在访问该属性时才执行计算是一个好的开始。 你需要的是在计算之后缓存数据,然后仅使用缓存的数据。 但是,将这些数据缓存在何处以便于访问呢? 最简单的方法是定义一个具有相同名称的属性,并将其值设置为计算出的数据,如下所示:
class MyClass {
get data() {
const actualData = someExpensiveComputation();
Object.defineProperty(this, "data", {
value: actualData,
writable: false,
configurable: false,
enumerable: false
});
return actualData;
}
}
在这里,data
属性再次被定义为实例的getter
,但是这次它缓存了结果。 调用 Object.defineProperty()
创建了一个名为data
的新属性,该属性的固定值是actualData
,并且设置为不可写,不可配置和不可枚举(以匹配getter)。 之后,将返回值本身。 下次访问data
属性时,它将从新创建的属性中读取而不是调用getter:
const object = new MyClass();
// calls the getter
const data1 = object.data;
// reads from the data property
const data2 = object.data;
所有计算仅在第一次读取data
属性时完成。 后续每次读取data
属性都将返回缓存的值。
这种模式的一个缺点是data
属性开始时是不可枚举的原型属性,最后是不可枚举的实例属性:
const object = new MyClass();
console.log(object.hasOwnProperty("data")); // false
const data = object.data;
console.log(object.hasOwnProperty("data")); // true
尽管这种区别在很多情况下并不重要,但了解这种模式很重要,因为当对象传递时,这种模式可能会引起细微的问题。 幸运的是,使用更新的模式可以轻松解决此问题。
类的唯一延迟加载属性模式
如果你有这样一个使用场景: 实例上始终存在延迟加载的属性。那么可以使用Object.defineProperty()
在类的构造函数内部创建该属性。 与前面的示例相比,它有点复杂,但是可以确保该属性仅存在于实例上。示例如下:
class MyClass {
constructor() {
Object.defineProperty(this, "data", {
get() {
const actualData = someExpensiveComputation();
Object.defineProperty(this, "data", {
value: actualData,
writable: false,
configurable: false
});
return actualData;
},
configurable: true,
enumerable: true
});
}
}
在构造函数中使用Object.defineProperty()
创建data
访问器属性。 该属性是在实例上创建的(通过使用this
),定义了一个getter并设置属性是可枚举和可配置的(对象的实例属性通常都是这样)。将data
属性设置为可配置尤其重要,这样才可以再次对其调用Object.defineProperty()
。
然后,getter函数进行计算并再次调用Object.defineProperty()
。 现在将data
属性重新定义为具有特定值的属性,并且将其变为不可写且不可配置以保护最终数据。 然后,从getter返回计算的数据。 下次读取data
属性时,将返回缓存的值。 另外,data
属性现在仅作为实例自己的属性存在,并且在第一次读取之前和之后行为表现一致:
const object = new MyClass();
console.log(object.hasOwnProperty("data")); // true
const data = object.data;
console.log(object.hasOwnProperty("data")); // true
对Class
而言,这很可能是你要使用的模式。 另一方面,对象可以使用更简单的方法。
对象的延迟加载属性模式
如果使用的是对象而不是类,则过程要简单得多,因为在对象上定义的getter与数据属性一样被定义为可枚举的自身属性(而不是原型属性)。 这意味着你可以为对象使用复杂的延迟加载属性模式,而不会造成混乱:
const object = {
get data() {
const actualData = someExpensiveComputation();
Object.defineProperty(this, "data", {
value: actualData,
writable: false,
configurable: false,
enumerable: false
});
return actualData;
}
};
console.log(object.hasOwnProperty("data")); // true
const data = object.data;
console.log(object.hasOwnProperty("data")); // true
总结
在JavaScript中重新定义对象属性的能力提供了来缓存计算成本很高的数据的机会。把访问器属性重新定义为数据属性,你可以将耗时的计算推迟到第一次读取该属性时,然后将结果缓存起来以备后用。这种方法同时适用于类和对象,并且在对象中的使用更简单一些,因为你不必担心getter最终会出现在原型上。
改善性能的最好方法之一是避免重复两次相同的工作,因此,只要你可以缓存结果以备后用,就可以加快程序运行速度。诸如延迟加载属性模式之类的技术使任何属性都可以成为缓存层以提高性能。
原文链接: humanwhocodes.com/blog/2021/0…
原文作者是 尼古拉斯·扎卡斯(Nicholas C.Zakas),《Professional JavaScript for Web Developers, 3rd Edition》一书的作者。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!