Lodash源码阅读-isFlattenable

Lodash 源码阅读-isFlattenable

概述

isFlattenable 是 Lodash 内部的一个判断函数,用来检查一个值是否可以被"展平"。所谓"展平"就是把嵌套的数组拍扁成一维数组,比如把 [1, [2, 3], 4] 变成 [1, 2, 3, 4]。这个函数在 Lodash 的 flattenflattenDeep 等数组扁平化函数中起着决定性作用 ------ 它决定哪些东西需要被拆开,哪些应该保持原样。

前置学习

依赖函数

  • isArray:检查一个值是否为数组
  • isArguments:检查一个值是否为函数的 arguments 对象

技术知识

  • Symbol.isConcatSpreadable :ES6 引入的一个特殊标记,用来自定义对象在 concat 操作时是否应该被展开
  • 短路逻辑 :使用 || 运算符进行多条件判断的技巧
  • 类数组对象:长得像数组但不是真正数组的对象(比如 arguments)

源码实现

javascript 复制代码
function isFlattenable(value) {
  return (
    isArray(value) ||
    isArguments(value) ||
    !!(spreadableSymbol && value && value[spreadableSymbol])
  );
}

其中 spreadableSymbol 是这样定义的:

javascript 复制代码
var spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined;

实现思路

isFlattenable 的判断逻辑很直接:

  1. 是数组就可以展平
  2. 是 arguments 对象也可以展平
  3. 如果对象自己设置了 Symbol.isConcatSpreadable 为 true,也可以展平

只要满足以上任意一个条件,函数就返回 true,表示这个值可以被展平。

源码解析

数组和 arguments 检查

javascript 复制代码
isArray(value) || isArguments(value);

这段代码检查值是否为数组或 arguments 对象:

  • 数组是最常见的可展平类型,用 Array.isArray() 判断
  • arguments 是函数内部的一个特殊对象,它不是真正的数组,但也可以被展平

Symbol.isConcatSpreadable 检查

javascript 复制代码
!!(spreadableSymbol && value && value[spreadableSymbol]);

这段代码检查对象是否标记了自己应该被展平:

  • 首先确保环境支持 Symbol 且值不是 null
  • 然后看对象是否有 Symbol.isConcatSpreadable 属性为 true
  • !! 把结果转为布尔值

这是 ES6 提供的一种机制,让开发者可以控制自定义对象在 concat 操作中的行为。

Symbol.isConcatSpreadable 的使用场景与展开规则

Symbol.isConcatSpreadable 允许自定义对象控制它们在扁平化过程中的行为。这个特性非常强大,但理解它的工作原理很重要:它不是指定展开哪个特定属性,而是决定是否按照类数组对象的方式展开对象

扁平化时的展开规则

当一个对象被标记为可扁平化(Symbol.isConcatSpreadabletrue)时,扁平化操作会:

  • 按照从 0length-1 的数字索引,依次获取对象的属性值
  • 仅展开这些数字索引属性,其他属性(如 itemsother)不会被处理
  • 必须有 length 属性来指定要展开的元素数量

