赋值(Copy)
赋值是将某一数值或对象赋给某个变量的过程,分为两种情况:
- 基本数据类型:赋值,赋值之后两个变量互不影响。
- 引用数据类型:赋值(引用地址),两个变量具有相同的引用,指向同一个对象,互相之间有影响。
//基本数据类型
let name = 'hello'
let name2 = name
console.log(name) // hello
console.log(name2) // hello
name2 = 'hello world'
console.log(name) // hello
console.log(name2) // hello world 修改了 name2 的值,不影响 name 的值
内存中有一个变量name,值为hello。我们从变量name复制出一个变量name2,此时在内存中创建了一个块新的空间用于存储hello,虽然两者值是相同的,但是两者指向的内存空间完全不同,这两个变量参与任何操作都互不影响。
let obj = {
name: '小刘',
age: 18,
info: {
field: ['JS', 'CSS', 'HTML']
}
}
let obj2 = obj
console.log(obj) // {name: "小刘", age: 18, info: {field: ["JS", "CSS", "HTML"]}}
console.log(obj2) // {name: "小刘", age: 18, info: {field: ["JS", "CSS", "HTML"]}}
obj2.name = '小孙'
obj2.info.field = ['JavaScript']
console.log(obj) // {name: "小孙", age: 18, info: {field: ["JavaScript"]}}
console.log(obj2) // {name: "小孙", age: 18, info: {field: ["JavaScript"]}}
对引用类型进行赋值(引用地址)操作,两个变量指向同一个对象,改变变量 obj2 之后会影响变量 obj ,哪怕改变的只是对象 obj2 中的基本数据类型。
浅拷贝(Shallow Copy)
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
- 如果属性是基本类型,拷贝的就是基本类型的值;
- 如果属性是引用类型,拷贝的就是内存地址 ;如果其中一个对象改变了这个地址,就会影响到另一个对象。
Object.assign() (ES6)
用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。
let target = {}
let source = { a: { b: 2 } }
Object.assign(target, source)
console.log(target) // 修改前 {a: {b: 2}} 修改后 {a: {b: 10}}
//如果我们修改了 b 的属性
source.a.b = 10
console.log(source) // {a: {b: 10}}
console.log(target) // {a: {b: 10}}
由于修改后三个target里面的属性 b 都改变了,证明Objec.assign是一个浅拷贝。
注意:
- 拷贝的都是自有属性,不会拷贝对象继承的属性;
- 拷贝的都是可枚举属性;
- 可以拷贝
symbol类型的属性;- 原始类型会被包装为对象。
扩展运算符
实际上, 展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝(只遍历一层)。
let obj = { a: 1, b: { c: 1 } }
let obj2 = { ...obj }
obj.a = 2
console.log(obj) // 修改二层属性前 {a: 2, b: {c: 1}} 修改二层属性后 {a: 2, b: {c: 2}}
console.log(obj2) // 修改二层属性前 {a: 1, b: {c: 1}} 修改二层属性后 {a: 2, b: {c: 2}}
obj.b.c = 2
console.log(obj) // {a: 2, b: {c: 2}}
console.log(obj2) // {a: 1, b: {c: 2}}
Array.prototype.slice()
返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。
let a = [0, "1", [2, 3]]
let b = a.slice(1)
console.log(b) // ["1", [4, 3]]
a[1] = '99'
a[2][0] = 4
console.log(a) // [0, "99", [4, 3]]
console.log(b) // ["1", [4, 3]]
改变a[1]之后b[0]的值并没有发生变化,但是改变a[2][0]之后,相应的b[1][0]的值也发生变化。
说明slice()方法是浅拷贝。
Array.prototype.concat()
用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
let arr = [{ a: 1 }, { a: 1 }, { a: 1 }]
let arr2 = [{ b: 1 }, { b: 1 }, { b: 1 }]
let arr3 = arr.concat(arr2)
arr2[0].b = 123
console.log(arr3) // [{a: 1},{a: 1},{a: 1},{ b: 123},{b: 1},{b: 1}]
改变arr2[0].b 之后,arr3的值也发生了变化,这说明concat也是浅拷贝。
手写浅拷贝
function shallowClone(source) {
let target = {}
for (const key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key]
}
}
return target
}
深拷贝(Deep Copy)
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
JSON.parse(JSON.stringify())
JSON.stringify()方法将一个 JavaScript 对象或值转换为 JSON 字符串。JSON.parse()方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。
通过JSON.stringify()把一个对象序列化成为一个 JSON 字符串,将对象的内容转换成字符串的形式保存,再用JSON.parse() 反序列化将 JSON 字符串变成一个新对象。
const arr = [
1, 6, {
name: "小刘",
age: 18
}
]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[2].name = "小孙"
console.log(arr, arr2) // [1,6,{name: "小刘", age: 18}] [1,6,{name: "小孙", age: 18}]
需要注意的是:
let test = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
nan: NaN,
infi: Infinity,
infi1: -Infinity,
obj: {
name: '我是一个对象',
id: 1
},
arr: [0, 1, 2],
func: function () {
console.log('我是一个函数')
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
err: new Error('我是一个错误')
}
console.log(JSON.parse(JSON.stringify(test)))
//循环引用
let obj = {
a: 1,
b: {
c: 2
}
}
obj.a = obj.b
obj.b.c = obj.a
let test = JSON.parse(JSON.stringify(obj)) // Uncaught TypeError: Converting circular structure to JSON
//不可枚举属性被忽略
let test1 = JSON.stringify(
Object.create(
null,
{
x: { value: 'x', enumerable: false },
y: { value: 'y', enumerable: true }
}
)
)
console.log(test1) // {"y":"y"}
-
JSON 会忽略
undefiend,symbol,function。 -
date对象Mon Jun 28 2021 13:28:59 GMT+0800 (中国标准时间)转变为字符串 "2021-06-28T05:28:49.680Z"。Date 日期调用了
toJSON()将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。 -
NaN,Infinity,-Infinity会转变为null。 -
RegExp,Error会变成空对象 {}。 -
循环引用的情况下,会报
Uncaught TypeError: Converting circular structure to JSON错误。 -
其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
由此可见,使用 JSON 可以实现数组或对象深拷贝,但是处理其它类型对象会有问题。
手写深拷贝(ConardLi大佬版)
深拷贝,考虑我们要拷贝的对象不知道有多少曾深度,我们可以用递归来解决问题:
- 如果是原始类型,无需继续拷贝,直接返回;
- 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆的对象的属性执行深拷贝后依次添加到新对象上。
如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:
function clone(source) {
if (typeof source === 'object') {
let target = {}
for (const key in source) {
if (source.hasOwnProperty(key)) {
target[key] = clone(source[key])
}
return target
}
} else {
return source
}
}
考虑数组
我们可以看到,返回结果中数组的返回值是arr:{ 0:0, 1:1, 2:2 }。这说明我们没有判断对象是数组的情况。
function clone(source) {
if (typeof source === 'object') {
let target = Array.isArray(source) ? [] : {}
for (const key in source) {
target[key] = clone(source[key])
}
return target
}
return source
}
循环引用
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
}
target.target = target
运行上面一段代码,我们发现栈内存溢出了。原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身。
解决循环引用的问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中寻找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解了循环引用的问题。
这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:
- 检查
map中有无克隆过的对象。- 有,直接返回。
- 没有,将当前对象作为
key,克隆对象作为value进行存储。
- 继续克隆。
function clone(source, map = new Map()) {
if (typeof source === 'object') {
let target = Array.isArray(source) ? [] : {}
if (map.get(source)) {
return map.get(source)
}
map.set(source, target)
for (const key in source) {
target[key] = clone(source[key], map)
}
return target
}
return source
}
Map可以用WeakMap来代替,这样的好处是,不需要手动清除Map的属性,等下一次垃圾回收机制执行时,这块内存就会被释放掉。坏处是,
WeakMap是 ES6 新增的集合类型,兼容性没Map好。
性能优化
在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的,下面我们来对比下常用的三种循环for,while,for in的执行效率:
//先建立一个40000000级别的字符串数组
const array = new Array(40000000).fill('hello')
const length = array.length
let i = 0
let sum = 0
console.time('while')
while (i < length) {
const element = array[i]
sum += element
i++
}
console.timeEnd('while') // while: 3408.120849609375 ms
console.time('for')
for (let i = 0; i < length; i++) {
const element = array[i]
sum += element
}
console.timeEnd('for') // for: 6242.840087890625 ms
console.time('for in')
sum = 0
for (const key in array) {
const element = array[key]
sum += element
}
console.timeEnd('for in') // for in: 29896.768310546875 ms
由此可见,while的效率要高于for和for in。我们将for in遍历改写为while遍历。
我们先使用while来实现一个通用的foreach遍历,iteratee是遍历的回调函数,它可以接收每次遍历的value和index两个参数:
function forEach(array, iteratee) {
let index = -1
const length = array.length
while (++index < length) {
iteratee(array[index], index)
}
return array
}
下面我们对clone函数进行改写:
- 当遍历数组时,直接使用
forEach进行遍历; - 当遍历对象时,使用
Object.keys取出所有的key进行遍历; - 然后在遍历时把
forEach回调函数的value当做key使用。
function clone(source, map = new WeakMap()) {
//是对象的情况下
if (typeof source === 'object') {
//判定是数组还是对象
const isArray = Array.isArray(source)
let target = isArray ? [] : {}
//map中有克隆过的对象直接返回
if (map.get(source)) {
return map.get(source)
}
//map中没有克隆过的对象进行存储
map.set(source, target)
//遍历数组时,使用forEach遍历;遍历对象时,使用 Object.keys (返回 key 组成的数组)取出所用的 key 进行遍历
const keys = isArray ? undefined : Object.keys(source)
// undefined || array -> array
// array || object -> array
forEach(keys || source, (value, key) => {
//如果是数组
if (keys) {
//将回调函数的 value 当做 key 使用
key = value
}
//将对象储存在 target 中 ,多层对象递归
target[key] = clone(source[key], map)
})
//返回对象
return target
} else {
//返回原始类型
return source
}
}
合理的判断引用类型
上面,我们只考虑了object和array两种数据类型,实际上所用的引用类型还有很多。
我们还需要考虑function和null两种特殊的数据类型。
function isObject(source) {
const type = typeof source
return source !== null && (type === 'object' || type === 'function')
}
if(!isObject(source)){
return source
}
获取数据类型
我们可以使用toString()来获取准确的引用类型:
每一个引用类型都有
toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString()返回"[object type]",其中type是对象的类型。
注意,上面提到了如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。
我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。
function getType(source){
return Object.prototype.toString.call(source)
}
下面我们抽离出一些常用的数据类型以便后面使用:
const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const numberTag = '[object Number]'
const regexpTag = '[object RegExp]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
在上面的集中类型中,我们简单将他们分为两类:
- 可以继续遍历的类型。
- 不可以继续遍历的类型。
我们分别为它们做不同的拷贝。
可继续遍历的类型
object,array,Map,Set这几种类型都属于可持续遍历的类型,需要进行递归。
我们首先要获得它们的初始化数据,例如上面的[]和{},我们可以通过拿到constructor的方式来通用的获取。
这种方法有一个好处:因为我们还使用了原对象的构造方法,所以他可以保留对象原型上的数据,如果直接使用普通的{},那么原型必然会丢失的。
function clone(source, map = new WeakMap()) {
//克隆原始类型
if (!isObject(source)) {
return source
}
//初始化
const type = getType(source)
let target
if (deepTag.includes(type)) {
target = getInit(source, type)
}
//防止循环引用
if (map.get(source)) {
return map.get(source)
}
map.set(source, target)
//克隆set
if (type === setTag) {
source.forEach(value => {
target.add(clone(value, map))
})
return target
}
//克隆map
if (type === mapTag) {
source.forEach((value, key) => {
target.set(key, clone(value, map))
})
return target
}
//克隆对象和数组
//运算符优先级 === > ...? :... > =
const keys = type === arrayTag ? undefined : Object.keys(source)
forEach(keys || source, (value, key) => {
if (keys) {
key = value
}
target[key] = clone(target[key], map)
})
return target
}
不可继续遍历的类型
其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理:
Boolean、Number、String、String、Date、Error这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:
function cloneOtherType(targe, type) {
const Ctor = targe.constructor
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe)
case regexpTag:
return cloneReg(targe)
case symbolTag:
return cloneSymbol(targe)
case funcTag:
return cloneFunction(targe)
default:
return null
}
}
克隆Symbol类型:
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe))
}
克隆Regexp类型:
function cloneReg(targe) {
const reFlags = /\w*$/
const result = new targe.constructor(targe.source, reFlags.exec(targe))
result.lastIndex = targe.lastIndex
return result
}
克隆function类型:
实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的,lodash对函数的处理是直接返回:
const isFunc = typeof value == 'function'
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
下面来分析一下函数类型怎么克隆:
- 首先,我们可以通过
protptype来区分箭头函数和普通函数,箭头函数是没有prototype的。 - 我们可以直接使用
eval和函数字符串来重新生成一个箭头函数。- 这种方法不适用于普通函数。
- 我们可以用正则来处理普通函数。
- 分别使用正则取出函数体和函数参数,然后使用
new Function([arg1,[arg2,[...argN]]],functionBody)构造函数重新构造一个新的函数。
- 分别使用正则取出函数体和函数参数,然后使用
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m
const paramReg = /(?<=\().+(?=\)\s+{)/
const funcString = func.toString()
if (func.prototype) {
console.log('普通函数')
const param = paramReg.exec(funcString)
const body = bodyReg.exec(funcString)
if (body) {
console.log('匹配到函数体:', body[0])
if (param) {
const paramArr = param[0].split[',']
console.log('匹配到参数:', paramArr)
return new Function(...paramArr, body[0])
} else {
return new Function(body[0])
}
} else {
return null
}
} else {
return eval(funcString)
}
}
综合
//可继续遍历的数据类型
const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'
const argsTag = '[object Arguments]'
//不可继续遍历的数据类型
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const numberTag = '[object Number]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const errorTag = '[object Error]'
const regexpTag = '[object RegExp]'
const funcTag = '[object Function]'
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag]
//通用 while 循环
function forEach(array, iteratee) {
let index = -1
const length = array.length
while (++index < length) {
iteratee(array[index], index)
}
return array
}
//判断是否为引用类型
function isObject(source) {
const type = typeof source
return source !== null && (type === 'object' || type === 'function')
}
//获取实际类型
function getType(source) {
return Object.prototype.toString.call(source)
}
//初始化被克隆的对象
function getInit(source) {
const Ctor = source.constructor
return new Ctor()
}
//克隆Symbol
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe))
}
//克隆正则
function cloneReg(targe) {
//意思是匹配字符串尾部字母
const reFlags = /\w*$/
//targe.constructor 就是 RegExp 构造函数
//正则分为源码(source)和修饰符(flags),targe.source 获取源码,也就是//里面的数据,reFlags.exec(targe) 获取修饰符,也就是 //后面的gim
const result = new targe.constructor(targe.source, reFlags.exec(targe))
//克隆lastIndex,lastIndex 表示每次匹配时的开始位置。
result.lastIndex = targe.lastIndex
return result
}
//克隆函数
function cloneFunction(func) {
//后行断言 匹配 { + (非\n\r的所有字符或\r) + 先行断言 匹配 } ,也就是匹配函数体
//后行断言 匹配 ( + 非\n\r的所有字符 + 先行断言 匹配 ) + 空格 + { ,也就是匹配函数参数
const bodyReg = /(?<={)(.|\n)+(?=})/m
const paramReg = /(?<=\().+(?=\)\s*{)/
const funcString = func.toString()
if (func.prototype) {
const param = paramReg.exec(funcString)
const body = bodyReg.exec(funcString)
if (body) {
if (param) {
const paramArr = param[0].split(',')
return new Function(...paramArr, body[0])
} else {
return new Function(body[0])
}
} else {
return null
}
} else {
return eval('(' + funcString + ')')
}
}
//克隆不可遍历类型
function cloneOtherType(targe, type) {
const Ctor = targe.constructor
switch (type) {
case boolTag:
return Object(Boolean.prototype.valueOf.call(targe)) //为了修正Boolean(false)判定为true
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe)
case regexpTag:
return cloneReg(targe)
case symbolTag:
return cloneSymbol(targe)
case funcTag:
return cloneFunction(targe)
default:
return null
}
}
function clone(source, map = new WeakMap()) {
// 原始类型直接返回
if (!isObject(source)) {
return source
}
// 初始化
const type = getType(source)
let target
if (deepTag.includes(type)) {
target = getInit(source, type)
} else {
return cloneOtherType(source, type)
}
// 防止循环引用
if (map.get(source)) {
return map.get(source)
}
map.set(source, target)
// 克隆set
if (type === setTag) {
source.forEach(value => {
target.add(clone(value, map))
})
return target
}
// 克隆map
if (type === mapTag) {
source.forEach((value, key) => {
target.set(key, clone(value, map))
})
return target
}
// 克隆对象和数组
// 优先级 === > ?: > =
const keys = type === arrayTag ? undefined : Object.keys(source)
forEach(keys || source, (value, key) => {
//对象的情况下,value 当做 key 使用
if (keys) {
key = value
}
//递归
target[key] = clone(source[key], map)
})
return target
}
测试:
const map = new Map()
map.set('key', 'value')
map.set('xiaoliu', 'hello world')
const set = new Set()
set.add('xiaoliu')
set.add('hello world')
const source = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
empty: null,
map,
set,
bool: new Boolean(false),
num: new Number(2),
str: new String(2),
symbol: Object(Symbol(1)),
date: new Date(),
reg: /\d+/,
error: new Error(),
func1: () => {
console.log('hello world')
},
func2: function (a, b) {
return a + b
}
}
手写深拷贝(简化版)
//ES5
function deepCopy(obj, hashMap = new WeakMap()) {
var result
const hashKey = hashMap.get(obj)
if (hashKey) {
return hashKey
}
//引用类型分数组和对象分别递归
if (Object.prototype.toString.call(obj) === '[object Array]') {
result = []
hashMap.set(obj, result)
for (var i = 0; i < obj.length; i++) {
result[i] = deepCopy(obj[i], hashMap)
}
} else if (Object.prototype.toString.call(obj) === '[object Object]') {
result = {}
hashMap.set(obj, result)
for (var key in obj) {
result[key] = deepCopy(obj[key], hashMap)
}
} else {
return obj
}
return result
}
//ES6
function deepClone(origin, hashMap = new WeakMap()) {
// 如果是原始类型,直接返回
if (origin === null || typeof origin !== 'object') {
return origin
}
// Date 返回构造函数
if (origin instanceof Date) {
return new Date(origin)
}
// Regexp 返回构造函数
if (origin instanceof RegExp) {
return new RegExp(origin)
}
// 使用 hashMap 保存键名,如果存在,返回键名,这样可以解决循环引用问题
const hashKey = hashMap.get(origin)
if (hashKey) {
return hashKey
}
// 克隆空的源对象,这样就不需要判断数组或是对象了
const target = new origin.constructor()
// hashMap 没有的键名,直接添加进源对象和克隆的对象
hashMap.set(origin, target)
// 遍历源对象的属性
for (let k in origin) {
// 如果不是原型上的属性
if (origin.hasOwnProperty(k)) {
// 源对象属性克隆给 target ,属性可能是对象,所以递归
target[k] = deepClone(origin[k], hashMap)
}
}
//返回克隆的对象
return target
}
ES5版
ES6版
这个版本的问题是无法识别包装类以及 Symbol 没有 constructor 会报错