JavaScript手录06-函数
函数是 JavaScript 中核心的编程概念,它允许我们封装可重用的代码块,实现逻辑模块化和复用。以下是 JavaScript 函数的核心知识:
一、函数的基本概念
函数是一段具有特定功能的代码集合,能够接收输入(参数)、执行操作并返回输出(返回值)。
核心作用:
- 代码复用:避免重复编写相同逻辑
- 逻辑抽象:隐藏实现细节,只暴露调用接口
- 流程控制:将复杂任务分解为多个函数,使代码结构清晰
二、函数的定义方式
JavaScript 提供了多种定义函数的方式,各有特点:
1. 函数声明(Function Declaration)
// 语法:function 函数名(参数列表) { 函数体 }
function add(a, b) {return a + b; // return 语句返回结果并终止函数
}
特点:
- 存在函数提升:可以在声明之前调用(解析器会优先读取函数声明)
- 必须指定函数名,通过函数名直接调用
2. 函数表达式(Function Expression)
// 语法:const 变量名 = function(参数列表) { 函数体 }
const multiply = function(a, b) {return a * b;
};
特点:
- 赋值给变量,通过变量名调用
- 不存在函数提升:必须在声明后调用
- 可以是匿名函数(省略函数名)
3. 箭头函数(Arrow Function,ES6+)
// 完整语法
const subtract = (a, b) => {return a - b;
};// 简化语法(单表达式可省略 {} 和 return)
const divide = (a, b) => a / b;
特点:
- 语法简洁,适合短逻辑函数
- 没有自己的
this
(继承外层作用域的this
) - 不能用作构造函数(不能用
new
调用) - 没有
arguments
对象
4. 构造函数方式(不推荐)
通过 Function
构造函数创建函数(可读性差,几乎不用):
const power = new Function('base', 'exponent', 'return base ** exponent');
三、函数的参数
函数参数是函数与外部交互的入口,分为形参(定义时声明)和实参(调用时传入)。
1. 基本用法
// 形参:a 和 b
function sum(a, b) {return a + b;
}// 实参:2 和 3
console.log(sum(2, 3)); // 输出:5
2. 参数特性
参数数量不匹配时:
- 实参少于形参:多余形参值为
undefined
- 实参多于形参:多余实参可通过
arguments
或剩余参数获取
默认参数(ES6+):为形参设置默认值,当实参未传入时使用
function greet(name = 'Guest') {console.log(`Hello, ${name}`);
}
greet(); // 输出:Hello, Guest
剩余参数(ES6+):用 ...
收集多余实参为数组
function sumAll(...numbers) { // numbers 是数组return numbers.reduce((total, num) => total + num, 0);
}
console.log(sumAll(1, 2, 3)); // 输出:6
四、函数的调用方式
函数定义后需调用才能执行,不同调用方式会影响 this
指向:
1. 普通调用
function hello() {console.log('Hello');
}
hello(); // 直接调用,this 指向全局对象(浏览器中是 window)
2. 作为对象方法调用
const obj = {name: '张三',sayName() {console.log(this.name); // this 指向当前对象 obj}
};
obj.sayName(); // 输出:张三
3. 构造函数调用(new
关键字)
function Person(name) {this.name = name; // this 指向新创建的实例
}
const person = new Person('李四'); // 创建实例
4. 间接调用(改变 this
指向)
通过 call()
、apply()
、bind()
手动指定 this
:
function introduce() {console.log(`我是${this.name}`);
}
const user = { name: '王五' };introduce.call(user); // 输出:我是王五(this 指向 user)
五、函数的返回值
- 通过
return
语句返回结果,return
后函数立即终止 - 若没有
return
或return
后无值,函数默认返回undefined
function isEven(num) {if (num % 2 === 0) {return true; // 返回布尔值}return false; // 函数执行到此处终止
}
六、作用域
6.1 作用域(Scope)
在JavaScript中,作用域指的是**变量和函数可访问的范围**,它决定了代码中变量的可见性和生命周期。
JavaScript 的作用域是**静态作用域**(词法作用域),即变量的作用域在代码编写时就已确定,而非运行时。
1. 作用域的类型
JavaScript 中有三种主要的作用域类型:
(1)全局作用域(Global Scope)
- 定义:在所有函数外部声明的变量,或未声明直接赋值的变量(不推荐)。
- 特点:在整个脚本中都可访问,生命周期与页面一致(页面关闭前一直存在)。
// 全局变量
const globalVar = "我是全局变量";function test() {console.log(globalVar); // 可访问全局变量
}test(); // 输出:我是全局变量
(2)函数作用域(Function Scope)
- 定义:在函数内部声明的变量(包括函数参数)。
- 特点:仅在当前函数内部可访问,函数执行结束后变量会被销毁(闭包除外)。
function test() {// 函数内变量(函数作用域)const funcVar = "我是函数内变量";console.log(funcVar); // 可访问
}test();
console.log(funcVar); // 报错:funcVar 未定义(外部不可访问)
(3)块级作用域(Block Scope,ES6+)
- 定义:在代码块(
{}
)中通过let
或const
声明的变量(如if
、for
、while
的代码块)。 - 特点:仅在当前代码块内可访问,块执行结束后变量销毁。
if (true) {// 块级变量let blockVar = "我是块级变量";console.log(blockVar); // 可访问
}console.log(blockVar); // 报错:blockVar 未定义(块外部不可访问)
注意:var
声明的变量没有块级作用域,仅受函数作用域限制。
补充1:函数(方法)、语句、表达式
类别 | 定义 | 示例 | 区别 |
---|---|---|---|
表达式 | 等效于值的代码片段,一定有返回值。 反过来说,没有返回值的代码片段一定不是表达式。 | 1+2 //返回3 user.name //返回属性值 fn(1,2,3) // 返回函数调用结果 | 核心目的:产生值 有返回值 等效于值使用 |
语句 | 执行操作的指令,控制程序流程,没有返回值或者返回值为undefined。 | if语句 循环语句for while var a = 3 变量声明语句 | 执行操作 没有返回值或者返回值为undefined 独立存在(控制流程) |
函数(方法) | 用于封装可复用的代码块,可以接收参数并返回结果,本质上也可以算是一种特殊的值。 | function add(a,b){ return a+b} | 封装逻辑,可以调用 调用时可能有返回值 作为工具被调用 |
补充2:变量提升
变量提升是 JavaScript 解析器的一种特性:在代码执行前,解析器会将变量和函数的声明 “提升” 到其所在作用域的顶部,但赋值操作仍保留在原地。这意味着可以在声明前使用变量或函数。
- 变量提升的表现
- var 声明的变量:声明会被提升,赋值留在原地(未赋值时为 undefined)。在声明前使用不会报错,且值为undefined。
console.log(a); // 输出 undefined(声明被提升,赋值未执行)
var a = 10;
console.log(a); // 输出 10
- let/const 声明的变量:声明也会被提升,但存在 “暂时性死区”(TDZ),在声明前使用会报错。
console.log(b); // 报错:Cannot access 'b' before initialization
let b = 20;
- 函数声明:整个函数体都会被提升(可以在声明前调用)。
foo(); // 输出 "执行foo"(函数声明被完全提升)
function foo() {console.log("执行foo");
}
- 变量提升的本质
JavaScript 代码执行分为解析阶段和执行阶段:
- 解析阶段:扫描代码,将变量和函数声明添加到作用域中(提升)。
- 执行阶段:按顺序执行代码,处理赋值和函数调用。
提升仅提升 “声明”,不提升 “赋值”,这是理解变量提升的关键。
补充3:函数声明与函数表达式
特征 | 函数声明 | 函数表达式 |
---|---|---|
语法 | function 函数名字(参数) { ... } | const 变量名 = function(参数){ ... }; // 函数匿名或具名 |
提升行为 | 整个函数体被提升,可以在声明前调用,不会报错。 | 相当于将函数赋值给变量,因此仅变量声明被提升。此时如果在声明前调用会报错。 |
函数名 | 通过function声明的函数必须具名,通过函数名调用。 | 可以匿名,通过变量名调用。 |
语境 | 作为独立语句存在,不能进行赋值、参数传递等操作。 | 作为表达式的一部分存在,可以进行赋值、参数传递等操作。 |
2. 作用域链(Scope Chain)
当访问一个变量时,JavaScript 会按以下规则查找:
- 先在当前作用域中查找,找到则使用。
- 若未找到,向上查找父级作用域。
- 依次向上,直到全局作用域。若仍未找到,则认为该变量未定义(报错)。
这种层级查找关系称为作用域链。
// 全局作用域
const globalVar = "全局";function outer() {// 外层函数作用域const outerVar = "外层";function inner() {// 内层函数作用域const innerVar = "内层";// 查找变量:当前作用域 → 外层 → 全局console.log(innerVar); // 内层(当前作用域)console.log(outerVar); // 外层(父级作用域)console.log(globalVar); // 全局(顶级作用域)}inner();
}outer();
6.2 闭包(Closure)
闭包是 JavaScript 中最强大也最易混淆的特性之一,它与作用域紧密相关。
1. 闭包的定义
闭包指的是有权访问另一个函数作用域中变量的函数。通常是在一个函数内部定义另一个函数,内层函数引用外层函数的变量,即使外层函数执行完毕,这些变量仍会被保留。
function outer() {const outerVar = "外层变量"; // 外层函数的变量// 内层函数(闭包)function inner() {console.log(outerVar); // 引用外层函数的变量}return inner; // 返回内层函数
}// 外层函数执行后,返回内层函数
const closureFunc = outer();
closureFunc(); // 输出:外层变量(外层函数已执行,但变量仍被访问)
核心表现:外层函数执行完毕后,其作用域并未被销毁,因为内层函数(闭包)仍在引用其中的变量。
2. 闭包的形成条件
- 嵌套函数:存在函数内部定义的内层函数。
- 变量引用:内层函数引用外层函数的变量或参数。
- 外部访问:内层函数被返回到外层函数外部并被调用。
3. 闭包的实际应用场景
- 封装私有变量
JavaScript 没有原生的私有变量机制,但可通过闭包模拟:
function createCounter() {let count = 0; // 私有变量(外部无法直接访问)return {increment() { count++; },decrement() { count--; },getCount() { return count; }};
}const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 输出:2(只能通过暴露的方法访问 count)
console.log(counter.count); // 输出:undefined(直接访问私有变量失败)
- 延迟执行与保存状态
闭包可保存函数执行时的状态,常用于定时器、事件处理等场景:
function delayLog(message, delay) {setTimeout(function() {// 闭包引用了外层函数的 message 变量console.log(message);}, delay);
}delayLog("3秒后执行", 3000); // 3秒后输出:3秒后执行
- 模块化开发
通过闭包隔离不同模块的作用域,避免全局变量污染:
const module = (function() {// 模块内部私有变量const privateVar = "私有数据";// 暴露公共方法return {getPrivate() { return privateVar; },setPrivate(val) { /* 验证逻辑 */ }};
})();console.log(module.getPrivate()); // 输出:私有数据(通过公共接口访问)
4. 闭包的注意事项
- 内存占用问题
闭包会保留外层函数的作用域,导致变量不会被垃圾回收机制回收,过度使用可能造成内存泄漏。
解决:不再使用闭包时,手动解除引用(如赋值为 null
)。
let closureFunc = outer();
closureFunc();
closureFunc = null; // 解除引用,释放内存
- 循环中的闭包陷阱
经典问题:循环中创建的闭包可能共享同一变量,导致结果不符合预期。
// 问题代码
for (var i = 0; i < 3; i++) {setTimeout(function() {console.log(i); // 输出:3 3 3(所有闭包共享同一个 i)}, 1000);
}// 解决:使用 let 块级作用域,或立即执行函数
for (let i = 0; i < 3; i++) { // let 使每次循环的 i 是独立的setTimeout(function() {console.log(i); // 输出:0 1 2}, 1000);
}
6.3 作用域与闭包的关系
- 作用域是闭包的基础:闭包依赖于作用域链实现对外部变量的访问。
- 闭包是作用域的延伸:即使外层作用域已结束,闭包仍能维持对其变量的引用。
- 通俗理解:作用域规定了“变量在哪里可用”,闭包则实现了“即使作用域结束,变量仍可被访问”。
补充1 JavaScript中的垃圾回收机制(Garbage Collection)
JavaScript 具有自动垃圾回收机制,其核心目的是自动释放不再被使用的内存空间,避免内存泄漏(内存溢出)。开发者无需手动管理内存,由 JavaScript 引擎(如 V8)自动完成。
1. 垃圾回收的核心原理
垃圾回收的本质是:找出不再被引用的变量/对象,释放其占用的内存。
判断一个值是否“不再被使用”的核心标准是:该值是否还存在有效的引用。
- 如果一个值没有任何变量或对象引用它,它就是“垃圾”,会被回收。
- 如果仍有引用指向它,则会被保留在内存中。
2. 常见的垃圾回收算法
JavaScript 引擎主要使用两种算法:
(1)标记-清除算法(Mark-and-Sweep,最常用)
这是现代 JavaScript 引擎最主流的算法,分为两个阶段:
- 标记阶段:从全局对象(如
window
或global
)开始,遍历所有可访问的变量/对象,为它们打上“被引用”的标记。 - 清除阶段:遍历内存中所有值,清除没有被标记的对象(即无法访问的对象),释放其内存。
优点:能处理循环引用(如两个对象互相引用但均无外部引用的情况)。
(2)引用计数算法(Reference Counting,较少使用)
通过记录每个值被引用的次数来判断是否回收:
- 当一个值被引用时,引用计数 +1。
- 当引用消失时,引用计数 -1。
- 当引用计数为 0 时,释放内存。
缺点:无法解决循环引用问题(如 a.b = b; b.a = a;
会导致两者引用计数永远不为 0,内存无法释放),现代引擎已基本淘汰。
3. 垃圾回收的触发时机
垃圾回收是自动且周期性的:
- 引擎会在内存占用达到一定阈值时自动触发。
- 开发者无法直接调用垃圾回收,但可以通过解除引用(如赋值
<font style="background-color:#FBDE28;">null</font>
)帮助引擎识别垃圾。
补充2 为什么闭包中引用的变量不会被垃圾回收机制回收?
闭包中引用的变量之所以不会被回收,核心原因是**这些变量仍存在有效的引用**,不符合垃圾回收的“无引用”标准。
1. 闭包中变量的引用链分析
当内层函数(闭包)引用外层函数的变量时,会形成一条持久的引用链:
- 外层函数执行时,创建其作用域及内部变量。
- 内层函数(闭包)引用外层变量,并被返回到外层函数外部。
- 外部变量(如接收闭包的变量)引用该闭包,闭包又引用外层变量,形成“外部变量 → 闭包 → 外层变量”的引用链。
示例:
function outer() {const outerVar = "我是外层变量"; // 外层变量function inner() {console.log(outerVar); // 闭包引用外层变量}return inner; // 闭包被返回到外部
}const closureFunc = outer(); // 外部变量引用闭包
closureFunc(); // 执行闭包时仍能访问 outerVar
此时,outerVar
被 inner
(闭包)引用,inner
被 closureFunc
引用,closureFunc
是全局变量——整个引用链完整,outerVar
仍被有效引用,因此不会被回收。
2. 何时闭包中的变量会被回收?
只有当闭包本身不再被引用时,其引用的外层变量才会失去所有引用,进而被回收:
// 接上面的例子
closureFunc = null; // 解除对闭包的引用// 此时:closureFunc → inner(闭包)的引用消失
// inner → outerVar 的引用也随之失效
// outerVar 不再被任何对象引用,会被垃圾回收
七、函数的其他特性
1. 函数是一等公民
函数可以作为值赋值给变量、作为参数传递、作为返回值返回:
// 函数作为参数
function execute(func, a, b) {return func(a, b);
}
execute(add, 2, 3); // 输出:5(传递 add 函数)
2. 匿名函数
没有名称的函数,常用于临时场景(如回调函数):
// 匿名函数作为定时器回调
setTimeout(function() {console.log('1秒后执行');
}, 1000);
3. 立即执行函数(IIFE)
定义后立即执行,用于创建独立作用域:
(function() {const privateVar = '私有变量'; // 外部无法访问console.log(privateVar);
})();
拓展、闭包的实际应用场景
闭包在实际开发中应用非常广泛,其核心价值在于保留作用域和私有变量,以下列举一些典型的实际应用场景:
模块化与私有变量封装
JavaScript 没有原生私有变量机制,闭包可以模拟“私有成员”,实现变量的封装和保护,避免全局污染。
// 计数器模块(闭包实现私有变量)
const createCounter = () => {let count = 0; // 私有变量(外部无法直接访问)return {increment: () => count++, // 闭包:访问私有变量decrement: () => count--,getCount: () => count};
};// 使用模块
const counter1 = createCounter();
counter1.increment();
counter1.increment();
console.log(counter1.getCount()); // 2
console.log(counter1.count); // undefined(无法直接访问私有变量)// 多个实例互不干扰(各自的闭包保存独立状态)
const counter2 = createCounter();
counter2.increment();
console.log(counter2.getCount()); // 1
核心价值:通过闭包隔离不同实例的状态,仅暴露有限接口,保证数据安全性。
防抖与节流(性能优化)
在处理高频事件(如滚动、输入、窗口resize)时,闭包可保存定时器状态,避免函数频繁执行。
防抖(Debounce)
触发事件后延迟n秒执行,如果n秒内再次触发则重新计时(适用于搜索输入、表单提交)。
const debounce = (fn, delay = 300) => {let timer = null; // 闭包保存定时器IDreturn (...args) => {clearTimeout(timer); // 每次触发清除之前的定时器timer = setTimeout(() => {fn.apply(this, args); // 延迟执行原函数}, delay);};
};// 应用:搜索输入联想
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {console.log('搜索:', e.target.value); // 输入停止300ms后执行
}, 500));
节流(Throttle)
每隔n秒最多执行一次函数(适用于滚动加载、拖拽)。
const throttle = (fn, interval = 1000) => {let lastTime = 0; // 闭包保存上次执行时间return (...args) => {const now = Date.now();if (now - lastTime >= interval) { // 间隔达标才执行fn.apply(this, args);lastTime = now;}};
};// 应用:滚动监听
window.addEventListener('scroll', throttle(() => {console.log('滚动位置:', window.scrollY); // 每1秒最多执行一次
}, 1000));
函数柯里化(参数复用)
将多参数函数转化为单参数函数的链式调用,通过闭包保存已传入的参数。
// 柯里化函数:拼接API地址
const curryApi = (baseUrl) => {return (path) => {return (params) => {return `${baseUrl}${path}?${new URLSearchParams(params)}`;};};
};// 复用基础地址
const api = curryApi('https://api.example.com');
const userApi = api('/user'); // 闭包保存 baseUrl// 后续调用只需传入变化的参数
console.log(userApi({ id: 1 }));
// 输出: https://api.example.com/user?id=1
console.log(userApi({ name: 'test' }));
// 输出: https://api.example.com/user?name=test
核心价值:固定部分参数,简化重复调用,提高代码复用性。
延迟执行与状态保存
闭包可以“记住”函数创建时的环境状态,常用于定时器、事件回调等场景。
// 延迟打印消息(保存当前消息状态)
const delayLog = (message, delay) => {setTimeout(() => {console.log(message); // 闭包引用外部 message}, delay);
};delayLog('3秒后执行', 3000); // 3秒后输出:3秒后执行
核心价值:即使外部函数已执行完毕,闭包仍能保留对原始变量的引用。
React Hooks 底层原理
React 的 useState
、useEffect
等 Hooks 本质上依赖闭包保存组件状态。
// 简易模拟 useState(闭包保存状态)
let state; // 闭包变量保存状态
const useState = (initialValue) => {state = state ?? initialValue; // 首次初始化const setState = (newValue) => {state = newValue;// 模拟重新渲染render();};return [state, setState];
};// 组件中使用
const render = () => {const [count, setCount] = useState(0);console.log('当前计数:', count);
};render(); // 输出: 当前计数: 0
setCount(1); // 触发重新渲染,输出: 当前计数: 1
核心价值:在无类组件的情况下,通过闭包跨渲染周期保存状态。
练习-防抖
练习1:基础防抖实现(点击按钮)
目标:实现一个按钮点击防抖,多次点击后只执行最后一次(延迟1秒)。
<button id="btn">点击我(防抖)</button>
<script>// 1. 定义要执行的函数function handleClick() {console.log('按钮点击生效');}// 2. 实现防抖函数function debounce(fn, delay) {// 补充代码:实现防抖逻辑}// 3. 绑定事件(使用防抖)const btn = document.getElementById('btn');const debouncedClick = debounce(handleClick, 1000);btn.addEventListener('click', debouncedClick);
</script>
提示:
- 用闭包保存定时器ID
- 每次触发时清除之前的定时器,重新计时
练习2:输入框实时搜索防抖
目标:输入框输入时,停止输入1秒后才执行搜索(避免输入过程中频繁触发)。
<input type="text" id="searchInput" placeholder="输入搜索内容">
<script>// 1. 搜索函数function search(keyword) {console.log('搜索:', keyword);}// 2. 防抖函数(可复用练习1的实现)function debounce(fn, delay) {// 复用练习1的代码}// 3. 绑定输入事件const input = document.getElementById('searchInput');// 补充代码:使用防抖处理input事件
</script>
提示:
- 输入事件的回调需要获取输入框的值(
e.target.value
) - 防抖函数中通过
apply
传递事件对象
练习3:带立即执行选项的防抖
目标:扩展防抖函数,增加 immediate
参数(默认 false
),当为 true
时,第一次触发立即执行,后续触发才防抖。
<button id="btn1">普通防抖(延迟执行)</button>
<button id="btn2">立即执行+防抖</button>
<script>function log(message) {console.log(message);}// 扩展防抖函数:增加immediate参数function debounce(fn, delay, immediate) {// 补充代码:实现带立即执行的防抖}// 测试const btn1 = document.getElementById('btn1');const btn2 = document.getElementById('btn2');btn1.addEventListener('click', debounce(() => log('延迟执行'), 1000));btn2.addEventListener('click', debounce(() => log('立即执行'), 1000, true));
</script>
提示:
- 用一个变量判断是否是第一次触发
- 立即执行时,先执行函数再设置定时器(期间触发会清除定时器)
练习4:防抖取消功能
目标:给防抖函数增加取消功能,允许手动取消延迟执行。
<button id="submit">提交(防抖3秒)</button>
<button id="cancel">取消提交</button>
<script>function submit() {console.log('提交成功');}function debounce(fn, delay) {let timer;// 防抖处理函数const debounced = function(...args) {// 补充基础防抖逻辑};// 增加取消方法debounced.cancel = function() {// 补充代码:清除定时器,取消执行};return debounced;}// 测试const submitBtn = document.getElementById('submit');const cancelBtn = document.getElementById('cancel');const debouncedSubmit = debounce(submit, 3000);submitBtn.addEventListener('click', debouncedSubmit);cancelBtn.addEventListener('click', () => {debouncedSubmit.cancel();console.log('已取消提交');});
</script>
提示:
- 防抖函数返回的对象中增加
cancel
方法 cancel
方法内部清除定时器(clearTimeout(timer)
)
练习5:实战场景 - 窗口 resize 防抖
目标:监听窗口大小变化,当停止调整1秒后,才执行尺寸计算(避免调整过程中频繁触发)。
<script>// 计算窗口尺寸的函数function handleResize() {console.log('窗口尺寸:', window.innerWidth, 'x', window.innerHeight);}// 复用防抖函数function debounce(fn, delay) {// 复用之前的实现}// 绑定resize事件(带防抖)window.addEventListener('resize', debounce(handleResize, 1000));
</script>
提示:
- 窗口
resize
事件触发频率极高,防抖是必做优化 - 无需额外操作,直接将防抖后的函数绑定到事件即可
参考答案-防抖
练习1:基础防抖实现(点击按钮)
关键思路:
- 用闭包变量
timer
保存定时器ID,确保每次触发都能访问同一个定时器 - 每次点击先清除之前的定时器,再设置新的,实现“多次点击重新计时”
<!DOCTYPE html>
<html>
<body><button id="btn">点击我(防抖)</button><script>// 1. 定义要执行的函数function handleClick() {console.log('按钮点击生效');}// 2. 实现防抖函数function debounce(fn, delay) {let timer = null; // 闭包保存定时器IDreturn function(...args) {// 每次触发时清除之前的定时器(重新计时)clearTimeout(timer);// 设置新的定时器,延迟执行原函数timer = setTimeout(() => {// 确保this指向正确,参数正常传递fn.apply(this, args);}, delay);};}// 3. 绑定事件(使用防抖)const btn = document.getElementById('btn');const debouncedClick = debounce(handleClick, 1000); // 延迟1秒btn.addEventListener('click', debouncedClick);</script>
</body>
</html>
练习2:输入框实时搜索防抖
关键思路:
- 输入事件的回调通过
e.target.value
获取输入内容 - 防抖延迟设为500ms(比按钮场景短,提升用户体验)
<!DOCTYPE html>
<html>
<body><input type="text" id="searchInput" placeholder="输入搜索内容"><script>// 1. 搜索函数function search(keyword) {console.log('搜索:', keyword);}// 2. 防抖函数(复用练习1的实现)function debounce(fn, delay) {let timer = null;return function(...args) {clearTimeout(timer);timer = setTimeout(() => {fn.apply(this, args);}, delay);};}// 3. 绑定输入事件const input = document.getElementById('searchInput');// 防抖处理输入事件,延迟500msinput.addEventListener('input', debounce(function(e) {search(e.target.value); // 传递输入框的值}, 500));</script>
</body>
</html>
练习3:带立即执行选项的防抖
关键思路:
- 增加
immediate
参数控制执行时机,默认false
- 立即执行模式下,第一次触发直接执行函数,后续触发仅重置定时器
- 延迟结束后重置状态,确保下次触发能正常执行
<!DOCTYPE html>
<html>
<body><button id="btn1">普通防抖(延迟执行)</button><button id="btn2">立即执行+防抖</button><script>function log(message) {console.log(message);}// 扩展防抖函数:增加immediate参数function debounce(fn, delay, immediate = false) {let timer = null;let isExecuted = false; // 标记是否已立即执行return function(...args) {// 清除之前的定时器clearTimeout(timer);// 立即执行模式:第一次触发时立即执行if (immediate && !isExecuted) {fn.apply(this, args);isExecuted = true; // 标记为已执行}// 设置定时器:延迟后重置状态(或执行延迟逻辑)timer = setTimeout(() => {if (!immediate) {// 非立即模式:延迟后执行fn.apply(this, args);}isExecuted = false; // 重置状态,允许下次触发}, delay);};}// 测试const btn1 = document.getElementById('btn1');const btn2 = document.getElementById('btn2');btn1.addEventListener('click', debounce(() => log('延迟执行'), 1000));btn2.addEventListener('click', debounce(() => log('立即执行'), 1000, true));</script>
</body>
</html>
练习4:防抖取消功能
关键思路:
- 在防抖函数返回的对象上增加
cancel
方法 cancel
内部通过clearTimeout
清除定时器,阻止原函数执行
<!DOCTYPE html>
<html>
<body><button id="submit">提交(防抖3秒)</button><button id="cancel">取消提交</button><script>function submit() {console.log('提交成功');}function debounce(fn, delay) {let timer;// 防抖处理函数const debounced = function(...args) {clearTimeout(timer);timer = setTimeout(() => {fn.apply(this, args);}, delay);};// 增加取消方法debounced.cancel = function() {clearTimeout(timer); // 清除定时器,取消执行timer = null;};return debounced;}// 测试const submitBtn = document.getElementById('submit');const cancelBtn = document.getElementById('cancel');const debouncedSubmit = debounce(submit, 3000); // 延迟3秒提交submitBtn.addEventListener('click', debouncedSubmit);cancelBtn.addEventListener('click', () => {debouncedSubmit.cancel();console.log('已取消提交');});</script>
</body>
</html>
练习5:窗口 resize 防抖
关键思路:
- 窗口
resize
事件触发频率极高(拖动窗口边缘时每秒触发数十次) - 用防抖限制为“停止调整1秒后执行一次”,大幅减少函数执行次数
<!DOCTYPE html>
<html>
<body><script>// 计算窗口尺寸的函数function handleResize() {console.log('窗口尺寸:', window.innerWidth, 'x', window.innerHeight);}// 复用防抖函数function debounce(fn, delay) {let timer = null;return function(...args) {clearTimeout(timer);timer = setTimeout(() => {fn.apply(this, args);}, delay);};}// 绑定resize事件(带防抖)window.addEventListener('resize', debounce(handleResize, 1000));</script>
</body>
</html>