FormData
很多时候,在
post提交数据时我们常采用application/json、application/x-www-form-urlencoded等类型,也确实能够覆盖到大部分的场景,但是有一些场景下,比如文件上传的时候,就不算是好的解决方案了,application/json作为请求头Content-Type字段值时,表示告知服务端参数是序列化后的JSON字符串,所以一般在传参时都会用JSON.stringify序列化一下,且浏览器对 JSON.stringify API 支持程度比较高,但是JSON.stringify在转换某一些数据结构时会出问题,比如 会丢失 function 类型的参数、循环引用时会报错、Blob/File对象会被转化成{}等等,,可以参考 为何不推荐使用 JSON.stringify 做深拷贝,不过JSON.stringify还有第三个参数,有兴趣的同学可以去了解下,这是其一,其二,有同学要说了,如果要是图片那可以转换成base64格式进行上传解决,这种方式虽然可行,但是转换成base64格式需要很多字符,占用很多资源,而且很长,不便于阅读,另外就是服务端接收到这个参数还得解析,很麻烦,此时,FormData就可用上了。
为何不推荐使用JSON.stringify做深拷贝
- 如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式
- 如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
- 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
- JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
- 如果对象中存在循环引用的情况也无法正确实现深拷贝;
总结:
用法简单,然而使用这种方法会有一些隐藏的坑:因为在序列化JavaScript对象时,所有函数和原型成员会被有意忽略。
通俗点说,JSON.parse(JSON.stringfy(X)),其中X只能是Number, String, Boolean, Array, 扁平对象,即那些能够被 JSON 直接表示的数据结构。
如何提升JSON.stringify()的性能?
自己实现
- 如果是原始类型,无需继续拷贝,直接返回
- 如果是引用类型,就将更深层次的对象进行递归直到属性为原始类型
- 兼容数组
module.exports = function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
会存在循环引用,即对象的属性间接或直接的引用了自身的情况,target.target = target;递归进入死循环导致栈内存溢出了。
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,需要可以存储key-value形式的数据,即map和WeakMap
WeakMap:弱引用,垃圾回收机制会自动帮我们回收,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。
- 检查
map中有无克隆过的对象 - 有 - 直接返回
- 没有 - 将当前对象作为
key,克隆对象作为value进行存储 - 继续克隆
// 使用`while`来实现一个通用的`forEach`遍历,性能更快。
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
function clone(target, map = new WeakMap()) {
if (typeof target === 'object') {
const isArray = Array.isArray(target);
let cloneTarget = isArray ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
const keys = isArray ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone2(target[key], map);
});
return cloneTarget;
} else {
return target;
}
}
拷贝函数
首先,我们可以通过prototype来区分下箭头函数和普通函数,箭头函数是没有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);
}
}
能力:
-
基本实现
- 递归能力
-
循环引用
- 考虑问题的全面性
- 理解weakmap的真正意义
-
多种类型
- 考虑问题的严谨性
- 创建各种引用类型的方法,JS API的熟练程度
- 准确的判断数据类型,对数据类型的理解程度
-
通用遍历:
- 写代码可以考虑性能优化
- 了解集中遍历的效率
- 代码抽象能力
-
拷贝函数:
- 箭头函数和普通函数的区别
- 正则表达式熟练程度