Lodash源码阅读-Hash

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 对象的一层轻量级封装,通过以下方式优化了键值对的存取:

  1. 使用 Object.create(null) 创建没有原型链的纯净对象,避免原型污染
  2. 使用特殊标记 HASH_UNDEFINED 解决无法存储 undefined 值的问题
  3. 维护 size 属性,方便快速获取哈希表大小
  4. 提供标准的 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 参数,这是一个键值对数组。初始化过程如下:

  1. 初始化索引变量 index 为 -1
  2. 获取 entries 的长度,如果 entries 为 null 或 undefined,则长度为 0
  3. 调用 this.clear() 初始化内部存储结构
  4. 遍历 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,并创建一个新的空存储对象:

  1. 如果环境支持 Object.create,使用 Object.create(null) 创建没有原型链的纯净对象
  2. 否则,回退到普通对象字面量 {}
  3. 将 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 方法删除指定键的值:

  1. 使用 this.has(key) 检查键是否存在
  2. 如果存在,使用 JavaScript 的 delete 操作符删除该属性
  3. 如果删除成功,将 size 减 1
  4. 返回是否删除成功的布尔值

例如:

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 方法获取指定键的值,处理逻辑分两种情况:

  1. 如果使用了 Object.create(null)(nativeCreate 存在):

    • 直接获取 data[key] 的值
    • 如果值等于 HASH_UNDEFINED,返回真正的 undefined
    • 否则返回获取的值
  2. 如果使用普通对象(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 方法检查指定键是否存在,同样分两种情况处理:

  1. 如果使用了 Object.create(null)

    • 检查 data[key] !== undefined(注意这里不比较 HASH_UNDEFINED
  2. 如果使用普通对象:

    • 使用 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 方法设置指定键的值:

  1. 如果键不存在(!this.has(key)),将 size 加 1
  2. 如果值为 undefined 且使用了 Object.create(null),将值设为 HASH_UNDEFINED
  3. 否则,直接设置值
  4. 返回 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 中一个设计精妙的内部数据结构,它通过以下几个核心设计原则提供了高效的哈希表实现:

  1. 原型优化 :使用 Object.create(null) 创建无原型对象,避免原型链查找,提高性能
  2. 特殊值处理 :使用 HASH_UNDEFINED 标记解决 JavaScript 对象无法直接存储 undefined 值的问题
  3. API 设计:提供与 Map 类似的标准接口,使用简单直观
  4. 性能平衡:针对不同环境提供优雅降级,保证功能的同时优化性能

通过研究 Hash 的实现,我们可以学到:

  1. 如何基于 JavaScript 对象创建高效的哈希表
  2. 处理 JavaScript 特殊值(如 undefined)的技巧
  3. 面向接口编程的思想
  4. 如何针对不同环境优化代码实现

虽然现代 JavaScript 已经内置了 Map 和 Set 等数据结构,但 Hash 的实现依然值得学习,它展示了如何在特定场景下使用最小的代码实现高效的数据结构,是 Lodash 模块化、高性能设计理念的绝佳体现。

相关推荐
关山月几秒前
React 中的 SSR 深度探讨
前端
yzhSWJ35 分钟前
vue设置自定义logo跟标题
前端·javascript·vue.js
江沉晚呤时1 小时前
深入解析 C# 中的装饰器模式(Decorator Pattern)
java·开发语言·javascript·jvm·microsoft·.netcore
vvilkim1 小时前
Vue.js 中的 Tree Shaking:优化你的应用性能
前端·javascript·vue.js
杨超越luckly1 小时前
HTML应用指南:利用GET请求获取猫眼电影日票房信息——以哪吒2为例
前端·数据挖掘·数据分析·html·猫眼
Front_Yue1 小时前
Three.js中的加载器与资源管理:构建丰富3D场景的关键
javascript·3d·three.js
狼性书生1 小时前
uniapp 实现的下拉菜单组件
前端·uni-app·vue·组件·插件
浪裡遊1 小时前
uniapp中的vue组件与组件使用差异
前端·vue.js·uni-app
努力的飛杨2 小时前
学习记录-js进阶-性能优化
开发语言·javascript·学习
风无雨2 小时前
react 中 key 的使用
前端·react.js·前端框架