想象一下,我有一个函数可以访问常量(永不突变)(例如,查询表或数组)。在函数范围之外的任何地方都不会引用该常数。 我的直觉告诉我,应该在函数范围之外定义此常量(以下选项A ),以避免在每次函数调用时都(重新)创建该常量,但这确实是现代Javascript引擎的工作方式吗?我想认为现代引擎可以看到常量从未修改过,因此只需要创建和缓存一次即可(是否有术语吗?)。浏览器是否以相同的方式缓存闭包中定义的功能?
在访问函数的位置旁边(选项B ),简单地在函数内部定义常量是否有不可忽略的性能损失?对于更复杂的对象,情况是否有所不同?
// Option A:
function inlinedAccess(key) {
const inlinedLookupTable = {
a: 1,
b: 2,
c: 3,
d: 4,
}
return 'result: ' + inlinedLookupTable[key]
}
// Option B:
const CONSTANT_TABLE = {
a: 1,
b: 2,
c: 3,
d: 4,
}
function constantAccess(key) {
return 'result: ' + CONSTANT_TABLE[key]
}
我创建了一个 jsperf test ,用于比较不同的方法:
Object
-内联(选项A)Object
-常量(选项B)@jmrk建议的其他变体:
Map
-内联Map
-常量switch
-内联值初步发现(在我的计算机上,请自行尝试):
答案 0 :(得分:8)
V8开发人员在这里。您的直觉是正确的。
TL; DR: inlinedAccess
每次都会创建一个新对象。 constantAccess
效率更高,因为它避免了每次调用时都重新创建对象。为了获得更好的性能,请使用Map
。
“快速测试”对两个功能产生相同的计时事实说明,微基准测试容易引起误解;-)
b: new Array(100),
替换一个属性。'result: ' + ...
中,数字到字符串的转换以及随后的字符串连接对整个时间的贡献很大。您可以将其删除以获得更清晰的信号。Map
用于各种键的查找比对象属性查找要快。在2010年将对象用作地图是,现代JavaScript具有适当的Map
,所以请使用它们! :-)Array
元素查找甚至更快,但是当然只有键为整数时才可以使用它们。让我们将所有这些想法都放入代码中:
function inlinedAccess(key) {
const inlinedLookupTable = {
a: 1,
b: new Array(100),
c: 3,
d: 4,
}
return inlinedLookupTable[key];
}
const CONSTANT_TABLE = {
a: 1,
b: new Array(100),
c: 3,
d: 4,
}
function constantAccess(key) {
return CONSTANT_TABLE[key];
}
const LOOKUP_MAP = new Map([
["a", 1],
["b", new Array(100)],
["c", 3],
["d", 4]
]);
function mapAccess(key) {
return LOOKUP_MAP.get(key);
}
const ARRAY_TABLE = ["a", "b", "c", "d"]
function integerAccess(key) {
return ARRAY_TABLE[key];
}
function switchAccess(key) {
switch (key) {
case "a": return 1;
case "b": return new Array(100);
case "c": return 3;
case "d": return 4;
}
}
const kCount = 10000000;
let result = null;
let t1 = Date.now();
for (let i = 0; i < kCount; i++) {
result = inlinedAccess("a");
result = inlinedAccess("d");
}
let t2 = Date.now();
for (let i = 0; i < kCount; i++) {
result = constantAccess("a");
result = constantAccess("d");
}
let t3 = Date.now();
for (let i = 0; i < kCount; i++) {
result = mapAccess("a");
result = mapAccess("d");
}
let t4 = Date.now();
for (let i = 0; i < kCount; i++) {
result = integerAccess(0);
result = integerAccess(3);
}
let t5 = Date.now();
for (let i = 0; i < kCount; i++) {
result = switchAccess("a");
result = switchAccess("d");
}
let t6 = Date.now();
console.log("inlinedAccess: " + (t2 - t1));
console.log("constantAccess: " + (t3 - t2));
console.log("mapAccess: " + (t4 - t3));
console.log("integerAccess: " + (t5 - t4));
console.log("switchAccess: " + (t6 - t5));
我得到以下结果:
inlinedAccess: 1613
constantAccess: 194
mapAccess: 95
integerAccess: 15
switchAccess: 9
所有这些:这些数字是“ 1000万次查找的毫秒”。在实际的应用程序中,差异可能太小而无关紧要,因此您可以编写最易读/可维护的代码。例如,如果您仅进行100K查找,则结果为:
inlinedAccess: 31
constantAccess: 6
mapAccess: 6
integerAccess: 5
switchAccess: 4
顺便说一句,这种情况的常见变体是创建/调用函数。这个:
function singleton_callback(...) { ... }
function efficient(...) {
return singleton_callback(...);
}
比这要有效得多:
function wasteful(...) {
function new_callback_every_time(...) { ... }
return new_callback_every_time(...);
}
与此类似,
function singleton_method(args) { ... }
function EfficientObjectConstructor(param) {
this.___ = param;
this.method = singleton_method;
}
比这要有效得多:
function WastefulObjectConstructor(param) {
this.___ = param;
this.method = function(...) {
// Allocates a new function every time.
};
}
(当然,通常的做法是Constructor.prototype.method = function(...) {...}
,这也避免了重复创建函数。或者,如今,您可以只使用class
es。)