具体应用场景如下:

  1. 类数组对象的自定义扁平化行为

    javascript 复制代码
    const myArrayLike = {
      0: "a", // 会被展开
      1: "b", // 会被展开
      2: "c", // 会被展开
      length: 3,
      other: "不会被展开",
      items: ["也不会被展开"],
      [Symbol.isConcatSpreadable]: true,
    };
    
    // 在设置Symbol.isConcatSpreadable后
    _.flatten([1, myArrayLike, 2]);
    // 结果: [1, "a", "b", "c", 2]
    // 只有数字索引属性被展平,other和items属性不会被展开

    如果没有 length 属性,即使设置了 Symbol.isConcatSpreadable,也不会正确展开:

    javascript 复制代码
    const badObj = {
      0: "a",
      1: "b",
      // 没有 length 属性
      [Symbol.isConcatSpreadable]: true,
    };
    
    // 由于没有 length 属性,展开无效
    _.flatten([1, badObj, 2]); // [1, {0:"a", 1:"b", ...}, 2]
  2. 阻止数组被扁平化

    反过来,我们也可以阻止一个真正的数组被扁平化:

    javascript 复制代码
    const arr = [1, 2, 3];
    arr[Symbol.isConcatSpreadable] = false;
    
    // 设置Symbol.isConcatSpreadable为false后
    _.flatten([0, arr, 4]);
    // 结果: [0, [1, 2, 3], 4]
    // 尽管arr是数组,但由于显式设置了不展开,所以保持原样
  3. 自定义对象集合的扁平化

    当我们创建自定义集合类时,需要注意展开的是数字索引属性,不是其他普通属性:

    javascript 复制代码
    class MyCollection {
      constructor(items) {
        this.items = items;
        this.length = items.length;
    
        // 这段代码是关键 - 把items的元素复制到当前对象的数字索引属性上
        for (let i = 0; i < items.length; i++) {
          this[i] = items[i];
        }
      }
    
      // 控制扁平化行为的getter
      get [Symbol.isConcatSpreadable]() {
        return true; // 允许被扁平化
      }
    }
    
    const collection = new MyCollection(["x", "y", "z"]);
    
    _.flatten([1, collection, 2]);
    // 结果: [1, "x", "y", "z", 2]
    // 展开的是collection的0,1,2属性,不是items属性

    如果不把元素复制到数字索引上,扁平化会失败:

    javascript 复制代码
    class BadCollection {
      constructor(items) {
        this.items = items; // 有items数组
        this.length = items.length; // 有正确的length
        // 但没有把items的元素复制到数字索引上
      }
    
      get [Symbol.isConcatSpreadable]() {
        return true;
      }
    }
    
    const collection = new BadCollection(["x", "y", "z"]);
    _.flatten([1, collection, 2]);
    // 结果: [1, undefined, undefined, undefined, 2]
    // 因为对象有length=3,但0,1,2索引处的值都是undefined
  4. 条件性扁平化

    我们可以基于某些条件动态决定是否扁平化:

    javascript 复制代码
    const specialObject = {
      0: "a",
      1: "b",
      length: 2,
      _shouldFlatten: true,
    
      get [Symbol.isConcatSpreadable]() {
        return this._shouldFlatten;
      },
    };
    
    // 可以动态切换扁平化行为
    specialObject._shouldFlatten = true;
    _.flatten([1, specialObject, 2]); // [1, "a", "b", 2]
    
    specialObject._shouldFlatten = false;
    _.flatten([1, specialObject, 2]); // [1, {0:"a", 1:"b", length:2, ...}, 2]
  5. 与原生 Array.concat 保持一致

    这个行为是为了保持与原生数组 concat 方法的一致性:

    javascript 复制代码
    const arr = [1, 2];
    const obj = { 0: "a", 1: "b", length: 2, [Symbol.isConcatSpreadable]: true };
    
    // 原生 concat 方法也是按照数字索引展开
    arr.concat(obj); // [1, 2, "a", "b"]

总结来说,Symbol.isConcatSpreadable 标记的是"这个对象的行为应该像数组一样",当一个对象被标记为可扁平化时,将按照类数组对象的约定(数字索引 + length)进行展开。这也是为什么在使用这个特性时,通常需要使对象符合类数组对象的结构。

环境兼容处理

javascript 复制代码
var spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined;

这行代码是为了兼容旧环境:

  • 检查全局是否有 Symbol
  • 没有的话就用 undefined 代替,这样第三个条件永远不会满足
  • 这样在老旧浏览器中也能正常工作,只是功能会受限

在 Lodash 源码中,isFlattenable 主要用在 baseFlatten 函数里:

javascript 复制代码
function baseFlatten(array, depth, ...) {
  // ...
  for (const value of array) {
    if (depth > 0 && isFlattenable(value)) {
      // 需要展平的情况
      // ...
    } else {
      // 不展平的情况
      // ...
    }
  }
  // ...
}

这个 baseFlatten 函数是很多高级函数的基础,比如:

  • _.flatten:展平一层
  • _.flattenDeep:完全展平所有层级
  • _.flattenDepth:按指定深度展平

总结

isFlattenable 这个小函数展示了几个重要的编程思想:

  1. 关注点分离

    • 把"判断是否可展平"这个逻辑独立出来
    • 让代码更清晰,更容易维护
  2. 开放性设计

    • 支持通过 Symbol 扩展自定义对象的行为
    • 不需要修改源码就能改变函数的判断结果
  3. 兼容性思考

    • 考虑到了不同环境的差异
    • 在不支持新特性的环境中优雅降级
相关推荐
luoganttcc43 分钟前
Cesium 加载 本地 b3dm 格式文件 并且 获取鼠标点击处经纬度 (亲测可用)
前端·javascript·3d
云边有个稻草人2 小时前
【Web前端技术】第二节—HTML标签(上)
前端·html·html基本结构标签·html超链接标签·html中的注释和特殊字符·vscode的使用·vscode生成骨架标签
介si啥呀~2 小时前
解决splice改变原数组的BUG(拷贝数据)
java·前端·bug
太阳花ˉ2 小时前
BFC详解
前端
小满zs4 小时前
React-router v7 第五章(路由懒加载)
前端·react.js
Aotman_4 小时前
Vue el-from的el-form-item v-for循环表单如何校验rules(二)
前端·javascript·vue.js
BillKu5 小时前
Vue3父子组件数据双向绑定示例
javascript·vue.js·elementui
在无清风6 小时前
Java实现Redis
前端·windows·bootstrap
_一条咸鱼_7 小时前
Vue 配置模块深度剖析(十一)
前端·javascript·面试
yechaoa8 小时前
Widget开发实践指南
android·前端