一、关于对象“copy”深浅拷贝的探究
- 浅拷贝 对象的浅拷贝是其属性与拷贝源对象的属性共享相同引用(指向相同的底层值)的副本。(只能copy第一层)
在 JavaScript 中,所有标准的内置对象复制操作(展开语法、Array.prototype.concat()、Array.prototype.slice()、Array.from()、Object.assign() 和 Object.create())创建的是浅拷贝而不是深拷贝。
- 深拷贝 对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用的副本。
如果一个 JavaScript 对象可以被序列化,则存在一种创建深拷贝的方式:使用 JSON.stringify() 将该对象转换为 JSON 字符串,然后使用 JSON.parse() 将该字符串转换回(全新的)JavaScript 对象:
let ingredients_list = ["noodles", { list: ["eggs", "flour", "water"] }];
let ingredients_list_deepcopy = JSON.parse(JSON.stringify(ingredients_list));
// 改变深拷贝的对象不会影响到原来的对象
ingredients_list_deepcopy[1].list = ["rice flour", "water"];
console.log(ingredients_list[1].list);
// Array(3) [ "eggs", "flour", "water" ]
但许多 JavaScript 对象根本不能序列化——例如,函数(带有闭包)、Symbol、在 HTML DOM API 中表示 HTML 元素的对象、递归数据以及许多其他情况。在这种情况下,调用 JSON.stringify()
来序列化对象将会失败。所以没有办法对这些对象进行深拷贝。
-
手写全面的拷贝代码
1、先准备两个工具方法——检测数据类型、遍历数组和对象。
- 定义变量
let class2type = {}; let toString = class2type.toString; let hasOwn = class2type.hasOwnProperty; let utils = {}; [ "Boolean", "Number", "String", "Symbol", "BigInt", "Object", "Array", "Date", "RegExp", "Error", "Function", ].forEach((name) => { class2type[`[object ${name}]`] = name.toLowerCase(); }); //class2type: {[object Boolean]: 'boolean', [object Number]: 'number', [object String]: 'string',....} //参照格式:Object.proretype.toString.call(1) '[object Number]'
- 检测数组类型
//基本数据类型用typeof 判断,引用数据类型用toString 判断 const toType = function toType(obj) { if (obj == null) return obj + ""; return typeof obj === "object" || typeof obj === "function" ? class2type[toString.call(obj)] || "object" : typeof obj; };
- 判断是否是数组和类数组
// 检测是否为函数/window 返回布尔值 obj.nodeType 只读属性,当前节点的类型 const isFunction = function isFunction(obj) { return typeof obj === "function" && typeof obj.nodeType !== "number"; }; const isWindow = function isWindow(obj) { return obj != null && obj === obj.window; }; // 检测是否为数据或者类数组 const isArrayLike = function isArrayLike(obj) { let length = !!obj && "length" in obj && obj.length, //true or false 类数组中会有length属性 type = toType(obj); if (isFunction(obj) || isWindow(obj)) return false; return ( type === "array" || length === 0 || (typeof length === "number" && length > 0 && length - 1 in obj) ); };
- 遍历数组和对象
// 遍历数组/类数组/对象 const each = function each(obj, callback) { callback = callback || Function.prototype; //类数组 if (isArrayLike(obj)) { for (let i = 0; i < obj.length; i++) { let item = obj[i], result = callback.call(item, item, i); if (result === false) break; } return obj; } //对象 for (let key in obj) { //obj.hasOwnProperty 如果obj是一个空对象就break if (!hasOwn.call(obj, key)) break; let item = obj[key], result = callback.call(item, item, key); if (result === false) break; } return obj; };
- 挂载和导出
utils.toType = toType; utils.isFunction = isFunction; utils.isWindow = isWindow; utils.isArrayLike = isArrayLike; utils.each = each; // 暴露API:支持浏览器导入和CommonJS/ES6Module规范 if (typeof window !== "undefined") { window._ = window.utils = utils; } if (typeof module === "object" && typeof module.exports === "object") { module.exports = utils; }
2、实现不可枚举属性的浅拷贝
// 浅克隆 function shallowClone(obj) { let type = _.toType(obj), Ctor = obj.constructor; // 对于Symbol/BigInt if (/^(symbol|bigint)$/i.test(type)) return Object(obj); // 对于正则/日期的处理 if (/^(regexp|date)$/i.test(type)) return new Ctor(obj); // 对于错误对象的处理 if (/^error$/i.test(type)) return new Ctor(obj.message); // 对于函数 if (/^function$/i.test(type)) { // 返回新函数:新函数执行还是把原始函数执行,实现和原始函数相同的效果 return function () { return obj.call(this, ...arguments); }; } // 数组或者对象 if (/^(object|array)$/i.test(type)) { let keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)], result = new Ctor(); _.each(keys, key => { result[key] = obj[key]; }); return result; } //其他的基本数据类型 return obj; }
3、解决不能序列化对象的深拷贝
// 深克隆:只要有下一级的,我们就克隆一下(浅克隆)
function deepClone(obj, cache = new Set()) {
let type = _.toType(obj),
Ctor = obj.constructor;
//不是对象或数组走浅拷贝
if (!/^(object|array)$/i.test(type)) return shallowClone(obj);
// 利用cache.has(obj)来避免无限套娃 obj{xxx:obj}(这种就是套娃情况会一直找下去)
if (cache.has(obj)) return obj;
cache.add(obj);
let keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)],
result = new Ctor();
_.each(keys, (key) => {
// 再次调用deepClone的时候把catch传递进去,保证每一次递归都是一个cache
result[key] = deepClone(obj[key], cache);
});
return result;
}
二、关于对象“merge合并”的完整方案
我们常用到的对象合并方法:Object.assign()
静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。
但是object.assign()方法会直接替换同属性的值,不会让两个相同键(属性)的属性值进行合并 。解决办法如下代码:遍历赋值。
/*
* 两个对合并的意义:
* + 插件组件封装:参数处理
* + 业务需求
* + ...
*/
const options = {
url: '',
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
data: null,
arr: [10, 20, 30],
config: {
xhr: {
async: true,
cache: false
}
}
};
const params = {
url: 'http://www.zhufengpeixun.cn/api/',
headers: {
'X-Token': 'EF00F987DCFA6D31'
},
data: {
lx: 1,
from: 'weixin'
},
arr: [30, 40],
config: {
xhr: {
cache: true
}
}
};
// 基于浅比较实现的对象的合并
// let xx = Object.assign(options, params);
/*
* 几种情况的分析
* A->options中的key值 B->params中的key值
* 1.A&B都是原始值类型:B替换A即可
* 2.A是对象&B是原始值:抛出异常信息
* 3.A是原始值&B是对象:B替换A即可
* 4.A&B都是对象:依次遍历B中的每一项,替换A中的内容
*/
// params替换options
function isObj(value) {
// 是否为普通对象
return _.toType(value) === "object";
}
function merge(options, params = {}) {
_.each(params, (_, key) => {
let isA = isObj(options[key]),
isB = isObj(params[key]);
if (isA && !isB) throw new TypeError(`${key} in params must be object`);
if (isA && isB) {
options[key] = merge(options[key], params[key]);
return;
}
options[key] = params[key];
});
return options;
}
console.log(merge(options, params));
/*{
"url": "http://www.zhufengpeixun.cn/api/",
"method": "GET",
"headers": {
"Content-Type": "application/json",
"X-Token": "EF00F987DCFA6D31"
},
"data": {
"lx": 1,
"from": "weixin"
},
"arr": [
30,
40
],
"config": {
"xhr": {
"async": true,
"cache": true
}
}
}*/
三、“函数柯里化”的两大运用:bind&currying
官方的解释是,把接受多个参数的函数变换成只接后一个单一参数的函数,并返回接受余下的参数和结果的新函数的技术。
一个常用的用柯里化机制实现的就是bind方法:
Function.prototype.bind = function bind(context, ...params) {
// this/self->func context->obj params->[10,20]
let self = this;
return function proxy(...args) {
// 把func执行并且改变this即可 args->是执行proxy的时候可能传递的值
self.apply(context, params.concat(args));
};
};
function currying(fn, ...rest1) {
return function(...rest2) {
return fn.apply(null, rest1.concat(rest2))
}
}
用柯里化函数来实现正则验证:
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test1') //true
check(/[a-z]+/g, 'test') //true
// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
let hasNumber = curryingCheck(/\d+/g) //对应reg参数
hasNumber('test1') // true
hasNumber('testtest') // false
四、queryURLParams的三种实现方案
可以去参考知乎的这篇文章 2022年了!你有几种获取URL参数的方法? - 知乎
五、彻底玩转AOP面向切片编程
面向切面编程(Aspect-Oriented Programming,AOP)是一种编程范式,是在面向对象编程(OOP)的基础上的一种补充。AOP提供了一种灵活的、易于维护和跨多个对象和组件的切面(Aspect)的方式,可以在不修改原有代码的情况下实现代码的特定功能。
Function.prototype.before = function before(callback) {
if (typeof callback !== "function") throw new TypeError('callback must be function');
// this->func
let _self = this;
return function proxy(...params) {
// this !== func 调用时候才知道
//控制callback和func本身的先后执行顺序
callback.call(this, ...params);
return _self.call(this, ...params);
};
};
Function.prototype.after = function after(callback) {
if (typeof callback !== "function") throw new TypeError('callback must be function');
let _self = this;
return function proxy(...params) {
let res = _self.call(this, ...params);
callback.call(this, ...params);
return res;
};
};
let func = () => {
// 主要的业务逻辑
console.log('func');
};
/* func.before(() => {
console.log('===before===');
})(); */
func.before(() => {
console.log('===before===');
}).after(() => {
console.log('===after===');
})();
/* function handle(func, before, after) {
before();
func();
after();
} */