自定义工具函数库(三)

自定义工具函数库(三)

最终仓库:utils: 自定义工具库

1. 自定义instanceof

  • 语法: myInstanceOf(obj, Type)
  • 功能: 判断obj是否是Type类型的实例
  • 实现: Type的原型对象是否是obj的原型链上的某个对象, 如果是返回true, 否则返回false

之前的笔记:详解原型链

// 自定义instanceof

function myInstanceof(obj, fn) {
  let prototype = fn.prototype  // 获取函数的显示原型

  let proto = obj.__proto__   // 获取obj的隐式原型对象

  // 遍历原型链
  while (proto) {
    if (prototype === proto) {
      return true
    }
    proto = proto.__proto__
  }
  return false
}

测试

<!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:利用扩展运算符...实现
function shallowClone(target) {
  if (typeof target === 'object' && target !== null) {
    if (Array.isArray(target)) {
      return [...target]
    } else {
      return { ...target }
    }
  } else {
    return target   // 是null或者不是引用数据类型直接返回
  }
}

// const obj = { x: 'clz', y: { age: 21 } }
// const cloneObj = shallowClone(obj)
// console.log(cloneObj)

// obj.y.age = 111
// console.log(obj, cloneObj)    // 浅拷贝,修改旧对象的引用类型会同步修改新对象

// obj.x = 'xxx'               // 修改的若不是引用数据类型的没有影响
// console.log(obj, cloneObj)  
2.2.2 遍历实现
// 浅拷贝2:遍历实现
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   // 是null或者不是引用数据类型直接返回
  }
}

// const obj = { x: 'clz', y: { age: 21 } }
// const cloneObj = shallowClone(obj)
// console.log(cloneObj)

// obj.y.age = 111
// console.log(obj, cloneObj)    // 浅拷贝,修改旧对象的引用类型会同步修改新对象

// obj.x = 'xxx'               // 修改的若不是引用数据类型的没有影响
// console.log(obj, cloneObj)  

2.3 深拷贝

2.3.1 JSON转换

不能拷贝对象方法

// 深拷贝1: 通过JSON转换
function deepClone(target) {
  let str = JSON.stringify(target)    // 转换为字符串

  return JSON.parse(str)    // 再把字符串转换为对象
}

// const obj = { x: 'clz', y: { age: 21 }, z: { name: 'clz' }, f: function () { } }
// const cloneObj = deepClone(obj)
// console.log(cloneObj)

// // obj.y.z = obj.z
// // obj.z.y = obj.y  // 循环引用

// obj.y.age = 111
// console.log(obj, cloneObj)
2.3.2 递归
// 深拷贝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来存取拷贝过的数据,每次调用函数时判断有无拷贝过,有的话,直接返回之前拷贝的数据就行了。而且,这里还有个有意思的地方:递归调用函数需要共享变量时,可以通过添加一个参数,一直传同一个变量

改进后:

// 深拷贝2: 通过递归实现:使用map来存取拷贝过的数据,每次调用函数时判断有无拷贝过,有的话,直接返回之前拷贝的数据就行了。
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
  }
}

// const obj = { x: 'clz', y: { age: 21 }, z: { name: 'clz' }, f: function () { } }

// obj.y.z = obj.z
// obj.z.y = obj.y  // 循环引用可能会造成死循环

// const cloneObj = deepClone(obj)

// obj.y.age = 111
// console.log(obj, cloneObj)

优化遍历性能

  • 数组: 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事件回顾

<!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)    // true表示在事件捕获阶段, false或不传参表示在事件冒泡阶段

    inner.addEventListener('click', function () {
      console.log('捕获 inner')
    }, true)


    outter.addEventListener('click', function () {
      console.log('冒泡 outter')
    }, false)    // true表示在事件捕获阶段, false或不传参表示在事件冒泡阶段

    inner.addEventListener('click', function () {
      console.log('冒泡 inner')
    })   
  </script>

</body>

</html>

3.1 自定义事件委托函数

自定义事件委托函数关键:获取真正触发事件的目标元素,若和子元素相匹配,则使用call调用回调函数(this指向,变更为target)

function addEventListener(el, type, fn, selector) { // selector是子元素
 el = document.querySelector(el)

 if (!selector) {
   el.addEventListener(type, fn)  // 没有传递子元素的选择器,则是普通的给el元素绑定事件
 } else {
   el.addEventListener(type, function (e) {
     const target = e.target   // 获取真正触发事件的目标元素

     if (target.matches(selector)) {  // 判断选择器与目标元素是否符合
       fn.call(target, e)
     }
   })
 }
}

测试

 <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): 解绑指定事件名的事件监听, 如果没有指定解绑所有

// 事件总线
// on(eventName, listener): 绑定事件监听
// emit(eventName, data): 分发事件
// off(eventName): 解绑指定事件名的事件监听, 如果没有指定解绑所有

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 = {}
    }
  }
}

测试:

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.off('login')
// console.log(eventBus)

eventBus.emit('login', '赤蓝紫')
eventBus.emit('logout', '赤蓝紫')

4. 自定义发布订阅

// 自定义消息订阅与发布
// PubSub: 包含所有功能的订阅/发布消息的管理者
// PubSub.subscribe(msg, subscriber): 订阅消息: 指定消息名和订阅者回调函数
// PubSub.publish(msg, data): 发布消息: 指定消息名和数据
// PubSub.unsubscribe(flag): 取消订阅: 根据标识取消某个或某些消息的订阅
//  1).没有传值, flag为undefined:清空全部订阅
//  2).传入token字符串: 清除唯一订阅
//  3).msgName字符串: 清除指定消息的全部订阅
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    // 返回token。用于取消唯一订阅
  }

  publish(msg, data) {
    const callbacksOfmsg = this.callbacks[msg]
    if (callbacksOfmsg) {    // 有此消息的订阅
      Object.values(callbacksOfmsg).forEach(callback => {
        callback(data)
      })
    }
  }

  unsubscribe(flag) {
    if (flag === undefined) {  // 1. 清空全部订阅
      this.callbacks = {}
    } else if (typeof flag === 'string') {
      if (flag.indexOf('token') === 0) {   // 2. 取消唯一订阅 
        const callbacks = Object.values(this.callbacks).find(callbacksOfmsg => callbacksOfmsg.hasOwnProperty(flag))    // 找到flag对应的callbacks:callbacks: {pay: {token_1: f1, token_2: f2}}
        delete callbacks[flag]
      } else {    // 3. 取消指定消息的全部订阅
        delete this.callbacks[flag]
      }
    } else {
      throw new Error('如果传入参数, 必须是字符串类型')
    }
  }
}

测试:

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}`) })

// console.log(pubsub)

pubsub.publish('pay', {
  title: '炸鸡',
  msg: '预定11:11起送'
})

pubsub.publish('feedback', {
  title: '炸鸡',
  feedback: '还好'
})

console.log('%c%s', 'color: blue;font-size: 20px', '取消订阅')

// // 1. 取消全部订阅
// pubsub.unsubscribe()
// console.log(pubsub)

// // 2. 取消唯一订阅
// pubsub.unsubscribe(pid1)
// console.log(pubsub)

// 3. 取消指定消息的订阅
pubsub.unsubscribe('pay')
console.log(pubsub)

5. 封装axios

详见:axios笔记

// 封装axios
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]}&`       // 查询参数以key1=value1&key2=value2的形式连接起来
    })

    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' }));     // 把methods和url合并到options中去
}

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' }));
}

测试:

(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)
  }
})()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值