自定义工具函数库(三)
最终仓库:utils: 自定义工具库
1. 自定义 instanceof
- 语法: myInstanceOf(obj, Type)
- 功能: 判断 obj 是否是 Type 类型的实例
- 实现: Type 的原型对象是否是 obj 的原型链上的某个对象, 如果是返回 true, 否则返回 false
之前的笔记:详解原型链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
function myInstanceof(obj, fn) { let prototype = fn.prototype;
let proto = obj.__proto__;
while (proto) { if (prototype === proto) { return true; } proto = proto.__proto__; } return false; }
|
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>对象相关</title> <script src="./myInstanceof.js"></script> </head>
<body> <script> function Person() {}
let p = new Person();
console.log(myInstanceof(p, Person)); console.log(myInstanceof(p, Object)); console.log(myInstanceof(Person, Object)); console.log(myInstanceof(Person, Function)); console.log(myInstanceof(p, Function)); </script> </body> </html>
|
2. 对象/数组拷贝
2.1 浅拷贝与深拷贝
深拷贝和浅拷贝只针对 Object 和 Array 这样的引用数据类型。
- 浅拷贝:只复制某个对象的引用地址值,而不复制对象本身,新旧对象还是共享同一块内存(即修改旧对象引用类型也会修改到新对象)
- 深拷贝:新建一个一摸一样的对象,新对象与旧对象不共享内存,所以修改新对象不会跟着修改原对象。
2.2 浅拷贝
2.2.1 利用扩展运算符…实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function shallowClone(target) { if (typeof target === "object" && target !== null) { if (Array.isArray(target)) { return [...target]; } else { return { ...target }; } } else { return target; } }
|
2.2.2 遍历实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| function shallowClone(target) { if (typeof target === "object" && target !== null) { let ret = Array.isArray(target) ? [] : {};
for (let key in target) { if (target.hasOwnProperty(key)) { ret[key] = target[key]; } }
return ret; } else { return target; } }
|
2.3 深拷贝
2.3.1 JSON 转换
不能拷贝对象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function deepClone(target) { let str = JSON.stringify(target);
return JSON.parse(str); }
|
2.3.2 递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function deepClone(target) { if (typeof target === "object" && target !== null) { const ret = Array.isArray(target) ? [] : {};
for (const key in target) { if (target.hasOwnProperty(key)) { ret[key] = deepClone(target[key]); } }
return ret; } else { return target; } }
|
测试:
1 2 3 4 5
| const obj = { x: "clz", y: { age: 21 }, z: { name: "clz" }, f: function () {} }; const cloneObj = deepClone(obj);
obj.y.age = 111; console.log(obj, cloneObj);
|
开开心心收工?有点问题,如果对象中有循环引用,即”你中有我,我中有你”的话,就会导致形成死循环,会导致无法跑出结果,直到超出最大调用堆栈大小
怎么解决这个 bug 呢?使用 map 来存取拷贝过的数据,每次调用函数时判断有无拷贝过,有的话,直接返回之前拷贝的数据就行了。而且,这里还有个有意思的地方:递归调用函数需要共享变量时,可以通过添加一个参数,一直传同一个变量
改进后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| function deepClone(target, map = new Map()) { if (typeof target === "object" && target !== null) { const cache = map.get(target); if (cache) { return cache; }
const ret = Array.isArray(target) ? [] : {};
map.set(target, ret);
for (const key in target) { if (target.hasOwnProperty(key)) { ret[key] = deepClone(target[key], map); } }
return ret; } else { return target; } }
|
优化遍历性能:
- 数组: while | for | forEach() 优于 for-in | keys()&forEach()
- 对象: for-in 与 keys()&forEach() 差不多
变更部分:分成数组和对象分别处理,使用更优的遍历方式(个人看不出有什么大的区别,先记一下)
1 2 3 4 5 6 7 8 9
| if (Array.isArray(target)) { target.forEach((item, index) => { ret[index] = deepClone(item, map); }); } else { Object.keys(target).forEach((key) => { ret[key] = deepClone(target[key], map); }); }
|
3. 事件
JavaScript 事件回顾
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <style> .outter { position: relative; width: 200px; height: 200px; background-color: red; }
.inner { position: absolute; left: 0; right: 0; top: 0; bottom: 0; width: 100px; height: 100px; margin: auto; background-color: blue; } </style> </head>
<body> <div class="outter"> <div class="inner"></div> </div> <script> const outter = document.querySelector(".outter"); const inner = document.querySelector(".inner");
outter.addEventListener( "click", function () { console.log("捕获 outter"); }, true );
inner.addEventListener( "click", function () { console.log("捕获 inner"); }, true );
outter.addEventListener( "click", function () { console.log("冒泡 outter"); }, false );
inner.addEventListener("click", function () { console.log("冒泡 inner"); }); </script> </body> </html>
|
3.1 自定义事件委托函数
自定义事件委托函数关键:获取真正触发事件的目标元素,若和子元素相匹配,则使用 call 调用回调函数(this 指向,变更为 target)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function addEventListener(el, type, fn, selector) { el = document.querySelector(el);
if (!selector) { el.addEventListener(type, fn); } else { el.addEventListener(type, function (e) { const target = e.target;
if (target.matches(selector)) { fn.call(target, e); } }); } }
|
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <ul id="items"> <li>01</li> <li>02</li> <li>03</li> <li>04</li> <li>05</li> <p>06</p> </ul>
<script> addEventListener( "#items", "click", function () { this.style.color = "red"; }, "li" ); </script>
|
3.2 手写事件总线
on(eventName, listener): 绑定事件监听
emit(eventName, data): 分发事件
off(eventName): 解绑指定事件名的事件监听, 如果没有指定解绑所有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
class EventBus { constructor() { this.callbacks = {}; }
on(eventName, fn) { if (this.callbacks[eventName]) { this.callbacks[eventName].push(fn); } else { this.callbacks[eventName] = [fn]; } }
emit(eventName, data) { let callbacks = this.callbacks[eventName]; if (callbacks && this.callbacks[eventName].length !== 0) { callbacks.forEach((callback) => { callback(data); }); } }
off(eventName) { if (eventName) { if (this.callbacks[eventName]) { delete this.callbacks[eventName]; } } else { this.callbacks = {}; } } }
|
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const eventBus = new EventBus();
eventBus.on("login", function (name) { console.log(`${name}登录了`); });
eventBus.on("login", function (name) { console.log(`${name}又登录了`); });
eventBus.on("logout", function (name) { console.log(`${name}退出登录了`); });
eventBus.emit("login", "赤蓝紫"); eventBus.emit("logout", "赤蓝紫");
|
4. 自定义发布订阅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
class PubSub { constructor() { this.callbacks = {}; this.id = 0; }
subscribe(msg, subscriber) { const token = "token_" + ++this.id;
if (this.callbacks[msg]) { this.callbacks[msg][token] = subscriber; } else { this.callbacks[msg] = { [token]: subscriber, }; }
return token; }
publish(msg, data) { const callbacksOfmsg = this.callbacks[msg]; if (callbacksOfmsg) { Object.values(callbacksOfmsg).forEach((callback) => { callback(data); }); } }
unsubscribe(flag) { if (flag === undefined) { this.callbacks = {}; } else if (typeof flag === "string") { if (flag.indexOf("token") === 0) { const callbacks = Object.values(this.callbacks).find((callbacksOfmsg) => callbacksOfmsg.hasOwnProperty(flag) ); delete callbacks[flag]; } else { delete this.callbacks[flag]; } } else { throw new Error("如果传入参数, 必须是字符串类型"); } } }
|
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| const pubsub = new PubSub();
let pid1 = pubsub.subscribe("pay", (data) => { console.log("商家接单: ", data); }); let pid2 = pubsub.subscribe("pay", () => { console.log("骑手接单"); }); let pid3 = pubsub.subscribe("feedback", (data) => { console.log(`评价: ${data.title}${data.feedback}`); });
pubsub.publish("pay", { title: "炸鸡", msg: "预定11:11起送", });
pubsub.publish("feedback", { title: "炸鸡", feedback: "还好", });
console.log("%c%s", "color: blue;font-size: 20px", "取消订阅");
pubsub.unsubscribe("pay"); console.log(pubsub);
|
5. 封装 axios
详见:axios 笔记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| function axios({ url, method = "GET", params = {}, data = {} }) { return new Promise((resolve, reject) => { method = method.toUpperCase();
let queryString = ""; Object.keys(params).forEach((key) => { queryString += `${key}=${params[key]}&`; });
queryString = queryString.slice(0, -1); url += `?${queryString}`;
const xhr = new XMLHttpRequest(); xhr.open(method, url); if (method === "GET") { xhr.send(); } else { xhr.setRequestHeader("Content-type", "application/json;charset=utf-8"); xhr.send(JSON.stringify(data)); }
xhr.onreadystatechange = function () { if (xhr.readyState === 4) { const { status } = xhr; if (xhr.status >= 200 && xhr.status < 300) { const response = { status, data: JSON.parse(xhr.response), };
resolve(response); } else { reject(`${status}`); } } }; }); }
axios.get = (url, options) => { return axios(Object.assign(options, { url, method: "GET" })); };
axios.post = (url, options) => { return axios(Object.assign(options, { url, method: "POST" })); };
axios.put = (url, options) => { return axios(Object.assign(options, { url, method: "PUT" })); };
axios.delete = (url, options) => { return axios(Object.assign(options, { url, method: "DELETE" })); };
|
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| (async function () { try { const { data: data1 } = await axios({ url: "https://api.apiopen.top/getJoke", method: "get", params: { a: 10, b: 15, }, }); console.log(data1);
const { data: data2 } = await axios.post( "https://api.apiopen.top/getJoke", { params: { a: 1, b: 2, }, } ); console.log(data2); } catch (err) { console.log(err); } })();
|