JavaScript 延迟加载属性模式详解:从原型到实例的优化策略
本文深入解析 JavaScript 中的延迟加载属性模式,探讨如何通过 getter 和 Object.defineProperty 将昂贵计算推迟至首次访问时执行。文章对比了原型属性与实例属性的实现差异,提供了适用于类定义和对象字面量的完整代码方案,帮助开发者在保持代码简洁的同时显著提升运行效率。
在传统的 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
});
}
}
在上述构造函数中,data 属性通过 Object.defineProperty() 进行定义。该属性在实例上创建(通过指定 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
对于类定义而言,这很可能是推荐使用的模式;然而,如果是对象字面量,则可以使用更简单的方法。
对象字面量的延迟加载属性模式
如果你使用对象字面量而非类,处理过程要简单得多。因为在对象字面量中定义的 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 会错误地落在原型上。
提升性能的最佳策略之一是避免重复执行相同的工作。因此,只要有机会缓存结果以供后续使用,就能显著加快程序运行速度。延迟加载属性模式等技术使得任何属性都能成为提升性能的有效缓存层。
以上就是 JavaScript 中延迟加载属性模式的详细内容,更多关于 js 延迟加载属性的资料请关注本站其它相关文章!
配置 Matter.js 睡眠机制以优化静止物体性能
2026-11-05 15:43:12
JavaScript console.log高级用法:用table、group和time优化调试输出
2026-11-05 08:04:56
JS变量中存储HTML代码时的特殊字符转义方法
2026-11-04 19:04:09
JS怎么安全转换带千分位或货币符号的字符串为数字
2026-11-04 11:54:14
推荐几款稳定的在线JS反混淆工具,支持Obfuscator.io解密
2026-11-04 08:16:21
