阅读视图

发现新文章,点击刷新页面。

字符串处理实战:模板字符串、split/join、正则的 80% 用法

前言

前端里接口参数拼接、搜索条件、富文本简单处理,几乎都绕不开字符串:拼 URL、拆 query替换/截断文案。很多人习惯用 + 拼到眼花,或者到处 indexOf/substring,写多了难维护也容易出 bug。
用**模板字符串split/join正则**这三类能力,可以把「替换/匹配」写得更短、更稳。本文用 10 个左右常见场景,把日常该怎么选、为什么这么选、容易踩的坑讲清楚,只讲 80% 会用到的部分,不求覆盖所有正则语法。

适合读者:

  • 会写 JS,但对模板字符串/正则什么时候用、怎么写有点模糊
  • 刚学 JS,希望一开始就养成清晰的字符串写法
  • 有经验的前端,想统一团队里的 URL 拼接、搜索条件、简单富文本处理

一、先搞清楚:模板字符串split/join正则分别在干什么

能力 在干什么 典型用法
模板字符串 `${} 把变量嵌进字符串,支持换行 URL、拼文案、多行字符串
split 按分隔符把字符串拆成数组 query 拆成键值对、按逗号/换行拆列表
join 把数组用分隔符拼成字符串 把参数数组拼成 query、把标签数组拼成文案
正则 按模式匹配、替换、提取 替换占位符、校验格式、简单富文本处理
// 传统 + 拼接:多参数时很难看
const url = baseUrl + '/api/user?id=' + id + '&name=' + encodeURIComponent(name);

// 模板字符串:一眼看出「URL 长什么样」
const url = `${baseUrl}/api/user?id=${id}&name=${encodeURIComponent(name)}`;

记住一点:能一眼看出「最终长什么样」就用模板字符串;要「按规则拆开或拼起来」就用 split/join;要「按模式匹配或替换」就用正则

二、模板字符串的常见用法

1. 接口 URLquery 拼接(模板字符串 + 一层编码)

应用场景

  • 你要调一个列表接口(比如商品列表、用户列表),需要把「搜索关键词」「页码」「每页显示多少条」这些信息拼在接口地址后面,比如拼出 ?keyword=张三&page=1&pageSize=10 这种格式。

先搞懂一个核心问题:为啥不能直接拼?

  • 就像咱们寄快递要写规范的地址(省 - 市 - 区 - 街道),URL(接口地址)也有自己的「书写规范」—— 有些字符(比如中文、空格、&、=)直接写进去,服务器会 “看不懂”,甚至理解错意思。

举个最直白的例子

你要搜「用户 输入」(关键词里有空格),如果直接拼地址:/api/list?keyword=用户 输入&page=1

服务器会把「空格」当成 “参数分隔符”,以为「keyword = 用户」是一个参数,「输入 & page=1」是另一个参数,直接解析错了!

const baseUrl = '/api/list'; // 接口基础地址
const params = {
  keyword: '用户 输入',  // 要搜索的关键词(有中文+空格,是“违规字符”)
  page: 1,              // 第1页
  pageSize: 10          // 每页显示10条
};

// ✅ 推荐写法:用 URLSearchParams 当“翻译官”(自动处理违规字符)
// 你可以把 URLSearchParams 理解成:专门处理URL参数的“小工具”
const query = new URLSearchParams({
  keyword: params.keyword,
  page: String(params.page),    // 这个小工具只认字符串,数字要转一下
  pageSize: String(params.pageSize),
}).toString(); // 把处理好的参数转成字符串

// 用模板字符串拼最终地址,结构一眼能看懂
const url = `${baseUrl}?${query}`;
console.log('自动处理后的地址:', url);
// 输出:/api/list?keyword=%E7%94%A8%E6%88%B7+%E8%BE%93%E5%85%A5&page=1&pageSize=10
// 你看:“用户 输入”被翻译成了 %E7%94%A8%E6%88%B7+%E8%BE%93%E5%85%A5,服务器能看懂了!

// ❌ 反面示例:直接拼(不翻译违规字符)—— 服务器看不懂
const badUrl1 = `${baseUrl}?keyword=${params.keyword}&page=${params.page}&pageSize=${params.pageSize}`;
console.log('直接拼的错误地址:', badUrl1);
// 输出:/api/list?keyword=用户 输入&page=1&pageSize=10(空格、中文没翻译,服务器解析错)

// ⚠️ 手动翻译写法(麻烦,容易漏):
// encodeURIComponent 就是“单个字符翻译器”,只能翻译一个参数值
const encodedKeyword = encodeURIComponent(params.keyword); // 只翻译关键词
const encodedPage = encodeURIComponent(params.page);       // 翻译页码
const encodedPageSize = encodeURIComponent(params.pageSize); // 翻译每页条数
const goodUrlByHand = `${baseUrl}?keyword=${encodedKeyword}&page=${encodedPage}&pageSize=${encodedPageSize}`;
console.log('手动翻译的正确地址:', goodUrlByHand);
// 输出和自动处理的一样,但要写3次 encodeURIComponent,参数多了容易漏!

更直观的表格说明

名词 小白版解释 什么时候用
encodeURIComponent 单个 URL 参数的 “翻译器”:把中文、空格这些服务器看不懂的字符,翻译成服务器能懂的 “编码”(比如把 “用户” 译成 % E7%94% A8% E6%88% B7 手动拼接 URL 参数时,给每个参数值单独翻译
URLSearchParams 批量处理 URL 参数的 “智能翻译机”:你把所有参数丢给它,它会自动调用 encodeURIComponent 给每个参数翻译,还能拼成规范的参数串 推荐优先用!不管参数多少,一次搞定,不翻车

关键注意点(小白必看)

  1. 只要参数里有中文空格&= 这些字符,就必须 “翻译”,否则接口会调失败 / 返回错误数据;
  2. URLSearchParams 是 “懒人神器”:不用记 encodeURIComponent 怎么写,不用怕漏翻译某个参数,丢进去就自动处理;
  3. 小细节:URLSearchParams 只认字符串,所以数字类型的参数(比如 page:1)要转成 String (page),否则会报错。

2. 搜索条件:有值才带参数(过滤掉空值再拼)

场景: 只有 keyword 有值才带 keyword,只有 status 有值才带 status,避免 ?keyword=&status= 这种无意义参数。

const baseUrl = '/api/search';
const search = {
  keyword: '张三',  // 有实际值
  status: '',       // 空值(无意义)
  type: '1',        // 有实际值
};

// 第一步:筛选出非空的参数(去掉空字符串、全空格、null/undefined)
// Object.entries:把对象拆成[key, value]的数组,方便批量检查
// filter:筛选器,只留满足条件的参数
// trim():去掉字符串前后空格(比如用户只输空格也算空值)
const filtered = Object.fromEntries(
  Object.entries(search).filter(([_, value]) => {
    // 条件:值不是null/undefined,且去掉空格后不是空字符串
    return value != null && String(value).trim() !== '';
  })
);

// 第二步:用URLSearchParams自动编码参数,转成query字符串
const query = new URLSearchParams(filtered).toString();

// 第三步:拼接最终URL(有参数加?,没参数直接用基础地址)
const url = query ? `${baseUrl}?${query}` : baseUrl;
// 最终结果:/api/search?keyword=%E5%BC%A0%E4%B8%89&type=1
// 对比:如果没过滤,会是 /api/search?keyword=%E5%BC%A0%E4%B8%89&status=&type=1(多了无用的status=)
);
const query = new URLSearchParams(filtered).toString();
const url = query ? `${baseUrl}?${query}` : baseUrl;
// /api/search?keyword=%E5%BC%A0%E4%B8%89&type=1

核心名词小白解释:

代码片段 通俗理解
Object.entries(search) {keyword:'张三', status:'', type:'1'}拆成[['keyword','张三'], ['status',''], ['type','1']],方便逐个检查值是否为空
Object.fromEntries(数组) 把筛选后的数组(比如[['keyword','张三'], ['type','1']])还原成对象{keyword:'张三', type:'1'}
value.trim() 去掉字符串前后的空格,比如' 张三 '变'张三',' '变空字符串(避免 “只输空格” 被当成有效值)
filter(...) 只保留 “非空” 的参数,把status:''这种空值过滤掉

适用: 列表筛选项、搜索表单、任何「按条件带参」的接口。


3. 多行字符串、拼文案(模板字符串天然支持换行)

场景: 弹窗文案、邮件正文、多行提示。

const userName = '李四';
const count = 3;

const message = `尊敬的 ${userName}:
您有 ${count} 条待处理消息,请及时查看。`;
// 换行、变量都保留,不用 \n 和 + 拼

三、split / join 的常见用法

1. 把 URL 上的 search 拆成对象(split + 一次遍历)

场景:?id=1&name=test 得到 { id: '1', name: 'test' }

const search = '?id=1&name=test';

// 推荐:直接用 URLSearchParams 解析(和上面「拼」对应)
const params = Object.fromEntries(new URLSearchParams(search));
// { id: '1', name: 'test' }

// 若不能用地道 API,再用 split
const params2 = search
  .replace(/^\?/, '')
  .split('&')
  .reduce((acc, pair) => {
    const [key, value] = pair.split('=');
    acc[decodeURIComponent(key)] = decodeURIComponent(value ?? '');
    return acc;
  }, {});

注意: 值里可能带 =,所以「按第一个 = 拆」更稳,这里用 split('=') 只适合简单 value;复杂 query 建议统一用 URLSearchParams


2. 把「逗号分隔的 id」拆成数组,再拼回去(split + join)

场景: 接口返回 ids: "1,2,3",要转成数组处理;提交时再拼成 "1,2,3"

const idsStr = '1,2,3';

const ids = idsStr.split(',').map((id) => id.trim()).filter(Boolean);
// ['1', '2', '3']

// 提交时再拼回去
const idsStrAgain = ids.join(',');
// '1,2,3'

注意: split(',') 后习惯加 .map(s => s.trim()).filter(Boolean),避免空串和前后空格。


3. 按换行拆成数组(split('\n'))

场景: 用户输入多行标签、多行关键词,一行一个。

const input = '  tag1  \ntag2\n  tag3  ';
const tags = input.split('\n').map((s) => s.trim()).filter(Boolean);
// ['tag1', 'tag2', 'tag3']

四、正则的 80% 用法(小白友好版:从基础到实战)

先搞懂:正则的 “基础积木”(小白版) 先记住这几个最常用的符号,就像搭积木一样,组合起来就能实现大部分匹配 / 替换需求:

符号 / 语法 小白版解释 举例子
/内容/ 正则的 “容器”,所有匹配规则都写在两个/之间 /abc/ 表示匹配字符串里的 abc
/内容/g g = global(全局),表示匹配所有符合规则的内容,不是只匹配第一个 'aaa'.replace(/a/g, 'b')bbb(不加 g 只替换第一个 a,变成 baa
\w 匹配「字母、数字、下划线」(简单记:匹配 “单词字符”) /\w+/能匹配 name123order_001
\d 匹配「单个数字」(0-9) /\d/匹配 5/\d\d/ 匹配88
+ 表示 “前面的规则至少出现 1 次” /\d+/ 匹配 1 个或多个数字(比如 1123
* 表示 “前面的规则出现 0 次或多次”(用得少,优先记+ /\d*/ 能匹配空字符串1123
{n} 表示 “前面的规则正好出现 n 次” /\d{10}/匹配正好 10 个数字
^ 匹配 “字符串的开头”(锚定开头) /^1/只匹配以 1开头的字符串(比如 1380000 能匹配,a138 不能)
$ 匹配 “字符串的结尾”(锚定结尾) /\d$/ 只匹配以数字结尾的字符串
[^>] [] 表示 “匹配其中任意一个字符”,^ [] 里表示 “排除” /[^>]+/匹配 “除了 > 之外的任意字符,至少 1 个”
() 捕获组:把匹配到的内容 “抓出来”,后续能用到 /\{(\w+)\}/ 里的 (\w+) 会把 {name} 里的 name 抓出来
有没有同学看不懂 /\{(\w+)\}/ 的?

看这里:

  • \ 是转义符:正则里想匹配 {} / [] / () 等特殊符号本身时,必须加\
  • /\{(\w+)\}/ 的核心是匹配 {xxx} 格式的字符串,其中:
    • \{ / \} 匹配普通的{}
    • (\w+) 抓出 {} 中间的字母 / 数字 / 下划线(比如 name);
  • 新手写正则时,只要想匹配 “特殊符号本身”,先加 \ 转义,就不会出错。

用法 1:占位符替换(把 {name} 换成真实值)

场景:服务端返回模板 " 您好,{name},您的订单{orderId}已发货 ",前端替换成当前用户和订单。 步骤拆解(小白能懂):

1. 规则/\{(\w+)\}/g 解析:

  • \{:匹配左大括号 {(因为 { 是正则特殊符号,要加\ 转义,告诉正则 “这就是普通的 {”);
  • (\w+):捕获组,匹配字母 / 数字 / 下划线(比如 nameorderId),并把匹配结果存起来;
  • \}:匹配右大括号 }
  • g:全局匹配,把所有 {xxx} 都找出来。

2. replace 回调函数:(_, key) => data[key] ?? ''

  • 第一个参数 _:表示整个匹配的内容(比如 {name}),用不到就用 _ 占位;
  • 第二个参数 key:就是捕获组 (\w+) 抓到的内容(比如 name);
  • data[key] ?? '':从 data 里取对应的值,没有就用空串填充。
const template = '您好,{name},您的订单{orderId}已发货';
const data = { name: '王五', orderId: 'ORD001' };

// 核心代码
const result = template.replace(/\{(\w+)\}/g, (_, key) => data[key] ?? '');
console.log(result); // 输出:'您好,王五,您的订单ORD001已发货'

// 小白试错:如果不加g,只会替换第一个占位符
const badResult = template.replace(/\{(\w+)\}/, (_, key) => data[key] ?? '');
console.log(badResult); // 输出:'您好,王五,您的订单{orderId}已发货'

用法 2:富文本简单处理:去掉 HTML 标签只留纯文本

**场景:**列表摘要只展示纯文本,需要把 <p>xxx</p> 里的 xxx 拿出来,或去掉所有标签。 规则 /<[^>]+>/g 解析

  • <:匹配左尖括号;
  • [^>]+:匹配 “除了>之外的任意字符,至少 1 个”(比如 pstrongdiv class="title");
  • >:匹配右尖括号;
  • g:全局替换,把所有标签都去掉。
const html = '<p>这是一段<strong>加粗</strong>的文字&nbsp;还有空格</p>';

// 第一步:去掉所有HTML标签
const textWithoutTag = html.replace(/<[^>]+>/g, '');
console.log(textWithoutTag); // 输出:'这是一段加粗的文字&nbsp;还有空格'

// 第二步:还原常见的HTML实体(比如&nbsp;换成空格)
const text = textWithoutTag
  .replace(/&nbsp;/g, ' ')  // 空格实体转空格
  .replace(/&lt;/g, '<')    // < 实体转 <
  .replace(/&gt;/g, '>');   // > 实体转 >
console.log(text); // 输出:'这是一段加粗的文字 还有空格'

⚠️ 重要提醒:这个规则只适合「简单、可控」的富文本(比如自己系统生成的短文本)。如果是复杂 HTML(比如带注释、<script>标签、属性里有>的),正则会失效,建议用 DOM 或专业库(如 cheerio)处理。

用法 3:富文本简单处理:限制摘要长度(截断 + 省略号)

场景:列表里摘要最多显示 20 个字符,超出用 ...。 (先去标签再截断,避免截到标签中间,比如把<p>这是一段很长的文字</p>截成 <p>这是一段很长的文,导致标签不闭合)

// 封装成通用函数,小白直接用
const getSummary = (html, maxLen = 20) => {
  // 第一步:先去标签和还原实体
  const pureText = html
    .replace(/<[^>]+>/g, '')
    .replace(/&nbsp;/g, ' ')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>');
  // 第二步:判断长度,截断加省略号
  if (pureText.length > maxLen) {
    return pureText.slice(0, maxLen) + '...';
  }
  return pureText;
};

// 测试
const longHtml = '<div>这是一段非常非常长的富文本内容,需要截断显示</div>';
console.log(getSummary(longHtml, 10)); // 输出:'这是一段非常非常长...'

用法 4:简单格式校验(手机号、纯数字)

场景:表单里「手机号」「纯数字」的简单校验,用 正则.test(要校验的字符串),返回 true/false

1. 手机号校验

规则 /^1\d{10}$/ 解析:

  • ^:字符串开头;
  • 1:第一个字符必须是 1(手机号开头都是 1);
  • \d{10}:后面跟正好 10 个数字;
  • $:字符串结尾; → 整体表示:整个字符串必须是 “1 + 10 个数字”,长度正好 11 位。
// 封装手机号校验函数
const isPhoneValid = (phone) => {
  // 先排除空值、非字符串情况
  if (!phone || typeof phone !== 'string') return false;
  return /^1\d{10}$/.test(phone);
};

// 测试
console.log(isPhoneValid('13800138000')); // true(正确手机号)
console.log(isPhoneValid('1380013800'));  // false(只有10位)
console.log(isPhoneValid('12345678901')); // false(开头不是1)
console.log(isPhoneValid('1380013800a')); // false(包含字母)

2. 纯数字校验

规则 /^1\d{10}$/ 解析:

  • ^:开头;
  • \d+:至少 1 个数字;
  • \d{10}:后面跟正好 10 个数字;
  • $:结尾; → 整体表示:整个字符串只能是数字,不能有其他字符,且不能为空。
// 封装纯数字校验函数
const isPureNumber = (str) => {
  if (!str) return false; // 空串返回false
  return /^\d+$/.test(str);
};

// 测试
console.log(isPureNumber('12345')); // true
console.log(isPureNumber('123a5')); // false(含字母)
console.log(isPureNumber(''));      // false(空串)
console.log(isPureNumber('0'));     // true(单个0也符合)

用法总结

  1. 正则小白不用记所有语法,先掌握 /内容/g\w/\d+/{n}^/$() 这几个核心符号,就能搞定大部分场景;
  2. 正则的核心用法分 3 类:替换(replace)、校验(test)、提取(match),其中替换和校验是日常用得最多的;
  3. 写正则时,先拆解 “要匹配什么 / 排除什么”,再用基础符号组合,优先加 g(全局)、^/``$`(整串匹配)避免漏匹配 / 错匹配;
  4. 复杂 HTML 处理别用正则,优先用 DOM 或专业库,正则只适合简单片段。

