03 - 对象与数组
前置:函数与作用域。JS 的对象是”键值对的无序集合”,数组是特殊的对象——没有 Java 那种类与集合框架的严格分层,一切数据结构几乎都靠这两样搭出来。
1. 对象字面量
1.1 创建与属性访问
// 对象字面量:最常用的创建方式
const student = {
name: "张三", // 键可以不加引号(合法标识符时)
"home address": "北京", // 含空格等特殊字符必须加引号
age: 20,
scores: [92, 85, 78], // 值可以是任何类型,包括另一个对象
greet() { // 方法简写(ES6),等价于 greet: function() {}
return `我是 ${this.name}`;
},
};
// 点访问:键是合法标识符时使用
console.log(student.name); // "张三"
console.log(student.greet()); // "我是 张三"
// 方括号访问:键含特殊字符 / 是变量 / 是数字时必须用
console.log(student["home address"]); // "北京"
const key = "age";
console.log(student[key]); // 20 —— 点访问做不到动态键点 vs 方括号的选择规则一句话:静态已知键用点,动态拼接或非法标识符用方括号。类比 Java:JS 对象更像 Map<String, Object> 而不是某个具体类的实例——属性本质就是字符串键。
1.2 增删改与遍历
// 增:直接赋值即可,无需预先声明字段
student.gender = "男";
student["phone"] = "13800000000";
// 改
student.age = 21;
// 删:delete 操作符
delete student.gender;
// 遍历方式一:for...in(包含继承来的可枚举键,需配合 hasOwnProperty 过滤)
for (const key in student) {
if (Object.prototype.hasOwnProperty.call(student, key)) {
console.log(key, student[key]);
}
}
// 遍历方式二(推荐):Object.keys / values / entries 返回真数组
Object.keys(student); // ["name", "home address", "age", "scores", "greet", "phone"]
Object.values(student);
Object.entries(student); // [["name","张三"], ["age",21], ...]
// entries 配合 for...of + 解构是最清晰的遍历姿势
for (const [key, value] of Object.entries(student)) {
console.log(`${key}: ${value}`);
}判断属性是否存在用 "age" in student 或 student.age !== undefined(后者会把值为 undefined 的属性误判为不存在)。
2. this 在对象方法中
对象方法里的 this 指向调用时的点号前面那个对象:
const counter = {
count: 0,
increment() {
this.count++; // this === counter
},
};
counter.increment();
console.log(counter.count); // 1但一旦方法被”抽离”出来单独调用,this 就丢失了(上一章的规则 1):
const inc = counter.increment; // 只是拿到函数引用
inc(); // this 变成 window/undefined,count 变到全局去了典型翻车场景是把方法作为回调传递:
document.querySelector("button").addEventListener("click", counter.increment);
// 点击后 this 是 button 而不是 counter!
// 解法一:包一层箭头函数(推荐)
button.addEventListener("click", () => counter.increment());
// 解法二:bind 固定 this
button.addEventListener("click", counter.increment.bind(counter));3. 解构赋值
3.1 对象解构
const config = {
host: "localhost",
port: 8080,
options: { retry: true },
};
// 从对象中按名提取
const { host, port } = config;
// 重命名
const { host: hostname } = config; // 变量叫 hostname
// 默认值:属性不存在时生效
const { timeout = 5000 } = config; // timeout = 5000
// 嵌套解构
const { options: { retry } } = config; // retry = true最常见的用途是接收函数返回的多个值和函数参数解构:
function connect({ host, port, timeout = 3000 }) {
console.log(`连接 ${host}:${port},超时 ${timeout}ms`);
}
connect({ host: "example.com", port: 443 }); // 调用处一目了然,还自带命名参数效果对比 Java:Java 直到最近才用 record 类模拟这种体验;JS 的参数解构让每个函数天然支持”命名参数”,弥补了无重载带来的可读性问题。
3.2 数组解构
const rgb = [255, 128, 0];
const [r, g, b] = rgb;
const [first, , third] = [1, 2, 3]; // 跳位
const [head, ...rest] = [1, 2, 3, 4]; // head=1, rest=[2,3,4]
// 经典技巧:交换变量(无需临时变量)
let a = 1, b = 2;
[a, b] = [b, a];4. 展开运算符与浅拷贝陷阱
// 数组展开
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // 合并
Math.max(...arr1); // 展开传参,得 3
// 对象展开:后面的同名键覆盖前面的
const defaults = { theme: "light", lang: "zh" };
const userPref = { lang: "en" };
const merged = { ...defaults, ...userPref }; // { theme:"light", lang:"en" }
// 浅拷贝
const copy = { ...defaults };浅拷贝陷阱:展开只复制第一层,嵌套对象仍然共享引用——这与 Java 的 clone 浅拷贝、Arrays.copyOf 行为完全一致:
const original = { info: { age: 20 }, tags: ["a"] };
const shallow = { ...original };
shallow.info.age = 99;
console.log(original.info.age); // 99 !嵌套对象被改动了
// 深拷贝方案一:structuredClone(现代浏览器原生支持)
const deep1 = structuredClone(original);
deep1.info.age = 1;
console.log(original.info.age); // 99,不受影响
// 深拷贝方案二:JSON 序列化(有局限:丢函数/undefined/Symbol,Date 变字符串)
const deep2 = JSON.parse(JSON.stringify(original));记忆模型:JS 的引用类型变量存的都是”地址”,... 只复制地址本身一层;想彻底隔离就用 structuredClone。
5. 数组高阶方法:函数式思维
以下方法是现代 JS 数据处理的基石,思想与 Java Stream 完全同源——都是不修改原数据,通过函数变换产出新结果。对照阅读 Stream 与函数式 收获加倍。
先准备一份公共数据:
const students = [
{ name: "张三", score: 92, cls: "A" },
{ name: "李四", score: 58, cls: "B" },
{ name: "王五", score: 76, cls: "A" },
{ name: "赵六", score: 88, cls: "B" },
];5.1 map:一对一变换
const names = students.map(s => s.name);
// ["张三", "李四", "王五", "赵六"]
const withGrade = students.map(s => ({
...s,
grade: s.score >= 60 ? "及格" : "不及格",
}));
// 每个元素都变成新对象,原数组不变对应 Java:students.stream().map(s -> s.getName()).toList()。
5.2 filter:过滤
const passed = students.filter(s => s.score >= 60);
// 3 个元素的新数组,李四被淘汰对应 Java:.filter(s -> s.getScore() >= 60)。链式组合与 Stream 一模一样:
const aClsHonors = students
.filter(s => s.cls === "A")
.filter(s => s.score >= 90)
.map(s => s.name);
// ["张三"]5.3 reduce:聚合归并
// reduce(累积函数, 初始值)
const total = students.reduce((acc, s) => acc + s.score, 0);
// 314
// 进阶:分组(reduce 最能体现威力的用法)
const byCls = students.reduce((groups, s) => {
(groups[s.cls] ??= []).push(s);
return groups;
}, {});
// { A: [{张三}, {王五}], B: [{李四}, {赵六}] }对应 Java:.reduce(0, Integer::sum) 或 Collectors.groupingBy。(a ??= b) 是”空值合并后赋值”简写,见 ES6+ 特性。
5.4 find / findIndex / some / every
students.find(s => s.name === "李四"); // 第一个匹配的元素或 undefined
students.findIndex(s => s.score < 60); // 第一个匹配的下标或 -1
students.some(s => s.cls === "C"); // false:是否存在至少一个满足
students.every(s => s.score >= 60); // false:是否全部满足对照 Java:findFirst()、anyMatch()、allMatch()。注意 JS 版本短路求值同样成立——some 找到一个就停,every 遇到一个不满足就停。
| 方法 | 输出 | Java 对应 | 是否修改原数组 |
|---|---|---|---|
| map | 新数组(同长度) | map | 否 |
| filter | 子集新数组 | filter | 否 |
| reduce | 任意聚合值 | reduce/collect | 否 |
| find | 元素或 undefined | findFirst | 否 |
| some/every | boolean | anyMatch/allMatch | 否 |
5.5 sort 的坑:默认按字符串排序
const nums = [10, 1, 5, 100];
nums.sort();
console.log(nums); // [1, 10, 100, 5] !默认转字符串按字典序排
// 数字排序必须传入比较器(与 Java Comparator.compare 同构)
nums.sort((a, b) => a - b); // 升序:[1, 5, 10, 100]
nums.sort((a, b) => b - a); // 降序
// 更隐蔽的坑:sort 会原地修改原数组!
const original = [3, 1, 2];
const sorted = [...original].sort((a, b) => a - b); // 先拷贝再排序
// ES2023 提供了不改原数组的 toSorted()
const sorted2 = original.toSorted((a, b) => a - b);比较器返回负数/零/正数决定前后顺序,这条规则和 Java 一致,可以直接迁移经验。同理注意会修改原数组的还有 reverse/splice/push/pop/shift/unshift,不想动原数组就先展开拷贝或用 toReversed/toSpliced。
6. class 语法
ES6 引入的 class 让 JS 终于有了和 Java 接近的面向对象书写形式。底层实现仍是原型链(prototype),但日常开发可以把它当作语法糖使用,原型机制深入见后续章节。
class Animal {
// 构造器与实例字段
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() {
return `${this.name} 说 ${this.sound}`;
}
// getter / setter
get title() {
return `[${this.name}]`;
}
set nickname(value) {
this._nick = value.toUpperCase();
}
}
class Dog extends Animal {
constructor(name) {
super(name, "汪"); // 必须先调用 super 再用 this
this.legs = 4;
}
// 重写父类方法(无需 override 注解,直接覆盖)
speak() {
const base = super.speak();
return `${base},还会摇尾巴`;
}
}
const dog = new Dog("旺财");
console.log(dog.speak()); // "旺财 说 汪,还会摇尾巴"
console.log(dog.title); // "[旺财]" —— getter 当属性用
dog.nickname = "da huang";
console.log(dog._nick); // "DA HUANG"与 Java 的显著差异速记:无访问修饰符(私有字段要用 #field 语法)、成员之间不用逗号、方法不能重载、typeof 一个实例永远是 "object",判断类型用 instanceof。
7. JSON 一句带过
JSON 是 JS 对象字面量的序列化子集,前端与后端交换数据的事实标准:JSON.stringify(obj) 把对象变字符串,JSON.parse(str) 反向解析。详细规范、常见坑(日期、循环引用、注释不支持)见 JSON 基础。
8. 本章小结
- 对象是无类型键值容器:静态键用点访问,动态键用方括号;增删改直接赋值/delete。
- 遍历首选
Object.entries配合for...of。 - 方法中的 this 指向调用者,方法被抽离当回调时会丢失 this,用箭头函数包裹或 bind 补救。
- 解构让”多返回值”和”命名参数”成为可能;展开运算符只做浅拷贝,深拷贝用 structuredClone。
- 高阶方法 map/filter/reduce/some/every 就是轻量版 Stream,全部不改原数组;sort 默认字典序且原地修改,数字排序必传
(a,b)=>a-b。 - class 提供 constructor/extends/getter/setter 的熟悉外观,本质仍是原型。
下一章让这些数据”长”在页面上:DOM 操作。