【JS进阶-Day4】深浅拷贝、this指向与防抖节流
📺 对应视频:P188-P200 | 🎯 核心目标:掌握深浅拷贝实现、异常处理、call/apply/bind改变this、防抖节流原理与应用
一、深浅拷贝
1.1 为什么需要拷贝?
// 引用类型赋值是复制地址,修改会影响原对象
const obj = { name: '张三', info: { city: '北京' } }
const copy = obj // 只是复制了引用
copy.name = '李四'
console.log(obj.name) // '李四'(obj 也变了!)
1.2 浅拷贝
只复制第一层,嵌套的引用类型仍然共享。
const original = { a: 1, b: { c: 2 } }
// 方式一:展开运算符(最常用)
const clone1 = { ...original }
// 方式二:Object.assign
const clone2 = Object.assign({}, original)
// 方式三:手动实现
function shallowClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj
const result = Array.isArray(obj) ? [] : {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = obj[key] // 直接复制(引用类型只复制地址)
}
}
return result
}
// 验证浅拷贝
clone1.a = 99 // 不影响 original.a
clone1.b.c = 99 // ⚠️ 影响 original.b.c!(共享引用)
console.log(original.b.c) // 99
1.3 深拷贝
完全独立的副本,包括所有嵌套层级。
const original = { a: 1, b: { c: 2 }, arr: [1, 2, 3] }
// 方式一:JSON 序列化(简单,有限制)
const clone1 = JSON.parse(JSON.stringify(original))
// ❌ 局限性:
// - 不能拷贝函数(丢失)
// - 不能拷贝 undefined(丢失)
// - 不能拷贝 Symbol(丢失)
// - 不能拷贝循环引用(报错)
// - 不能拷贝 Date(转为字符串)
// - 不能拷贝 RegExp(转为 {})
// 方式二:structuredClone(现代方法,推荐!ES2022)
const clone2 = structuredClone(original)
// ✅ 支持:Date、RegExp、Map、Set、循环引用
// ❌ 仍不支持:函数、DOM节点、Symbol
// 方式三:递归手动实现(全能版)
function deepClone(obj, map = new WeakMap()) {
// 处理基本类型和 null
if (typeof obj !== 'object' || obj === null) return obj
// 处理特殊类型
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (obj instanceof Map) return new Map([...obj].map(([k, v]) => [deepClone(k, map), deepClone(v, map)]))
if (obj instanceof Set) return new Set([...obj].map(v => deepClone(v, map)))
// 处理循环引用
if (map.has(obj)) return map.get(obj)
const result = Array.isArray(obj) ? [] : {}
map.set(obj, result) // 先记录,防止循环引用
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key], map)
}
}
return result
}
// 测试
const deep = deepClone(original)
deep.b.c = 99
console.log(original.b.c) // 2(互不影响!)
二、异常处理
2.1 try...catch...finally
try {
// 可能出错的代码
const data = JSON.parse('invalid json')
console.log(data.name.toUpperCase())
} catch (error) {
// 捕获并处理错误
console.error('出错了:', error.message)
console.error('错误类型:', error.name) // SyntaxError / TypeError / ReferenceError...
} finally {
// 无论是否出错都会执行(常用于清理资源)
console.log('执行完毕')
}
2.2 错误类型
// ReferenceError:引用未声明的变量
undeclaredVar // Uncaught ReferenceError: undeclaredVar is not defined
// TypeError:对不合适的类型执行操作
null.property // Uncaught TypeError: Cannot read properties of null
// SyntaxError:语法错误(代码不合法,解析时报错)
JSON.parse('invalid') // SyntaxError
// RangeError:数值超出有效范围
new Array(-1) // RangeError: Invalid array length
// 自定义错误
throw new Error('自定义错误信息')
throw new TypeError('类型不对')
// 自定义错误类
class ValidationError extends Error {
constructor(message, field) {
super(message)
this.name = 'ValidationError'
this.field = field
}
}
throw new ValidationError('邮箱格式不正确', 'email')
2.3 实际应用
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new Error(`HTTP错误:${response.status}`)
}
const user = await response.json()
return user
} catch (error) {
if (error instanceof TypeError) {
console.error('网络错误,请检查网络连接')
} else {
console.error('获取用户失败:', error.message)
}
return null
}
}
三、this 指向
3.1 this 的四种绑定规则
// 1. 默认绑定(独立调用)
function fn() { console.log(this) }
fn() // 非严格模式:window | 严格模式:undefined
// 2. 隐式绑定(方法调用)
const obj = {
name: '张三',
greet() { console.log(this.name) }
}
obj.greet() // '张三'(this = obj)
// 3. 显式绑定(call/apply/bind)
function greet(greeting) {
console.log(`${greeting},我是${this.name}`)
}
greet.call({ name: '张三' }, '你好') // 你好,我是张三
greet.apply({ name: '李四' }, ['嗨']) // 嗨,我是李四
const boundGreet = greet.bind({ name: '王五' })
boundGreet('Hi') // Hi,我是王五
// 4. new 绑定(构造函数调用)
function Person(name) {
this.name = name // this = 新创建的实例
}
const p = new Person('张三') // this = p
3.2 call / apply / bind 详解
function introduce(greeting, punctuation) {
return `${greeting},我叫${this.name}${punctuation}`
}
const person = { name: '张三' }
// call:立即调用,参数逐个传
introduce.call(person, '你好', '!') // '你好,我叫张三!'
// apply:立即调用,参数以数组传
introduce.apply(person, ['嗨', '~']) // '嗨,我叫张三~'
// bind:返回绑定了 this 的新函数,不立即调用
const boundFn = introduce.bind(person, '哈喽') // 偏应用
boundFn('。') // '哈喽,我叫张三。'
// 实际应用
// 借用 Array 方法处理类数组
const nodeList = document.querySelectorAll('div')
Array.prototype.forEach.call(nodeList, el => { ... })
// 或:[...nodeList].forEach(el => { ... })
// Math.max 应用
const arr = [1, 5, 3, 2, 8]
Math.max.apply(null, arr) // 8(旧写法)
Math.max(...arr) // 8(ES6 展开,推荐)
3.3 箭头函数的 this
// 箭头函数没有自己的 this,继承定义时所在的外层 this
class Button {
constructor() {
this.count = 0
this.el = document.querySelector('button')
// 问题:普通函数的 this 丢失
this.el.addEventListener('click', function() {
this.count++ // ❌ this 是 el,不是 Button 实例
})
// 解决1:箭头函数
this.el.addEventListener('click', () => {
this.count++ // ✅ this 继承外层(Button 实例)
})
// 解决2:bind
this.el.addEventListener('click', function() {
this.count++
}.bind(this)) // ✅ 显式绑定 this
}
}
四、防抖(Debounce)
4.1 什么是防抖?
防抖:事件频繁触发时,只有最后一次触发后等待指定时间内没有再次触发,才执行。
类比:电梯关门——只要有人进来,就重新计时,最后一个人进来后等3秒才关门。
4.2 手动实现
function debounce(fn, delay) {
let timer = null
return function(...args) {
clearTimeout(timer) // 每次触发都清掉上一次的定时器
timer = setTimeout(() => {
fn.apply(this, args) // 等 delay 毫秒后执行
}, delay)
}
}
// 使用
const handleSearch = debounce(function(e) {
console.log('搜索:', e.target.value)
// 发起 API 请求
}, 500)
document.querySelector('#search').addEventListener('input', handleSearch)
// 用户停止输入 500ms 后才触发搜索
4.3 立即执行版防抖
function debounce(fn, delay, immediate = false) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
if (immediate && !timer) {
fn.apply(this, args) // 第一次立即执行
}
timer = setTimeout(() => {
timer = null
if (!immediate) fn.apply(this, args)
}, delay)
}
}
五、节流(Throttle)
5.1 什么是节流?
节流:在规定时间内,无论触发多少次,只执行一次。
类比:地铁进站——每5分钟发一班车,不管有多少人等,5分钟到了就走。
5.2 手动实现(定时器版)
function throttle(fn, interval) {
let timer = null
return function(...args) {
if (timer) return // 定时器存在,忽略本次触发
timer = setTimeout(() => {
fn.apply(this, args)
timer = null // 执行后清空,允许下次触发
}, interval)
}
}
5.3 时间戳版节流
function throttle(fn, interval) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= interval) {
lastTime = now
fn.apply(this, args)
}
}
}
// 使用
const handleScroll = throttle(() => {
console.log('滚动位置:', window.scrollY)
}, 100) // 每100ms最多执行一次
window.addEventListener('scroll', handleScroll)
5.4 防抖 vs 节流对比
场景 推荐
搜索框联想(输入后发请求) 防抖(只要最后一次)
提交按钮防重复点击 防抖(最后一次点击有效)
滚动事件更新状态 节流(保持频率执行)
窗口 resize 重布局 节流(保持频率执行)
鼠标移动绘图 节流(控制帧率)
六、综合知识图谱
深浅拷贝、this、防抖节流
├── 拷贝
│ ├── 浅拷贝:{...obj} / Object.assign(共享嵌套引用)
│ └── 深拷贝:structuredClone / 递归实现(完全独立)
├── 异常处理
│ ├── try/catch/finally
│ ├── 错误类型:ReferenceError/TypeError/SyntaxError
│ └── 自定义错误:extends Error
├── this 指向
│ ├── 默认:undefined(严格)/ window(非严格)
│ ├── 隐式:方法的调用者
│ ├── 显式:call/apply/bind
│ └── new:新创建的实例
├── call / apply / bind
│ ├── call:立即调用,参数逐个传
│ ├── apply:立即调用,参数数组传
│ └── bind:返回新函数,不立即调用
└── 防抖 & 节流
├── 防抖:停止触发后等待delay才执行(搜索、表单提交)
└── 节流:固定时间间隔最多执行一次(滚动、resize)
七、高频面试题
Q1:深拷贝和浅拷贝的区别?如何实现深拷贝?
浅拷贝只复制一层,嵌套对象仍然共享引用;深拷贝创建完全独立的副本。实现深拷贝:①
JSON.parse(JSON.stringify(obj))(简单,不支持函数/Date/循环引用);②structuredClone(obj)(现代,推荐,不支持函数);③ 递归手动实现(全能)。
Q2:call、apply、bind 的区别?
三者都能改变
this指向:call立即调用,参数逐个传(fn.call(obj, a, b));apply立即调用,参数以数组传(fn.apply(obj, [a, b]));bind返回绑定了 this 的新函数,不立即执行,可以偏应用。
Q3:防抖和节流的区别及应用场景?
防抖:频繁触发时,只有停止触发后等待指定时间才执行(适合搜索联想、表单验证、按钮防重复提交);节流:规定时间内无论触发多少次只执行一次(适合滚动事件、窗口resize、鼠标移动)。
Q4:如何确保 this 指向正确?
① 箭头函数(继承外层 this,最常用);②
.bind(this)显式绑定;③ 在构造函数中用const self = this(老方式);④ Class 中方法自动绑定(用装饰器或在构造函数中 bind)。
八、本系列完结
恭喜你学完了全部 200 集 JavaScript 课程!🎉
学习路线回顾:
JS 基础篇(Day1-5)
├── 变量、数据类型、类型转换
├── 运算符、分支、循环
├── 数组操作、排序算法
├── 函数、作用域、闭包初探
└── 对象、Math、引用类型
Web APIs 篇(Day1-7)
├── DOM 树、元素获取与操作
├── 事件监听、事件对象
├── 事件流、事件委托、页面交互
├── 节点操作、日期对象、Swiper
├── BOM、事件循环、本地存储
└── 正则表达式、综合实战
JS 进阶篇(Day1-4)
├── 作用域链、垃圾回收、闭包
├── 构造函数、高阶数组方法
├── 原型链、Class 继承
└── 深浅拷贝、this、防抖节流
下一步推荐:
- 📚 ES6+ 深入:Promise/async/await、模块化、Generator
- 📚 框架学习:Vue3 或 React(基于以上知识)
- 📚 工程化:Webpack/Vite、TypeScript
- 📚 项目实战:用所学知识做一个完整项目
⬅️ 上一篇:JS进阶Day3 - 原型与原型链 🏠 系列首篇:JS基础Day1 - JS入门