写在前面
本文覆盖四块核心语法:
- 运算符 :算术、比较、逻辑,重点讲
==/===的区别与判断规则; - 流程控制 :分支与循环,以及
for...of/for...in该用谁; - 函数:定义方式、参数处理、作用域与闭包;
- 数组与对象 :这是重头戏------数组高频 API 逐一梳理,明确区分哪些会修改原数组、哪些返回新数组,配合业务代码示例。
全篇以"能直接写业务代码"为目标,示例尽量贴近真实场景(购物车、去重、校验、筛选等)。
一、运算符
1.1 算术运算符
+ - * / %(取余)**(幂)++ --
js
console.log(7 % 3); // 1(取余)
console.log(2 ** 10); // 1024(幂)
console.log(5 + "5"); // "55"(+ 号遇字符串变拼接)
console.log(5 - "2"); // 3(- 号强制转数值)
1.2 比较运算符与 == / === 的本质区别
这是面试与业务排错的高频点。三条铁律:
===(严格相等):类型相同且值相同 才为true,不转换,推荐默认使用;==(宽松相等):比较前做隐式类型转换,规则复杂、易出错;Object.is:语义与===基本一致,但能区分NaN与±0。
js
1 === "1"; // false(类型不同)
1 == "1"; // true(字符串转数字)
null === undefined; // false
null == undefined; // true(== 的经典特例)
0 === -0; // true
Object.is(0, -0); // false
NaN === NaN; // false
Object.is(NaN, NaN); // true
== 的隐式转换核心规则(记几条常见的即可):
| 表达式 | 结果 | 原因 |
|---|---|---|
1 == "1" |
true |
字符串转数字 |
null == undefined |
true |
规范特例 |
0 == "" |
true |
空字符串转 0 |
0 == false |
true |
布尔转数字 |
[] == 0 |
true |
空数组转 "" → 0 |
[] == ![] |
true |
右 ![] 为 false → 0,左 [] 为 0 |
结论:业务代码一律用 === 。遇到 == 先怀疑是历史包袱。
1.3 逻辑运算符与短路
js
// &&:左侧为假则返回左侧,否则返回右侧
console.log(0 && 100); // 0
console.log(1 && 100); // 100
// ||:左侧为真则返回左侧,否则返回右侧
console.log(0 || 100); // 100
console.log(1 || 100); // 1
// !:取反
console.log(!0); // true
短路求值是常用技巧:左侧结果已能决定整个表达式时,右侧不再执行。
js
// 常用场景:函数参数兜底
function greet(name) {
name = name || "陌生人"; // name 为空时给默认值
console.log("你好," + name);
}
但 || 有陷阱:0、"" 也是假值,会被错误替换。ES2020 引入 ??(空值合并) :只在左侧为 null 或 undefined 时才取右侧:
js
const a = 0 ?? 100; // 0(0 不是 null/undefined,保留)
const b = null ?? 100; // 100
const c = "" ?? 100; // ""(空字符串保留)
配套的逻辑赋值运算符:
js
x ||= 10; // 等价 x = x || 10
x &&= 10; // 等价 x = x && 10
x ??= 10; // 等价 x = x ?? 10(仅在 null/undefined 时赋值)
1.4 其他常用运算符
js
// 三元
const status = age >= 18 ? "成年" : "未成年";
// 一元 + 转数值
const num = +"42"; // 42
// typeof
typeof undefined; // "undefined"
// instanceof:检查原型链
[] instanceof Array; // true
// in:检查属性是否存在
"name" in { name: "Tom" }; // true
// delete:删除属性
delete obj.name;
二、流程控制
2.1 分支
if / else if / else------最常用:
js
function level(score) {
if (score >= 90) return "优秀";
if (score >= 60) return "及格";
return "不及格";
}
switch ------匹配多个值时更清晰(注意 break 防穿透):
js
function weekday(n) {
switch (n) {
case 1: return "周一";
case 2: return "周二";
default: return "其他";
}
}
三元------只适合简单的二选一,嵌套会降低可读性。
2.2 循环
| 循环 | 适用场景 | 示例 |
|---|---|---|
for |
知道次数、需索引 | for (let i = 0; i < 5; i++) |
while |
不确定次数、条件驱动 | 轮询、输入校验 |
do...while |
至少执行一次 | 至少提示一次 |
for...of |
遍历可迭代对象的值(数组/字符串/Set) | for (const item of arr) |
for...in |
遍历对象的键(含原型链,慎用) | for (const key in obj) |
js
// for...of:遍历数组值,推荐
const arr = [10, 20, 30];
for (const item of arr) {
console.log(item); // 10 20 30
}
// for...in:遍历对象键,数组上慎用
const user = { name: "Tom", age: 18 };
for (const key in user) {
console.log(key, user[key]);
}
// 提前退出
for (const item of arr) {
if (item === 20) break; // 终止整个循环
// continue; // 跳过本次
}
for...in会遍历原型链上可枚举的属性 ,且遍历顺序不保证,遍历数组应改用for...of或数组方法。
三、函数
3.1 三种定义方式
js
// ① 函数声明:有提升(可在定义前调用)
function add(a, b) { return a + b; }
// ② 函数表达式:无提升
const sub = function (a, b) { return a - b; };
// ③ 箭头函数:ES6,简写 + 词法 this
const mul = (a, b) => a * b;
const log = () => console.log("hi");
| 对比 | 函数声明 | 函数表达式 | 箭头函数 |
|---|---|---|---|
| 提升 | 有 | 无 | 无 |
this |
动态(调用时决定) | 动态 | 词法(定义时决定) |
arguments |
有 | 有 | 无 |
| 简写 | - | - | 单表达式可省 {} 与 return |
箭头函数的 this 继承自外层作用域------这是它与普通函数最本质的区别,回调场景尤其重要:
js
const obj = {
name: "Tom",
// 普通函数:this 指向调用者 obj
sayName: function () {
console.log(this.name); // "Tom"
},
// 箭头函数:this 是定义时的外层(此处为全局)
sayNameArrow: () => {
console.log(this.name); // undefined
},
};
3.2 参数处理
js
// 默认参数:ES6
function greet(name = "陌生人", age = 0) {
return `${name}(${age})`;
}
// 实参多于形参:多余的被忽略
// 实参少于形参:缺失的为 undefined
// rest 参数:收集剩余实参为数组
function sum(...nums) {
return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3); // 6
// 旧方案 arguments(箭头函数没有)
function oldSum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) total += arguments[i];
return total;
}
3.3 作用域与闭包
作用域 :全局、函数、块级(let/const)。变量查找沿作用域链由内向外。
js
let x = 10; // 全局
function outer() {
let y = 20; // outer 作用域
function inner() {
let z = 30; // inner 作用域
console.log(x + y + z); // 60:逐层向上查找
}
inner();
}
闭包:函数记住并访问其定义时的作用域,即使外层函数已返回。业务中常用于"私有变量"与"缓存":
js
function createCounter() {
let count = 0; // count 被闭包捕获,外部无法直接访问
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
// count 外部不可见,但 counter 能持续修改它
IIFE(立即执行函数):定义后立刻执行,用于隔离作用域:
js
const result = (function (a, b) {
return a + b;
})(1, 2); // 3
四、数组高频 API(核心:是否修改原数组)
这是本篇最有价值的部分。先用一句话记住:push/pop/shift/unshift/splice/sort/reverse 修改原数组;map/filter/reduce/slice/concat 返回新数组。
4.1 修改原数组的方法(Mutating)
js
const arr = [1, 2, 3];
arr.push(4); // 尾部追加 → [1,2,3,4],返回新长度 4
arr.pop(); // 删除尾部 → [1,2,3],返回删除的元素 3
arr.unshift(0); // 头部插入 → [0,1,2,3],返回新长度
arr.shift(); // 删除头部 → [1,2,3],返回删除的元素 0
arr.splice(1, 1); // 从索引 1 删 1 个 → [1,3](最灵活)
const b = [3, 1, 2];
b.sort(); // 默认按字符串排序 → [1,2,3]
b.reverse(); // 反转 → [3,2,1]
arr.fill(0); // 全部填充 0 → [0,0,0]
sort默认按字符串 排序,数字需传比较函数:arr.sort((a, b) => a - b)。
4.2 返回新数组的方法(Non-mutating)------ 业务主力
js
const arr = [1, 2, 3, 4, 5];
// map:映射,长度不变
arr.map(x => x * 10); // [10,20,30,40,50]
// filter:筛选,长度可变
arr.filter(x => x > 2); // [3,4,5]
// slice:切片(不含结束索引),最常用来"复制"数组
arr.slice(1, 3); // [2,3]
arr.slice(); // [1,2,3,4,5] 浅拷贝
// concat:拼接
arr.concat([6, 7]); // [1,2,3,4,5,6,7]
// join:转字符串
arr.join("-"); // "1-2-3-4-5"
// flat:拍平嵌套数组
[1, [2, [3]]].flat(2); // [1,2,3]
// 查找
arr.find(x => x > 3); // 4(返回第一个满足的值)
arr.findIndex(x => x > 3); // 3(返回索引)
arr.includes(3); // true
arr.indexOf(3); // 2
// 判断
arr.some(x => x > 4); // true(任一满足)
arr.every(x => x > 0); // true(全部满足)
// reduce:归并
arr.reduce((acc, x) => acc + x, 0); // 15(求和)
map 不修改原数组------这是业务中最容易踩的坑:
js
const prices = [10, 20, 30];
prices.map(p => p * 2); // 结果被丢弃,原数组不变
const doubled = prices.map(p => p * 2); // 必须接收返回值
4.3 遍历:forEach
js
arr.forEach((item, index) => {
console.log(index, item);
});
// 无法 break/continue;需要提前退出用 for...of 或 some
4.4 原地 vs 非原地速查表
| 方法 | 修改原数组? | 返回 |
|---|---|---|
push / pop / shift / unshift |
✅ | 新长度 / 删除的元素 |
splice |
✅ | 删除的元素数组 |
sort / reverse |
✅ | 原数组本身 |
fill |
✅ | 原数组本身 |
map / filter |
❌ | 新数组 |
slice / concat |
❌ | 新数组 |
reduce |
❌ | 任意值 |
find / findIndex / includes |
❌ | 值 / 索引 / 布尔 |
some / every |
❌ | 布尔 |
join / flat |
❌ | 字符串 / 新数组 |
forEach |
❌(只遍历) | 无 |
五、对象操作
5.1 增删改查
js
const user = { name: "Tom", age: 18 };
user.age = 19; // 改
user.email = "t@e.com"; // 增
delete user.email; // 删
// 方括号访问:键是动态变量时
const key = "name";
user[key]; // "Tom"
// 动态键创建
const dynamic = "role";
const obj = { [dynamic]: "admin" }; // { role: "admin" }
5.2 遍历对象
js
const user = { name: "Tom", age: 18 };
Object.keys(user); // ["name","age"]
Object.values(user); // ["Tom",18]
Object.entries(user); // [["name","Tom"],["age",18]]
for (const [key, value] of Object.entries(user)) {
console.log(key, value);
}
5.3 合并与拷贝(浅)
js
// Object.assign:把源对象属性复制到目标
const base = { a: 1, b: 2 };
const extra = { b: 99, c: 3 };
Object.assign({}, base, extra); // { a:1, b:99, c:3 }
// 展开运算符:更常用
const merged = { ...base, ...extra }; // 同上
const copy = { ...base }; // 浅拷贝
两者都是浅拷贝:嵌套对象仍共享。
5.4 解构赋值
js
// 对象解构:提取 + 重命名 + 默认值
const { name, age, city = "北京" } = user;
const { name: userName } = user; // 重命名为 userName
// 数组解构 + 剩余
const [first, second, ...rest] = [1, 2, 3, 4];
// first=1, second=2, rest=[3,4]
// 函数参数解构
function printUser({ name, age }) {
console.log(name, age);
}
printUser(user);
5.5 判断属性
js
"name" in user; // true(含原型链)
Object.hasOwn(user, "name"); // true(只查自身,ES2022 推荐)
user.hasOwnProperty("name"); // true(旧写法)
5.6 Object.freeze 与 Set/Map(补充)
js
const frozen = Object.freeze({ a: 1 });
// frozen.a = 2; // 严格模式下报错
const set = new Set([1, 2, 2, 3]); // Set 自动去重
[...set]; // [1,2,3]
const map = new Map([["k", "v"]]);
map.get("k"); // "v"
六、实战:业务代码综合示例
实战 1:购物车合计与数量统计
js
const cart = [
{ id: 1, name: "手机", price: 2999, count: 1 },
{ id: 2, name: "耳机", price: 399, count: 2 },
{ id: 3, name: "充电器", price: 99, count: 3 },
];
// 合计金额:reduce
const total = cart.reduce((sum, item) => sum + item.price * item.count, 0);
// 商品总件数
const totalCount = cart.reduce((sum, item) => sum + item.count, 0);
// 满 3000 减 100 优惠
const payable = total >= 3000 ? total - 100 : total;
console.log(`合计 ${total} 元,共 ${totalCount} 件,应付 ${payable} 元`);
实战 2:数组去重与频次统计
js
const nums = [1, 2, 2, 3, 3, 3, 4];
// 去重方案一:Set
const unique = [...new Set(nums)]; // [1,2,3,4]
// 去重方案二:filter + indexOf(保留首次出现)
const unique2 = nums.filter((v, i) => nums.indexOf(v) === i);
// 统计每个数字出现次数
const freq = nums.reduce((acc, v) => {
acc[v] = (acc[v] || 0) + 1;
return acc;
}, {});
// {1:1, 2:2, 3:3, 4:1}
实战 3:表单校验(短路 + every)
js
const form = {
username: "tom",
password: "123456",
email: "t@e.com",
};
const errors = [];
// 逐项校验:短路避免空值继续判断
if (!form.username) errors.push("用户名必填");
if (form.password.length < 6) errors.push("密码至少 6 位");
if (!form.email.includes("@")) errors.push("邮箱格式不正确");
// 或:用 every 批量校验
const valid = ["username", "password", "email"].every((key) =>
Boolean(form[key])
);
console.log(errors.length ? errors : "校验通过");
实战 4:列表筛选 + 排序 + 分页
js
const products = [
{ name: "A", price: 100, inStock: true },
{ name: "B", price: 50, inStock: false },
{ name: "C", price: 200, inStock: true },
{ name: "D", price: 80, inStock: true },
];
// 链式调用:都不修改原数组
const pageSize = 2;
const page = 1;
const result = products
.filter(p => p.inStock) // 筛选有货
.sort((a, b) => a.price - b.price) // 按价格升序
.slice((page - 1) * pageSize, page * pageSize); // 分页
console.log(result); // 第一页 2 条
实战 5:对象深合并(递归)
js
function deepMerge(target, source) {
for (const key of Object.keys(source)) {
const sv = source[key];
if (
sv && typeof sv === "object" && !Array.isArray(sv) &&
target[key] && typeof target[key] === "object"
) {
deepMerge(target[key], sv); // 两边都是对象则递归
} else {
target[key] = sv;
}
}
return target;
}
const defaults = { theme: { color: "blue", size: 12 } };
const custom = { theme: { color: "red" } };
deepMerge(defaults, custom);
// { theme: { color: "red", size: 12 } } ------ size 被保留
七、高频误区清单
| 误区 | 正解 |
|-----------------------------|-----------------------------|-------------------|-------------------------------|
| 1 == "1" 为 true 就用 == | 一律用 ===,== 隐式转换是坑源 |
| 用 ` | | 兜底0/""` 被误替换 | 用 ??(只兜 null/undefined) |
| arr.map(...) 后没接收返回值 | map 返回新数组,必须接住 |
| sort() 直接对数字排序 | 数字须传 (a, b) => a - b |
| for...in 遍历数组 | 用 for...of 或数组方法 |
| 认为箭头函数有 arguments | 箭头函数无 arguments,用 rest 参数 |
| Object.assign 是深拷贝 | 是浅拷贝,嵌套对象共享 |
| 修改了 splice 得到的新数组 | splice 修改原数组,返回的是被删元素 |
八、总结
用一张表收束本篇核心:
| 主题 | 一句话记住 |
|---|---|
| 比较 | 默认 ===,兜底 ??,杜绝裸 == |
| 分支 | if 打天下,多值用 switch,简单二选一用三元 |
| 循环 | 数组用 for...of,对象用 Object.entries,别用 for...in 遍历数组 |
| 函数 | 箭头函数无自己的 this/arguments,回调优先箭头 |
| 数组 | 改原数组:push/pop/shift/unshift/splice/sort/reverse;返回新数组:map/filter/reduce/slice/concat |
| 对象 | 增删查走 Object.keys/entries,拷贝用展开,深合并用递归 |
三条实践心法收尾:
- 先想"会不会改原数组" :写
map/filter时记得接住返回值; - 优先不可变风格:用返回新数组的方法链式处理数据,状态更可控;
- 短路与解构提效率 :
&&/||/??精简判断,解构让取值更清晰。