五、容易踩的坑

1. 模板字符串里要算表达式,用 ${} 包起来

const a = 1, b = 2;
const wrong = `${a} + ${b} = a + b`;   // '1 + 2 = a + b'
const right = `${a} + ${b} = ${a + b}`; // '1 + 2 = 3'

2. query 里的中文、空格、特殊字符必须编码

const name = '张 三';
const bad = `/api?name=${name}`;  // 空格和中文会破坏 URL
const good = `/api?name=${encodeURIComponent(name)}`;
// 或统一用 URLSearchParams

3. split 不传参时按每个字符拆

'abc'.split();   // ['abc']
'abc'.split(''); // ['a','b','c']

要按「分隔符」拆就明确传参,例如 split(',')split('\n')


4. 空字符串 split 得到的是 ['']

''.split(',');   // ['']
''.split(',').filter(Boolean); // []

拼 query、拼列表前若可能为空,先判断或 filter(Boolean),避免出现 ?key= 或末尾多余逗号。


5. 正则「去标签」不能覆盖所有 HTML 情况

// 像 <div class="a"> 这种可以匹配
// 但 <script>...</script>、注释、属性里的 > 等,正则容易出错

仅用于「自己能控制的、结构简单的」富文本片段;其它用 DOM 或专业库。

六、实战推荐写法模板

