各位靓仔靓女,老少爷们儿,大家好!我是你们的贴心小棉袄(技术层面的),今天咱们来聊聊JavaScript里一个非常实用,但又容易让人掉坑的家伙——Object.assign()
。
这玩意儿,说它简单吧,assign
嘛,不就是“分配、赋值”的意思?说它复杂吧,一不小心就给你整出个深浅拷贝的幺蛾子,让你debug到怀疑人生。所以,今天咱们就来把它扒个精光,看看它到底是个什么玩意儿。
开场白:浅拷贝的世界你不懂
首先,我们得明确一个概念:JavaScript里的对象,那都是引用类型。啥意思呢?简单来说,你用等号=
赋值的时候,赋的不是对象本身,而是指向这个对象在内存里的地址的“指针”。
let obj1 = { name: '张三', age: 20 };
let obj2 = obj1;
obj2.age = 25;
console.log(obj1.age); // 输出:25 !!! obj1也被修改了
看到没?明明改的是obj2
的age
,obj1
的age
也跟着变了!这就是引用类型的特性,obj1
和obj2
指向的是同一个内存地址,修改其中一个,另一个自然也跟着变。
这种赋值方式,我们通常称之为“浅拷贝”。浅拷贝只是复制了对象的引用,而不是对象本身。
Object.assign()
:浅拷贝的福音?还是深坑?
Object.assign()
,顾名思义,就是把一个或多个源对象(source objects)的属性复制到目标对象(target object)上。语法是这样的:
Object.assign(target, ...sources)
target
: 目标对象,属性会被复制到这个对象上。...sources
: 一个或多个源对象,它们的属性会被复制到目标对象上。
看起来很美好,对不对? 我们来试试:
let obj1 = { name: '张三', age: 20 };
let obj2 = Object.assign({}, obj1); // 注意,这里target是一个空对象{}
obj2.age = 25;
console.log(obj1.age); // 输出:20 !!! obj1没变了
console.log(obj2.age); // 输出:25
咦?这次obj1
的age
没变!难道Object.assign()
实现了深拷贝? 先别高兴得太早,我们再来试试更复杂的情况:
let obj1 = {
name: '张三',
age: 20,
address: {
city: '北京',
street: '长安街'
}
};
let obj2 = Object.assign({}, obj1);
obj2.address.city = '上海';
console.log(obj1.address.city); // 输出:上海 !!! obj1的address也被修改了
console.log(obj2.address.city); // 输出:上海
纳尼?obj1
的address.city
也被修改了! 这说明了什么? Object.assign()
本质上还是浅拷贝! 它只会复制源对象的第一层属性,如果属性值是对象或者数组,那么复制的仍然是引用。
Object.assign()
的深度剖析:一层拷贝,深度共享
为了更清楚地了解Object.assign()
的工作原理,我们来详细分析一下它的拷贝过程。
- 创建目标对象: 如果
target
参数是一个已经存在的对象,那么Object.assign()
会直接修改这个对象。如果target
参数是一个空对象{}
,那么Object.assign()
会创建一个新的对象作为目标对象。 - 遍历源对象:
Object.assign()
会遍历所有源对象,并按照源对象出现的顺序,依次将它们的属性复制到目标对象上。 -
属性复制: 对于每一个属性,
Object.assign()
会执行以下操作:- 如果源对象中存在该属性,并且目标对象中也存在同名属性,那么源对象的属性值会覆盖目标对象的属性值。
- 如果源对象中存在该属性,但是目标对象中不存在同名属性,那么该属性会被添加到目标对象中。
- 如果源对象中的属性值是基本类型(string, number, boolean, null, undefined, symbol),那么
Object.assign()
会直接复制这个值。 - 如果源对象中的属性值是对象或数组,那么
Object.assign()
会复制这个对象的引用! 这就是浅拷贝的本质。
Object.assign()
的各种用法:不仅仅是拷贝
虽然Object.assign()
是浅拷贝,但它仍然非常有用。 我们来看看它的各种用法:
-
合并对象: 这是
Object.assign()
最常见的用法。 你可以将多个源对象的属性合并到一个目标对象中。let obj1 = { name: '张三', age: 20 }; let obj2 = { city: '北京', job: '程序员' }; let obj3 = { hobby: 'coding', gender: 'male' }; let mergedObj = Object.assign({}, obj1, obj2, obj3); console.log(mergedObj); // 输出:{ name: '张三', age: 20, city: '北京', job: '程序员', hobby: 'coding', gender: 'male' }
-
给对象添加属性: 你可以使用
Object.assign()
给一个已经存在的对象添加新的属性。let obj = { name: '张三' }; Object.assign(obj, { age: 20, city: '北京' }); console.log(obj); // 输出:{ name: '张三', age: 20, city: '北京' }
-
设置默认值: 你可以使用
Object.assign()
来设置对象的默认值。 如果源对象中没有某个属性,那么目标对象就会使用默认值。let defaults = { name: '匿名', age: 18, city: '未知' }; let user = { name: '张三', age: 20 }; let mergedUser = Object.assign({}, defaults, user); console.log(mergedUser); // 输出:{ name: '张三', age: 20, city: '未知' } (city使用了默认值)
-
创建对象的副本(浅拷贝): 这就是我们一开始看到的例子。 使用空对象
{}
作为目标对象,可以创建一个对象的浅拷贝。let obj1 = { name: '张三', age: 20 }; let obj2 = Object.assign({}, obj1);
Object.assign()
的注意事项:坑就在细节里
在使用Object.assign()
的时候,有一些细节需要注意,否则很容易掉坑里。
-
target
参数是必须的:Object.assign()
必须至少有一个target
参数。 如果没有target
参数,会报错。// Object.assign(); // 报错:TypeError: Cannot convert undefined or null to object
-
源对象的属性会覆盖目标对象的同名属性: 如果源对象和目标对象有同名属性,那么源对象的属性值会覆盖目标对象的属性值。 后面的源对象的属性会覆盖前面源对象的同名属性。
let obj1 = { name: '张三', age: 20 }; let obj2 = { name: '李四', city: '北京' }; let mergedObj = Object.assign({}, obj1, obj2); console.log(mergedObj.name); // 输出:李四 (obj2的name覆盖了obj1的name)
-
Object.assign()
只能复制可枚举属性:Object.assign()
只会复制源对象的可枚举属性。 不可枚举属性会被忽略。let obj1 = {}; Object.defineProperty(obj1, 'name', { value: '张三', enumerable: false // 不可枚举 }); let obj2 = Object.assign({}, obj1); console.log(obj2.name); // 输出:undefined (name属性没有被复制)
-
Object.assign()
会触发setter
: 如果目标对象有setter
方法,那么Object.assign()
会触发setter
方法。let obj1 = { set name(value) { this._name = value.toUpperCase(); }, get name() { return this._name; } }; let obj2 = {}; Object.assign(obj2, { name: '张三' }); console.log(obj2.name); // 输出:张三 (setter方法被触发,但是并没有实际改变值)
(注意:这里输出的
张三
是因为赋值的时候,obj2
并没有_name
属性,所以get name
方法返回了undefined
。如果你在obj2
中预先定义了_name
属性,那么get name
方法会返回_name
的值。) -
Object.assign()
对null
和undefined
的处理: 如果源对象是null
或undefined
,那么Object.assign()
会跳过它们,不会报错。let obj = { name: '张三' }; Object.assign(obj, null, undefined, { age: 20 }); console.log(obj); // 输出:{ name: '张三', age: 20 }
深拷贝的解决方案:告别浅拷贝的烦恼
既然Object.assign()
是浅拷贝,那么如果我们需要深拷贝,该怎么办呢? 有几种常见的解决方案:
-
JSON.parse(JSON.stringify(obj)): 这是最简单粗暴的方法。 先将对象转换为JSON字符串,然后再将JSON字符串解析为对象。 但是,这种方法有一些限制:
- 不能复制函数。
- 不能复制
undefined
。 - 不能复制
Symbol
类型的值。 - 如果对象中存在循环引用,会报错。
let obj1 = { name: '张三', age: 20, address: { city: '北京', street: '长安街' }, func: function() { console.log('hello'); }, sym: Symbol('test'), undef: undefined }; let obj2 = JSON.parse(JSON.stringify(obj1)); obj2.address.city = '上海'; console.log(obj1.address.city); // 输出:北京 (深拷贝成功) console.log(obj2.func); // 输出:undefined (函数被忽略了) console.log(obj2.sym); // 输出:undefined (Symbol被忽略了) console.log(obj2.undef); // 输出:undefined (Undefined被忽略了) obj1.circular = obj1; //JSON.parse(JSON.stringify(obj1)); // 报错:TypeError: Converting circular structure to JSON
-
递归拷贝: 这是最通用的深拷贝方法。 递归遍历对象的每一个属性,如果属性值是对象或数组,那么递归调用拷贝函数。
function deepClone(obj) { if (typeof obj !== 'object' || obj === null) { return obj; // 如果不是对象或数组,直接返回 } let clonedObj = Array.isArray(obj) ? [] : {}; // 创建一个新的对象或数组 for (let key in obj) { if (obj.hasOwnProperty(key)) { clonedObj[key] = deepClone(obj[key]); // 递归调用拷贝函数 } } return clonedObj; } let obj1 = { name: '张三', age: 20, address: { city: '北京', street: '长安街' }, func: function() { console.log('hello'); } }; let obj2 = deepClone(obj1); obj2.address.city = '上海'; obj2.func = function() { console.log('world'); }; console.log(obj1.address.city); // 输出:北京 (深拷贝成功) console.log(obj2.func); //输出: [Function (anonymous)]
这个递归拷贝的函数,可以处理函数,但是仍然不能处理循环引用的情况,为了处理循环引用,需要使用WeakMap来记录已经拷贝过的对象。
-
使用lodash的
_.cloneDeep()
方法: lodash是一个非常流行的JavaScript工具库,它提供了很多实用的函数,包括深拷贝函数_.cloneDeep()
。const _ = require('lodash'); // 或者 import _ from 'lodash'; let obj1 = { name: '张三', age: 20, address: { city: '北京', street: '长安街' } }; let obj2 = _.cloneDeep(obj1); obj2.address.city = '上海'; console.log(obj1.address.city); // 输出:北京 (深拷贝成功)
总结:Object.assign()
是把好用的瑞士军刀,用对了很爽
Object.assign()
是一个非常实用的工具,它可以用来合并对象、添加属性、设置默认值等等。但是,它本质上是浅拷贝,所以在使用的时候需要注意。如果需要深拷贝,可以使用JSON.parse(JSON.stringify(obj))
、递归拷贝或者lodash的_.cloneDeep()
方法。
记住:
Object.assign()
是浅拷贝。Object.assign()
会覆盖目标对象的同名属性。Object.assign()
只能复制可枚举属性。Object.assign()
会触发setter
方法。Object.assign()
对null
和undefined
的处理是跳过。
希望今天的讲解能帮助大家更好地理解和使用Object.assign()
。 下次再见!