Skip to content

JavaScript

温馨提示

高阶应用学习可前往 MDN 官网

递归运用

1. 扁平化

js
// 示例数据
const treeData = [
  {
    id: 1,
    name: "Node 1",
    children: [
      { id: 2, name: "Node 2", children: [] },
      {
        id: 3,
        name: "Node 3",
        children: [{ id: 4, name: "Node 4", children: [] }],
      },
    ],
  },
];
function flattTree(tree) {
  let result = [];
  for (let node of tree) {
    result.push({ ...node });
    if (node.children) {
      result = result.concat(flattTree(node.children));
    }
  }
  return result;
}

let a = flattTree(treeData);
console.log(a);
// [
//   { id: 1, name: "Node 1", children: [[Object], [Object]] },
//   { id: 2, name: "Node 2", children: [] },
//   { id: 3, name: "Node 3", children: [[Object]] },
//   { id: 4, name: "Node 4", children: [] },
// ];

2. 表单校验

js
function validateTree(tree) {
  for (let node of tree) {
    if (!node.name) return false;
    if (node.children && validateTree(node.children)) return false;
  }
  return true;
}
// 示例数据
const treeData = [
  {
    id: 1,
    name: "Node 1",
    children: [
      { id: 2, name: "", children: [] }, // 无效
      { id: 3, name: "Node 3", children: [] },
    ],
  },
];
// 使用
const isValid = validateTree(treeData);
console.log(isValid); // false

3. 累加

js
// 示例数据
const treeData = [
  {
    id: 1,
    value: 10,
    weight: 5,
    children: [
      { id: 2, value: 20, weight: 10, children: [] },
      {
        id: 3,
        value: 30,
        weight: 15,
        children: [{ id: 4, value: 40, weight: 20, children: [] }],
      },
    ],
  },
];
function sumFnc(tree, attr) {
  let sum = 0;
  for (let node of tree) {
    sum += node[attr] || 0;
    if (node.children) {
      sum += sumFnc(node.children, attr);
    }
  }
  return sum;
}
// 使用:累加 `weight` 属性
const totalWeight = sumFnc(treeData, "weight");
console.log(totalWeight); // 输出: 50

4. 查找数据

js
// 示例数据
const treeData = [
  {
    id: 1,
    name: "Node 1",
    children: [
      { id: 2, name: "Node 2", children: [] },
      {
        id: 3,
        name: "Node 3",
        children: [{ id: 4, name: "Node 4", children: [] }],
      },
    ],
  },
];
function queryId(tree, id) {
  let result = {};
  for (let node of tree) {
    if (node.id === id) return (result = { ...node });
    if (node.children) {
      return queryId(node.children, id);
    }
  }
  return result;
}
let result = queryId(treeData, 4);
console.log(result);

5. 面包屑

js
function generateBreadCrumb(node) {
  const breadcrumb = [];
  let current = node.parent;
  while (node) {
    breadcrumb.unshift(node.name); // 从当前节点往根节点方向收集名称
    current = node.parent;
  }
  return breadcrumb;
}

// 示例数据
const currentNode = {
  id: 4,
  name: "Sub-subcategory 1",
  parent: {
    id: 3,
    name: "Subcategory 2",
    parent: {
      id: 1,
      name: "Category 1",
      parent: null,
    },
  },
};

// 使用
const breadcrumb = generateBreadCrumb(currentNode);
console.log(breadcrumb);
// 输出: ['Category 1', 'Subcategory 2', 'Sub-subcategory 1']

节流

动作

保证在一定时间间隔内最多执行一次

场景

滚动,鼠标拖拽

完整版

js
/**
 * @param {Function} fn 需要节流的函数
 * @param {number} wait 等待时间(毫秒)
 * @param {Object} options 配置选项
 * @param {boolean} [options.leading=true] 是否立即执行
 * @param {boolean} [options.trailing=true] 是否在等待时间结束后执行
 */
export function throttle(fn, wait, options = {}) {
  let timer = null;
  let previous = 0; // 上次执行时间

  // 默认配置
  const { leading = true, trailing = true } = options;

  return function (...args) {
    const now = Date.now();

    // 如果是第一次调用且不需要首次执行
    if (!previous && leading === false) {
      previous = now;
    }

    const remaining = wait - (now - previous); // 剩余等待时间

    // 如果已经到了等待时间或者是第一次调用
    if (remaining <= 0 || remaining > wait) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      previous = now;
      fn.apply(this, args);
    } else if (!timer && trailing) {
      // 如果还在等待时间内,且允许尾部执行,设置定时器
      timer = setTimeout(() => {
        previous = leading ? Date.now() : 0;
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

丐版

js
/* 等待执行 */
function throrrle(fnc, wait) {
  let timer = null;
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fnc(...args);
        timer = null;
      }, wait);
    }
  };
}
/* 立即执行 */
function throttle(fnc, wait) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= wait) {
      fnc(...args);
      lastTime = now;
    }
  };
}