接口 GET 参数拼接(带空值过滤):

const baseUrl = '/api/list';
const params = { keyword: '...', page: 1, pageSize: 10, status: '' };
const query = new URLSearchParams(
  Object.fromEntries(
    Object.entries(params).filter(([_, v]) => v != null && String(v).trim() !== '')
  )
).toString();
const url = query ? `${baseUrl}?${query}` : baseUrl;

从当前页 search 取参数:

const params = Object.fromEntries(new URLSearchParams(location.search));
const keyword = params.keyword ?? '';

逗号分隔字符串 ↔ 数组:

const toIds = (s) => (s ?? '').split(',').map((id) => id.trim()).filter(Boolean);
const toStr = (arr) => (arr ?? []).filter(Boolean).join(',');

简单占位符替换:

const fillTemplate = (template, data) =>
  template.replace(/\{(\w+)\}/g, (_, key) => data[key] ?? '');

富文本摘要(去标签 + 截断):

const toSummary = (html, maxLen = 20) => {
  const text = html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ');
  return text.length > maxLen ? text.slice(0, maxLen) + '...' : text;
};

七、小结

场景 推荐写法
拼 URL、拼文案、多行字符串 模板字符串 `${base}?${query}`
拼/解析 query URLSearchParams + 模板字符串 或 split/reduce
有值才带参 filterURLSearchParams,再拼到 URL
逗号/换行拆成数组 split(',') / split('\n') + trim + filter(Boolean)
数组拼成字符串 join(',')
占位符替换 {key} replace(/\{(\w+)\}/g, (_, key) => data[key])
简单去 HTML 标签 replace(/<[^>]+>/g, '')(仅简单片段)
摘要截断 先去标签再 slice(0, len) + '...'
简单格式校验 /^1\d{10}$/.test(phone)

记住:拼用模板字符串 + URLSearchParams,拆用 split/URLSearchParams,替换/匹配用正则。日常写接口参数、搜索条件、简单富文本时,先想清楚是「拼、拆、还是替换/校验」,再选对应方式,代码会清晰很多,也少踩编码和空值的坑。

特别提醒:

  • query 里的中文和特殊字符一定要编码(URLSearchParamsencodeURIComponent)。
  • 空数组/空字符串在 split/join 时要考虑 filter(Boolean) 和「是否带问号」。
  • 正则只用于简单、可控的富文本;复杂 HTML 用 DOM 或专门库。

以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

对象数组的排序与分组:sort / localeCompare / 自定义 compare

日常开发里,列表、表格、统计几乎都绕不开「对象数组」的排序和分组。本文不讲底层原理,只讲怎么选、为什么选、容易踩哪些坑。适合会写 JS 但概念有点混的同学,也适合想补齐基础的前端老手。

一、Array.sort 到底在干什么

1.1 三个关键点

要点 说明
原地排序 sort() 会直接修改原数组,不会返回新数组
默认行为 不传比较函数时,按字符串逐个字符比较
compare 返回值 负数:a 排前面;0:不变;正数:b 排前面

有没有同学会有这样的疑问:compare 返回值?这是啥? 解释:

  • 这里的 compare 指的是 Array.sort() 方法中传入的比较函数(也就是你后面写的 (a, b) => a - b 这种形式)。
  • 简单说:当你用sort()排序时,传入的这个函数就是 compare,它的作用是告诉 sort() 两个元素(ab)该怎么排,返回值直接决定排序结果,和表格里的说明完全对应。
  • 比如 nums.sort((a, b) => a - b) 中,(a, b) => a - b 就是 compare 比较函数。

1.2 第一个坑:数字数组直接用 sort

const nums = [10, 2, 1];
nums.sort(); // 这一步已经把原数组 nums 改了!以为会得到 [1, 2, 10]
console.log(nums); // 打印的是被修改后的原数组,不是初始值。实际得到 [1, 10, 2] —— 按字符串 "10"、"2"、"1" 比较了!
// ✅ 正确写法
nums.sort((a, b) => a - b);   // 升序 [1, 2, 10]
nums.sort((a, b) => b - a);   // 降序 [10, 2, 1]

:为什么按字符串比较会得到 [1, 10, 2]? sort() 默认的字符串比较规则是「逐字符按 Unicode 码点比较」,不是看数字大小,步骤拆解如下:

  1. 先把数组里的数字都转成字符串:10→"10"、2→"2"、1→"1";
  2. 从第一个字符开始比,字符的 Unicode 码点:"1"(码点 49)< "2"(码点 50);
  3. 具体比较过程:
    • 比较 "1" 和 "10":第一个字符都是 "1"(码点相同),但 "1" 没有第二个字符,所以 "1" < "10";
    • 比较 "10" 和 "2":第一个字符 "1" < "2",所以 "10" < "2"

1.3 第二个坑:原数组被改了

const original = [3, 1, 2];
const sorted = original.sort((a, b) => a - b);

console.log(sorted);   // [1, 2, 3]
console.log(original); // [1, 2, 3] —— 原数组也被改了!

// ✅ 需要保留原数组时,先浅拷贝再排序
const sorted2 = [...original].sort((a, b) => a - b);

二、对象数组按不同字段排序

2.1 按数字排序

const users = [
  { name: '张三', age: 25 },
  { name: '李四', age: 18 },
  { name: '王五', age: 30 }
];

// 按 age 升序
users.sort((a, b) => a.age - b.age);
// 结果:李四(18) → 张三(25) → 王五(30)

// 按 age 降序
users.sort((a, b) => b.age - a.age);

写法记忆:升序 a - b,降序 b - a

2.2 按字符串排序

// 按 name 字母/拼音顺序
users.sort((a, b) => a.name.localeCompare(b.name));

直接用 a.name > b.name ? 1 : -1 可以工作,但遇到中文、大小写、多语言时容易出问题,所以更推荐 localeCompare,后面会细讲。

2.3 按日期排序

日期有两种常见形式:字符串和时间戳。

