JavaScript 高阶技巧
0x01 深浅拷贝
-
开发中经常需要拷贝(复制)一个对象,如果直接赋值,则对拷贝对象的修改会影响到源对象
const o1 = { a: 1, b: 2 } const o2 = o1 console.log(o2) // { a: 1, b: 2 } o2.a = 3 console.log(o1) // { a: 3, b: 2 } console.log(o2) // { a: 3, b: 2 }
原因在于,直接赋值的方法是在拷贝对象数据在栈中的地址,即两个变量操作同一个位置的数据
-
深拷贝与浅拷贝只针对引用类型
(1)浅拷贝
- 浅拷贝只将对象或数组的第一层进行复制,其他层级复制的是所存储的内存地址
a. 对象
-
方法:
Object.assign(to, from)
或{...obj}
-
案例:
const o1 = { a: 1, b: 2 } const o2 = {} Object.assign(o2, o1) console.log(o2) // { a: 1, b: 2 } o2.a = 3 console.log(o1) // { a: 1, b: 2 } console.log(o2) // { a: 3, b: 2 } const o3 = {...o1} console.log(o3) // { a: 1, b: 2 }
-
浅拷贝仅拷贝第一层数据,而不会深入
const o1 = { a: 1, b: { c: 2 } } const o2 = {} Object.assign(o2, o1) o2.b.c = 3 console.log(o1) // { a: 1, b: { c: 3 } } console.log(o2) // { a: 1, b: { c: 3 } }
b. 数组
-
方法:
Array.prototype.concat()
或[...array]
-
案例:
const a1 = [1, 2, 3] const a2 = a1.concat([]) console.log(a2) // [ 1, 2, 3 ] const a3 = [...a1] console.log(a3) // [ 1, 2, 3 ]
(2)深拷贝
-
深拷贝会构造一个新的复合数组或对象,遇到引用所指向的引用数据类型会继续执行拷贝
-
常见方法:
a. 递归方法
-
递归应用举例:通过
setTimeout
模拟setInterval
效果实现页面中的时钟每秒刷新document.body.appendChild(document.createElement('div')) function getTime() { document.querySelector('div').innerHTML = new Date().toLocaleString() setTimeout(getTime, 1000) } getTime()
-
基于递归方法的深拷贝案例:
const o1 = { a: 1, b: { c: 2 } } const o2 = {} function deepCopy(newObj, oldObj) { // 遍历oldObj中的所有属性 for (let key in oldObj) { // 如果属性值是对象,则递归进行深度复制 if (typeof oldObj[key] === 'object') { newObj[key] = {} // 为newObj创建一个空对象作为属性值 deepCopy(newObj[key], oldObj[key]) // 递归调用deepCopy函数复制属性值中的所有属性 } else { // 如果属性值不是对象,直接复制 newObj[key] = oldObj[key] } } } deepCopy(o2, o1) console.log(o2) // { a: 1, b: { c: 2 } } o2.b.c = 3 console.log(o1) // { a: 1, b: { c: 2 } } console.log(o2) // { a: 1, b: { c: 3 } }
b. Lodash
- Lodash 是第三方 JS 库,官网链接
- 引入 Lodash:https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
const o1 = {
a: 1,
b: {
c: 2
}
}
const o2 = _.cloneDeep(o1)
console.log(o2)
c. JSON 方法
const o1 = {
a: 1,
b: {
c: 2
}
}
const o2 = JSON.parse(JSON.stringify(o1))
console.log(o2)
0x02 异常处理
- 定义:预估代码运行过程中可能发生的错误,使用特定的方法对这些错误进行合适的处理
- 意义:有助于提高代码健壮性
(1)throw 抛出异常
function division(x, y) {
if(!x || !y) {
throw "The parameter cannot be empty"
}
if(y === 0) {
throw new Error("Divisor cannot be zero")
}
}
division()
throw
抛出异常信息,程序也会中止Error
对象常配合throw
使用,能够设置更详细的错误消息
(2)try...catch 捕获异常
document.body.appendChild(document.createElement('p'))
function fun() {
try {
// Correct: document.querySelector('p').style.color = 'red'
document.querySelector('.p').style.color = 'red'
} catch (e) {
console.log("Catch a error: ", e.message)
} finally {
console.log("Finally")
}
}
fun()
- 用于捕获错误信息
try
中写入可能会发生错误的代码catch
中写入捕获错误后的处理finally
中写入无论是否出错都会执行的代码
(3)debugger
const btn = document.createElement('button')
btn.onclick = function () {
debugger
}
btn.textContent = 'Debug'
document.body.appendChild(btn)
- 使用
debugger
可以进入逐步调试
0x03 处理 this
(1)this 指向
a. 普通函数
-
普通函数的调用方式决定了
this
的值const obj = { a: function fun() { console.log(this); } } obj.a() // {a:f}
-
没有明确的调用方法时,
this
的值为window
function fun() { console.log(this) // [object Window] } fun()
-
严格模式下没有明确的调用方法时,
this
的值为undefined
'use strict' function fun() { console.log(this) // undefined } fun()
b. 箭头函数
-
箭头函数不存在
this
- 箭头函数会默认绑定外层
this
的值 - 箭头函数中的
this
引用的是最近作用域中的this
- 箭头函数会向外层作用域中一层层查找
this
,直至找到有this
的定义
const obj = { a: () => { console.log(this) } } obj.a() // [object Window]
- 箭头函数会默认绑定外层
-
在开发中,使用箭头函数前需要考虑函数中
this
的值const btn = document.createElement("button") btn.textContent = "Click" btn.addEventListener("click", function() { console.log(this) // <button>Click</button> }) btn.addEventListener("click", () => { console.log(this) // [object Window] }) document.body.appendChild(btn)
-
基于原型的面向对象不推荐采用箭头函数
function Obj() {} Obj.prototype.a = () => { console.log(this) } const obj = new Obj() obj.a() // [object Window]
(2)改变 this
- 有三个方法可以动态指定普通函数中
this
的指向
a. call()
-
语法:
call(thisArg, arg1, arg2, ...)
thisArg
:在函数运行时,指定this
的值arg1, arg2, ...
:传参- 返回值就是函数的返回值
-
举例
const obj = { a: 0 } function fun(x, y) { console.log(x, y, this) // 1 2 {a: 0} } fun.call(obj, 1, 2)
b. apply()
-
语法:
fun.apply(thisArg, [argsArray])
thisArg
:在函数运行时,指定this
的值argsArray
:传参,必须包含在数组里面- 返回值就是函数的返回值
- 因此
apply
主要跟数组有关系
-
举例
function sum(x, y) { console.log(this) // [object Window] return x + y } console.log(sum.apply(null, [1, 2])) // 3 console.log(Math.max.apply(Math, [1, 2, 3])) // 3
c. bind()
-
bind
方法不会调用函数,但是也可以改变函数内部的this
指向 -
语法:
bind(thisArg, arg1, arg2, ...)
thisArg
:在函数运行时,指定this
的值arg1, arg2, ...
:传参,必须包含在数组里面- 返回值由指定的
this
值和初始化参数改造的原函数拷贝
-
举例
const obj = { a: 0 } function fun(x, y) { console.log(x, y, this) } fun.bind(obj, 1, 2)() // 1 2 {a: 0}
0x04 性能优化
(1)防抖
-
防抖(debounce):单位时间内,频繁触发事件,只执行最后一次
- 触发事件后,在 \(n\) 秒内函数只能执行一次,如果在 \(n\) 秒内再次被触发,则重新计算函数执行时间
-
使用场景:常用于输入事件的处理中,以减少不必要的计算或操作
-
举例:鼠标在盒子上移动,每 500ms 盒内数字加一
const box = document.createElement('div') document.body.appendChild(box) let cnt = 1 function add() { box.innerHTML = cnt++ } function debounce(func, timeMs) { let timer return function () { if (timer) clearTimeout(timer) timer = setTimeout(function () { func() }, timeMs) } } box.addEventListener("mousemove", debounce(add, 500))
-
防抖函数的封装说明
/** * @param {Function} func 要执行的函数。 * @param {number} timeMs 延迟的时间,单位为毫秒。 * @returns {Function} 返回一个新的函数,该函数具有防抖功能。 */ function debounce(func, timeMs) { let timer // 用于存储定时器的变量 // 返回一个新的函数,该函数会延迟执行传入的func函数 return function () { if (timer) clearTimeout(timer) // 如果存在定时器,则清除,以防止之前设定的执行被触发 // 设定一个新的定时器,当延迟时间过去后,执行func函数 timer = setTimeout(function () { func() }, timeMs) } }
(2)节流
-
节流(throttle):单位时间内,频繁触发事件,只执行一次
- 连续触发事件,但在 \(n\) 秒内仅执行一次函数
-
使用场景:常用于高频事件的处理中,以减少不必要的性能消耗
-
举例:鼠标在盒子上移动,每隔 500ms 盒内数字加一
const box = document.createElement('div') document.body.appendChild(box) let cnt = 1 function add() { box.innerHTML = cnt++ } function throttle(func, timeMs) { let timer = null return function () { if (!timer) { timer = setTimeout(function() { func() timer = null }, timeMs) } } } box.addEventListener("mousemove", throttle(add, 500))
-
节流函数封装说明
/** * @param {Function} func 要节流的函数 * @param {number} timeMs 节流的时间间隔(毫秒) * @returns {Function} 返回一个新函数,新函数将控制原函数在指定时间间隔内只执行一次 */ function throttle(func, timeMs) { let timer = null // 利用闭包保存一个定时器变量 return function () { // 如果定时器不存在,则设置定时器 if (!timer) { timer = setTimeout(function() { func() // 在指定时间间隔后执行原函数 timer = null // 执行后重置定时器变量 }, timeMs) } } }
-End-