防抖

动作

在 n 秒内多次执行,只执行最后一次,如果在 n 秒内再次触发,则重新计时

场景

搜索框输入,窗口大小改变

完整版

js
/**
 * @param {Function} fn 需要防抖的函数
 * @param {number} wait 等待时间(毫秒)
 * @param {Object} options 配置选项
 *
 */
export function debounce(fn, wait, options = {}) {
  let timer = null;

  // 默认配置
  const { leading = false, trailing = true } = options;

  return function (...args) {
    if (timer) {
      clearTimeout(timer);
    }

    if (leading) {
      const callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, wait);
      if (callNow) {
        fn.apply(this, args);
      }
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args);
      }, wait);
    }
  };
}

丐版

js
function debounce(fn, wait) {
  let timer = null;
  return function (...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn(...args);
    }, wait);
  };
}

元素类型判断选择

  • 基本数据类型判断

tex
选择 typeof 性能优于 Object.prototype.toString.call()
- typeof的缺陷
console.log(typeof null); // "object" (这是 JS 的历史遗留问题)
console.log(typeof {}); // "object"
console.log(typeof []); // "object" (数组也是对象)
  • 数据类型不确定,且要求精确度

js
选择 Object.prototype.toString.call()
console.log(Object.prototype.toString.call(123)); // "[object Number]"
console.log(Object.prototype.toString.call('hello')); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(function(){})); // "[object Function]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
console.log(Object.prototype.toString.call(/abc/)); // "[object RegExp]"
tex
选择Array.isArray()最高效标准

数据比较

1. 浮点数对比

提示

直接使用 === 比较浮点数可能会导致意外结果。推荐使用一个容忍误差的比较方法。

js
function areNumbersEqual(num1, num2, precision = 1e-10) {
  return Math.abs(num1 - num2) < precision;
}

console.log(areNumbersEqual(0.1 + 0.2, 0.3)); // 输出: true
console.log(areNumbersEqual(0.1 + 0.2, 0.3000000001)); // 输出: false

业务场景较多,建议使用

专业的数字处理库(如 Decimal.jsBigNumber.js)来进行高精度计算

特性Decimal.jsBigNumber.js
精度高精度的浮点数计算,支持任意精度高精度的数字处理,支持任意精度
性能性能较慢,适合需要高精度的应用性能较好,尤其是在处理较大数值时
库体积相对较大相对较小
兼容性支持现代浏览器和 Node.js,需引入额外的 Polyfill支持现代浏览器和 Node.js,无需 Polyfill
API 风格提供了类似数值的 API提供了更为传统的链式 API
使用场景需要高精度、浮动计算的场景,如金融计算需要高精度、快速计算的场景,如大数计算
API 易用性API 简单,类似数学运算API 也较为简单,但支持更多的自定义操作
依赖性无依赖无依赖

Decimal.js 示例

javascript
// 引入 Decimal.js
import Decimal from "decimal.js";

// 创建 Decimal 实例
const num1 = new Decimal(0.1);
const num2 = new Decimal(0.2);

// 加法操作
const result = num1.plus(num2);

console.log(result.toString()); // 输出 "0.3"

BigNumber.js 示例

javascript复制代码// 引入 BigNumber.js
import BigNumber from 'bignumber.js';

// 创建 BigNumber 实例
const num1 = new BigNumber(0.1);
const num2 = new BigNumber(0.2);

// 加法操作
const result = num1.plus(num2);

console.log(result.toString()); // 输出 "0.3"

总结

  • Decimal.js 更适合于需要极高精度的计算,特别是在金融、科学计算等领域。
  • BigNumber.js 更适合快速处理大数值计算,且性能较优。

2.数据对比

场景

用户个人信息修改时的检查

2.1 数据量较小的情况下

提醒

数据量超过 1000,频繁触发,就会出现性能问题

js
function compareData(localData, remoteData) {
  // 比较两个数组中的对象,找出差异
  let added = localData.filter((item) => !remoteData.includes(item));
  let removed = remoteData.filter((item) => !localData.includes(item));
  let updated = localData.filter(
    (item) =>
      remoteData.includes(item) &&
      item !== remoteData.find((d) => d.id === item.id)
  );
  return { added, removed, updated };
}

2.2 降低时间复杂度

