源码里面的文件位置:/src/util.js
英文注释是源码里面本来有的,英语不太好,也没咋看,直接上自己的中文注释
/**
* Get the first item that pass the test
* by second argument function
*
* @param {Array} list数组
* @param {Function} f 函数
* @return {*}
*/
// 找到数组中第一个符合条件的元素
export function find (list, f) {
return list.filter(f)[0]
}
/**
* Deep copy the given object considering circular structure.
* This function caches all nested objects and its copies.
* If it detects circular structure, use cached copy to avoid infinite loop.
*
* @param {*} obj
* @param {Array<Object>} cache
* @return {*}
*/
// 深拷贝
// 整体思路-递归
export function deepCopy (obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}
// if obj is hit, it is in circular structure
// 这里其实是通过下面的cache缓存当前嵌套的对象
// 然后通过find方法判断是否存在循环引用,
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive deepCopy
// 缓存副本
cache.push({
original: obj,
copy
})
// 递归调用深拷贝函数
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
/**
* forEach for object
*/
// 遍历对象的属性和值
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
// 判断是否为除了null之外的对象
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
// 判断promise对象
export function isPromise (val) {
return val && typeof val.then === 'function'
}
// assert node中的断言
// 断言抛出异常
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
// 保留原始参数的闭包函数
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
这次我们重点理理它这个深拷贝
在这之前,废话不多说直接打开谷歌控制台使用下面的对象来测试一下vuex的深拷贝,自己先去发现一下有没有什么问题
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj.loop = obj // 设置loop成循环引用的属性
这里我就不贴结果了,自己运行的话,很快就能发现
- Date和RegExp类型的直接被实例化为一个空对象
- 拷贝得到的对象中并没有Symbol和不可枚举的属性
Symbol是es6新出的一种基本数据类型(不知道的去恶补吧)
通过Object.defineProperty设置的不可枚举属性,不了解的,可以去了解下js对象属性的描述符(如果了解vue双向绑定原理的一定知道这个方法和对象属性的描述符get和set,这里的enumerable是一类东西)
抛出了问题,试着改进下吧,注意看注释
循环引用的问题,还是沿用它自己的,有兴趣的可以去试着用WeakMap处理一下循环引用的问题
// 改进版的深拷贝
function deepClone (obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}
// 手动处理Date和正则类型会返回空对象的问题
// 这里我们直接自己返回一个new的实例
if (obj.constructor === Date) return new Date(obj)
if (obj.constructor === RegExp) return new RegExp(obj)
// 循环引用这里还是保留之前的处理方式
// find(上面代码第一个方法)
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
cache.push({
original: obj,
copy
})
// 递归调用深拷贝函数
// Reflect.ownKeys除了可枚举属性还可以返回不可枚举的属性和es6的symbol属性
Reflect.ownKeys(obj).forEach(key => {
copy[key] = deepClone(obj[key], cache)
})
return copy
}
说到深拷贝,不得不提一下乞丐版JSON.stringfy
深拷贝
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
JSON.stringfy
是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用JSON.parse()
的方法将JSON 字符串生成一个新的对象。示例代码如下所示。
let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2); //{a:1,b:[1,2,3]}
obj1.a = 2;
obj1.b.push(4);
console.log(obj1); //{a:2,b:[1,2,3,4]}
console.log(obj2); //{a:1,b:[1,2,3]}
从上面的代码可以看到,通过 JSON.stringfy 可以初步实现一个对象的深拷贝,通过改变 obj1 的 b 属性,其实可以看出 obj2 这个对象也不受影响。
但是使用 JSON.stringfy 实现深拷贝还是有一些地方值得注意,如果你此时用上面代码示例的那个比较复杂的对象来测试,你就会发现问题,总结下来主要有这几点:
- 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
- 拷贝 Date 引用类型会变成字符串;
- 无法拷贝不可枚举的属性;
- 无法拷贝对象的原型链;
- 拷贝 RegExp 引用类型会变成空对象;
- 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
- 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
所以在开发中,JSON.stringfy只能解决普通对象的深拷贝,如果对象中还包括更复杂的类型,这种方法是有缺陷的,这时候就需要自己去手写一个符合实际情况的深拷贝方法