自定义工具函数库(三)
最终仓库: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);   } })();
 
  |