const orders = [
  { id: 1, date: '2025-02-15' },
  { id: 2, date: '2025-01-20' },
  { id: 3, date: '2025-02-10' }
];

// 方式一:YYYY-MM-DD 格式的字符串可以直接用 localeCompare
orders.sort((a, b) => a.date.localeCompare(b.date));

// 方式二:转时间戳(适用各种日期格式)
orders.sort((a, b) => new Date(a.date) - new Date(b.date));

建议:后端返回的日期如果是 YYYY-MM-DD,用 localeCompare 即可;格式不统一时,统一用 new Date() 转时间戳再比较。

2.4 多字段排序

先按 A 排序,A 相同再按 B 排序,可以用 || 链式比较:

users.sort((a, b) => {
  if (a.age !== b.age) return a.age - b.age;  // 先按年龄
  return a.name.localeCompare(b.name);        // 年龄相同再按姓名
});

// 更简洁的写法
users.sort((a, b) => a.age - b.age || a.name.localeCompare(b.name));

原理a.age - b.age 为 0 时,0 || xxx 会取后面的 localeCompare 结果。

三、localeCompare:字符串排序的正确姿势

3.1 为什么不用 >、< 比较字符串?

const arr = ['张三', '李四', '王五', 'apple', 'Apple'];
arr.sort((a, b) => a > b ? 1 : -1);  // 按 Unicode 比较,中文结果不符合直觉
arr.sort((a, b) => a.localeCompare(b));  // 按语言规则,更符合人类习惯

localeCompare 可以:

  • 中文按拼音
  • 控制大小写敏感
  • 数字按数值比较(如 "10" 在 "2" 后面)

3.2 常用用法

// 指定语言(中文按拼音)
'张三'.localeCompare('李四', 'zh-CN');  // 负数,张在李后面

// 忽略大小写
'apple'.localeCompare('Apple', undefined, { sensitivity: 'base' });  // 0,视为相等

// 数字按数值比较
['10', '2', '1'].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
// 结果:['1', '2', '10']

3.3 兼容性说明

现代浏览器和 Node 都支持 localeCompare。带 options 配置的 localeCompare 写法,在老环境(旧浏览器 / 旧 Node 版本)中可能表现不一致,生产环境建议先小范围验证。

// 忽略大小写(options:{ sensitivity: 'base' })
'apple'.localeCompare('Apple', undefined, { sensitivity: 'base' });
// 数字按数值比较(options:{ numeric: true })
['10','2'].sort((a,b) => a.localeCompare(b, undefined, { numeric: true }));

老环境问题:像旧版 IE、低版本 Node(比如 Node.js 10 以下),对这些options配置支持不完善(比如不识别numeric: true),导致排序结果出错,所以生产环境要先小范围验证。

3.4 补充localeCompareoptions写法 老环境兼容技巧

核心兼容思路:降级处理——先判断环境是否支持localeCompareoptions配置,支持则用带options的简洁写法,不支持则降级为基础写法,保证排序效果一致,且代码简单可直接套用(无需额外引入兼容库)。

场景1:忽略大小写排序(对应options: { sensitivity: 'base' })音标:/sensəˈtɪvəti/

老环境兼容写法(适配旧IE、低版本Node):

// 兼容函数:忽略大小写比较两个字符串
function compareIgnoreCase(a, b) {
  // 先统一转小写,再用基础localeCompare(老环境均支持无options写法)
  const lowerA = a.toLowerCase();
  const lowerB = b.toLowerCase();
  return lowerA.localeCompare(lowerB, 'zh-CN'); // 中文场景可加语言标识
}

// 用法(和带options写法效果一致)
const arr = ['apple', 'Apple', 'Banana', 'banana'];
arr.sort(compareIgnoreCase); // 结果:['apple', 'Apple', 'Banana', 'banana']

