React 17

1 React 状态更新的 "不可变性" 原则

要理解 React 中操作数组 state 的这张参考表,需从 React 状态更新的 "不可变性" 原则 入手:React 依赖状态的 "引用变化" 来识别更新,若直接修改原始数组(可变操作),React 可能无法检测到变化,导致组件不重新渲染。以下对表格内容逐一详解:

1. 添加元素

  • 避免使用push(在数组末尾添加元素)、unshift(在数组开头添加元素)。原因:这两个方法会直接修改原始数组,破坏状态的不可变性。
  • 推荐使用concat展开语法 ([...arr])
    • concat:它会返回一个新数组 ,包含原始数组和新增元素。例如:const newArr = arr.concat(newItem)
    • 展开语法:通过 [...原数组, 新元素] 生成新数组。例如:const newArr = [...arr, newItem](末尾添加),const newArr = [newItem, ...arr](开头添加)。

2. 删除元素

  • 避免使用pop(删除数组最后一个元素)、shift(删除数组第一个元素)、splice(通过索引删除元素)。原因:这些方法会直接修改原始数组
  • 推荐使用filterslice
    • filter:通过回调函数筛选元素,返回新数组 (不包含被删除的元素)。例如,删除值为 target 的元素:const newArr = arr.filter(item => item !== target)
    • slice:用于截取数组片段,返回新数组 。例如,删除第一个元素:const newArr = arr.slice(1);删除最后一个元素:const newArr = arr.slice(0, -1)

3. 替换元素

  • 避免使用splice(通过索引替换元素)、直接对数组索引赋值(如 arr[i] = newVal)。原因:这两种方式都会直接修改原始数组

  • 推荐使用mapmap 会遍历数组,对每个元素执行回调函数并返回新数组 。例如,将索引为 i 的元素替换为 newVal

    javascript 复制代码
    const newArr = arr.map((item, index) => {
      if (index === i) return newVal;
      return item;
    });

4. 排序

  • 避免使用reverse(反转数组)、sort(排序数组)。原因:这两个方法会直接修改原始数组

  • 推荐使用先复制数组,再执行排序操作 。先通过展开语法或 slice 复制数组,再调用排序方法(此时修改的是复制后的新数组,不影响原始状态)。例如:

    javascript 复制代码
    // 排序
    const newArr = [...arr].sort((a, b) => a - b);
    // 反转
    const newArr = [...arr].reverse();

补充:Immer 库的作用

如果觉得上述 "不可变操作" 过于繁琐,可以使用 Immer 库 。它允许你以 "可变" 的写法操作状态,底层会自动生成不可变的新状态。这样你就可以直接使用 pushsplice 等方法,而不用担心破坏 React 状态的不可变性。

2 slicesplice 方法

1. 方法作用差异

方法 作用 是否修改原始数组
slice 拷贝数组或数组的一部分 否(返回新数组)
splice 插入或删除元素 是(直接修改)

2. React 中的使用建议

在 React 中,由于状态更新需要遵循不可变性原则 (即不能直接修改原始状态,需返回新状态让 React 识别更新),因此更推荐使用 slice,而应避免使用会直接修改原始数组的 splice

3 JavaScript 的引用类型特性状态管理的不可变性原则

1. 数组的 "浅拷贝" 问题

