自定义工具函数库(三)
最终仓库:utils: 自定义工具库
1. 自定义 instanceof
- 语法: myInstanceOf(obj, Type)
- 功能: 判断 obj 是否是 Type 类型的实例
- 实现: Type 的原型对象是否是 obj 的原型链上的某个对象, 如果是返回 true, 否则返回 false
之前的笔记:详解原型链
|
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 转换
不能拷贝对象方法
|
function deepClone(target) {
let str = JSON.stringify(target);
return JSON.parse(str);
}
|
2.3.2 递归
|
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;
}
}
|
测试:
| 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() 差不多
变更部分:分成数组和对象分别处理,使用更优的遍历方式(个人看不出有什么大的区别,先记一下)
| 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);
}
})();
|