Lodash 源码阅读-getMapData
概述
getMapData 是 Lodash 内部使用的一个辅助函数,用于根据键的类型从 MapCache 中获取对应的数据存储结构。它是 MapCache 实现中的核心路由函数,通过智能判断键的类型来决定使用哪种存储结构,从而优化不同类型键的存取性能。
前置学习
- MapCache:Lodash 内部的高效键值对缓存结构
- Hash:用于存储基本类型键的哈希表结构
- Map:原生 Map 对象,用于存储引用类型键
- ListCache:用于不支持 Map 环境下的降级实现
- isKeyable:判断键是否可直接用于哈希表存储的辅助函数
技术知识:
- JavaScript 的类型系统与 typeof 运算符
- JavaScript 中的 Map 数据结构
- 哈希表原理
- JavaScript 对象属性访问机制
源码实现
javascript
/**
* Gets the data for `map`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key)
? data[typeof key == "string" ? "string" : "hash"]
: data.map;
}
实现思路
getMapData 函数的核心思想是根据键的类型智能选择最合适的存储结构。MapCache 内部维护了三种不同的存储结构:
- string:专门用于存储字符串类型的键
- hash:用于存储其他基本类型键(数字、布尔值、符号等)
- map:用于存储引用类型键(对象、数组等)
这种分类存储的策略能够:
- 提高基本类型键的存取性能
- 避免引用类型键的哈希冲突
- 优化内存使用
- 提供更好的类型安全性
源码解析
javascript
function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key)
? data[typeof key == "string" ? "string" : "hash"]
: data.map;
}
让我们逐步分析这个函数的实现:
-
参数说明:
map
:MapCache 实例,包含__data__
属性key
:要查询的键
-
获取数据对象:
javascriptvar data = map.__data__;
MapCache 实例的
__data__
属性包含三种存储结构:javascript{ 'hash': new Hash, // 存储基本类型键 'map': new (Map || ListCache), // 存储引用类型键 'string': new Hash // 专门存储字符串键 }
-
键类型判断:
javascriptisKeyable(key);
使用 isKeyable 函数判断键是否可以直接用于哈希表存储。可哈希的键包括:
- 字符串(除了
__proto__
) - 数字
- 布尔值
- 符号
- null
- 字符串(除了
-
存储结构选择:
javascript? data[typeof key == 'string' ? 'string' : 'hash'] : data.map;
- 如果键可哈希:
- 当键是字符串时,使用
data.string
- 其他基本类型键使用
data.hash
- 当键是字符串时,使用
- 如果键不可哈希,使用
data.map
- 如果键可哈希:
例如:
javascript
const cache = new MapCache();
// 字符串键 -> 使用 string 存储
cache.set("name", "John");
// getMapData(cache, 'name') 返回 data.string
// 数字键 -> 使用 hash 存储
cache.set(42, "answer");
// getMapData(cache, 42) 返回 data.hash
// 对象键 -> 使用 map 存储
cache.set({ id: 1 }, "object");
// getMapData(cache, { id: 1 }) 返回 data.map
为什么要区分字符串和其他基本类型键?
getMapData 函数中有一个关键的判断:typeof key == 'string' ? 'string' : 'hash'
,这个判断将字符串键和其他基本类型键区分开来存储。为什么 Lodash 要这样设计呢?主要有以下几个原因:
1. 类型安全和避免冲突
如果不区分类型,当使用 1
和 "1"
这样的键时会产生冲突。在单一哈希表中,这两个键会被视为相同的键,因为 JavaScript 对象会自动将属性键转换为字符串。通过分开存储,可以保持类型的完整性:
javascript
// 在单一哈希表中会冲突
const singleHash = new Hash();
singleHash.set(1, "number one");
singleHash.set("1", "string one");
console.log(singleHash.get(1)); // 'string one' - 原值被覆盖了!
console.log(singleHash.get("1")); // 'string one'
// 在 MapCache 中正确区分
const mapCache = new MapCache();
mapCache.set(1, "number one");
mapCache.set("1", "string one");
console.log(mapCache.get(1)); // 'number one'
console.log(mapCache.get("1")); // 'string one'
2. 性能优化
字符串键是 JavaScript 中最常用的键类型,V8 引擎对字符串属性访问有专门的优化。通过将字符串键单独处理,可以利用这些优化:
javascript
// 性能测试
const iterations = 1000000;
// 区分存储
const separateCache = new MapCache();
console.time("分开存储");
for (let i = 0; i < iterations; i++) {
const key = i % 2 === 0 ? `key${i}` : i;
separateCache.set(key, i);
separateCache.get(key);
}
console.timeEnd("分开存储");
// 单一存储
const singleCache = {
__data__: { hash: new Hash() },
set(key, value) {
this.__data__.hash.set(key, value);
},
get(key) {
return this.__data__.hash.get(key);
},
};
console.time("单一存储");
for (let i = 0; i < iterations; i++) {
const key = i % 2 === 0 ? `key${i}` : i;
singleCache.set(key, i);
singleCache.get(key);
}
console.timeEnd("单一存储");
在大多数现代浏览器中,分开存储方式通常比单一存储更快,特别是在混合键的情况下。
3. 内存布局优化
V8 引擎内部对不同类型的属性使用不同的内存布局。通过分开存储,可以让 V8 更好地优化每种类型的内存布局:
- 字符串键通常有特殊的缓存和快速路径
- 数字键可以使用更紧凑的内存表示
- 对于纯数字数组,V8 可以使用更高效的内存布局(Smi 元素)
总结
getMapData 是一个小而精的辅助函数,但它为 MapCache 提供了强大的数据路由能力。它体现了以下设计思想:
- 类型感知:根据键的类型选择最优的存储结构
- 性能优化:为不同类型的键提供专门的存储结构
- 安全性:通过 isKeyable 函数防止不安全的键类型
- 可扩展性:支持添加新的存储结构类型
这个函数的设计启示我们在处理复杂数据结构时:
- 应该根据数据类型选择最合适的存储方式
- 可以通过分类存储来提高性能
- 要注意类型安全性
- 保持代码的简洁性和可维护性
getMapData 虽小,但它是 Lodash 缓存系统高效运行的关键组件之一,展示了良好的编程实践和深思熟虑的系统设计。
相关资源链接
- V8 引擎官方博客: Fast properties in V8 - V8 团队详细解释了对象属性的内部表示和优化
- MDN 文档: 使用对象 - 官方文档关于 JavaScript 对象属性的解释