2022-05-11
代码
0
请注意,本文编写于 1142 天前,最后修改于 517 天前,其中某些信息可能已经过时。

目录

函数相关
call()
apply()
bind()
函数节流
数组相关
map()
reduce()
unique() 数组去重
concat() 数组合并
slice() 数组切片
flatten() 数组扁平化
chunk() 数组分块
drop() & dropRight() 获取数组元素
对象相关
自定义new
自定义instanceof
合并多个对象
对象 / 数组拷贝
浅拷贝
深拷贝
删除空元素
字符串相关
DOM事件监听
事件冒泡
事件委托
事件总线
Ajax请求

函数相关

call()

call()函数的作用与函数原型对象上的call方法是一样的:改变函数运行时的this值,并得到函数运行的执行结果

js
function call(Fn, obj, ...args) { // 判断obj if(obj === undefined || obj === null) { obj = globalThis // globalThis: ES11新特性,用来指向全局对象 } // 为obj添加临时的方法 obj.temp = Fn // 调用temp方法 let result = obj.temp(...args) // 删除temp方法 delete obj.temp // 返回执行结果 return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title> <script src="./1_call.js"></script> </head> <body> <script> // 声明一个函数 function add(a, b) { console.log('this', this) return a + b + this.c } // 声明一个对象 let obj = { c: 123 } // 添加全局属性 window.c = 456 // 执行call函数 console.log(call(add, obj, 10, 20)) console.log(call(add, null, 20, 50)) console.log(obj) </script> </body> </html>

image.png

apply()

apply()函数作用和call()函数相同,区别在于,apply()函数的参数是一个数组,而call()函数的参数是单独存在的

js
function apply(Fn, obj, args) { // 判断 if (obj === undefined || obj === null) { obj = globalThis } // 为obj添加临时方法 obj.temp = Fn // 执行方法 let result = obj.temp(...args) // 删除临时属性 delete obj.temp // 返回执行结果 return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title> <script src="./2_apply.js"></script> </head> <body> <script> // 声明一个函数 function add(a, b) { console.log('this', this) return a + b + this.c } // 声明一个对象 let obj = { c: 123 } // 添加全局属性 window.c = 456 // 执行apply函数 console.log(apply(add, obj, [10, 20])) console.log(apply(add, null, [20, 50])) console.log(obj) </script> </body> </html>

image.png

bind()

bind在call的基础上会返回一个函数

js
function bind(Fn, obj, ...args) { // 返回一个新的函数 return function(...args2) { // 执行call函数 return call(Fn, obj, ...args, ...args2) } }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title> <script src="./1_call.js"></script> <script src="./3_bind.js"></script> </head> <body> <script> // 声明一个函数 function add(a, b) { console.log('this', this) return a + b + this.c } // 声明一个对象 let obj = { c: 123 } // 添加全局属性 window.c = 456 // 执行bind函数 let fn = bind(add, obj, 10, 20) console.log(fn()) let fn2 = bind(add, obj) console.log(fn2(30, 40)) </script> </body> </html>

image.png

函数节流

函数节流(throttle):控制事件执行的时间间隔(在函数需要频繁触发时,函数执行一次后,只有大于设定的执行周期后才会执行第二次,适合多次事件按时间做平均分配触发)

场景:

  • 窗口调整(resize)
  • 页面滚动(scroll)
  • DOM元素的拖拽功能实现(mousemove)
  • 抢购疯狂点击(click)
js
function throttle(callback, wait) { // 定义开始时间 let start = 0 // 返回结果是一个函数 return function(event) { // 获取当前时间戳 let now = Date.now() // 判断 if (now - start >= wait) { // 若满足条件则执行回调函数 callback.call(this, event) // 更新开始时间 start = now } } }
  • 语法: throttle(callback, wait)
  • 功能: 创建一个节流函数,在 wait 毫秒内最多执行 callback 一次

测试:

html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>函数节流</title> <style> body { height: 2000px; background: linear-gradient(to right bottom, rgb(0, 255, 240), rgb(92, 159, 247) 40%, rgb(211, 34, 255) 80%); } </style> <script src="./throttle.js"></script> </head> <body> <script> // 绑定滚动事件 /* window.addEventListener('scroll', function() { console.log(Date.now()) }) */ window.addEventListener('scroll', throttle(function() { console.log(Date.now()) }, 500)) </script> </body> </html>

函数防抖

函数防抖(debounce):在函数需要频繁触发时,在规定时间内,只让最后一次生效,前面的不生效,适合多次事件一次响应的情况。

场景:

  • 输入框实时搜索联想(keyup/input)
js
function debounce(callback, wait) { // 定时器变量 let timeId = null // 返回一个函数 return function(event) { // 判断 if (timeId !== null) { // 已经有定时器在运行 // 清空定时器 clearTimeout(timeId) } // 启动定时器 timeId = setTimeout(() => { // 执行回调 callback.call(this, event) // 重置定时器变量 clearTimeout(timeId) }, wait) } }
  • 语法: debounce(callback, wait)
  • 功能: 创建一个防抖动函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 callback

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>函数防抖</title> <script src="./debounce.js"></script> </head> <body> <input type="text"> <script> let input = document.querySelector('input') /* input.onkeydown = function(e) { console.log(e.key) } */ input.onkeydown = debounce(function(e) { console.log(e.key) }, 1000) </script> </body> </html>

GIF 2022-4-15 20-41-46.gif

数组相关

map()

map()方法创建一个新数组,其结果是该数组中的每个元素是调用一次 提供的函数 后的返回值

js
/** * map() * * @param {Array} arr * @param {Function} callback * @return {*} */ function map(arr, callback) { // 声明空数组 let result = [] // 遍历数组 for (let i = 0; i < arr.length; i++) { // 执行回调 // result[i] = callback(arr[i], i) result.push(callback(arr[i], i)) } return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> const arr = [1, 2, 3, 4, 5] const result = map(arr, (item) => { return item * 10 }) console.log(result) </script> </body> </html>

image.png

reduce()

reduce()从左到右为每个数组元素执行一次回调函数,并把上次回调函数的返回值作为参数传给下次回调函数,并返回最后一次回调函数的返回值

js
/** * reduce() * * @param {Array} arr * @param {Function} callback * @param {*} initValue * @return {*} */ function reduce(arr, callback, initValue) { let result = initValue // 遍历数组 for (let i = 0; i < arr.length; i++) { // 判断initValue if (result === undefined) { result = arr[i] continue } // 执行回调 result = callback(result, arr[i]) } return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> const arr = [1, 2, 3, 4, 5] let result = reduce(arr, (prev, current) => { return prev + current }, 0) console.log(result) </script> </body> </html>

filter()

filter()将所有在过滤函数中返回 true 的数组元素放进一个新数组中并返回

js
/** * filter() * * @param {Array} arr * @param {Function} callback */ function filter(arr, callback) { // 声明空数组,接收满足条件的结果值 let result = [] // 遍历数组 for (let i = 0; i < arr.length; i++) { // 执行回调 let res = callback(arr[i], i) // 为真则压入数组 if (res) { result.push(arr[i]) } } return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> const arr = [1, 2, 3, 4, 5] const result = filter(arr, item => item%2 === 1) console.log(result) </script> </body> </html>

find() & findIndex()

find() 找到第一个满足测试函数的元素并返回那个元素的值,如果找不到,则返回 undefined

findIndex() 找到第一个满足测试函数的元素并返回那个元素的索引,如果找不到,则返回 -1

js
/** * find() * * @param {Array} arr * @param {Function} callback */ function find(arr, callback) { // 遍历数组 for (let i = 0; i < arr.length; i++) { // 执行回调 let res = callback(arr[i], i) if (res) { return arr[i] } } return undefined } /** * findIndex() * * @param {Array} arr * @param {Function} callback */ function findIndex(arr, callback) { for (let i = 0; i < arr.length; i++) { let res = callback(arr[i], i) if (res) { return i } } return -1 }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> const arr = [1, 2, 3, 4, 5] const result = find(arr, item => { return item > 3 }) console.log(result) const res = findIndex(arr, item => { return item > 3 }) console.log(res) </script> </body> </html>

every() & some()

every(): 如果数组中的每个元素都满足测试函数,则返回 true,否则返回 false

some(): 如果数组中至少有一个元素满足测试函数,则返回 true,否则返回 false

js
/** * every() * * @param {Array} arr * @param {Function} callback * @returns */ function every(arr, callback) { for (let i = 0; i < arr.length; i++) { let res = callback(arr[i], i) if (!res) { return false } } return true } /** * some() * * @param {Array} arr * @param {Function} callback * @returns */ function some(arr, callback) { for (let i = 0; i < arr.length; i++) { let res = callback(arr[i], i) if (res) { return true } } return false }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> const arr = [1, 2, 3, 4, 5] // every const result1 = every(arr, item => { return item > 3 }) const result2 = every(arr, item => { return item > 0 }) console.log('result1', result1) console.log('result2', result2) //some const result3 = some(arr, item => { return item > 3 }) const result4 = some(arr, item => { return item > 30 }) console.log('result3', result3) console.log('result4', result4) </script> </body> </html>

image.png

unique() 数组去重

  • 方法1: 利用forEach()indexOf()
    • 说明: 本质是双重遍历, 效率差些
  • 方法2: 利用forEach() + 对象容器
    • 说明: 只需一重遍历, 效率高些
  • 方法3: 利用ES6语法: from + Set 或者 ... + Set
    • 说明: 编码简洁
js
/** * unique * forEach() + indexOf() * * @param {Array} arr */ function unique1(arr) { const result = [] arr.forEach(item => { if(result.indexOf(item) === -1) { result.push(item) } }) return result } /** * unique * forEach() + 对象容器 * * @param {Array} arr */ function unique2(arr) { const result = [] const obj = {} arr.forEach(item => { if (obj[item] === undefined) { // 将item作为下标存储在obj中 obj[item] = true result.push(item) } }) return result } /** * unique * 或者 ... + Set * * @param {Array} arr */ function unique3(arr) { // 将数组转化为集合Set let set = new Set(arr) // 将Set展开创建数组 let result = [...set] return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> const arr = [1, 3, 5, 2, 2, 1, 2, 3, 4, 5] console.log('@', unique1(arr)) console.log('#', unique2(arr)) console.log('$', unique2(arr)) </script> </body> </html>

image.png

concat() 数组合并

js
/** * concat() * * @param {Array} arr * @param {...any} args */ function concat(arr, ...args) { const result = [...arr] args.forEach(item => { // 判断item是否为数组 if (Array.isArray(item)) { result.push(...item) } else { result.push(item) } }) return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> let arr = [1, 2, 3] const result = concat(arr, [3, 4, 5], 6, 7, [7, 6, 8]) console.log(result) </script> </body> </html>

image.png

slice() 数组切片

js
/** * slice() * * @param {Array} arr * @param {Number} begin * @param {Number} end */ function slice(arr, begin, end) { // 若arr数组为空 if (arr.length === 0) { return [] } // 判断begin和end begin = begin || 0 end = end || arr.length if (begin >= arr.length || end < 0) { return [] } begin = begin<0 ? begin+arr.length : begin let result = [] for (let i = 0; i < arr.length; i++) { if (i >= begin && i < end) { result.push(arr[i]) } } return result }

测试:

js
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> let arr = [1, 2, 3, 4, 5, 6, 7, 8] console.log('无参数', slice(arr)) console.log('两个参数', slice(arr, 2, 6)) console.log('begin为负', slice(arr, -3, 8)) console.log('参数超范围', slice(arr, -51, 122)) console.log('end大于length', slice(arr, 12, 15)) console.log('begin小于0', slice(arr, -8, -5)) console.log('空数组', slice([])) </script> </body> </html>

image.png

flatten() 数组扁平化

取出嵌套数组(多维)中的所有元素放到一个新数组(一维)中,如: [1, [3, [2, 4]]] ==> [1, 3, 2, 4]

  • 方法一:递归 + reduce() + concat()
  • 方法二:... + some() + concat()
js
/** * flatten() * 递归 + reduce() + concat() * * @param {Array} arr */ function flatten1(arr) { let result = [] arr.forEach(item => { if (Array.isArray(item)) { result = result.concat(flatten1(item)) } else { result = result.concat(item) } }) return result } /** * flatten() * ... + some() + concat() * * @param {Array} arr */ function flatten2(arr) { let result = [...arr] // 循环判断 while (result.some(item => Array.isArray(item))) { result = [].concat(...result) } return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> let arr = [1, 2, [3, 4, [5, 6, 7], 8], 9, [10, 11]] console.log('first', flatten1(arr)) console.log('second', flatten2(arr)) </script> </body> </html>

image-20220416151857320

chunk() 数组分块

chunk()将数组拆分成多个 size 长度的区块,每个区块组成小数组,整体组成一个二维数组

js
/** * chunk() * * @param {Array} arr * @param {Number} size */ function chunk(arr, size = 1) { if (arr.length === 0) { return [] } let result = [] let temp = [] arr.forEach(item => { // 判断temp长度 if (temp.length === 0) { // 将temp压入result中 result.push(temp) } // 将元素压入临时数组temp中 temp.push(item) // 判断temp长度 if (temp.length === size) { temp = [] } }) return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> console.log(chunk([1, 2, 3, 4, 5, 6, 7, 8], 3)) </script> </body> </html>

difference() 数组差集

difference(arr1, arr2)得到arr1中所有不在arr2中的元素组成的数组(不改变原数组)

js
/** * difference() * * @param {Array} arr1 * @param {Array} arr2 */ function difference(arr1, arr2=[]) { if (arr1.length === 0) { return [] } if (arr2.length === 0) { return arr1.slice() // 返回一个新数组 } return arr1.filter(item => !arr2.includes(item)) }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> console.log(difference([1, 3, 5 ,7], [2, 5 ,8])) </script> </body> </html>

pull() & pullAll() 删除数组元素

  • pull(array, ...values):
    • 删除原数组中与value相同的元素, 返回所有删除元素的数组
    • 说明: 原数组发生了改变
    • 如: pull([1, 3, 5, 3, 7], 2, 7, 3, 7) ===> 原数组变为[1, 5], 返回值为[3, 3, 7]
  • pullAll(array, values):
    • 功能与pull一致, 只是参数变为数组
    • 如: pullAll([1, 3, 5, 3, 7], [2, 7, 3, 7]) ===> 数组1变为[1, 5], 返回值为[3, 3, 7]

原数组发生改变

js
/** * * @param {Array} arr * @param {...any} args * @returns */ function pull(arr, ...args) { // 保存删掉的元素 const deleted = [] for (let i = 0; i < arr.length; i++) { // 判断当前元素是否存在于args中 if (args.includes(arr[i])) { // 将当前元素存入deleted deleted.push(arr[i]) // 删除当前的元素 arr.splice(i, 1) // 下标自检 i-- } } return deleted } /** * pullAll() * * @param {Array} arr * @param {Array} args * @returns */ function pullAll(arr, args) { return pull(arr, ...args) }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> let arr = [1, 3, 5, 3, 7] console.log(pull(arr, 2, 7, 3, 7)) console.log(arr) let array = [1, 3, 5, 3, 7] console.log(pullAll(array, [2, 7, 3, 7])) console.log(array) </script> </body> </html>

image.png

drop() & dropRight() 获取数组元素

  • drop(array, count)
    • 得到当前数组过滤掉左边count个后剩余元素组成的数组
    • 说明: 不改变当前数组, count默认是1
    • 如: drop([1,3,5,7], 2) ===> [5, 7]
  • dropRight(array, count)
    • 得到当前数组过滤掉右边count个后剩余元素组成的数组
    • 说明: 不改变当前数组, count默认是1
    • 如: dropRight([1,3,5,7], 2) ===> [1, 3]
js
/** * drop() * * @param {Array} arr * @param {Number} count */ function drop(arr, count) { // 过滤源数组 产生新数组 return arr.filter((value, index) => { return index >= count }) } /** * dropRight() * * @param {Array} arr * @param {Number} count */ function dropRight(arr, count) { return arr.filter((value, index) => index < arr.length - count) }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>数组API</title> <script src="./utils.js"></script> </head> <body> <script> console.log(drop([1, 3, 5, 7, 9, 11], 2)) console.log(dropRight([1, 3, 5, 7, 9, 11], 2)) </script> </body> </html>

image.png

对象相关

自定义new

  • 语法: newInstance(Fn, ...args)
  • 功能: 创建Fn构造函数的实例对象
js
/** * newInstance(Fn, ...args) * * @param {Function} Fn * @param {...any} args */ function newInstance(Fn, ...args) { // 1.创建一个新对象 const obj = {} // 2.修改函数内部this指向新对象 并执行 Fn.call(obj, ...args) // 3.修改新对象的原型对象 obj.__proto__ = Fn.prototype // 4.返回新对象 return obj }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>对象API - newInstance</title> <script src="./utils.js"></script> </head> <body> <script> function Person(name, age) { this.name = name this.age = age } function Person2(name, age) { this.name = name this.age = age return {a: 100} } function Person3(name, age) { this.name = name this.age = age return [1, 2, 3] } function Person4(name, age) { this.name = name this.age = age return function(){} } function Person5(name, age) { this.name = name this.age = age return 'morales' } // 调用 newInstance 函数创建对象 console.log(newInstance(Person, 'ABC', 18)) console.log(newInstance (Person2, 'ABC', 18)) console.log(newInstance(Person3, 'ABC', 18)) console.log(newInstance(Person4, 'ABC', 18)) console.log(newInstance(Person5, 'ABC', 18)) </script> </body> </html>

image.png

自定义instanceof

  • 语法: myInstanceOf(obj, Type)
  • 功能: 判断obj是否是Type类型的实例
  • 实现: Type的原型对象是否是obj的原型链上的某个对象, 如果是返回true, 否则返回false
js
/** * myInstanceof(obj, Fn) * * @param {Object} obj * @param {Function} Fn */ function myInstanceof(obj, Fn) { // 获取函数的显式原型 let prototype = Fn.prototype // 获取obj的隐式原型对象 let proto = obj.__proto__ // 遍历原型链 while (proto) { // 检测原型对象是否相等 if (prototype === proto) { return true } proto = proto.__proto__ } return false }

合并多个对象

  • 语法: object mergeObject(...objs)
  • 功能: 合并多个对象, 返回一个合并后对象(不改变原对象)
  • 例子:
    • obj1: { a: [{ x: 2 }, { y: 4 }], b: 1}
    • obj2: { a: { z: 3}, b: [2, 3], c: 'foo'}
    • 合并后: { a: [ { x: 2 }, { y: 4 }, { z: 3 } ], b: [ 1, 2, 3 ], c: 'foo' }
js
/** * mergeObject(...objList) * * @param {...any} objList */ function mergeObject(...objList) { // 声明空对象 const result = {} // 遍历所有参数对象 objList.forEach((obj) => { // 获取当前对象所有属性 Object.keys(obj).forEach((key) => { // 检测 result 中是否存在 key 属性 if (result.hasOwnProperty(key)) { result[key] = [].concat(result[key], obj[key]) } else { // 直接写入 result[key] = obj[key] } }) }) return result }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>合并多个对象</title> <script src="./utils.js"></script> </head> <body> <script> const obj1 = { a: [{x: 2}, {y: 4}], b: 1 } const obj2 = { a: {z: 3}, b: [2, 3], c: 'abc' } console.log(mergeObject(obj1, obj2)) </script> </body> </html>

image.png

对象 / 数组拷贝

  • 纯语言表达:
    • 浅拷贝: 只是复制了对象属性或数组元素本身(只是引用地址值)
    • 深拷贝: 不仅复制了对象属性或数组元素本身, 还复制了指向的对象(使用递归)
  • 举例说明: 拷贝 persons 数组(多个人对象的数组)
    • 浅拷贝: 只是拷贝了每个 person 对象的引用地址值, 每个person对象只有一份
    • 深拷贝: 每个 person 对象也被复制了一份新的

浅拷贝

方法1:利用ES6语法:展开运算符

js
/** * clone1(target) * ... * * @param {*} target * @returns */ function clone1(target) { // 类型判断 if(typeof target === 'object' && target !== null) { // 判断为数组 if(Array.isArray(target)) { return [...target] } else { return {...target} } } else { return target } }

测试:

html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>对象 / 数组拷贝</title> <script src="clone.js"></script> </head> <body> <script> const obj = {x: 'abc', y: {a: 1}} const result = clone1(obj) // 修改某个引用类型的值 result.y.a = 2 console.log('obj', obj) console.log('result', result) </script> </body> </html>

image.png

方法2:利用ES5语法:for...in

js
/** * clone2(target) * for...in * * @param {*} target * @returns */ function clone2(target) { // 类型判断 if(typeof target === 'object' && target !== null) { // 创建一个容器 const result = Array.isArray(target) ? [] : {} // 遍历target数据 for(let key in target) { // 判断当前对象是否包含该属性 if(target.hasOwnProperty(key)) { result[key] = target[key] } } return result } else { return target } }

测试:

js
const obj = {x: 'abc', y: {a: 1}} const result = clone2(obj) // 修改某个引用类型的值 result.y.a = 3 console.log('obj', obj) console.log('result', result) console.log(obj === result)

image.png

深拷贝

1 - 大众乞丐版

  1. 问题1:函数属性会丢失
  2. 问题2:循环引用会出错
js
/** * deepClone1(target) * 大众乞丐版 * * @param {*} target * @returns */ function deepClone1(target) { // 通过数据创建json格式字符串 let str = JSON.stringify(target) // 将json字符串创建为JS数据 let data = JSON.parse(str) return data }

测试:

const obj = { a: 1, b: ['e', 'f', 'g'], c: {h: 'i', j: 30}, d: function(){} } const result = deepClone1(obj) result.c.j = 1909 console.log('obj', obj) console.log('result', result)

image.png

循环引用报错:

js
// 循环引用 obj.b.push(obj.c) obj.c.k = obj.b

image.png

2 - 面试基础版(递归)

解决了函数属性丢失的问题

js
/** * deepClone2(target) * 面试基础版 * * @param {*} target * @returns */ function deepClone2(target) { // 检测数据类型 if(typeof target === 'object' && target !== null) { // 创建容器 const result = Array.isArray(target) ? [] : {} // 遍历对象 for(let key in target) { // 检测该属性是否为target本身的属性(不能拷贝原型对象上的属性) if(target.hasOwnProperty(key)) { // 拷贝 result[key] = deepClone2(target[key]) } } return result } else { return target } }

测试:

js
const obj = { a: 1, b: ['e', 'f', 'g'], c: {h: 'i', j: 30}, d: function(){} } // 循环引用 - 报错 // obj.b.push(obj.c) // obj.c.k = obj.b const result = deepClone2(obj) result.c.j = 2004 console.log('obj', obj) console.log('result', result)

image.png

循环引用会导致栈溢出:

image.png

3 - 面试加强版

解决了循环引用出错的问题

js
/** * deepClone3(target) * 面试加强版 * * @param {*} target * @returns */ function deepClone3(target, map=new Map()) { // 检测数据类型 if(typeof target === 'object' && target !== null) { // 克隆数据之前判断:数据是否克隆过 let cache = map.get(target) if(cache) { return cache } // 创建容器 const result = Array.isArray(target) ? [] : {} // 将新的结果存到容器中 map.set(target, result) // 遍历对象 for(let key in target) { // 检测该属性是否为target本身的属性(不能拷贝原型对象上的属性) if(target.hasOwnProperty(key)) { // 拷贝 result[key] = deepClone3(target[key], map) } } return result } else { return target } }

测试:

js
const obj = { a: 1, b: ['e', 'f', 'g'], c: {h: 'i', j: 30}, d: function(){} } obj.b.push(obj.c) obj.c.k = obj.b const result = deepClone3(obj) result.c.j = '20H2' console.log('obj', obj) console.log('result', result)

image.png

4 - 面试加强优化版

优化了遍历性能

  • 数组: while | for | forEach() 优于 for-in | keys()&forEach()
  • 对象: for-inkeys()&forEach() 差不多
js
/** * deepClone4(target) * 面试加强版 - 优化遍历性能 * * @param {*} target * @returns */ function deepClone4(target, map=new Map()) { // 检测数据类型 if(typeof target === 'object' && target !== null) { // 克隆数据之前判断:数据是否克隆过 let cache = map.get(target) if(cache) { return cache } // 判断目标数据类型 let targetIsArray = Array.isArray(target) // 创建容器 const result = targetIsArray ? [] : {} // 将新的结果存到容器中 map.set(target, result) // 遍历对象 // 如果目标数据为数组 if(targetIsArray) { // forEach target.forEach((item, index) => { result[index] = deepClone4(item, map) }) } else { Object.keys(target).forEach(key => { result[key] = deepClone4(target[key], map) }) } return result } else { return target } }

测试:

js
const obj = { a: 1, b: ['e', 'f', 'g'], c: {h: 'i', j: 30}, d: function(){} } obj.b.push(obj.c) obj.c.k = obj.b const result = deepClone4(obj) result.c.j = '21H2' console.log('obj', obj) console.log('result', result)

image.png

删除空元素

需求:给出一个对象,要求删除对象中的空字符串、null、空数组、空对象

js
function check(target) { if(target === '') { return true } if(target === null) { return true } if(typeof target === 'object') { if(Array.isArray(target)) { if(JSON.stringify(target) === '[]') { return true } } else { if(JSON.stringify(target) === '{}') { return true } } } return false } function clearValue(obj) { Object.keys(obj).forEach(key => { if (check(obj[key])) { delete obj[key] } else if(typeof obj[key] === 'object') { clearValue(obj[key]) } }) }

测试:

js
const obj = { a: 1, b: '', c: [], d: { e: [], f: {}, g: 'abc' } } console.log('Before', obj) clearValue(obj) console.log('After', obj)

image.png

字符串相关

  • 字符串倒序
    • 语法: reverseString(str)
    • 功能: 生成一个倒序的字符串
  • 字符串是否是回文
    • 语法: palindrome(str)
    • 功能: 如果给定的字符串是回文,则返回 true ;否则返回 false
  • 截取字符串
    • 语法: truncate(str, len)
    • 功能: 如果字符串的长度超过了len, 截取前面len长度部分, 并以...结束
js
/** * reverseString(str) * * @param {String} str * @returns */ function reverseString(str) { // 将字符串转为数组 // return str.split('').reverse().join('') // return [...str].reverse().join('') return Array.from(str).reverse().join('') } /** * palindrome(str) * * @param {String} str * @returns */ function palindrome(str) { return reverseString(str) === str } /** * truncate(str, len) * * @param {String} str * @param {Number} len * @returns */ function truncate(str, len) { return str.slice(0, len) + '...' }

测试:

html
<!DOCTYPE html> <html> <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="string.js"></script> </head> <body> <script> let str1 = 'moralesrx' let str2 = 'win101niw' let str3 = '哔哩哔哩干杯 - 哔哩哔哩' // 字符串翻转 console.log(reverseString(str1)) // 回文字符串 console.log(palindrome(str2)) // 字符串截断 console.log(truncate(str3, 10)) </script> </body> </html>

image.png

DOM事件监听

事件冒泡

image.png

html
<!DOCTYPE html> <html> <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> <style> .outter { margin-left: 50px; margin-top: 50px; width: 200px; height: 200px; background-color: #16a086; border-radius: 50%; position: relative; } .inner { position: absolute; left: 50px; top: 50px; width: 100px; height: 100px; background-color: #f39c11; border-radius: 50%; } </style> </head> <body> <div class="outter"><div class="inner"></div></div> <script> let outter = document.querySelector('.outter') let inner = document.querySelector('.inner') inner.addEventListener('click', function() { console.log('捕获 inner') }, true) // true:捕获,false(默认):冒泡 outter.addEventListener('click', function() { console.log('捕获 outter') }, true) inner.addEventListener('click', function() { console.log('冒泡 inner') }) outter.addEventListener('click', function() { console.log('冒泡 outter') }) </script> </body> </html>

事件冒泡与事件委托

  • 事件冒泡的流程
    • 基于DOM树形结构
    • 事件在目标元素上处理后, 会由内向外(上)逐层传递
    • 应用场景: 事件代理/委托/委派

事件委托

  • 事件委托/代理
    • 将多个子元素的同类事件监听委托给(绑定在)共同的一个父组件上
    • 好处:
      • 减少内存占用(事件监听回调从n变为1)
      • 动态添加的内部元素也能响应
js
// 父级元素选择器,绑定事件类型,事件处理程序,绑定元素选择器 /** * addEventListener(el, type, fn, selector) * 如果selector没有,直接给element绑定事件 * 如果selector有,将selector对应的多个元素的事件委托绑定给父元素element * * @param {String} el 父级元素选择器 * @param {String} type 绑定事件类型 * @param {Function} fn 事件处理程序 * @param {String} selector 绑定元素选择器 */ function addEventListener(el, type, fn, selector) { // 判断el类型 if(typeof el === 'string') { 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) } }) } }

测试:

html
<!DOCTYPE html> <html> <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="dom.js"></script> </head> <body> <ul id="items"> <li>AAAA</li> <li>BBBB</li> <li>CCCC</li> <li>DDDD</li> <div>EEEE</div> <div>FFFF</div> </ul> <script> // 调用函数绑定事件 addEventListener('#items', 'click', function() { // 输出标签文本 console.log(this.innerHTML) }, 'li') </script> </body> </html>

GIF 2022-5-9 16-16-19.gif

事件总线

  1. eventBus: 包含所有功能的事件总线对象
  2. eventBus.on(eventName, listener): 绑定事件监听
  3. eventBus.emit(eventName, data): 分发事件
  4. eventBus.off(eventName): 解绑指定事件名的事件监听, 如果没有指定解绑所有

在绑定时把事件类型与事件回调保存起来,在触发以后查找该类型是否有与之对应的回调,如果有则取出并执行

js
const eventBus = { // 保存类型与回调的容器 callbacks: {} } /** * 绑定事件 * * @param {String} type * @param {Function} callback */ eventBus.on = function(type, callback) { // 判断 if(this.callbacks[type]) { this.callbacks[type].push(callback) } else { this.callbacks[type] = [callback] } } /** * 触发事件 * * @param {String} type * @param {*} data */ eventBus.emit = function(type, data) { // 判断 if(this.callbacks[type] && this.callbacks[type].length > 0) { // 遍历数组 this.callbacks[type].forEach(callback => { // 执行回调 callback(data) }) } } /** * 事件解绑 * * @param {*} eventName */ eventBus.off = function(eventName) { // 若传入eventName if(eventName) { // 删除eventName对应事件的回调 delete this.callbacks[eventName] } else { this.callbacks = {} } }

测试:

html
<!DOCTYPE html> <html> <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="event-bus.js"></script> </head> <body> <script> // eventBus eventBus.on('login', data => { console.log(data + '用户已登录') }) eventBus.on('login', data => { console.log(data + '登录数据已写入') }) // 触发 setTimeout(() => { eventBus.emit('login', 'Morales') }, 2000) // 解绑事件 eventBus.off('login') // eventBus.off() // console.log(eventBus) </script> </body> </html>

消息订阅与发布

  1. PubSub: 包含所有功能的订阅/发布消息的管理者
  2. PubSub.subscribe(msg, subscriber): 订阅消息: 指定消息名和订阅者回调函数
  3. PubSub.publish(msg, data): 异步发布消息: 指定消息名和数据
  4. PubSub.publishSync(msg, data): 同步发布消息: 指定消息名和数据
  5. PubSub.unsubscribe(flag): 取消订阅: 根据标识取消某个或某些消息的订阅
js
const Pubsub = { // 订阅唯一id id: 1, // 频道与回调保存容器 callbacks: { /*pay: { token_1: fn1, token_2: fn2 }*/ } } Pubsub.subscribe = function(channel, callback) { // 为每个订阅创建唯一编号 let token = "token_" + this.id++ // 判断callbacks属性中是否存在pay if(this.callbacks[channel]) { this.callbacks[channel][token] = callback } else { this.callbacks[channel] = { [token]: callback } } return token } Pubsub.publish = function(channel, data) { // 获取当前频道中所有回调 if(this.callbacks[channel]) { Object.values(this.callbacks[channel]).forEach(callback => { // 执行回调 callback(data) }) } } Pubsub.unsubscribe = function(flag) { // 没有传值, flag为undefined:全部取消 // 传入token字符串 // msgName字符串 if(flag === undefined) { this.callbacks = {} } else if(typeof flag === 'string') { // 判断是否为token_开头 if(flag.indexOf('token_') === 0) { let callbackObj = Object.values(this.callbacks).find(obj => obj.hasOwnProperty(flag)) if(callbackObj) { delete callbackObj[flag] } } else { delete this.callbacks[flag] } } }

测试:

html
<!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="pubsub.js"></script> </head> <body> <script> // 订阅一个频道 Pubsub.subscribe('pay', data => { console.log('商家接单,准备制作', data.name) }) Pubsub.subscribe('pay', data => { console.log('骑手接单,准备取餐') }) Pubsub.subscribe('cancel', data => { console.log('取消了') }) console.log(Pubsub) // 发布消息 // Pubsub.publish('pay', { // name: '水煮肉片', // price: 13, // position: '松新街21号' // }) // 取消订阅 // Pubsub.unsubscribe('pay') Pubsub.unsubscribe('token_2') console.log(Pubsub) Pubsub.publish('pay', { name: '水煮肉片', price: 13, position: '松新街21号' }) </script> </body> </html>

image.png

Ajax请求

  1. 函数的参数为一个配置对象

    { url: '', // 请求地址 method: '', // 请求方式GET/POST/PUT/DELETE params: {}, // GET/DELETE请求的query参数 data: {}, // POST或DELETE请求的请求体参数 }

  2. 返回值: 函数的返回值为promise, 成功的结果为response, 失败的结果为error

  3. 能处理多种类型的请求: GET / POST / PUT / DELETE

  4. 响应json数据自动解析为js的对象/数组

  • axios(options)
    • 参数配置对象:url, method, paramsdata
    • 返回值为:promise对象
js
function axios({method, url, params, data}) { method = method.toUpperCase() return new Promise((resolve, reject) => { // 1.创建Ajax对象 const xhr = new XMLHttpRequest() // 2.初始化 // 处理 params 对象:a=100&b=200 let str = '' for(let k in params) { str += `${k}=${params[k]}&` } str = str.slice(0, -1) xhr.open(method, url+'?'+str) // 3.发送 if(method === 'POST' || method === 'PUT' || method === 'DELETE') { // 告诉服务器请求体的格式是json xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8') // 设置请求体 xhr.send(JSON.stringify(data)) } else { xhr.send() } // 设置响应结果的类型为json xhr.responseType = 'json' // 4.绑定状态改变的监听 xhr.onreadystatechange = function() { if(xhr.readyState === 4) { if(xhr.status >= 200 && xhr.status < 300) { // 成功的状态 resolve({ status: xhr.status, message: xhr.statusText, body: xhr.response }) } else { reject(new Error('请求失败,状态码为:' + xhr.status)) } } } }) }

测试:

html
<!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>Axios</title> <script src="axios.js"></script> </head> <body> <script> axios({ method: 'get', url: 'https://v1.hitokoto.cn', params: { c: 'd', encode: 'json' }, data: { a: 100, b: 200 } }).then(response => { console.log(response) }, reason => { console.log(reason) }) axios({ method: 'get', url: 'https://v118.hitokoto.cn', params: { c: 'd', encode: 'json' }, data: { a: 100, b: 200 } }).then(response => { console.log(response) }, reason => { console.log(reason) }) </script> </body> </html>

发送特定请求的静态方法:

  • axios.get(url, options)
  • axios.post(url, data, options)
  • axios.put(url, data, options)
  • axios.delete(url, options)
js
axios.get = function(url, options) { return axios(Object.assign(options, {method: 'GET', url: url})) } axios.post = function(url, options) { return axios(Object.assign(options, {method: 'POST', url: url})) } axios.put = function(url, options) { return axios(Object.assign(options, {method: 'PUT', url: url})) } axios.delete = function(url, options) { return axios(Object.assign(options, {method: 'DELETE', url: url})) }

本文作者:Morales

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 License 许可协议。转载请注明出处!