代码中 const myNextList = [...myList]; 是对 myList 数组的浅拷贝

  • 浅拷贝只会复制数组的 "表层结构"(即数组的长度、元素的引用),但数组内部的对象元素(如 artwork并不会被重新创建,而是仍然指向原数组中对象的内存地址
  • 换句话说,myNextListmyList 虽然是两个不同的数组,但它们内部的 artwork 对象是 "同一个"(共享同一块内存)。

2. 直接修改对象带来的副作用

当执行 artwork.seen = nextSeen; 时,因为 artwork 是原数组 myList 中对象的引用,所以这个修改会直接影响原数组中的对象

  • 如果还有其他地方(比如 yourList)也引用了这个 artwork 对象,那么这些地方的状态也会被意外修改,从而引发难以排查的 bug。

3. 如何解决?(遵循 "不可变" 原则)

要避免直接修改原有对象,需要创建新的对象副本 来承载修改。可以用 map 方法实现:

javascript 复制代码
const myNextList = myList.map(item => {
  if (item.id === artworkId) {
    // 对匹配的对象,返回一个新的对象(包含修改后的属性)
    return { ...item, seen: nextSeen };
  }
  // 不匹配的对象,返回原对象(保持不变)
  return item;
});
setMyList(myNextList);

这样做的核心是:修改时创建新对象,而非直接修改原有对象,从而保证原数组的状态不会被意外污染,也符合 React 等框架对状态 "不可变" 的设计预期。

4 状态管理中数组操作的 "不可变性" 原则

这部分内容是关于状态管理中数组操作的 "不可变性" 原则(常见于 React 等前端框架的状态管理场景),以下是逐条详解:

1. "你可以把数组放入 state 中,但你不应该直接修改它。"

  • 框架(如 React)的状态更新机制依赖 "引用变化" 来识别更新。如果直接修改 state 中的数组(比如 state.arr[0] = 1),数组的引用地址没有变化,框架可能无法识别到状态更新,导致界面不渲染;同时直接修改会破坏 "不可变性",引发难以排查的副作用(比如多个地方共享状态时的意外污染)。

2. "不要直接修改数组,而是创建它的一份新的拷贝,然后使用新的数组来更新它的状态。"

  • 为了保证状态的 "不可变性",任何对数组的修改都需要创建新的数组实例 。例如原数组是 [1,2,3],要修改第一个元素,需创建 [4,2,3] 这个新数组,再用它更新状态。这样框架能通过引用变化识别更新,也能避免副作用。

3. "你可以使用 [...arr, newItem] 这样的数组展开语法来向数组中添加元素。"

  • 这是创建 "新数组" 的常用技巧。例如原数组 arr = [1,2],执行 const newArr = [...arr, 3] 后,newArr[1,2,3],且 newArr全新的数组引用 ,原 arr 不会被修改。这种方式既实现了 "添加元素" 的逻辑,又保证了不可变性。

4. "你可以使用 filter()map() 来创建一个经过过滤或者变换的数组。"

  • filter():用于 "过滤元素",会返回一个新数组 。例如 const newArr = arr.filter(item => item > 2),原 arr 不变,newArr 是过滤后的新数组。
  • map():用于 "变换元素",会返回一个新数组 。例如 const newArr = arr.map(item => item * 2),原 arr 不变,newArr 是元素变换后的新数组。
  • 这两个方法天然符合 "不可变性",因为它们都不会修改原数组,而是返回新数组。

5. "你可以使用 Immer 来保持代码简洁。"

  • Immer 是一个 JavaScript 库,它的核心是 "用 mutable 的写法实现 immutable 的效果 "。在处理复杂状态(比如嵌套数组、对象)时,直接写 "修改式" 代码会很繁琐,而 Immer 可以让你以更简洁的方式创建新状态。例如:

    javascript 复制代码
    import { produce } from 'immer';
    
    const newState = produce(originalState, draft => {
      draft.arr[0] = 1; // 看似直接修改,但 Immer 会自动生成新的不可变状态
    });

    它隐藏了 "创建新拷贝" 的细节,让代码更简洁易读。

总结 :这些规则的核心是保证状态的 "不可变性"------任何状态修改都要通过 "创建新引用" 来实现,而非直接修改原状态。这既是前端框架状态管理的最佳实践,也能从根源上避免因共享引用导致的副作用 bug。

5 JavaScript 分号使用与数组更新注意事项

一、分号的正确使用规范

在 JavaScript 中,分号用于标记语句的结束,虽然存在 "自动分号插入(ASI)" 机制,但并非所有场景都能可靠生效,因此建议主动添加分号以避免语法错误。

1. 必须添加分号的场景

  • 变量声明语句结尾

    javascript 复制代码
    const initialProducts = [/* ... */]; // 数组赋值后需加分号
    const [products, setProducts] = useState(initialProducts); // 解构赋值后需加分号
  • 函数体内的执行语句结尾

    javascript 复制代码
    function handleIncreaseClick(productId) {
      setProducts(products.map(product => {
        if (product.id === productId) {
          return { ...product, count: product.count + 1 }; // return语句的对象后加分号
        }
        return product; // return语句的返回值后加分号
      })); // map函数调用结束后加分号
    }
  • 对象 / 数组字面量作为独立语句时

    javascript 复制代码
    // 错误示例(可能被解析为其他语法)
    const obj = { x: 1 }
    [1, 2, 3].forEach(...)
    
    // 正确示例
    const obj = { x: 1 };
    [1, 2, 3].forEach(...);

2. 分号的作用

  • 明确语句边界,避免 JavaScript 引擎误解析(例如将换行后的代码拼接为同一语句)。
  • 提高代码可读性,让开发者清晰区分不同语句的范围。

二、数组更新中的核心注意事项(以购物车为例)

使用 map 方法更新数组时,需注意以下两点:

1. 正确引用当前元素的属性

map 循环中,需通过循环变量(如 product)访问当前元素的属性,避免因变量未定义导致错误。

javascript 复制代码
// 错误写法(未指定具体元素的count)
return { ...product, count: count + 1 };

// 正确写法(通过product访问当前元素的count)
return { ...product, count: product.count + 1 };

2. 确保所有分支都有返回值

map 方法需要为数组中的每个元素返回一个值,否则会生成 undefined 元素,导致数组结构异常。

javascript 复制代码
// 错误写法(未匹配时无返回值)
products.map(product => {
  if (product.id === productId) {
    return { ...product, count: product.count + 1 };
  }
  // 缺少else分支的返回值
});

// 正确写法(所有情况均返回值)
products.map(product => {
  if (product.id === productId) {
    return { ...product, count: product.count + 1 };
  }
  return product; // 未匹配时返回原元素
});

三、总结

  1. 分号使用:主动为语句添加分号,尤其在变量声明、函数调用、对象 / 数组字面量结尾,避免依赖自动分号插入。
  2. 数组更新 :使用 map 时,需通过循环变量访问属性,并确保每个分支都有返回值,保证数组结构完整。

遵循以上规范可减少语法错误,提高代码的可靠性和可维护性。

6 JS代码修改对比(语法错误)

原错误代码(存在问题的版本)

javascript 复制代码
import { useState } from 'react';

const initialProducts = [{
  id: 0,
  name: 'Baklava',
  count: 1,
}, {
  id: 1,
  name: 'Cheese',
  count: 5,
}, {
  id: 2,
  name: 'Spaghetti',
  count: 2,
}];

export default function ShoppingCart() {
  const [
    products,
    setProducts
  ] = useState(initialProducts)

  function handleIncreaseClick(productId) {
    // 问题1:循环变量与数组重名(products)
    // 问题2:对象语法错误(多余括号、分号、未正确引用属性)
    // 问题3:缺少闭合括号
    setProducts(products.map(products => {
      if(product.id !== productId){
        return product;
      }
      else{
        return{(
          ...products,
          count : count + 1 ;
        )}
      }
    });
  }

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          {' '}
          (<b>{product.count}</b>)
          <button onClick={() => {
            handleIncreaseClick(product.id);
          }}>
            +
          </button>
        </li>
      ))}
    </ul>
  );
}

