【JS进阶-Day4】深浅拷贝、this指向与防抖节流

3 阅读8分钟

【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入门