JavaScript 数据类型 拷贝
·
一、数据类型:基本类型与引用类型
JavaScript 的数据类型核心分为基本数据类型(原始类型) 和引用数据类型,二者在存储方式、值特性、比较规则上存在本质差异。
1. 基本数据类型(6 种)
- 类型列表:Undefined、Null、Boolean、Number、String、Symbol(ES6 新增,代表独一无二的值)。
- 核心特性:
- 按值访问:变量直接存储具体的值,而非引用地址。
- 值不可变:无法直接修改基本类型的值,所有看似 “修改” 的操作(如字符串方法)都会返回新值,原变量值不变。
var str = "123hello321"; str.toUpperCase(); // 返回"123HELLO321"(新值) console.log(str); // 仍为"123hello321"(原值不变) - 比较规则:值比较:
==仅比较值(会触发隐式类型转换),===既比较值也比较数据类型(无隐式转换)。var a = 1; var b = true; console.log(a == b); // true(值通过隐式转换后相等) console.log(a === b); // false(数据类型不同:Number vs Boolean) - 存储位置:栈内存(Stack):变量标识符(变量名)和值直接存储在栈中,赋值时直接拷贝值,修改一个变量不影响另一个。
var a = "zyj"; var b = a; // 栈中拷贝"zyj"给b a = "呵呵"; // 修改a的栈值,不影响b console.log(a); // "呵呵" console.log(b); // "zyj"
2. 引用数据类型
- 类型范围:除 6 种基本类型外,其余均为引用类型,核心包括 Object、Array、Date、RegExp、Function 等,统称为 Object 类型。
- 核心特性:
- 按引用访问:变量存储的是 “指向堆内存中对象的指针”,而非对象本身。
- 值可修改:可直接修改对象的属性或方法(本质是修改堆内存中的对象内容)。
var obj = {name: "zyj"}; obj.name = "percy"; // 直接修改堆中对象的name属性 obj.age = 21; // 给堆中对象新增age属性 - 比较规则:引用比较:即使两个对象内容完全相同,只要它们在堆内存中是不同的对象(指针不同),比较结果就为 false。
var obj1 = {}; // 堆中创建对象1,obj1存指向它的指针 var obj2 = {}; // 堆中创建对象2,obj2存指向它的指针 console.log(obj1 == obj2); // false(指针不同) console.log(obj1 === obj2); // false(同上) - 存储位置:栈 + 堆内存:
- 栈内存:存储变量标识符和 “指向堆内存的指针”。
- 堆内存:存储对象的具体内容(属性、方法)。
- 赋值时仅拷贝指针,修改一个变量的对象内容,会影响所有指向该堆对象的变量。
var a = {name: "percy"}; // 栈存a的指针,堆存{name: "percy"} var b = a; // 栈拷贝a的指针,b与a指向同一堆对象 a.name = "zyj"; // 修改堆对象的name属性 console.log(b.name); // "zyj"(b的指针未变,堆对象已改)
二、浅拷贝与深拷贝(面试必考)
拷贝的核心区别:修改原数据后,新数据是否会同步变化。
1. 浅拷贝
- 定义:仅拷贝 “表层数据”,若原数据包含引用类型(如数组、对象),新数据会与原数据共享同一堆内存中的引用类型,修改引用类型会同步影响两者。
- 常见实现方式:
- 直接赋值(=):完全共享引用,修改任意一方的引用类型均同步变化。
var person1 = {name: "张三", hobby: ['篮球','足球']}; var person2 = person1; // 浅拷贝(共享引用) person2.name = "hello"; // 原数据person1.name也变为"hello" person1.hobby[0] = "乒乓球"; // 新数据person2.hobby[0]也变为"乒乓球" - 循环遍历赋值:仅拷贝第一层基本类型,深层引用类型仍共享。
var person1 = {name: "张三", hobby: ['篮球','足球']}; var person2 = {}; for (let k in person1) { person2[k] = person1[k]; // 第一层name(基本类型)拷贝值,hobby(数组)拷贝引用 } person1.name = "李四"; // person2.name不变(基本类型独立) person1.hobby[0] = "羽毛球"; // person2.hobby[0]同步变化(引用共享) - 扩展运算符(...):同循环遍历,仅拷贝第一层,深层引用类型共享。
var person2 = {...person1}; // 浅拷贝,hobby仍与person1共享 - 数组 concat () 方法:数组拼接时,仅拷贝第一层元素,深层数组仍共享引用。
var arr1 = ['张三', 18, ['篮球','足球']]; var arr2 = arr1.concat('郑州'); // 浅拷贝 arr1[0] = "李四"; // arr2[0]不变(基本类型独立) arr1[2][0] = "羽毛球"; // arr2[2][0]同步变化(深层数组共享) - Object.assign():用于对象合并,仅拷贝第一层基本类型,深层引用类型共享(常被误认是深拷贝,实际是浅拷贝)。
var obj1 = {a: {c: 1}, b: 2}; var obj2 = Object.assign({}, obj1); // 浅拷贝 obj2.a.c = 10; // obj1.a.c同步变为10(深层对象共享)
- 直接赋值(=):完全共享引用,修改任意一方的引用类型均同步变化。
2. 深拷贝
- 定义:完全拷贝原数据的所有层级(包括深层引用类型),新数据与原数据在堆内存中是独立的两个对象,修改任意一方均不影响另一方。
- 常见实现方式:
- 递归实现(核心,需手写):通过递归遍历原数据的所有层级,遇到引用类型则创建新的对应类型(数组 / 对象),再继续递归拷贝深层数据。
function deepClone(obj) { // 边界处理:若不是对象(基本类型或null),直接返回值 if (obj === null || typeof obj !== "object") return obj; // 判断原数据是数组还是对象,创建对应的新容器 let clone = Array.isArray(obj) ? [] : {}; // 遍历原数据的自有属性(排除原型链属性) for (let key in obj) { if (obj.hasOwnProperty(key)) { // 递归拷贝深层数据 clone[key] = deepClone(obj[key]); } } return clone; } // 测试:修改原数据深层引用类型,新数据不变 var obj = {a: 1, b: [1,2,3], c: {c1: 1}}; var obj1 = deepClone(obj); obj1.b.push(4); console.log(obj.b); // [1,2,3](原数据不变) console.log(obj1.b); // [1,2,3,4](新数据独立)- 递归的问题:无法处理循环引用(如
obj.self = obj),会导致栈溢出;解决方案:用WeakMap或Map记录已拷贝的对象,避免重复递归。
- 递归的问题:无法处理循环引用(如
- JSON.parse(JSON.stringify()):简单易用,通过 JSON 序列化与反序列化实现深拷贝,但存在明显局限性。
var arr = ['hello', ['world', ['张三']]]; var newArr = JSON.parse(JSON.stringify(arr)); arr[1][1][0] = '1'; console.log(newArr[1][1][0]); // "张三"(新数据独立)- 缺点:
- 无法处理函数、undefined(会被过滤);
- 无法处理循环引用(会报错);
- 特殊对象(RegExp、Error、Date)会被转换成空对象或错误格式;
- 丢失对象原型链上的属性。
- 缺点:
- 递归实现(核心,需手写):通过递归遍历原数据的所有层级,遇到引用类型则创建新的对应类型(数组 / 对象),再继续递归拷贝深层数据。
三、数组扁平化
- 定义:将多维数组(嵌套数组)转换为一维数组,简化数据遍历与处理。
- 核心场景:处理多层嵌套的数组数据(如
[1, [2, [3, 4]]]→[1,2,3,4])。 - 实现方式:
- 递归实现:遍历数组,若元素是数组则递归扁平化,否则直接加入结果数组。
function flattenArray(arr) { let flattened = []; arr.forEach(item => { // 若元素是数组,递归处理后合并 if (Array.isArray(item)) { flattened = flattened.concat(flattenArray(item)); } else { // 非数组元素直接加入 flattened.push(item); } }); return flattened; } // 测试 const result = flattenArray([1, [2, 3], [4, [5, 6]]]); console.log(result); // [1,2,3,4,5,6] - 原生方法:
Array.prototype.flat(depth):参数depth指定扁平化深度(默认 1,Infinity表示完全扁平化)。[1, [2, [3, 4]]].flat(Infinity); // [1,2,3,4]Array.prototype.flatMap(callback):先对数组元素执行map操作,再对结果执行flat(1)(仅扁平化 1 层)。
- 递归实现:遍历数组,若元素是数组则递归扁平化,否则直接加入结果数组。
四、函数柯里化(Currying)
- 定义:将 “接受多个参数的函数” 转换为 “一系列接受单个参数的函数”,每次接受一个参数,直到收集完所有必要参数后返回最终结果。
- 核心作用:提高函数复用性、实现函数组合(如 React 中的事件绑定优化)。
- 实现方式:
- 简单柯里化(固定参数数量):
// 原函数:接受3个参数 function add(a, b, c) { return a + b + c; } // 柯里化工具函数 function curry(fn) { // curried函数收集参数 return function curried(...args) { // 若收集的参数数量 >= 原函数参数数量,执行原函数 if (args.length >= fn.length) { return fn.apply(null, args); } else { // 否则返回新函数,继续收集参数 return function(...moreArgs) { return curried.apply(null, args.concat(moreArgs)); }; } }; } // 测试:支持分批次传参 const curriedAdd = curry(add); console.log(curriedAdd(1)(2)(3)); // 6(分3次传参) console.log(curriedAdd(1, 2)(3)); // 6(先传2个,再传1个) console.log(curriedAdd(1)(2, 3)); // 6(先传1个,再传2个) - 实用场景:固定部分参数,生成新函数(如固定接口请求的基础 URL)。
// 原函数:请求接口(baseUrl + path) function request(baseUrl, path) { return baseUrl + path; } // 柯里化后,固定baseUrl为"https://api.example.com" const apiRequest = curry(request)("https://api.example.com"); // 后续请求只需传path console.log(apiRequest("/user")); // "https://api.example.com/user" console.log(apiRequest("/order")); // "https://api.example.com/order"
- 简单柯里化(固定参数数量):
更多推荐


所有评论(0)