修改后代码(正确版本)

javascript 复制代码
import { useState } from 'react';

const initialProducts = [{
  id: 0,
  name: 'Baklava',
  count: 1,
}, {
  id: 1,
  name: 'Cheese',
  count: 5,
}, {
  id: 2,
  name: 'Spaghetti',
  count: 2,
}];

export default function ShoppingCart() {
  const [
    products,
    setProducts
  ] = useState(initialProducts)

  function handleIncreaseClick(productId) {
    // 修复1:循环变量用单数(product),与数组(products)区分
    // 修复2:对象语法正确(去掉多余括号和分号,用product.count)
    // 修复3:补全闭合括号
    setProducts(products.map(product => {
      if (product.id === productId) {
        return {
          ...product,
          count: product.count + 1 
        };
      } else {
        return product;
      }
    }));
  }

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          {' '}
          (<b>{product.count}</b>)
          <button onClick={() => {
            handleIncreaseClick(product.id);
          }}>
            +
          </button>
        </li>
      ))}
    </ul>
  );
}

关键修改点说明

  1. 循环变量命名 原代码用 products 作为循环变量(与数组重名),修改后用 product(单数),明确区分 "数组整体" 和 "单个元素",避免 product.id 被误解析。

  2. 对象语法修正 原代码中 return{( ...products, count : count + 1 ; )} 存在多处错误:

    • 去掉多余的括号嵌套({( )}{ });
    • 移除对象内部的分号(count: count + 1;count: product.count + 1);
    • product.count 正确引用当前元素的数量属性(原代码漏写 product.)。
  3. 语法完整性 原代码 setProducts(...) 末尾缺少闭合括号 ),修改后补全,确保函数调用语法正确。