js
// 方案1 使用Set数据结构
// 差异检测:通过比较 id 来找出新增和删除的记录,使用 JSON.stringify() 来深度比较对象的内容,避免了对象属性顺序不同的问题。对于复杂的对象比较,可以根据需要自定义深度比较的方式。
function compareData(localData, remoteData) {
  const added = [];
  const removed = [];
  const updated = [];

  const localDataMap = new Map(localData.map((item) => [item.id, item]));
  const remoteDataMap = new Map(remoteData.map((item) => [item.id, item]));

  // 遍历本地数据
  for (const [id, localItem] of localDataMap) {
    const remoteItem = remoteDataMap.get(id);

    if (!remoteItem) {
      added.push(localItem); // 新增的记录
    } else if (JSON.stringify(localItem) !== JSON.stringify(remoteItem)) {
      updated.push(localItem); // 更新的记录
    }
  }

  // 遍历远程数据,找出删除的记录
  for (const [id, remoteItem] of remoteDataMap) {
    if (!localDataMap.has(id)) {
      removed.push(remoteItem); // 删除的记录
    }
  }

  return { added, removed, updated };
}
// 方案2 进阶 一次遍历
function compareData(localData, remoteData) {
  const added = [];
  const removed = [];
  const updated = [];

  const localDataMap = new Map(localData.map((item) => [item.id, item]));
  const remoteDataMap = new Map(remoteData.map((item) => [item.id, item]));

  // 遍历本地数据
  for (const [id, localItem] of localDataMap) {
    const remoteItem = remoteDataMap.get(id);

    if (!remoteItem) {
      added.push(localItem); // 新增的记录
    } else if (JSON.stringify(localItem) !== JSON.stringify(remoteItem)) {
      updated.push(localItem); // 更新的记录
    }
  }

  // 遍历远程数据,找出删除的记录
  for (const [id, remoteItem] of remoteDataMap) {
    if (!localDataMap.has(id)) {
      removed.push(remoteItem); // 删除的记录
    }
  }

  return { added, removed, updated };
}

3. 数据合并

对象合并

js
function mergeData(oldData, newData) {
  for (let key in newData) {
    if (newData[key] !== oldData[key]) {
      oldData[key] = newData[key]; // 更新旧数据
    }
  }
  return oldData;
}

场景:数组对象合并

检查 形参1 中是否有与 形参2 匹配的 id,如果没有匹配到,就将 形参2 中的对象添加到 形参1 中。最终结果是:将 形参2 中的数据合并到 形参1 中,同时保证所有的 id 都被包含

js
function mergeArrays(arr1, arr2) {
  // 将 arr1 和 arr2 合并,首先合并 arr1 中已有的匹配项
  arr2.forEach((item2) => {
    // 查找 arr1 中是否有与 item2 相同 id 的项
    const index = arr1.findIndex((item1) => item1.id === item2.id);

    if (index !== -1) {
      // 如果找到匹配项,则合并数据
      arr1[index] = { ...arr1[index], ...item2 };
    } else {
      // 如果没有匹配项,则将 item2 添加到 arr1
      arr1.push(item2);
    }
  });

  return arr1;
}

// 示例数据
const arr1 = [
  { id: 1, name: "John", age: 28 },
  { id: 2, name: "Jane", age: 24 },
];

const arr2 = [
  { id: 1, name: "John名字改变了", address: "New York" }, 
  { id: 2, address: "Los Angeles" },
  { id: 3, name: "Tom", age: 30 },
];

// 调用 mergeArrays 函数
const mergedResult = mergeArrays(arr1, arr2);

// 输出合并后的结果
console.log(mergedResult);
[
  {
    id: 1,
    name: "John名字改变了",
    age: 28,
    address: "New York",
  },
  {
    id: 2,
    name: "Jane",
    age: 24,
    address: "Los Angeles",
  },
  {
    id: 3,
    name: "Tom",
    age: 30,
  },
];

4. 数组去重

4.1 一维 数据量庞大

TIP

当数组的数量非常大时,性能最佳的去重方案通常依赖于使用 Set 或 Map 数据结构,因为它们在大多数情况下能提供常数时间复杂度 (O(1)) 的查找和插入操作。

js
function removeDuplicatesById(arr) {
  const seen = new Map();
  const result = [];

  for (const item of arr) {
    if (!seen.has(item.id)) {
      seen.set(item.id, true); // 只要没有这个 id,就插入 Map
      result.push(item);
    }
  }
  return result;
}
const arr = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 1, name: "Alice Updated" },
  { id: 3, name: "Charlie" },
  { id: 2, name: "Bob Updated" },
  { id: 4, name: "David" },
];

const uniqueArr = removeDuplicatesById(arr);
console.log(uniqueArr);
// 输出: [
//   { id: 1, name: 'Alice' },
//   { id: 2, name: 'Bob' },
//   { id: 3, name: 'Charlie' },
//   { id: 4, name: 'David' }
// ]

4.2 多维

数组对象中,有多级嵌套

1 先递归处理

