JavaScript手录08-对象
在 JavaScript 中,对象(Object)是一种复杂数据类型,用于存储键值对(key-value pairs)的集合,是实现复杂功能的核心数据结构之一。
一、前置知识:JavaScript中的数据类型
在JavaScript中,数据类型分为基本数据类型和引用数据类型。
基本数据类型
基本数据类型是不可变的简单数据值,直接存储在栈内存中。JavaScript中有7种基本数据类型。
JavaScript中的基本数据类型
- Number:数字类型,包括整数、浮点数、NaN、Infinity等
let num = 42;
let float = 2.22223;
let notANum = '1' / '2a'; // NaN
let infi = 1/0; // Infinity
- String:字符串
let str1 = 'hello';
let str2 = "world";
let str3 = `${str1} ${str2} !` // hello world !
- Boolean:布尔值 true 或者 false
let isDone = true
let hasError = false
let isBig = 2 > 1 // true
- null:空值
let emptyValue = null
- undefined:未定义,变量声明后未赋值时的默认值
let unde;
console.log(unde); // undefined
- Symbol:ES6新增,用于创建唯一标识符(即使描述相同,值也不相同)
let sym1 = Symbol('id');
let sym2 = Symbol('id');
console.log(sym1 === sym2); // false
- bigInt:ES6新增,用于表示超出Number范围的大整数(结尾加n)
let bigNum = 1234567890123456789012345678901234567890n;
基本数据类型的特性
- 存储方式:值直接存储在栈内存中,变量指向具体的值。
- 不可变性:无法直接修改原始值。对基本数据类型的任何修改都会创建新值。
- 赋值行为:赋值时传递的是值的副本。(值传递)
- 比较方式:直接比较值是否相等。
引用数据类型
引用数据类型是复杂数据结构,存储在堆内存中,变量仅指向堆内存的引用地址。
JavaScript中的引用数据类型
- Object:对象,键值对集合。
let obj = {name: 'Amy',age: 18
}
- Array:数组,有序数据集合。
let arr = [1,2,3,4,5]
- Function:函数,可执行代码块。
function fn(arg) {console.log(arg)
}
fn('test!')
- 其他内置对象:例如Date对象、RegExp对象、Map、Set等
const arr = [1, 2, 3];
arr.push(4); // 直接修改原数组
console.log(arr); // [1, 2, 3, 4]
引用数据类型的特性
- 存储方式:值存储在堆内存中,变量存储的是指向该值的引用地址(存储在栈内存中)。
- 可变性:可以直接修改对象的属性或内容。(不会创建新对象)
- 赋值行为:赋值时传递的是引用地址的副本(引用传递),多个变量指向同一个对象
- 比较方式:比较的是引用地址是否相同。(即使内容相同,不同对象也不相等。)
区别与联系
特性 | 基本数据类型 | 引用数据类型 |
---|---|---|
存储位置 | 栈内存(直接存值) | 堆内存(存值)+栈内存(存引用) |
赋值方式 | 复制值(值传递) | 复制引用地址(引用传递) |
比较方式 | 比较值是否相等 | 比较引用地址是否相同 |
可变性 | 不可变(修改会创建新值) | 可变(直接修改原对象) |
占用内存 | 固定(根据类型) | 不固定(动态分配) |
类型举例 | Number、String、Boolean等 | Object、Array、Function等 |
数据类型的判断
typeof 运算符
适合判断基本数据类型。
typeof 42; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol(); // "symbol"
typeof 123n; // "bigint"
typeof null; // "object"(历史遗留问题)
typeof {}; // "object"
typeof []; // "object"(数组本质是对象)
typeof (() => {}); // "function"(函数特殊处理)
instanceof 运算符
适合判断引用数据类型。
[] instanceof Array; // true
{} instanceof Object; // true
(() => {}) instanceof Function; // true
Object.prototype.toString.call()
最准确的判断方式。
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
Array.isArray()
用于判断数组。
let arr = [1,2,3]
Array.isArray(arr); // true
二、对象
概念
对象由属性(property)和方法(method)组成:
- 属性:描述对象的特征(键值对中的值可以是任意数据类型)
- 方法:描述对象的行为(值为函数的属性)
// 一个简单的对象
const person = {name: "Alice", // 属性age: 30, // 属性sayHello: function() { // 方法console.log(`Hello, I'm ${this.name}`);}
};
对象的创建方式
对象字面量(最常用)
使用 {}
直接定义对象,简洁直观:
const obj = {key1: value1,key2: value2,// ...
};
构造函数
- 使用
new Object()
创建:
const obj = new Object();
obj.name = "Bob";
obj.age = 25;
- 自定义构造函数(用于创建同一类型的多个对象):
function Person(name, age) {this.name = name;this.age = age;this.sayHi = function() {console.log(`Hi, I'm ${this.name}`);};
}const person1 = new Person("Charlie", 28);
const person2 = new Person("Diana", 22);
Object.create()
基于现有对象创建新对象(继承原型):
const parent = { type: "human" };
const child = Object.create(parent);
child.name = "Eve";
console.log(child.type); // "human"(继承自parent)
对象属性的操作
访问属性
- 点语法:
obj.property
- 方括号语法:
obj["property"]
(适合属性名含特殊字符或动态获取时)
const car = { brand: "Tesla", "model year": 2023 };
console.log(car.brand); // "Tesla"
console.log(car["model year"]); // 2023(必须用方括号)// 动态属性名
const prop = "brand";
console.log(car[prop]); // "Tesla"
修改/添加属性
直接赋值即可(存在则修改,不存在则添加):
const phone = { brand: "Apple" };
phone.brand = "Samsung"; // 修改现有属性
phone.price = 5999; // 添加新属性
删除属性
使用 delete
关键字:
const book = { title: "JS Guide", author: "John" };
delete book.author;
console.log(book.author); // undefined
检查属性是否存在
使用 in
运算符:
const fruit = { name: "apple", color: "red" };
console.log("name" in fruit); // true
console.log("size" in fruit); // false
对象的特殊属性
可枚举性(enumerable)
控制属性是否能被 for...in
循环遍历或 Object.keys()
获取:
const obj = { a: 1 };
Object.defineProperty(obj, "b", {value: 2,enumerable: false // 不可枚举
});console.log(Object.keys(obj)); // ["a"](只包含可枚举属性)
可配置性(configurable)
控制属性是否能被删除或修改特性:
const obj = { a: 1 };
Object.defineProperty(obj, "a", {configurable: false // 不可配置
});
delete obj.a; // 无效,无法删除
可写性(writable)
控制属性是否能被重新赋值:
const obj = { a: 1 };
Object.defineProperty(obj, "a", {writable: false // 只读
});
obj.a = 2; // 无效,无法修改
对象的常用方法
对象的遍历方法
用于获取对象的键、值、或者键值对,适用于需要遍历对象属性的场景。
- Object.keys(obj)
- **功能:**返回一个包含对象自身可枚举属性的键名数组。(不包括继承的属性和不可枚举的属性)
- 示例
let obj = {name: 'Amy',age: 18,gender: '女'
}
console.log(Object.keys(obj)); // ['name', 'age', 'gender']
- Object.values(obj)
- 功能:返回一个包含对象自身可枚举属性的值数组。
- 示例
let obj = {name: 'Alice',age: 25,gender: 'female'
}
console.log(Object.values(obj)); // ["Alice", 25, "female"]
- Object.entries(obj)
- 功能:返回一个包含对象自身可枚举属性的键值对数组(每个元素是
<font style="color:rgb(0, 0, 0);">[key, value]</font>
形式) - 示例
let obj = {name: 'Alice',age: 25,gender: 'female'
}
console.log(Object.entries(obj));
// [["name", "Alice"], ["age", 25], ["gender", "female"]]
- 应用:常用于将对象转换为Map或者遍历键值对。
// 转换为 Map
const map = new Map(Object.entries(obj));
- for…in循环
- 功能:遍历对象自身及继承的可枚举属性(需配合
<font style="color:rgb(0, 0, 0);">hasOwnProperty</font>
过滤继承属性) - 示例
for (const key in obj) {if (obj.hasOwnProperty(key)) { // 只处理自身属性console.log(`${key}: ${obj[key]}`);}
}
对象的拷贝与合并方法
- Object.assign(target, …source)
- 功能:将一个或多个源对象的可枚举属性复制到目标对象,返回目标对象(浅拷贝)
- 特点:
- 同名属性会被后面的源对象覆盖
- 只拷贝自身可枚举属性
- 属于浅拷贝(嵌套对象仍然共享引用)
- 示例
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { a: 3, c: 4 };Object.assign(target, source1, source2);
console.log(target); // { a: 3, b: 2, c: 4 }(a被覆盖)
- **应用:**常用于对象合并或创建对象副本。
// 创建浅拷贝
const copy = Object.assign({}, obj);
- 扩展运算符( … )
- 功能:ES6 新增,用于对象的浅拷贝和合并(效果类似
<font style="color:rgb(0, 0, 0);">Object.assign</font>
) - 示例
// 浅拷贝
const copy = { ...obj };// 合并对象
const merged = { ...source1, ...source2 }; // { b: 2, a: 3, c: 4 }
对象属性的检查方法
- obj.hasOwnProperty(key)
- 功能:判断某个属性是否为对象自身的属性(非继承)
- 示例
const obj = { a: 1 };
console.log(obj.hasOwnProperty("a")); // true
console.log(obj.hasOwnProperty("toString")); // false(toString是继承属性)
- in 运算符
- 功能:判断属性是否存在于对象中(包括继承的属性)
- 示例
console.log("a" in obj); // true
console.log("toString" in obj); // true(继承自Object.prototype)
- Object.prototype.hasOwnProperty.call(obj, key)
- 功能:安全地检查属性是否为自身属性(避免对象本身重写了
<font style="color:rgb(0, 0, 0);">hasOwnProperty</font>
方法) - 示例
const obj = { hasOwnProperty: () => false, a: 1 };
// 直接调用会出错,因为obj重写了hasOwnProperty
console.log(Object.prototype.hasOwnProperty.call(obj, "a")); // true
对象的冻结与密封方法
- Object.freeze(obj)
- 功能:冻结对象,使其无法被修改.
- 被冻结的对象
- 不能添加新属性
- 不能删除现有属性
- 不能修改属性值
- 不能修改属性的特性(如可枚举性)
- 示例
const obj = { a: 1 };
Object.freeze(obj);obj.a = 2; // 无效(严格模式下会报错)
delete obj.a; // 无效
obj.b = 3; // 无效
console.log(obj); // { a: 1 }
- Object.seal(obj)
- 功能:密封对象。
- 与冻结的区别
- 不能添加属性
- 不能删除属性
- 可以修改现有属性的值
- 示例
const obj = { a: 1 };
Object.seal(obj);obj.a = 2; // 有效
delete obj.a; // 无效
obj.b = 3; // 无效
console.log(obj); // { a: 2 }
- 检查对象状态
<font style="color:rgb(0, 0, 0);">Object.isFrozen(obj)</font>
:判断对象是否被冻结<font style="color:rgb(0, 0, 0);">Object.isSealed(obj)</font>
:判断对象是否被密封<font style="color:rgb(0, 0, 0);">Object.isExtensible(obj)</font>
:判断对象是否可扩展(能否添加新属性)
对象的原型操作方法
用于操作对象的原型链。
- Object.create(proto[, propertiesObject])
- 功能:创建一个新对象,其原型指向指定的
<font style="color:rgb(0, 0, 0);">proto</font>
- 示例
console.log(Object.getPrototypeOf(child) === parent); // true
- Object.getPrototypeOf(obj)
- 功能:获取对象的原型(等同于
<font style="color:rgb(0, 0, 0);">obj.__proto__</font>
,但更推荐使用) - 示例
const obj = { a: 1 };
Object.seal(obj);obj.a = 2; // 有效
delete obj.a; // 无效
obj.b = 3; // 无效
console.log(obj); // { a: 2 }
- Object.setPrototypeOf(obj, proto)
- 功能:设置对象的原型(不推荐频繁使用,会影响性能)
- 示例:
const newProto = { greet: () => "Hi" };
Object.setPrototypeOf(child, newProto);
console.log(child.greet()); // "Hi"
其他实用方法
- Object.defineProperty(obj, prop, descriptor)
- 功能:精确设置对象属性的特性(如可写性、可枚举性等)
- 示例
const obj = {};
Object.defineProperty(obj, "id", {value: 123,writable: false, // 只读enumerable: false, // 不可枚举configurable: false // 不可删除/修改特性
});
- Object.is(value1, value2)
- 功能:判断两个值是否严格相等(比
<font style="color:rgb(0, 0, 0);">===</font>
更严格,处理了特殊情况) - 示例
console.log(Object.is(NaN, NaN)); // true(=== 会返回false)
console.log(Object.is(+0, -0)); // false(=== 会返回true)
对象的原型与继承
JavaScript 基于原型(prototype)实现继承:
- 每个对象都有
__proto__
属性,指向其原型对象 - 原型对象的属性和方法可被所有实例共享
// 构造函数
function Animal(name) {this.name = name;
}// 在原型上定义方法(所有实例共享)
Animal.prototype.eat = function() {console.log(`${this.name} is eating`);
};const dog = new Animal("Buddy");
dog.eat(); // "Buddy is eating"
console.log(dog.__proto__ === Animal.prototype); // true
注意事项
- 对象是引用类型:赋值时传递的是引用,而非值本身
const a = { x: 1 };
const b = a;
b.x = 2;
console.log(a.x); // 2(a和b指向同一对象)
- 属性名自动转换:对象的键总是字符串或 Symbol 类型
const obj = { 123: "number", true: "boolean" };
console.log(obj["123"]); // "number"(数字键被转为字符串)
- ES6 增强特性:支持计算属性名、简写属性、方法简写等
const key = "name";
const obj = {[key]: "Frank", // 计算属性名age: 30,sayHi() { ... } // 方法简写
};
三、深复制与浅复制
在 JavaScript 中,复制数据时需要区分浅复制(Shallow Copy)和深复制(Deep Copy),这两种复制方式的核心区别在于是否递归复制嵌套对象的内容。
浅复制(Shallow Copy)
浅复制只复制对象的顶层属性,对于嵌套的引用类型属性,仅复制其引用地址(原对象和复制对象会共享嵌套对象)。
适用场景
- 复制简单对象(无嵌套引用类型)
- 性能优先,不需要完全隔离嵌套对象时
实现方式
(1)对象扩展运算符 { ...obj }
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };// 顶层属性独立
shallowCopy.a = 100;
console.log(original.a); // 1(不受影响)// 嵌套对象共享引用
shallowCopy.b.c = 200;
console.log(original.b.c); // 200(原对象被修改)
(2)Object.assign()
方法
const shallowCopy = Object.assign({}, original);
// 特性同上:顶层属性独立,嵌套对象共享
(3)数组浅复制(slice()
、[...arr]
)
const arrOriginal = [1, [2, 3]];
const arrShallowCopy1 = arrOriginal.slice();
const arrShallowCopy2 = [...arrOriginal];// 顶层元素独立
arrShallowCopy1[0] = 100;
console.log(arrOriginal[0]); // 1(不受影响)// 嵌套数组共享引用
arrShallowCopy2[1][0] = 200;
console.log(arrOriginal[1][0]); // 200(原数组被修改)
深复制(Deep Copy)
深复制会递归复制对象的所有层级,包括嵌套的引用类型属性,最终得到一个与原对象完全独立的副本(两者修改互不影响)。
适用场景
- 复制复杂嵌套对象(如多层级的对象/数组)
- 需要完全隔离原对象和复制对象时(修改副本不影响原对象)
实现方式
(1)JSON.parse(JSON.stringify())
(简单场景)
利用 JSON 序列化与反序列化实现深复制(有局限性):
const original = { a: 1, b: { c: 2 }, d: [3, 4] };
const deepCopy = JSON.parse(JSON.stringify(original));// 嵌套对象完全独立
deepCopy.b.c = 200;
deepCopy.d[0] = 300;
console.log(original.b.c); // 2(不受影响)
console.log(original.d[0]); // 3(不受影响)
局限性:
- 不能复制函数、Symbol、正则表达式等特殊类型
- 不能处理循环引用(对象引用自身会报错)
- 会丢失
undefined
、NaN
、Infinity
等特殊值的精度
(2)递归实现深复制(通用方案)
手动编写递归函数,处理所有数据类型:
/*** 深复制函数 - 递归复制所有层级的属性* @param {any} value - 需要复制的值* @returns {any} 复制后的新值*/
function deepCopy(value) {// 1. 处理基本数据类型(直接返回值)if (typeof value !== 'object' || value === null) {return value;}// 2. 处理日期对象if (value instanceof Date) {return new Date(value);}// 3. 处理正则表达式if (value instanceof RegExp) {return new RegExp(value.source, value.flags);}// 4. 处理数组(创建新数组并递归复制元素)if (Array.isArray(value)) {return value.map(item => deepCopy(item));}// 5. 处理普通对象(创建新对象并递归复制属性)const copy = {};for (const key in value) {if (value.hasOwnProperty(key)) {copy[key] = deepCopy(value[key]);}}return copy;
}// 测试
const original = {a: 1,b: { c: 2 },d: [3, [4, 5]],e: new Date(),f: /pattern/g
};const copied = deepCopy(original);
copied.b.c = 200;
copied.d[1][0] = 400;console.log(original.b.c); // 2(不受影响)
console.log(original.d[1][0]); // 4(不受影响)
console.log(copied.e instanceof Date); // true(类型保留)
console.log(copied.f.flags); // "g"(正则属性保留)
浅复制与深复制的核心区别
特性 | 浅复制 | 深复制 |
---|---|---|
复制层级 | 仅复制顶层属性 | 递归复制所有层级(包括嵌套对象) |
嵌套对象关联性 | 共享引用(修改会相互影响) | 完全独立(修改互不影响) |
性能 | 效率高(无需递归) | 效率较低(递归处理所有层级) |
适用场景 | 简单对象、无嵌套引用类型 | 复杂嵌套对象、需要完全隔离 |
实现复杂度 | 简单(内置方法即可) | 复杂(需处理多种数据类型) |
使用建议
- 优先使用浅复制:当对象结构简单(无嵌套引用类型)时,浅复制更高效。
- **谨慎使用 **
JSON.parse(JSON.stringify())
:适合纯 JSON 数据(无函数、正则等特殊类型),不适合复杂对象。 - 复杂场景用递归深复制:需自己实现或使用成熟库(如 Lodash 的
_.cloneDeep()
)。 - 注意循环引用:若对象存在循环引用(如
obj.self = obj
),需在深复制函数中特殊处理(如用 WeakMap 记录已复制对象)。
练习-对象与数组嵌套数据处理
练习1:扁平化嵌套数组
需求:将多层嵌套的数组转换为一维数组(如 [1, [2, [3, 4], 5]]
→ [1, 2, 3, 4, 5]
)。
提示:使用递归或 Array.flat()
方法(注意 flat(depth)
的参数控制深度)。
// 实现一个通用的数组扁平化函数
function flattenArray(arr) {// 你的代码
}// 测试用例
const nestedArr = [1, [2, [3, [4]], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
练习2:深层查找对象属性
需求:在嵌套对象中查找指定属性的值(如在 { a: { b: { c: 100 } } }
中查找 c
的值)。
提示:使用递归遍历对象的所有属性,遇到对象则继续深入。
// 在嵌套对象中查找指定属性
function findDeepProperty(obj, targetKey) {// 你的代码
}// 测试用例
const nestedObj = {x: 10,y: {z: 20,w: {v: 30,u: { t: 40 }}}
};
console.log(findDeepProperty(nestedObj, 't')); // 40
console.log(findDeepProperty(nestedObj, 'none')); // undefined
练习3:修改嵌套数组中的值
需求:将嵌套数组中所有偶数乘以2(如 [1, [2, [3, 4], 5]]
→ [1, [4, [3, 8], 5]]
)。
提示:递归遍历数组,遇到数组则继续处理,遇到数字则判断是否为偶数。
// 处理嵌套数组中的偶数
function processNestedEvenNumbers(arr) {// 你的代码
}// 测试用例
const testArr = [1, [2, [3, 4], 5], 6];
console.log(processNestedEvenNumbers(testArr)); // [1, [4, [3, 8], 5], 12]
练习4:统计嵌套对象中某属性的总和
需求:计算嵌套对象中所有 value
属性的总和(支持数组嵌套)。
示例数据:
const data = {value: 10,children: [{ value: 20, children: [{ value: 30 }] },{ value: 40 }]
};
// 预期结果:10 + 20 + 30 + 40 = 100
// 计算所有value的总和
function sumNestedValues(data) {// 你的代码
}// 测试用例
console.log(sumNestedValues(data)); // 100
练习5:将嵌套数据转换为树形结构
需求:将扁平数组转换为嵌套树形结构(如菜单数据)。
示例数据:
const flatData = [{ id: 1, name: '菜单1', parentId: 0 },{ id: 2, name: '菜单1-1', parentId: 1 },{ id: 3, name: '菜单1-2', parentId: 1 },{ id: 4, name: '菜单2', parentId: 0 },{ id: 5, name: '菜单2-1', parentId: 4 }
];
// 预期结果:
// [
// { id: 1, name: '菜单1', children: [...] },
// { id: 4, name: '菜单2', children: [...] }
// ]
// 扁平数组转树形结构
function buildTree(flatData) {// 你的代码
}// 测试用例
console.log(buildTree(flatData));
练习6:深层克隆带循环引用的对象
需求:实现一个深克隆函数,能处理循环引用(如 obj.self = obj
)。
提示:使用 WeakMap
存储已克隆的对象,避免无限递归。
// 处理循环引用的深克隆
function deepCloneWithCycle(obj, map = new WeakMap()) {// 你的代码
}// 测试用例
const obj = { name: 'test' };
obj.self = obj; // 循环引用
const cloned = deepCloneWithCycle(obj);
console.log(cloned.self === cloned); // true(克隆后仍保持循环引用)
练习7:过滤嵌套数组中的元素
需求:过滤出嵌套数组中所有大于10的数字(保留原数组结构)。
示例:[5, [15, [8, 20], 3], 12]
→ [[15, [, 20]], 12]
// 过滤嵌套数组中大于10的元素
function filterNestedNumbers(arr) {// 你的代码
}// 测试用例
const input = [5, [15, [8, 20], 3], 12];
console.log(filterNestedNumbers(input)); // [[15, [, 20]], 12]
参考答案
练习1:扁平化嵌套数组
// 方法1:递归实现
function flattenArray(arr) {return arr.reduce((acc, item) => {// 如果是数组则递归扁平化,否则直接添加到结果return acc.concat(Array.isArray(item) ? flattenArray(item) : item);}, []);
}// 方法2:使用ES6的flat方法(参数Infinity表示无限深度)
// function flattenArray(arr) {
// return arr.flat(Infinity);
// }// 测试用例
const nestedArr = [1, [2, [3, [4]], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
练习2:深层查找对象属性
function findDeepProperty(obj, targetKey) {// 遍历当前对象的所有键for (const key in obj) {// 找到目标键,返回对应值if (key === targetKey) {return obj[key];}// 如果当前值是对象且非null,递归查找if (typeof obj[key] === 'object' && obj[key] !== null) {const result = findDeepProperty(obj[key], targetKey);// 找到结果后直接返回,停止递归if (result !== undefined) {return result;}}}// 未找到返回undefinedreturn undefined;
}// 测试用例
const nestedObj = {x: 10,y: {z: 20,w: {v: 30,u: { t: 40 }}}
};
console.log(findDeepProperty(nestedObj, 't')); // 40
console.log(findDeepProperty(nestedObj, 'none')); // undefined
练习3:修改嵌套数组中的值
function processNestedEvenNumbers(arr) {// 遍历数组元素,递归处理return arr.map(item => {// 如果是数组,递归处理子数组if (Array.isArray(item)) {return processNestedEvenNumbers(item);}// 如果是偶数,乘以2;否则保持原值return typeof item === 'number' && item % 2 === 0 ? item * 2 : item;});
}// 测试用例
const testArr = [1, [2, [3, 4], 5], 6];
console.log(processNestedEvenNumbers(testArr)); // [1, [4, [3, 8], 5], 12]
练习4:统计嵌套对象中某属性的总和
function sumNestedValues(data) {let total = 0;// 如果当前对象有value属性,累加if (data.hasOwnProperty('value')) {total += data.value;}// 如果有children数组,递归处理每个子元素if (data.children && Array.isArray(data.children)) {data.children.forEach(child => {total += sumNestedValues(child);});}return total;
}// 测试用例
const data = {value: 10,children: [{ value: 20, children: [{ value: 30 }] },{ value: 40 }]
};
console.log(sumNestedValues(data)); // 100
练习5:将嵌套数据转换为树形结构
function buildTree(flatData) {// 1. 用Map存储所有节点,方便通过id快速查找const nodeMap = new Map();flatData.forEach(item => {// 为每个节点初始化children数组nodeMap.set(item.id, { ...item, children: [] });});// 2. 构建树形结构const tree = [];flatData.forEach(item => {const currentNode = nodeMap.get(item.id);if (item.parentId === 0) {// parentId为0的是根节点,直接加入树tree.push(currentNode);} else {// 找到父节点,将当前节点加入父节点的childrenconst parentNode = nodeMap.get(item.parentId);if (parentNode) {parentNode.children.push(currentNode);}}});return tree;
}// 测试用例
const flatData = [{ id: 1, name: '菜单1', parentId: 0 },{ id: 2, name: '菜单1-1', parentId: 1 },{ id: 3, name: '菜单1-2', parentId: 1 },{ id: 4, name: '菜单2', parentId: 0 },{ id: 5, name: '菜单2-1', parentId: 4 }
];
console.log(buildTree(flatData));
练习6:深层克隆带循环引用的对象
function deepCloneWithCycle(obj, map = new WeakMap()) {// 处理基本数据类型和nullif (obj === null || typeof obj !== 'object') {return obj;}// 处理日期对象if (obj instanceof Date) {return new Date(obj);}// 处理正则对象if (obj instanceof RegExp) {return new RegExp(obj.source, obj.flags);}// 检查是否已克隆过(解决循环引用)if (map.has(obj)) {return map.get(obj);}// 处理数组和对象const clone = Array.isArray(obj) ? [] : {};// 存储克隆关系,避免循环引用导致的无限递归map.set(obj, clone);// 递归克隆属性Reflect.ownKeys(obj).forEach(key => {clone[key] = deepCloneWithCycle(obj[key], map);});return clone;
}// 测试用例
const obj = { name: 'test' };
obj.self = obj; // 循环引用
const cloned = deepCloneWithCycle(obj);
console.log(cloned.self === cloned); // true(保持循环引用)
console.log(cloned.name === obj.name); // true(属性值一致)
练习7:过滤嵌套数组中的元素
function filterNestedNumbers(arr) {return arr.reduce((acc, item) => {// 处理嵌套数组if (Array.isArray(item)) {const filteredChild = filterNestedNumbers(item);// 即使子数组过滤后为空,也保留数组结构acc.push(filteredChild);}// 处理数字:只保留大于10的else if (typeof item === 'number' && item > 10) {acc.push(item);}// 非数字或不满足条件的数字不加入结果(相当于过滤)return acc;}, []);
}// 测试用例
const input = [5, [15, [8, 20], 3], 12];
console.log(filterNestedNumbers(input)); // [[15, [ , 20]], 12]
练习-深复制与浅复制
练习1:识别浅复制的局限性
需求:创建一个包含嵌套对象的原始对象,使用浅复制方法复制,然后修改复制对象的嵌套属性,观察原始对象是否受影响。
// 1. 创建原始对象(包含嵌套对象)
const original = {name: "测试",details: {age: 20,score: 90}
};// 2. 使用浅复制方法(如扩展运算符)
const shallowCopy = { ...original };// 3. 修改复制对象的顶层属性
shallowCopy.name = "浅复制修改";// 4. 修改复制对象的嵌套属性
shallowCopy.details.age = 30;// 5. 观察原始对象的变化
console.log("原始对象name:", original.name); // 预期:"测试"(不受影响)
console.log("原始对象details.age:", original.details.age); // 预期:30(受影响)
问题:为什么原始对象的嵌套属性会被修改?
答案:浅复制只复制顶层属性,嵌套对象仍然共享引用,所以修改复制对象的嵌套属性会影响原始对象。
练习2:实现深复制解决嵌套问题
需求:对练习1的原始对象进行深复制,修改复制对象的嵌套属性,确保原始对象不受影响。
// 1. 实现深复制函数
function deepCopy(obj) {if (obj === null || typeof obj !== 'object') {return obj;}const copy = Array.isArray(obj) ? [] : {};for (const key in obj) {if (obj.hasOwnProperty(key)) {copy[key] = deepCopy(obj[key]);}}return copy;
}// 2. 使用深复制
const deepCopied = deepCopy(original);// 3. 修改深复制对象的嵌套属性
deepCopied.details.age = 40;// 4. 观察原始对象
console.log("原始对象details.age:", original.details.age); // 预期:30(不受影响)
console.log("深复制对象details.age:", deepCopied.details.age); // 预期:40
练习3:对比不同复制方式的效果
需求:用多种方式复制同一对象,比较它们对嵌套数组的影响。
const data = {id: 1,items: [10, 20, 30]
};// 1. 浅复制 - Object.assign
const copy1 = Object.assign({}, data);// 2. 浅复制 - 扩展运算符
const copy2 = { ...data };// 3. 深复制 - 自定义函数
const copy3 = deepCopy(data);// 修改所有复制对象的嵌套数组
copy1.items.push(40);
copy2.items.push(50);
copy3.items.push(60);// 观察结果
console.log("原始对象items:", data.items); // 预期:[10, 20, 30, 40, 50]
console.log("copy1 items:", copy1.items); // 预期:[10, 20, 30, 40, 50]
console.log("copy2 items:", copy2.items); // 预期:[10, 20, 30, 40, 50]
console.log("copy3 items:", copy3.items); // 预期:[10, 20, 30, 60]
练习4:处理特殊类型的深复制
需求:尝试复制包含函数、日期、正则等特殊类型的对象,观察普通深复制方法的局限性。
const specialObj = {func: () => console.log("函数"),date: new Date(2023, 0, 1),regex: /test/g,nested: { value: 100 }
};// 使用JSON方法复制(有局限性)
const jsonCopy = JSON.parse(JSON.stringify(specialObj));// 使用自定义深复制函数
const customDeepCopy = deepCopy(specialObj);// 观察结果
console.log("JSON复制的函数:", jsonCopy.func); // 预期:undefined(函数无法被JSON序列化)
console.log("自定义复制的函数:", typeof customDeepCopy.func); // 预期:"function"
console.log("JSON复制的日期:", jsonCopy.date instanceof Date); // 预期:false(变成字符串)
console.log("自定义复制的日期:", customDeepCopy.date instanceof Date); // 预期:true
问题:如何改进深复制函数以支持这些特殊类型?
提示:在深复制函数中添加对 Date
、RegExp
、Function
等类型的特殊处理。
练习5:修复浅复制导致的bug
需求:下面的代码存在bug(修改购物车副本影响了原始购物车),请使用深复制修复。
// 原始购物车数据
const cart = {user: "张三",products: [{ id: 1, name: "商品1", quantity: 2 }]
};// 错误的复制方式(浅复制)
function addToCart(cart, product) {const newCart = { ...cart }; // 问题所在newCart.products.push(product);return newCart;
}// 添加商品
const newCart = addToCart(cart, { id: 2, name: "商品2", quantity: 1 });// 意外:原始购物车也被修改了
console.log("原始购物车商品数:", cart.products.length); // 预期:1,实际:2// 修复代码:使用深复制
// 请在此处修改addToCart函数
修复方案:将浅复制改为深复制,确保 products
数组不会被共享。
参考答案
练习1:识别浅复制的局限性
参考答案:
// 1. 创建原始对象(包含嵌套对象)
const original = {name: "测试",details: {age: 20,score: 90}
};// 2. 浅复制(扩展运算符)
const shallowCopy = { ...original };// 3. 修改复制对象的顶层属性
shallowCopy.name = "浅复制修改";// 4. 修改复制对象的嵌套属性
shallowCopy.details.age = 30;// 5. 观察结果
console.log("原始对象name:", original.name); // 输出:"测试"(顶层属性独立)
console.log("原始对象details.age:", original.details.age); // 输出:30(嵌套属性共享引用)
关键解析:
浅复制仅复制对象的顶层键值对,对于嵌套的 details
对象,复制的是引用地址(而非新对象)。因此,shallowCopy.details
和 original.details
指向同一个内存地址,修改其中一个会影响另一个。
练习2:实现深复制解决嵌套问题
参考答案:
// 1. 完善的深复制函数(支持对象和数组)
function deepCopy(obj) {// 处理基本数据类型和nullif (obj === null || typeof obj !== "object") {return obj;}// 处理数组if (Array.isArray(obj)) {return obj.map(item => deepCopy(item));}// 处理对象const copy = {};for (const key in obj) {if (obj.hasOwnProperty(key)) {copy[key] = deepCopy(obj[key]); // 递归复制嵌套属性}}return copy;
}// 2. 使用深复制
const deepCopied = deepCopy(original);// 3. 修改深复制对象的嵌套属性
deepCopied.details.age = 40;// 4. 观察结果
console.log("原始对象details.age:", original.details.age); // 输出:30(不受影响)
console.log("深复制对象details.age:", deepCopied.details.age); // 输出:40(独立修改)
关键解析:
深复制通过递归遍历所有层级,对嵌套对象/数组重新创建新实例,彻底切断与原始对象的引用关联。因此,修改深复制对象的任何属性都不会影响原始对象。
练习3:对比不同复制方式的效果
参考答案:
const data = {id: 1,items: [10, 20, 30]
};// 1. 浅复制(Object.assign)
const copy1 = Object.assign({}, data);// 2. 浅复制(扩展运算符)
const copy2 = { ...data };// 3. 深复制(自定义函数)
const copy3 = deepCopy(data);// 修改嵌套数组
copy1.items.push(40);
copy2.items.push(50);
copy3.items.push(60);// 观察结果
console.log("原始对象items:", data.items); // 输出:[10, 20, 30, 40, 50](受浅复制影响)
console.log("copy1 items:", copy1.items); // 输出:[10, 20, 30, 40, 50](与原始共享数组)
console.log("copy2 items:", copy2.items); // 输出:[10, 20, 30, 40, 50](与原始共享数组)
console.log("copy3 items:", copy3.items); // 输出:[10, 20, 30, 60](独立数组)
关键解析:
Object.assign
和扩展运算符都是浅复制,它们复制的items
数组是引用,因此修改copy1.items
或copy2.items
会影响原始对象。- 深复制的
copy3.items
是全新数组,与原始对象完全独立,修改不会相互影响。
练习4:处理特殊类型的深复制
参考答案:
// 改进的深复制函数(支持特殊类型)
function deepCopySpecial(obj) {if (obj === null || typeof obj !== "object") {return obj;}// 处理日期if (obj instanceof Date) {return new Date(obj);}// 处理正则if (obj instanceof RegExp) {return new RegExp(obj.source, obj.flags);}// 处理数组if (Array.isArray(obj)) {return obj.map(item => deepCopySpecial(item));}// 处理普通对象const copy = {};for (const key in obj) {if (obj.hasOwnProperty(key)) {copy[key] = deepCopySpecial(obj[key]);}}return copy;
}// 测试数据
const specialObj = {func: () => console.log("函数"),date: new Date(2023, 0, 1),regex: /test/g,nested: { value: 100 }
};// JSON方法复制(有局限)
const jsonCopy = JSON.parse(JSON.stringify(specialObj));// 自定义深复制(支持特殊类型)
const customDeepCopy = deepCopySpecial(specialObj);// 观察结果
console.log("JSON复制的函数:", jsonCopy.func); // 输出:undefined(JSON不支持函数)
console.log("自定义复制的函数:", typeof customDeepCopy.func); // 输出:"function"(保留函数)
console.log("JSON复制的日期类型:", jsonCopy.date instanceof Date); // 输出:false(变成字符串)
console.log("自定义复制的日期类型:", customDeepCopy.date instanceof Date); // 输出:true(保留日期类型)
console.log("JSON复制的正则flags:", jsonCopy.regex?.flags); // 输出:undefined(正则被转为空对象)
console.log("自定义复制的正则flags:", customDeepCopy.regex.flags); // 输出:"g"(保留正则属性)
关键解析:
JSON.parse(JSON.stringify())
无法处理函数、Date
、RegExp
等特殊类型(会丢失类型或值)。- 自定义深复制函数通过判断对象类型(
instanceof
),对特殊类型单独处理(如new Date(obj)
复制日期),确保类型和值都被正确复制。
练习5:修复浅复制导致的bug
参考答案:
// 原始购物车数据
const cart = {user: "张三",products: [{ id: 1, name: "商品1", quantity: 2 }]
};// 修复后的函数(使用深复制)
function addToCart(cart, product) {// 深复制整个购物车(包括products数组)const newCart = deepCopySpecial(cart);newCart.products.push(product); // 修改副本的数组,不影响原始数据return newCart;
}// 添加商品
const newCart = addToCart(cart, { id: 2, name: "商品2", quantity: 1 });// 观察结果
console.log("原始购物车商品数:", cart.products.length); // 输出:1(不受影响)
console.log("新购物车商品数:", newCart.products.length); // 输出:2(正确添加)
关键解析:
原代码的 { ...cart }
是浅复制,newCart.products
与 cart.products
共享同一个数组引用,导致修改副本时原始数据被意外改变。
修复方案通过深复制(deepCopySpecial
)创建完全独立的 newCart
,确保 products
数组修改仅影响副本。
总结
- 浅复制:仅复制顶层结构,嵌套的引用类型(对象/数组)共享引用,适合简单对象且无需隔离嵌套数据的场景。
- 深复制:递归复制所有层级,完全隔离原始数据,适合复杂嵌套对象,但性能开销更高。
- 实践建议:
- 简单场景用浅复制(扩展运算符/
Object.assign
)。 - 复杂场景用成熟方案(如 Lodash 的
_.cloneDeep
)或完善的自定义深复制函数(处理特殊类型)。 - 避免用
JSON.parse(JSON.stringify())
处理含特殊类型的数据。
- 简单场景用浅复制(扩展运算符/