7 filter删除数组

filter 就像个 "过滤器",专门从数组里挑出符合条件的元素,组成一个新数组 ------ 简单说就是 "留下想要的,扔掉不想要的"。

基本用法:

javascript 复制代码
const 新数组 = 原数组.filter(元素 => 条件);
  • 遍历原数组的每个元素,把符合 "条件" 的元素放进新数组;
  • 不修改原数组,而是返回一个全新的数组(这点对 React state 很重要!)。

拿你的删除功能举例:

你要删除 id = todoId 的任务,意思就是 "留下所有 id 不等于 todoId 的任务":

javascript 复制代码
function handleDeleteTodo(todoId) {
  // 原数组是 todos,过滤出所有 id 不等于 todoId 的元素
  const newTodos = todos.filter(todo => todo.id !== todoId);
  // 用新数组更新 state
  setTodos(newTodos);
}
  • todo => todo.id !== todoId 就是筛选条件:每个 todo 都要检查,如果它的 id 不等于要删除的 todoId,就留下它;
  • 最后 newTodos 里就是所有没被删除的任务,用 setTodos 更新后,页面就会同步显示删除后的列表。

再举个通俗例子:

假设数组是 [1,2,3,4,5],想筛选出所有偶数:

javascript 复制代码
const evenNumbers = [1,2,3,4,5].filter(num => num % 2 === 0);
// evenNumbers 结果是 [2,4]

和删除逻辑一样:符合 "是偶数" 条件的留下,不符合的扔掉。

记住核心:

filter 不会动原数组,只会返回一个 "符合你要求" 的新数组 ------ 这正好符合 React 中 "不能直接修改 state,要返回新值" 的规矩,所以删除、筛选这类操作特别适合用它。

下次再用就想:"我要留下什么样的元素?" 把这个条件写成箭头函数,传给 filter 就行~

相关推荐
一雨方知深秋2 小时前
2.fs模块对计算机硬盘进行读写操作(Promise进行封装)
javascript·node.js·promise·v8·cpython
报错小能手2 小时前
C++笔记——STL map
c++·笔记
谷歌开发者3 小时前
Web 开发指向标 | Chrome 开发者工具学习资源 (六)
前端·chrome·学习
一晌小贪欢3 小时前
【Html模板】电商运营可视化大屏模板 Excel存储 + 一键导出(已上线-可预览)
前端·数据分析·html·excel·数据看板·电商大屏·大屏看板
发现你走远了3 小时前
连接模拟器网页进行h5的调试(使用Chrome远程调试(推荐)) 保姆级图文
前端·chrome
lkbhua莱克瓦244 小时前
Java基础——集合进阶3
java·开发语言·笔记
街尾杂货店&4 小时前
css - 实现三角形 div 容器,用css画一个三角形(提供示例源码)简单粗暴几行代码搞定!
前端·css
顺凡4 小时前
删一个却少俩:Antd Tag 多节点同时消失的原因
前端·javascript·面试
小白路过4 小时前
CSS transform矩阵变换全面解析
前端·css·矩阵