Lodash 源码阅读-Hash
概述
Hash 是 Lodash 内部实现的一个简单高效的哈希表数据结构,专门用于存储字符串和其他基本类型键值对。它是 MapCache 等更复杂数据结构的基础组件,为 Lodash 的缓存系统提供了高效的数据存取能力。
前置学习
- Object.create:用于创建一个新对象,使用现有的对象来作为新创建对象的原型
- hasOwnProperty:用于检查对象是否拥有特定的属性
- HASH_UNDEFINED:Lodash 内部用于表示 undefined 值的特殊标记
技术知识:
- JavaScript 对象属性访问机制
- 哈希表原理与实现
- JavaScript 原型链
- JavaScript 中的 null 原型对象
源码实现
javascript
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Hash(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the hash.
*
* @private
* @name clear
* @memberOf Hash
*/
function hashClear() {
this.__data__ = nativeCreate ? nativeCreate(null) : {};
this.size = 0;
}
/**
* Removes `key` and its value from the hash.
*
* @private
* @name delete
* @memberOf Hash
* @param {Object} hash The hash to modify.
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function hashDelete(key) {
var result = this.has(key) && delete this.__data__[key];
this.size -= result ? 1 : 0;
return result;
}
/**
* Gets the hash value for `key`.
*
* @private
* @name get
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function hashGet(key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return hasOwnProperty.call(data, key) ? data[key] : undefined;
}
/**
* Checks if a hash value for `key` exists.
*
* @private
* @name has
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function hashHas(key) {
var data = this.__data__;
return nativeCreate
? data[key] !== undefined
: hasOwnProperty.call(data, key);
}
/**
* Sets the hash `key` to `value`.
*
* @private
* @name set
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
function hashSet(key, value) {
var data = this.__data__;
this.size += this.has(key) ? 0 : 1;
data[key] = nativeCreate && value === undefined ? HASH_UNDEFINED : value;
return this;
}
// Add methods to `Hash`.
Hash.prototype.clear = hashClear;
Hash.prototype["delete"] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;
实现思路
Hash 是对 JavaScript 对象的一层轻量级封装,通过以下方式优化了键值对的存取:
- 使用
Object.create(null)
创建没有原型链的纯净对象,避免原型污染 - 使用特殊标记
HASH_UNDEFINED
解决无法存储 undefined 值的问题 - 维护
size
属性,方便快速获取哈希表大小 - 提供标准的 Map 类似接口(clear、delete、get、has、set)
Hash 适合存储字符串和基本类型键,在 MapCache 中被用于存储 string 和 hash 类型的键值对。相比于完整的 Map 实现,Hash 更轻量且性能更好,但只支持字符串和基本类型作为键。
源码解析
构造函数
javascript
function Hash(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
Hash 构造函数接收一个可选的 entries
参数,这是一个键值对数组。初始化过程如下:
- 初始化索引变量
index
为 -1 - 获取 entries 的长度,如果 entries 为 null 或 undefined,则长度为 0
- 调用
this.clear()
初始化内部存储结构 - 遍历 entries 数组,依次调用
this.set()
设置每个键值对
例如:
javascript
// 创建一个空的 Hash 实例
const emptyHash = new Hash();
// 内部结构: __data__ = {}, size = 0
// 创建一个带有初始数据的 Hash 实例
const hash = new Hash([
["name", "John"],
["age", 30],
]);
// 内部结构: __data__ = {name: 'John', age: 30}, size = 2
clear 方法
javascript
function hashClear() {
this.__data__ = nativeCreate ? nativeCreate(null) : {};
this.size = 0;
}
clear 方法重置哈希表,将 size 设为 0,并创建一个新的空存储对象:
- 如果环境支持
Object.create
,使用Object.create(null)
创建没有原型链的纯净对象 - 否则,回退到普通对象字面量
{}
- 将 size 计数器设为 0
使用 Object.create(null)
的好处是:
- 避免原型链查找,提高属性访问速度
- 防止原型污染和属性冲突(如
__proto__
、constructor
等) - 更安全的键值对存储
例如:
javascript
const hash = new Hash();
hash.set("name", "John");
hash.size; // => 1
hash.clear();
hash.size; // => 0
hash.get("name"); // => undefined
delete 方法
javascript
function hashDelete(key) {
var result = this.has(key) && delete this.__data__[key];
this.size -= result ? 1 : 0;
return result;
}
delete 方法删除指定键的值:
- 使用
this.has(key)
检查键是否存在 - 如果存在,使用 JavaScript 的
delete
操作符删除该属性 - 如果删除成功,将 size 减 1
- 返回是否删除成功的布尔值
例如:
javascript
const hash = new Hash();
hash.set("name", "John");
hash.size; // => 1
hash.delete("name"); // => true
hash.size; // => 0
hash.delete("age"); // => false (键不存在)
get 方法
javascript
function hashGet(key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return hasOwnProperty.call(data, key) ? data[key] : undefined;
}
get 方法获取指定键的值,处理逻辑分两种情况:
-
如果使用了
Object.create(null)
(nativeCreate 存在):- 直接获取
data[key]
的值 - 如果值等于
HASH_UNDEFINED
,返回真正的undefined
- 否则返回获取的值
- 直接获取
-
如果使用普通对象(nativeCreate 不存在):
- 使用
hasOwnProperty.call(data, key)
检查属性是否直接存在于对象上 - 如果存在,返回
data[key]
,否则返回undefined
- 使用
这里使用 HASH_UNDEFINED
是为了解决 JavaScript 对象无法直接存储 undefined
值的问题,因为在对象中设置 obj[key] = undefined
和删除该属性效果相同。
例如:
javascript
const hash = new Hash();
hash.set("name", "John");
hash.set("age", undefined);
hash.get("name"); // => 'John'
hash.get("age"); // => undefined (实际存储了特殊标记)
hash.get("city"); // => undefined (键不存在)
has 方法
javascript
function hashHas(key) {
var data = this.__data__;
return nativeCreate
? data[key] !== undefined
: hasOwnProperty.call(data, key);
}
has 方法检查指定键是否存在,同样分两种情况处理:
-
如果使用了
Object.create(null)
:- 检查
data[key] !== undefined
(注意这里不比较HASH_UNDEFINED
)
- 检查
-
如果使用普通对象:
- 使用
hasOwnProperty.call(data, key)
检查属性是否直接存在于对象上
- 使用
例如:
javascript
const hash = new Hash();
hash.set("name", "John");
hash.set("age", undefined);
hash.has("name"); // => true
hash.has("age"); // => true (虽然值为 undefined,但键存在)
hash.has("city"); // => false
set 方法
javascript
function hashSet(key, value) {
var data = this.__data__;
this.size += this.has(key) ? 0 : 1;
data[key] = nativeCreate && value === undefined ? HASH_UNDEFINED : value;
return this;
}
set 方法设置指定键的值:
- 如果键不存在(
!this.has(key)
),将 size 加 1 - 如果值为
undefined
且使用了Object.create(null)
,将值设为HASH_UNDEFINED
- 否则,直接设置值
- 返回 Hash 实例本身,支持链式调用
例如:
javascript
const hash = new Hash();
hash.set("name", "John").set("age", 30);
hash.size; // => 2
hash.set("name", "Jane"); // 更新现有键
hash.size; // => 2 (size不变)
hash.set("isActive", undefined);
// 内部存储为 { name: 'Jane', age: 30, isActive: '__lodash_hash_undefined__' }
总结
Hash 是 Lodash 中一个设计精妙的内部数据结构,它通过以下几个核心设计原则提供了高效的哈希表实现:
- 原型优化 :使用
Object.create(null)
创建无原型对象,避免原型链查找,提高性能 - 特殊值处理 :使用
HASH_UNDEFINED
标记解决 JavaScript 对象无法直接存储 undefined 值的问题 - API 设计:提供与 Map 类似的标准接口,使用简单直观
- 性能平衡:针对不同环境提供优雅降级,保证功能的同时优化性能
通过研究 Hash 的实现,我们可以学到:
- 如何基于 JavaScript 对象创建高效的哈希表
- 处理 JavaScript 特殊值(如 undefined)的技巧
- 面向接口编程的思想
- 如何针对不同环境优化代码实现
虽然现代 JavaScript 已经内置了 Map 和 Set 等数据结构,但 Hash 的实现依然值得学习,它展示了如何在特定场景下使用最小的代码实现高效的数据结构,是 Lodash 模块化、高性能设计理念的绝佳体现。