2 在展平数组的过程中,我们使用 MapSet 来确保每个 id 只出现一次。

js
function flattenArray(arr) {
  const result = [];

  // 递归扁平化数组
  arr.forEach((item) => {
    result.push(item); // 加入当前对象
    if (item.children && item.children.length > 0) {
      result.push(...flattenArray(item.children)); // 递归扁平化 children 数组
    }
  });

  return result;
}

function removeDuplicatesById(arr) {
  const flattenedArr = flattenArray(arr); // 扁平化处理
  const seen = new Map();
  const result = [];

  for (const item of flattenedArr) {
    if (item && item.id && !seen.has(item.id)) {
      seen.set(item.id, true); // 使用 Map 按 id 去重
      result.push(item);
    }
  }

  return result;
}
const arr = [
  {
    id: 1,
    name: "Alice",
    children: [
      {
        id: 4,
        name: "David",
        children: [
          {
            id: 5,
            name: "Eva",
            children: [],
          },
        ],
      },
    ],
  },
  {
    id: 2,
    name: "Bob",
  },
  {
    id: 3,
    name: "Alice Updated",
  },
  {
    id: 1,
    name: "Alice Duplicate",
    children: [],
  },
];

const uniqueArr = removeDuplicatesById(arr);
console.log(uniqueArr);
// [
//   { id: 1, name: "Alice", children: [[Object]] },
//   { id: 2, name: "Bob" },
//   { id: 3, name: "Alice Updated" },
//   { id: 4, name: "David", children: [[Object]] },
//   { id: 5, name: "Eva", children: [] },
// ];

5.树形结构数据映射

场景

树形控件,勾选的节点数据,映射到另外一个树形控件,且子夫关系要包含进去。左:默认值范围,右:默认值

js
// 源树的数据
const sourceData = [
  {
    label: "节点1",
    id: 1,
    children: [
      {
        label: "子节点1-1",
        id: 11,
        children: [{ label: "子节点1-1-1", id: 111 }],
      },
      { label: "子节点1-2", id: 12 },
    ],
  },
  {
    label: "节点2",
    id: 2,
    children: [
      { label: "子节点2-1", id: 21 },
      { label: "子节点2-2", id: 22 },
    ],
  },
];

// 用于存储选中节点的 ID 数组
let checkedNodeIds = [1, 11, 111, 22]; // 假设选中了节点1、子节点1-1、子节点1-1-1 和 子节点2-2

// 递归函数:根据选中的节点生成目标树
function createNode(node, selectedNodeIds) {
  // 如果节点为空,则返回 null
  if (!node) return null;

  // 判断当前节点是否被选中
  const isSelected = selectedNodeIds.includes(node.id);

  // 递归处理子节点,确保每层子节点都正确处理
  const children = node.children
    ? node.children
        .map((childNode) => createNode(childNode, selectedNodeIds))
        .filter(Boolean)
    : [];

  // 如果当前节点被选中,或者它有选中的子节点,则将其添加到目标树
  if (isSelected || children.length > 0) {
    return {
      ...node,
      children, // 保持子节点关系
    };
  }

  return null;
}

// 遍历源树数据,生成目标树
function generateTargetTree(sourceData, checkedNodeIds) {
  return sourceData
    .map((rootNode) => createNode(rootNode, checkedNodeIds))
    .filter(Boolean); // 过滤掉没有选中或无子节点的节点
}

// 生成目标树的数据
const targetData = generateTargetTree(sourceData, checkedNodeIds);

// 输出目标树的数据
console.log("目标树数据:", JSON.stringify(targetData, null, 2));
// 目标树数据: [
//   {
//     "label": "节点1",
//     "id": 1,
//     "children": [
//       {
//         "label": "子节点1-1",
//         "id": 11,
//         "children": [
//           {
//             "label": "子节点1-1-1",
//             "id": 111
//           }
//         ]
//       }
//     ]
//   },
//   {
//     "label": "节点2",
//     "id": 2,
//     "children": [
//       {
//         "label": "子节点2-2",
//         "id": 22
//       }
//     ]
//   }
// ]

函数柯里化

作用

一个多参数的函数转换成一系列每次接受一个参数的函数,并且返回接收余下参数的新函数

例子

jsx
function add(a, b) {
  return a + b;
}

如果我们使用柯里化,将其转换成一系列接受单一参数的函数:

jsx
function curriedAdd(a) {
  return function (b) {
    return a + b;
  };
}

封装

js
function curry(fn) {
  const arity = fn.length; // 获取原函数的参数个数
  return function curried(...args) {
    if (args.length >= arity) {
      return fn(...args); // 参数满足数量,执行原函数
    } else {
      return function (...next) {
        return curried(...args, ...next); // 继续接收参数
      };
    }
  };
}