场景2:数字字符串按数值排序(对应options: { numeric: true }

老环境兼容写法(避免老环境不识别numeric 音标:/njuːˈmerɪk/ 配置导致排序错乱):

// 兼容函数:数字字符串按数值排序
function compareNumericStr(a, b) {
  // 降级思路:转成数字比较(贴合原文数字排序逻辑,老环境完全支持)
  const numA = Number(a);
  const numB = Number(b);
  return numA - numB; // 升序,降序则改为numB - numA
}

// 用法(和带options写法效果一致)
const arr = ['10', '2', '1', '25'];
arr.sort(compareNumericStr); // 结果:['1', '2', '10', '25']

关键注意点

  • 无需判断环境:上述兼容写法兼容所有环境(老环境正常运行,新环境也不影响效果),不用额外写环境判断代码,简化开发。

  • 生产环境验证:如果老环境占比极低,可直接用带options写法,上线前用老环境(如IE11、Node.js 8)简单测试1个排序案例即可。

四、分组统计:从排序到 groupBy 【分组】

排序和分组是两个不同操作:

  • 排序:改变顺序,不拆分数组
  • 分组:按某个字段把数组拆成多组

JS 没有内置 groupBy,可以用 reduce 实现:

const orders = [
  { id: 1, status: 'paid', amount: 100 },
  { id: 2, status: 'pending', amount: 50 },
  { id: 3, status: 'paid', amount: 200 }
];

const byStatus = orders.reduce((acc, item) => {
  const key = item.status;
  if (!acc[key]) acc[key] = [];
  acc[key].push(item);
  return acc;
}, {});

// 结果:
// {
//   paid: [{ id: 1, ... }, { id: 3, ... }],
//   pending: [{ id: 2, ... }]
// }

分组后再排序

分组后,如果每组内部还要排序:

Object.keys(byStatus).forEach(key => {
  byStatus[key].sort((a, b) => b.amount - a.amount);  // 每组按金额降序
});

分组 + 统计

需要同时统计每组数量或汇总值时:

const stats = orders.reduce((acc, item) => {
  const key = item.status;
  if (!acc[key]) {
    acc[key] = { list: [], total: 0, count: 0 };
  }
  acc[key].list.push(item);
  acc[key].total += item.amount;
  acc[key].count += 1;
  return acc;
}, {});

// 结果示例:{ paid: { list: [...], total: 300, count: 2 }, ... }

五、踩坑速查表

坑点 错误表现 正确写法
数字数组排序错乱 [10, 2, 1].sort()[1, 10, 2] arr.sort((a, b) => a - b)
原数组被修改 排序后原数组也变了 [...arr].sort(...)
中文排序不对 直接用 >< 比较 a.localeCompare(b, 'zh-CN')
多字段排序只写了一层 只按第一个字段排 a.age - b.age || a.name.localeCompare(b.name)
日期格式不统一 字符串比较出错 new Date(a.date) - new Date(b.date)

六、小结

  1. 数字排序:用 (a, b) => a - bb - a,不要用默认 sort()
  2. 字符串排序:优先用 localeCompare,尤其是中文和多语言场景。
  3. 日期排序YYYY-MM-DDlocaleCompare,其他格式用时间戳。
  4. 多字段排序:用 || 串联多个比较。
  5. 分组:用 reducegroupBy,再按需对每组排序或统计。
  6. 保留原数组:排序前先 [...arr] 浅拷贝。

这些写法足够覆盖大部分日常需求,记住上面的速查表,可以少踩很多坑。


以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

数组查找与判断:find / some / every / includes 的正确用法

今天是2026年2月17日农历正月初一,在2026 愿大家:身体健康无病痛,收入翻番钱包鼓! 代码 0 Error 0 Warning,需求一次过,上线零回滚!策马扬鞭,从小白进阶专家,新年一路 “狂飙”!🧧🐎 给大家拜年啦~

前言

前端里权限判断、表单校验、勾选状态,几乎都要判断「数组里有没有某个值」或「是否全部满足条件」。很多人习惯用 for 循环 + if 一把梭,或者 indexOf 判断,写多了既啰嗦又容易漏边界情况。
find / some / every / includes 这四个方法,可以把「查找 → 判断 → 校验」写得更短、更语义化,也更好处理边界情况。本文用 10 个常见场景,把日常该怎么选、为什么这么选、容易踩的坑讲清楚。

适合读者:

  • 会写 JS,但对 find/some/every/includes 用哪个、什么时候用有点模糊
  • 刚学 JS,希望一开始就养成清晰的数组判断写法
  • 有经验的前端,想统一团队里的权限/校验/状态判断写法

一、先搞清楚:find / some / every / includes 在干什么

这四个方法都不是黑魔法,本质是:在不动原数组的前提下,用一次遍历完成「查找 / 判断是否存在 / 判断是否全部满足」

方法 在干什么 返回值 什么时候停
find 找第一个符合条件的元素 找到的元素,找不到返回 undefined 找到第一个就停
some 判断是否至少有一个满足条件 truefalse 找到第一个就停(短路)
every 判断是否全部满足条件 truefalse 遇到第一个不满足就停(短路)
includes 判断数组里是否包含某个值(严格相等) truefalse 遍历完或找到就停
// 传统 for:意图分散,还要自己管 break
let found = null;
for (let i = 0; i < users.length; i++) {
  if (users[i].id === targetId) {
    found = users[i];
    break;
  }
}

// find:一眼看出「找第一个 id 匹配的」
const found = users.find((u) => u.id === targetId);

记住一点:能用语义化方法就不用循环,用 find/some/every/includes 把「要查什么、要判断什么」写清楚,比「怎么循环、怎么 break」更重要。

二、数组查找与判断的 10 个常用场景

假设接口返回的数据类似:

const users = [
  { id: 1, name: '张三', role: 'admin', status: 'active' },
  { id: 2, name: '李四', role: 'user', status: 'active' },
  { id: 3, name: '王五', role: 'user', status: 'inactive' },
];

const permissions = ['read', 'write', 'delete'];
const selectedIds = [1, 2];

下面 10 个写法,覆盖权限判断、表单校验、勾选状态等真实场景。

场景 1:找第一个符合条件的对象(find

const admin = users.find((user) => user.role === 'admin');
// { id: 1, name: '张三', role: 'admin', status: 'active' }

// 找不到返回 undefined
const superAdmin = users.find((user) => user.role === 'superAdmin');
// undefined

适用: 默认选中第一项、取第一个有效配置、根据 id 找对象等。
注意: find 找不到返回 undefined,后续解构或访问属性要处理,用 ?? 给默认值。

场景 2:判断是否至少有一个满足条件(some

const hasAdmin = users.some((user) => user.role === 'admin');
// true

const hasInactive = users.some((user) => user.status === 'inactive');
// true

适用: 权限判断「是否有任一管理员」、表单校验「是否有错误项」、状态判断「是否有未完成项」等。
注意: 空数组时 some 返回 false,业务上要结合「空列表算通过还是不算」处理。

场景 3:判断是否全部满足条件(every

const allActive = users.every((user) => user.status === 'active');
// false(因为有王五是 inactive)

const allHaveId = users.every((user) => user.id != null);
// true

适用: 表单校验「是否全部勾选」、权限判断「是否全部有权限」、状态判断「是否全部完成」等。
注意: 空数组时 every 返回 true(空真),业务上要结合「空列表算通过还是不算」处理。

场景 4:判断数组是否包含某个值(includes

const hasRead = permissions.includes('read');
// true

const hasExecute = permissions.includes('execute');
// false

适用: 简单值数组的包含判断、权限列表判断、标签列表判断等。
注意: includes 底层用 严格相等=== 做比较,这对「简单值(string / number / boolean)」很友好,但对「对象 / 数组」这类引用类型完全不适用,因为===比较的是内存地址而非内容。

场景 5:权限判断:是否有某个权限(some + includes

const userPermissions = ['read', 'write'];
const requiredPermission = 'delete';

const hasPermission = userPermissions.includes(requiredPermission);
// false

// 或判断多个权限中是否有任一
const requiredPermissions = ['delete', 'admin'];
const hasAnyPermission = requiredPermissions.some((perm) => 
  userPermissions.includes(perm)
);
// false

适用: 按钮权限控制、路由权限控制、功能权限判断等。
推荐: 简单值用 includes,复杂条件用 some + 回调。

场景 6:表单校验:是否全部必填项已填(every

const formFields = [
  { name: 'username', value: '张三', required: true },
  { name: 'email', value: '', required: true },
  { name: 'phone', value: '13800138000', required: false },
];

const allRequiredFilled = formFields
  .filter((field) => field.required)
  .every((field) => field.value.trim() !== '');
// false(email 为空)

适用: 表单提交前校验、批量操作前校验、多步骤流程校验等。
推荐:filter 筛出必填项,再用 every 判断是否全部有值。

场景 7:勾选状态:是否全部选中(every

const checkboxes = [
  { id: 1, checked: true },
  { id: 2, checked: true },
  { id: 3, checked: false },
];

const allChecked = checkboxes.every((item) => item.checked);
// false

const hasChecked = checkboxes.some((item) => item.checked);
// true

适用: 全选/反选功能、批量操作按钮状态、表格多选状态等。
推荐: every 判断全选,some 判断是否有选中项。

场景 8:找第一个并给默认值(find+ ??

const defaultUser = users.find((user) => user.role === 'admin') ?? {
  id: 0,
  name: '默认用户',
  role: 'guest',
};

适用: 默认选中第一项、取第一个有效配置、兜底默认值等。
注意: find 找不到返回 undefined,用 ?? 可以统一成默认对象,避免后面解构报错。

场景 9:对象数组是否包含某个 id(some

const targetId = 2;
const exists = users.some((user) => user.id === targetId);
// true

// 或判断多个 id 中是否有任一存在
const targetIds = [2, 5];
const hasAny = targetIds.some((id) => users.some((user) => user.id === id));
// true(2 存在)

适用: 判断选中项是否在列表里、判断 id 是否已存在、去重前判断等。
注意: 对象数组不能用 includes,要用 some + 条件判断。

场景 10:组合判断:全部满足 A 且至少一个满足 B(every +some

const allActive = users.every((user) => user.status === 'active');
const hasAdmin = users.some((user) => user.role === 'admin');

// 业务逻辑:全部激活 且 有管理员
const canOperate = allActive && hasAdmin;
// false(因为有 inactive 的)

适用: 复杂业务规则判断、多条件组合校验、权限组合判断等。
推荐: 把每个条件拆成变量,用名字表达「这一步在判断什么」,可读性和调试都会好很多。

三、容易踩的坑

1. find 找不到返回 undefined,直接解构会报错

const user = users.find((u) => u.id === 999);
const { name } = user; // TypeError: Cannot read property 'name' of undefined

正确:?? 给默认值,或先判断再解构。

const user = users.find((u) => u.id === 999) ?? { name: '未知' };
// 或
const user = users.find((u) => u.id === 999);
if (user) {
  const { name } = user;
}

2. 空数组时 every 返回 truesome 返回 false

[].every((x) => x > 0); // true(空真)
[].some((x) => x > 0);  // false

业务上要结合「空列表算通过还是不算」处理。例如表单校验,空列表可能应该算「未填写」而不是「通过」。

const fields = [];
const allFilled = fields.length > 0 && fields.every((f) => f.value);
// 先判断长度,再 every

3. includes 只能判断简单值,对象数组要用 some

const users = [{ id: 1 }, { id: 2 }];
users.includes({ id: 1 }); // false(对象引用不同)

// 正确:用 some + 条件判断
users.some((user) => user.id === 1); // true

4. findfilter 的区别:find 只找第一个,filter 找全部

const firstAdmin = users.find((u) => u.role === 'admin');
// 返回第一个对象或 undefined

const allAdmins = users.filter((u) => u.role === 'admin');
// 返回数组,可能为空数组 []

要「第一个」用 find,要「全部」用 filter,别混用。

5. someevery 的短路特性:找到就停

const users = [
  { id: 1, role: 'admin' },
  { id: 2, role: 'user' },
  { id: 3, role: 'admin' },
];

// some:找到第一个 admin 就停,不会继续遍历
users.some((u) => {
  console.log(u.id); // 只打印 1
  return u.role === 'admin';
});

// every:遇到第一个不是 admin 就停
users.every((u) => {
  console.log(u.id); // 打印 1, 2(遇到 user 就停)
  return u.role === 'admin';
});

性能上这是好事,但如果有副作用(如打印、修改外部变量),要注意只执行到第一个匹配项。

四、实战推荐写法模板

权限判断(是否有某个权限):

const userPermissions = response?.data?.permissions ?? [];
const canDelete = userPermissions.includes('delete');

// 或判断多个权限中是否有任一
const canManage = ['delete', 'admin'].some((perm) => 
  userPermissions.includes(perm)
);

表单校验(是否全部必填项已填):

const fields = formData?.fields ?? [];
const isValid = fields
  .filter((field) => field.required)
  .every((field) => field.value?.trim() !== '');

// 或更严格的校验
const isValid = fields.length > 0 && 
  fields.filter((f) => f.required).every((f) => f.value?.trim() !== '');

勾选状态(全选/部分选中):

const items = tableData ?? [];
const allChecked = items.length > 0 && items.every((item) => item.checked);
const hasChecked = items.some((item) => item.checked);

// 全选按钮状态
const selectAllDisabled = items.length === 0;
const selectAllChecked = allChecked;

找第一个并给默认值:

const defaultItem = (response?.data?.list ?? []).find(
  (item) => item.isDefault
) ?? {
  id: 0,
  name: '默认选项',
  value: '',
};

对象数组是否包含某个 id:

const selectedIds = [1, 2, 3];
const targetId = 2;
const isSelected = selectedIds.includes(targetId);

// 对象数组
const users = response?.data?.users ?? [];
const targetId = 2;
const exists = users.some((user) => user.id === targetId);

五、小结

场景 推荐写法 返回值
找第一个符合条件的对象 list.find(item => ...) 对象或 undefined
判断是否至少有一个满足 list.some(item => ...) truefalse
判断是否全部满足 list.every(item => ...) truefalse
判断是否包含某个值(简单值) list.includes(value) truefalse
找第一个并给默认值 list.find(...) ?? 默认值 对象或默认值
对象数组是否包含某个 id list.some(item => item.id === id) truefalse
表单校验:全部必填已填 list.filter(...).every(...) truefalse
勾选状态:全部选中 list.every(item => item.checked) truefalse

记住:find 负责「找」,some 负责「至少一个」,every 负责「全部」,includes 负责「简单值包含」。日常写权限、校验、状态判断时,先想清楚是要找对象、判断存在、判断全部,还是简单值包含,再选方法,代码会干净很多,也少踩坑。

特别提醒:

  • find 找不到返回 undefined,记得用 ?? 给默认值
  • 空数组时 everytruesomefalse,业务上要结合长度判断
  • 对象数组不能用 includes,要用 some + 条件判断

文章到这里结束。如果你日常写权限判断、表单校验、勾选状态时经常纠结用哪个方法,希望这篇能帮你定个型。

以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

《对象与解构赋值:接口数据解包的 10 个常见写法》

前言

后台接口返回的数据,常常是嵌套对象或数组,很多人习惯一层层 data.user.name 这样写,既啰嗦又容易在某一层是 undefined 时直接报错。
解构赋值 + 默认值,可以把取数写得又短又安全。本文用 10 个常见写法,帮你把「接口数据解包」这件事理清楚。

适合读者:

  • 已经会写 JS,但对解构、默认值组合用法不熟
  • 刚学 JS,希望一开始就养成规范写法
  • 有一定经验,想统一团队里的接口数据处理方式

一、先搞清楚:解构在干什么

解构不是黑魔法,本质是按结构从对象/数组中「拆包」出变量,语法更短,逻辑更直观。

// 传统写法:手动挨个取值
const user = { name: '张三', age: 28, city: '北京' };
const name = user.name;
const age = user.age;

// 解构写法:一次性拆出来
const { name, age } = user;

如果接口返回的 user 某天变成 null,传统写法会在 user.name 直接报错,解构可以配合默认值一起用,后面会展开。

二、接口数据解包的 10 个常见写法

假设后台返回结构类似:

{
  code: 200,
  data: {
    user: {
      id: 1,
      name: '李四',
      profile: {
        avatar: 'https://xxx/avatar.png',
        bio: '前端工程师'
      }
    },
    list: [
      { id: 1, title: '文章1' },
      { id: 2, title: '文章2' }
    ]
  }
}

下面 10 个写法,都是日常会用到的。

写法 1:只解构第一层,其余用 rest 收走

const { user, list, ...rest } = response.data;
// user、list 单独用,其他字段在 rest 里

适用: 只需要其中几个字段,但不想丢掉其他字段。
注意: rest 不会包含已解构的 userlist

写法 2:解构 + 默认值,防止 undefined

const { user = {}, list = [] } = response.data || {};

适用: 接口可能返回 datanullundefined,或字段缺失。
注意: 默认值只在值为 undefined 时生效,null 不会触发默认值。

写法 3:多层嵌套一次解构

const { user: { profile: { avatar, bio } = {} } = {} } = response.data || {};

适用: 需要深层字段,不想写 data.user.profile.avatar
踩坑: 每一层都要给默认值 = {},否则中间某层是 undefined 会报错。

写法 4:解构时重命名,避免变量冲突

const { user: currentUser, list: articleList } = response.data || {};

适用: 接口字段名不直观,或和已有变量重名。
语法: 原属性名: 新变量名

写法 5:解构 + 默认值 + 重命名一起用

const { user: currentUser = {}, list: articleList = [] } = response.data || {};

适用: 既要改名,又要防缺。
推荐: 作为接口数据解包的常规写法,可读性和安全性都较好。

写法 6:数组解构取首项

const [firstItem] = response.data?.list || [];

适用: 列表只关心第一项(例如「最新一条」)。
注意: 用可选链 ?.|| [] 避免 listnull/undefined 时报错。

ps· 如果你不知道可选链请点击这里,一文让你轻松了解

写法 7:解构数组元素并设默认值

const [first = {}, second = {}] = response.data?.list || [];

适用: 需要前几项,且要保证拿到的一定是对象。
注意: 空数组时 firstsecond 都是 {}

写法 8:在 map 中解构,简化遍历

const titles = (response.data?.list || []).map(({ id, title }) => title);

适用: 列表只需部分字段,不想写 item.iditem.title
好处: 代码短,意图清晰。

写法 9:解构函数参数,配合默认值

function renderUser({ name = '游客', avatar = '/default.png' } = {}) {
  // 函数内部直接用 name、avatar
}
renderUser(response.data?.user); // 即使传入 undefined 也不报错

适用: 组件、工具函数接收配置对象时。
双重默认值:

  • = {}:整个参数缺失时
  • name = '游客'name 缺失时

写法 10:安全取出深层字段的「一层层解构」写法

const { data } = response || {};
const { user } = data || {};
const { profile } = user || {};
const { avatar } = profile || {};

// 或者一行(每层都要默认值)
const avatar = ((response || {}).data || {}).user?.profile?.avatar ?? '默认头像';

适用: 接口结构不稳定,或经常变更。
建议: 优先用可选链 ?. 和空值合并 ??,逻辑更简洁。

三、容易踩的坑

1. 默认值只对 undefined 生效

const { name = '默认' } = { name: null };
// name 是 null,不是 '默认'

需要兼容 null 时,用空值合并运算符 ??

const name = (obj.name ?? '默认');

2. 嵌套解构少了中间层的默认值

const { user: { profile } } = response.data;  // 若 user 为 undefined,直接报错
const { user: { profile } = {} } = response.data;  // 依然可能报错,user 本身可能 undefined
const { user: { profile } = {} } = response.data || {};  // 正确:两层都要有兜底

3. 解构赋值和变量声明混在一起

const obj = { a: 10 };

// ✅ 正确:声明+解构一步完成({}是声明语法的一部分,解析器认解构)
let {a} = obj; 

let b;
// {b} = obj; // ❌ 报错:语句开头的{}被解析为“块级作用域”,而非解构
({b} = obj);  // ✅ 正确:括号让{}变成表达式,解析器认解构

4. 把 rest 用在已解构过的属性上

const obj = { a: 1, b: 2, c: 3 };
// 解构:单独取出a,剩余属性打包到rest
const { a, ...rest } = obj;

console.log(a);    // 输出:1(单独提取的a)
console.log(rest); // 输出:{ b: 2, c: 3 }(rest不含已解构的a)

四、实战推荐写法模板

通用接口解包:

const response = {
  code: 200,
  msg: "请求成功",
  data: {
    user: {
      name: "张三",
      age: 25,
      profile: {
        avatar: "https://example.com/avatar.jpg"
      }
    },
    list: [
      { id: 1, title: "文章1", content: "内容1" },
      { id: 2, title: "文章2", content: "内容2" }
    ]
  }
};

// 1. 最外层兜底:避免response/null/undefined导致解构报错
const { data = {} } = response || {};
// 2. 解构data层:给user/List设默认值,避免属性不存在
const { user = {}, list = [] } = data;

// 3. 深层解构user:给profile兜底,避免profile为undefined时报错
const { name, profile: { avatar } = {} } = user;

// 4. 列表解构:只提取需要的id/title,过滤无用字段
const items = list.map(({ id, title }) => ({ id, title }));

// 输出结果(验证解构效果)
console.log(name);   // 张三
console.log(avatar); // https://example.com/avatar.jpg
console.log(items);  // [{id:1,title:"文章1"}, {id:2,title:"文章2"}]

封装成工具函数:

function parseUserResponse(response) {
  const { data: { user = {} } = {} } = response || {};
  const { name = '未知', profile: { avatar = '/default.png' } = {} } = user;
  return { name, avatar };
}

五、小结

场景 推荐写法
防缺 const { a = {} } = obj || {}
嵌套解构 每一层都写 = {} 兜底
需要改名 const { a: newName } = obj
取列表首项 const [first] = list || []
列表 map list.map(({ id, title }) => ...)
函数参数 ({ a = 1 } = {}) 双重默认值

记住一点:解构是语法糖,默认值是兜底,把两者结合起来,接口数据处理会干净很多,也更容易排查问题。


以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

《this、箭头函数与普通函数:后台项目里最容易写错的几种场景》

前言

无论你是刚学 JavaScript 的小白,还是已经写了几年代码的前端,只要在写后台管理系统,大概率都踩过 this 和箭头函数的坑。

这篇文章不讲特别玄学的底层原理,只回答三个问题:

  1. 日常写代码该怎么选?(普通函数 vs 箭头函数)
  2. 为什么这么选?
  3. 坑最容易出在哪里?

一、一个真实的报错场景

先看一段后台管理系统里常见的代码:

// 表格操作列有个「删除」按钮
methods: {
  handleDelete(id) {
    this.$confirm('确定删除吗?').then(() => {
      this.deleteApi(id);  // ❌ 报错:Cannot read property 'deleteApi' of undefined
    });
  }
}

很多人会疑惑:我明明在 methods 里写的,this 怎么会是 undefined

问题在于:this 不是由「你在哪写的」决定的,而是由「谁在调用这个函数」决定的。$confirm().then() 里的回调,是 Promise 内部在调用,普通函数不会自动带上 Vue 实例的 this

如果把 .then() 里的回调改成箭头函数,就不会报错了。后面会详细说明原因。

二、基础扫盲:this 到底是谁决定的

核心结论:this 由「调用方式」决定,而不是由「定义位置」决定。

调用方式 this 指向 典型场景
作为对象方法调用 该对象 obj.fn() → this 是 obj
直接调用 fn() 严格模式:undefined;非严格:window 孤立的函数调用
new 调用 新创建的对象 new Foo()
call/apply/bind 传入的第一个参数 显式指定 this
作为回调传入 谁调就指向谁,通常丢 this setTimeout(fn)、Promise.then(fn)

关键点:当函数被当作回调传给别人时,谁调这个函数,this 就由谁决定。 比如 setTimeout(fn) 里,是浏览器在调 fn,所以 this 通常是 windowundefined,而不是你组件里的 this

三、箭头函数 vs 普通函数:本质区别

对比项 普通函数 箭头函数
this 有属于自己的 this,由调用方式决定 没有自己的 this,使用外层作用域的 this
arguments 没有(可用 ...args 替代)
能否 new 可以 不可以
能否作为构造函数 可以 不可以

一句话区分:

  • 普通函数:有「自己的」this,谁调我,this 就指向谁。
  • 箭头函数:没有「自己的」this,用的是「定义时所在作用域」的 this

因此,在需要「继承」外层 this 的场景(例如 PromisesetTimeout 回调),用箭头函数;在对象方法、构造函数等需要「自己的」this 的场景,用普通函数。

四、后台项目里最容易写错的 5 种场景

场景 1:Element UI / Ant Design 表格里的回调

// ❌ 错误写法:在模板里用箭头函数包装,可能拿不到正确的 this
<el-table-column label="操作">
  <template slot-scope="scope">
    <el-button @click="() => this.handleEdit(scope.row)">编辑</el-button>
  </template>
</el-table-column>

// ✅ 正确写法:直接传方法引用,Vue 会帮你绑定 this
<el-button @click="handleEdit(scope.row)">编辑</el-button>

原因: 模板里的事件绑定,Vue 会自动把组件的 this 绑定到方法上。用箭头函数包装后,this 会在定义时就固定,可能指向 windowundefined,反而拿不到组件实例。

结论: 模板事件尽量直接写方法名,或写 (arg) => this.method(arg),不要在模板里随便包箭头函数。

场景 2:Promise / async 里的 this

// ❌ 错误:.then 里用普通函数,this 丢失
handleSubmit() {
  this.validateForm().then(function(res) {
    this.submitForm();  // this 是 undefined!
  });
}

// ✅ 正确:用箭头函数,继承外层的 this
handleSubmit() {
  this.validateForm().then((res) => {
    this.submitForm();  // this 正确指向组件实例
  });
}

原因: .then() 的回调是 Promise 内部调用的,普通函数不会自动绑定组件 this。用箭头函数可以继承 handleSubmit 所在作用域的 this,即组件实例。

结论:Promiseasync/awaitsetTimeout 等异步回调里,需要访问组件/外层 this 时,用箭头函数。

场景 3:对象方法 / API 封装

// ❌ 错误:箭头函数作为对象方法,this 指向外层(window)
const api = {
  baseUrl: '/api',
  getList: () => {
    return axios.get(this.baseUrl + '/list');  // this.baseUrl 是 undefined!
  }
};

// ✅ 正确:用普通函数
const api = {
  baseUrl: '/api',
  getList() {
    return axios.get(this.baseUrl + '/list');
  }
};

原因: 箭头函数没有自己的 this,会去外层找。这里的 getList 定义在对象字面量里,外层是全局,this 就是 window(或 undefined),自然拿不到 baseUrl

结论: 对象方法、Class 方法需要用到 this 时,用普通函数,不要用箭头函数。

场景 4:事件监听器(addEventListener)

// 场景:监听 window 滚动,组件销毁时需要移除监听

// ❌ 错误:箭头函数每次都是新引用,无法正确 removeEventListener
mounted() {
  window.addEventListener('scroll', () => this.handleScroll());
},
beforeDestroy() {
  window.removeEventListener('scroll', () => this.handleScroll());  // 移除失败!引用不同
}

// ✅ 正确:保存同一个函数引用
mounted() {
  this.boundHandleScroll = this.handleScroll.bind(this);
  window.addEventListener('scroll', this.boundHandleScroll);
},
beforeDestroy() {
  window.removeEventListener('scroll', this.boundHandleScroll);
}

原因: removeEventListener 必须传入和 addEventListener 时完全相同的函数引用。每次写 () => this.handleScroll() 都会生成新函数,所以无法正确移除。

结论: 需要手动移除监听时,用 bind 或普通函数,并把引用存到实例上,保证添加和移除用的是同一个函数。

场景 5:数组方法的回调(forEachmapfilter 等)

// 在 Vue 组件里
methods: {
  processList() {
    const list = [1, 2, 3];
    
    // ❌ 错误:普通函数作为 forEach 回调,this 会丢
    list.forEach(function(item) {
      this.doSomething(item);  // this 是 undefined
    });
    
    // ✅ 正确:箭头函数继承外层的 this
    list.forEach((item) => {
      this.doSomething(item);
    });
  }
}

原因: forEach 等方法的回调是由数组方法内部调用的,普通函数不会绑定组件 this。用箭头函数可以继承 processListthis

结论:forEachmapfilterreduce 等回调里需要访问外层 this 时,用箭头函数;不需要 this 时,两者都可以。

五、决策清单:什么时候用谁

可以按下面几条来选:

  1. 对象方法、Class 方法、构造函数 → 用普通函数。
  2. Promise、setTimeout、数组方法等回调里要访问外层 this → 用箭头函数。
  3. Vue 模板事件 → 直接写方法名,或 (arg) => this.method(arg),避免乱包箭头函数。
  4. 需要 arguments → 用普通函数,或箭头函数 + ...args
  5. addEventListener / removeEventListener → 用 bind 或保存同一引用,保证添加和移除是同一个函数。

六、一句话口诀

  • 普通函数:有自己的 this,谁调我,this 就指向谁。
  • 箭头函数:没有自己的 this,用的是「定义时所在外层」的 this

需要「动态 this」用普通函数,需要「固定外层 this」用箭头函数。

总结

this 和箭头函数本身不复杂,容易出错的是「在错误场景选错写法」。后台项目里,最容易踩坑的就是:Promise 回调、对象方法、模板事件、事件监听器这几处。记住「谁在调用」「外层 this 是谁」,选普通函数还是箭头函数就不容易错。

以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

《变量与作用域:var / let / const 到底怎么选?》

写 JS 时用 varlet 还是 const?很多人要么凭感觉,要么“一律用 const”。这篇文章不讲特别玄的底层,只讲三件事:基础概念别混、日常怎么选、坑在哪。适合:已经会写 JS 但概念有点混的、从零开始的小白、以及想打牢基础、校准习惯的前端。

一、先搞清楚:三个关键字分别是什么

1.1 一句话区别

关键字 出现时间 作用域 能否重复声明 能否先使用再声明
var ES5 函数作用域 可以 可以(会提升)
let ES6 块级作用域 不可以 不可以(暂时性死区)
const ES6 块级作用域 不可以 不可以(暂时性死区)

用人话说:

  • var:老写法,按“函数”划分地盘,容易踩坑。
  • let:按“块”划分地盘,不能重复声明,更符合直觉。
  • const:和 let 一样是块级,但声明后不能重新赋值(注意:引用类型里的属性可以改)。

1.2 作用域:函数作用域 vs 块级作用域

函数作用域(var): 只认 function,不认 if/for/while 等块。

function fn() {
  if (true) {
    var a = 1;
  }
  console.log(a);  // 1 —— if 块挡不住 var
}

块级作用域(let/const):{},包括 ifforwhile、单独 {}

function fn() {
  if (true) {
    let a = 1;
    const b = 2;
  }
  console.log(a);  // ReferenceError: a is not defined
  console.log(b);  // ReferenceError: b is not defined
}

日常结论: 在块里声明的变量,如果希望“只在这个块里有效”,用 let/const;用 var 会“漏”到整个函数,容易产生隐蔽 bug。

1.3 变量提升(Hoisting)/ˈhɔɪstɪŋ/ 与暂时性死区(TDZ)

ps· TDZ全称:Temporal Dead Zone 音标:/ˈtempərəl/, /ded/ ,/zəʊn/

var:会提升,先使用再声明也不会报错(只是值为 undefined

console.log(x);  // undefined
var x = 10;
console.log(x);  // 10

let/const:有暂时性死区,在声明之前访问会报错

console.log(y);  // ReferenceError: Cannot access 'y' before initialization
let y = 10;

日常结论: 养成“先声明、再使用”的习惯,用 let/const 可以避免“还没赋值就被用”的坑。

1.4 const 不是“完全不能改”

const 限制的是绑定(不能重新赋值),不限制引用类型内部的修改

const obj = { name: '小明' };
obj.name = '小红';   // ✅ 可以,改的是对象内部
obj = {};            // ❌ 报错,不能换一个对象

const arr = [1, 2, 3];
arr.push(4);         // ✅ 可以
arr = [];            // ❌ 报错

所以:const 适合“这个变量指向的引用不变”的场景,不是“对象/数组内容不能动”。

二、日常写代码:到底怎么选?

2.1 推荐原则(可直接当规范用)

  1. 默认用 const
    只要这个变量不会在逻辑里被重新赋值,就用 const。包括:对象、数组、函数、配置、导入的模块等。

  2. 需要“会变”的变量用 let
    例如:循环计数器、会随逻辑重新赋值的中间变量、交换两数等。

  3. 新代码里不用 var
    除非维护老项目且项目约定用 var,否则一律 let/const

2.2 按场景选

场景 推荐 原因
导入模块、配置对象、API 地址等 const 不打算换引用
普通对象、数组(内容会增删改) const 引用不变,只改内部
for 循环里的下标 / 循环变量 let 每次迭代会变
需要先声明、后面再赋值的变量 let const 声明时必须赋初值
交换变量、累加器、临时中间变量 let 会重新赋值
老项目、历史代码 按项目规范,能改则逐步改为 let/const 避免混用加重混乱

2.3 简单示例

// ✅ 用 const:引用不变
const API_BASE = 'https://api.example.com';
const user = { name: '张三', age: 25 };
user.age = 26;  // 可以

// ✅ 用 let:会重新赋值
let count = 0;
count++;
let temp;
if (condition) temp = a; else temp = b;

// ❌ 不要用 var(新代码)
var oldStyle = 1;  // 容易漏出块、提升导致误用

三、常见坑:会踩在哪?

3.1 坑一:循环里用 var,回调里拿到的是“最后的那个值”

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(共用一个 i,循环结束后 i 已是 3)

正确写法:let,每次迭代都是新的绑定。

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2

3.2 坑二:同一作用域里重复声明 let/const 会报错

let a = 1;
let a = 2;  // SyntaxError: Identifier 'a' has already been declared

var 可以重复声明(不报错),但可读性和维护性差。用 let/const 可以尽早发现“名字写重了”的问题。

3.3 坑三:const 声明时必须赋初值

const x;  // SyntaxError: Missing initializer in const declaration
const y = 1;  // ✅

如果“现在不知道值,后面才赋值”,用 let

3.4 坑四:以为 const 对象/数组“完全不能改”

再次强调:const 限制的是「变量与引用类型的绑定关系」(变量不能指向新的引用地址),而非对象的属性值 / 数组的元素值。我们可以修改的是 “引用类型内部的内容”,比如对象的value、数组的元素。

3.5 坑五:老项目里 varlet/const混用

同一函数里既有 var 又有 let,作用域和提升行为不一致,排查问题会很难。建议:新加的逻辑一律 let/const,老代码有机会就逐步替换成 let/const

四、和“作用域”相关的两个小点

4.1 块级作用域对 if/else 很有用

if (condition) {
  const message = 'yes';
  // 只用在这里
} else {
  const message = 'no';
  // 只用在这里
}
// message 在块外不可见,不污染外部

var 的话,message 会跑到整个函数里,容易重名或误用。

4.2 模块、全局与 window

  • ES Module 里,顶层的 const/let 不会挂到 window 上,和“全局变量”是两回事。
  • 传统脚本里,顶层 var 会变成 window 的属性。
  • 日常:用模块 + const/let,减少全局污染。

五、总结:一张表 + 一句话

要点 说明
默认 能用 const 就用 const
会重新赋值 let
新项目/新代码 不用 var
循环 + 异步/回调 let,避免 var 的“最后一个值”
const 不能重新赋值,但对象/数组内部可以改

一句话: 日常写 JS,默认 const,要改再用 let,别再写 var。先把“选谁”的习惯固定下来,再结合作用域和 TDZ 理解“为什么”,就能少踩坑、代码也更清晰。

以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

枚举不理解?一文让你醍醐灌顶

一、什么是枚举?先做 1 个生活化类比(核心)

JavaScript 对象想象成一个抽屉,属性就是抽屉里的文件:

  • 「可枚举」的文件:贴了「可展示」标签 → 别人来翻你的抽屉(遍历),能看到这份文件;
  • 「不可枚举」的文件:没贴「可展示」标签 → 别人翻抽屉看不到,但你自己知道文件在哪,能直接拿出来用

枚举(enumerable)就是这个「可展示」标签 —— 唯一作用:决定属性是否能被 “遍历工具” 看到,和属性本身是否存在、能否使用无关。

二、 用 3 行极简代码,看遍枚举的所有区别

我只写最核心的代码,逐行解释,你可以直接复制到浏览器控制台运行:

// 1. 创建抽屉(对象),放1个“可展示”文件(默认可枚举属性)
const drawer = {
  文件A: "购物清单" // 没特殊说明,默认贴“可展示”标签(可枚举)
};

// 2. 往抽屉里加1个“不可展示”文件(手动设为不可枚举)
Object.defineProperty(drawer, "文件B", {
  value: "私密日记", // 文件内容
  enumerable: false // 核心:撕掉“可展示”标签(不可枚举)
});

// 3. 演示:别人翻抽屉(遍历)能看到什么?
console.log("别人翻抽屉看到的:", Object.keys(drawer)); 
// 输出:别人翻抽屉看到的:['文件A'] → 只看到可枚举的文件A

// 4. 演示:你自己拿文件(直接访问)能拿到什么?
console.log("你直接拿文件A:", drawer.文件A); // 输出:购物清单
console.log("你直接拿文件B:", drawer.文件B); // 输出:私密日记 → 虽然看不到,但能直接用!

三、 再补 1 个最常用的 “遍历工具” 对比(只看枚举的影响)

还是用上面的 drawer 对象,看最常用的 for...in 遍历:

// 别人翻抽屉(for...in遍历)
console.log("遍历结果:");
for (let 文件名称 in drawer) {
  console.log(文件名称); // 只输出“文件A” → 还是看不到文件B
}

四、 关键追问:为什么要搞 “不可枚举”?

举个实际开发的例子:你写了一个用户对象,想存「公开信息」和「私密信息」:

const user = {
  昵称: "小明", // 公开(可枚举,别人能看到)
};
// 身份证号是私密的,设为不可枚举
Object.defineProperty(user, "身份证号", {
  value: "110xxxx",
  enumerable: false
});

// 场景1:展示用户信息(遍历)→ 只显示公开的昵称,不会泄露身份证号
console.log("用户公开信息:", Object.keys(user)); // ['昵称']

// 场景2:后台验证(直接访问)→ 能拿到身份证号做校验
console.log("验证身份:", user.身份证号); // 110xxxx

五、 总结(只记 2 个核心点,多了不记)

  1. 枚举的唯一作用:给属性贴 “可展示” 标签,决定 Object.keys()for...in 等「遍历工具」能不能看到这个属性;
  2. 关键区别:不可枚举的属性只是 “隐身”,不是 “消失”—— 遍历看不到,但能直接访问使用。

以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

❌