十二. 高阶函数和柯里化应用场景
1.高阶函数
高阶函数是接收函数作为参数或返回一个函数的函数
2.应用场景
-
回调函数 Array.prototype.map/filter 接收一个函数处理每一项
-
事件处理 element.addEventListener(fn) 传入一个函数作为回调
-
封装通用逻辑
-
函数节流/防抖
经典高阶函数示例
// map 就是⼀个⾼阶函数,它接受⼀个函数作为参数
const numbers = [1, 2, 3, 4, 5]
const squaredNumbers = numbers.map(function(num) { return num * num; });
console.log(squaredNumbers); // [1, 4, 9, 16, 25]
map 函数遍历数组,并对数组中的每个元素应用传入的函数,然后返回一个新数组
简单封装示例
function withLog(fn) {
...args 剩余语法参数 把函数调用时多出来的实参打包成数组,适合不定参数场景
return function (...args) {
console.log('开始执行');
const result = fn(...args);
console.log('执行结束');
return result;
};
}
const sayHi = () => console.log('hi');
const loggedSayHi = withLog(sayHi);
loggedSayHi();
2.柯里化
把一个接受多个参数的函数,拆成多个只接受一个参数的函数,每次返回 一个新函数直到参数完整.
function curryAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}console.log(curryAdd(1)(2)(3)); // 6
总结
1.代码服用与抽象: 高阶函数可以将重复的逻辑抽象出来,形成通用函数,提高代码的复用性,柯李华可以将复杂的函数调用转化为一系列简单的步骤,便于部分应用和服用
2.函数组合: 高阶函数和柯里化是构建函数式编程风格的关键,它们使的函数的组合变得更容易,从而构建更复杂的功能
3.延迟执行与配置化: 柯里化可以将函数执行延迟到所有参数都传入后进行,同时允许在传入部分参数时进行配置,生成定制化的新函数
十三. 节流和防抖
节流(Throttling)和防抖都是用于控制事件在一定事件内触发频率的技术,它们本质上是为了优化性能,减少不必要的计算或DOM操作,尤其是在处理高频事件(如窗口滚动,鼠标移动,键盘输入缩放)
为什么需要?
1. 优化性能
2. 减少请求
3. 提升用户体验
1.节流
规定在一个周期内,事件最多触发一次,如果事件在周期内再次触发,则忽略
常见实现思路:
function throttle(fn,delay){
let last=0; //记录上次触发时间
//...args会将调用节流函数传入的所有参数,无论数量打包成一个数组args
//这里会形成闭包
return function(...args){
console.log(...args) //小明
const now=Date.now();
if(now-last>=delay){
//距离上次触发时间超过延迟,可以执行
last=now; //更新上次触发时间
//强制将原函数的this绑定到调用时的上下文,确保函数颞部逻辑依赖的this正确
fn.apply(this,args);
}
};
}
//示例
function sayHi(name){
console.log('Hi,', name, '时间:', new Date().toLocaleTimeString());
}
//节流包装
const throttledSayHi = throttle(sayHi, 2000);
setInterval(() => {
throttledSayHi('小明');
}, 500);
2.防抖
规定在事件触发后,等待一定时间再执行回调,如果在等待时间内时间再次触发,则重新计时。只有当事件停止触发一段时间后,回调函数才会执行。
常见实现思路: 使用setTimeout 设置一个定时器,如果事件在定时器到期前再次触发,则清除之前的定时器,并重新设置一个新的定时器
//经典基于settimeout的防抖实现
function debounce(fn,delay){
let timer=null //存储定时器id
return function(...args){
const context=this;
//如果已有定时器,则清除前一个定时器,重新计时
if(timer){
clearTimeout(timer);
}
//设置新的定时器
timer=setTimeout(()=>{
func.apply(context,args);
timer=null; //定时器执行完毕清空
},delay);
}
}
//示例用法
const inputElement = document.getElementById('myInput');
if (inputElement) {
inputElement.addEventListener('input', debounce((event) => {
console.log('输⼊停⽌了,执⾏搜索:', event.target.value);
}, 500)); // ⽤户停⽌输⼊ 500ms 后才执⾏回调
}
根本区别:
- 节流: 保证一定时间内至少执行一次(或最多执行一次),周期性执行
- 防抖: 保证事件停止触发后才执行一次,更适用于行为结束后才需要执行的场景
应用场景:
- 节流: 适用于需要周期性执行的操作,如滚动加载,缩放手势处理,高频鼠标事件
- 防抖: 适用于只需要在事件停止后执行一次的操作,如搜索框输入,窗口resize结束,拖拽结束
实现方式:
- 节流通常通过时间戳或定时器结合标志位实现
- 防抖主要通过setTimeout的清除与重设实现
第一次立即执行防抖函数
function debounce(fn, delay, immediate = true) { let timer; return function (...args) { const context = this; if (timer) clearTimeout(timer); if (immediate) { const callNow = !timer; // 只有第一次会为 true timer = setTimeout(() => { timer = null; // 延迟后允许再次触发 }, delay); if (callNow) { fn.apply(context, args); // 第一次立即执行 } } else { timer = setTimeout(() => { fn.apply(context, args); }, delay); } }; }
十四. bind,call,apply的用法
1.核心概念
call,apply和bind是js中Function.prototype 对象上的三个方法,主要用于控制函数执行时的上下文(即this的指向). 它们允许开发者显式地指定函数内部this关键字的值
2.用法
1.call(thisArg,arg1,arg2,....) 立即执行
- 作用: 调用一个函数,并显示指定该函数内部this的值,以及按顺序传递参数
- 参数:
thisArg: 在fun函数运行时指定的this值,如果thisArg为null,undefined,或者是在非严格模式下,thisArg会被自动替换为全局对象(window或global),基本类型值会被包装成对应的包装对象 - arg1,arg2,...: 传入函数的参数序列
- 返回值: 函数的返回值
function sum(a,b,c){
console.log(this.name,a+b+c)
}
const obj={ name: "Calculator" };
const numbers=[1,2,3];
//使用apply 调用sum,指定this为obj,通过数组传递参数greet.call(person,"hello","!"); //输出: Hello,Alice!
2.apply(thisArg,argArray)
- 作用: 调用一个函数,显式指定this的值,并通过一个数组来传递参数.
- 参数:
thisArg: 同 call.
argsArray: 一个数组或类数组对象,其元素作为单独的参数传递给函数 - 返回值: 函数的返回值
function sum(a,b,c){
console.log(this.name,a+b+c);
}
const obj={name:"Calculator"};
const numbers=[1,2,3];
//使用apply 调用sum,指定this为obj,通过数组传递参数
sum.apply(obj,numbers); //输出 Calculator 6
3. bind(thisArg,arg1,arg2,...)
-
创建一个新的函数,当这个新函数被调用时,其this值会绑定到thisArg,参数会被预设为从第二个参数开始的值,bind不会立即执行函数
-
参数:
thisArg: 作为绑定函数的this对象
arg1,arg2,...: 当绑定函数被调用时,这些参数将作为预设参数放在最前面传递给原始函数 -
返回值: 一个新的,原函数的拷贝,其this已被绑定,并可能预设了部分参数
function sayHello(greeting){
console.log(`${greeting},${this.name}`);
}
const user={name: 'Bob'; }
//使用bind 创建一个新的函数 boundSayHellowconst boundSayHello = sayHello.bind(user,"HI");
//调用新函数,this已固定user,"Hi"是预设参数boundSayHello(); //输出: Hi,Bob
4.关键注意事项
1. call和apply的主要区别: 仅在与传递参数的方式: call 接收参数序列,apply接收参数数组,在性能上,通常两者差异微小,但在参数数量极大时,某些引擎中apply可能稍有优势(但不是通用规则)
2.bidn的非立即执行特性: bind返回的是一个新函数,原函数并不会立即执行,这使得bind特别适用于需要延迟执行或作为回调函数,同时又需要固定this的场景(如事件监听,异步处理).
3.绑定后的函数作为构造函数: 通过bind创建的新函数,如果在new操作符下调用,this的指向会变为新创建的实例对象,而不是bind时指定的thisArg,但是,bind时指定的参数仍会传递
十五. 讲一下立即执行函数
立即执行函数指的是定义完就立即执行的函数,语法上是一个函数表达式后面紧跟一对小括号( )来执行它
(function () {
// 函数体
})();// 或使用箭头函数
(() => {
// 函数体
})();
1.为什么要用IIFE?
- 创建私有作用域,避免变量污染全局作用域
var name = "Global";
(function () {
var name = "IIFE";
console.log("Inside IIFE:", name); // 输出 IIFE
})();console.log("Outside IIFE:", name); // 输出 Global
- 封装代码模块
const counter = (function () {
let count = 0;
return {
add() {
count++;
return count;
},
reset() {
count = 0;
}
};
})();console.log(counter.add()); // 1
console.log(counter.add()); // 2
counter.reset();
console.log(counter.add()); // 1- 可用于初始化代码执行后释放上下文
2.关键注意事项:
1. 语法强制为表达式(function(){...})
2. 返回值: IIFE 返回的是函数的执行结果,如果函数没有显示return语句,它会隐式返回undefined.
3.参数传递: IIFE可以接受参数,允许将外部变量"注入"到其独立作用域中
4.ES6及以后: 随着ES6模块和块级作用域(let,const)的引入,IIFE在现代js开发中核心作用(隔离作用域和防止全局污染)已部分被替代,模块系统提供了更健壮,标准的模块封装方式,而let/const提供了块级作用域,然而,IIFE在一些特定场景(如旧项目,立即执行某些初始化代码,某些库封装模式)仍然可能使用
十六. 纯函数、副作⽤与函数式编程初识
1.概念
- 纯函数: 指满足两个条件的函数: 给定相同的输入,总是返回相同的输出;并且执行过程中没有任何可观察的副作用.
function add(a, b) {
return a + b; // 不修改参数,不使用外部变量
}//关键在于 只要函数的输出(返回值) 仅取决于它的输入,而且不影响外部代码,就是纯函数
- 副作用: 指函数执行时,除了返回显而易见的计算结果外,还对外部世界产生了可观察的影响,例如修改全局变量,I/O操作(读写文件,网络请求),修改DOM等
let count = 0;
function add(num) {
count += num; // 修改了外部变量,产生副作用
return count;
}
- 函数式编程: 一种编程范式,强调使用纯函数,避免改变状态和可变数据,并使用函数作为一等公民,它将计算视为数学函数的求值
2.作用
- 可预测性与可测试性: 纯函数由于其确定的输入和输出,极大地提高了代码的可预测性,使得单元测试变得非常简单,只需测试与输出的关系即可,
- 并发与并行安全性: 纯函数不依赖或修改外部状态,天然适用于并发和并行环境,避免了多线程或多进程共享状态带来的复杂同步问题(如竞态条件)
十七. 手写call和apply
1.call
//context: 想要绑定的this指向对象
//args.; 从第二个参数开始是传给函数的实参,使用rest参数收集
Function.prototype.myCall=function(context,...args){
//如果没有传context 或传的是null/undefined,则默认绑定到全局对象
context=context || globalThis;
//给context临时添加一个唯一的属性,避免覆盖已有属性
const fnKey= Symbol('fn');
//this 指向当前要执行的函数,把它挂在到context上 , 指的是"调用myCall的哪个函数本身"
context[fnKey]=this;
//使用展开运算符,将所有实参传入函数中
const result=context[FnKey](...args);
delete context[fnKey]
return result;
}
关键语句
context[fnKey]=this;
-
this是调用myCall的原始函数
-
把它变成了context对象的一个方法(临时)
-
所以当你这样调用时:
context[fnKey](...args);
js会将this指向context
等价于
function greet(){
console.log(this.name)
}
const obj={name: 'Tom'};greet.call(obj);
//实际变成了
obj.greet=greet;
obj.greet(); //this就是obj
delete obj.greet
2.手写apply
参数说明:
- 第一个参数: context 要绑定的this
- 第二个参数: args 必须是一个数组或伪数组,表示实参列表
Fcuntion.prototype.myApply=function(context,args){
context=context || globalThis;
const fnKey=Symbol('tempFn');
context[fnKey] = this;
let result;
if(Array.isArray(args)){
//用展开运算符解开数组作为函数参数
result=context[fnKey](...args)
}else if (args==null){
//如果没有传参数,相当于穿空参数
result=context[fnKey]();
}else{
//非数组类型参数,抛出错误
throw new TypeError('CreateListFromArrayLike called on non-object');
}
delete context[fnKey];
return result;
}
十八. setTimeout 和 setInterval 的陷阱
1.setTimeout(fn,delay,...args); //延迟执行一次
- callback: 要执行的函数
- delay: 延迟的毫秒数,如果省略,默认为0,请注意,这不是精确的延迟,受时间循环影响
- ...args(可选): 传递给callback函数的额外参数
- 返回值: 一个数字ID,可用于clearTimeout( ),取消延时
setTimeout(() => {
console.log('timeout');
}, 0);
console.log('sync');
- 即使设置0ms,也不会立即执行,它会被放到宏任务队列中,等待主线程空闲时再执行
- 浏览器最小延迟一般为4ms(老浏览器中甚至是10-15ms)
2.setInterval(fn,delay,...args); // 每隔一段时间重复执行
- callback: 要重复执行的函数
- delay: 重复执行之间的时间间隔(毫秒),同样受事件循环影响
- ...args(可选): 传递给callback函数的额外参数
- 返回值: 一个数字ID,可用于clearInterval() 取消重复
setInterval(() => {
// 如果里面逻辑执行了 100ms,而 interval 是 50ms
// 会造成函数排队、卡顿
}, 50);
注意:
1.setInterval 可能导致任务堆积
- 如果setInterval的回调函数执行时间大于指定的delay时间,那么在回调函数尚未执行完毕时,下一个回调任务可能已经根据delay被添加到消息队列了.
- 这会导致消息对垒中累计待执行的同一回调函数实例,当主线程空闲时,这些任务会接连执行,而非间隔delay时间执行,这可能导致程序行为异常或性能问题
替代方案: 使用递归的setTimeoout可以避免这个问题,在每次setTimeout的回调函数执行完毕后,再根据需要调用下一个setTImeout
//避免setInterval 堆积的递归 setTimeout方法
function recursiveTimeout(){//执行任务....
//模拟一个耗时操作(>100ms)
let start=Date.now( );
while(Date.now( )-start<200);
//任务执行完后再调度下一个
setTimeout(recursiveTimeout,100); //永远保持100ms的间隔(在任务完成后)
}
2.this的指向问题
原始的setTimeout 和 setInterval 调用中的回调函数,如果在非箭头函数中使用function 关键字定义,其内部的this默认指向全局对象(在浏览器通常是window,在严格模式下是undefined),而不是调用它们的上下文对象
解决方案:
使用箭头函数作为回调,箭头函数没有自己的this,会捕获其定义时的上下文的this
使用bind( ) 方法显示绑定this
在外部保存this的引用 (如const self =this;)并在回调中使用self
/**
* 演示 setTimeout 中 this 指向问题及解决方案
*/
class MyClass {
constructor() {
this.value = 'initial'; // 实例属性
}
// ============== 问题方法 ==============
methodWithProblem() {
setTimeout(function() {
// 🚫 问题:传统函数中 this 指向 window 或 undefined
console.log(this.value); // 输出:undefined ❌
}, 100);
}
// ============== 解决方案1:箭头函数(推荐) ==============
methodWithArrow() {
setTimeout(() => {
// ✅ 解决:箭头函数继承外层 this(指向 MyClass 实例)
console.log(this.value); // 输出:'initial' ✅
}, 100);
}
// ============== 解决方案2:bind 绑定 ==============
methodWithBind() {
setTimeout(function() {
// ✅ 解决:通过 bind 绑定 this
console.log(this.value); // 输出:'initial' ✅
}.bind(this), 100); // 关键:绑定当前 this
}
}
// ============== 测试代码 ==============
const instance = new MyClass();
// 问题方法(会输出 undefined)
instance.methodWithProblem();
// 解决方案1(箭头函数)
instance.methodWithArrow();
// 解决方案2(bind 绑定)
instance.methodWithBind();
3.取消定时器
- 如果不显式取消,使用setInterval设置的定时器会一直运行,知道页面关闭
- 即使使用setTimeout设置的延迟执行,如果回调函数在执行前不再需要,也应当使用clearTimeout取消,以避免不必要的资源消耗和潜在的副作用(例如,在组件卸载后仍然在尝试更新DOM)
- 在使用React 这样的框架时,在组件卸载(componentWillUnmount 或 useEffect 的清理函数)时,清理定时器是防止内存泄露和错误行为的常见最佳实践
// 全局变量:存储定时器ID
let intervalId = null;
/**
* 启动定时数据获取
* 功能:每5秒执行一次数据获取操作
*/
function startFetching() {
// 设置定时器,每5000毫秒(5秒)执行一次
intervalId = setInterval(() => {
console.log('Fetching data...'); // 日志输出
// 实际数据获取逻辑(示例)
try {
// fetch('https://api.example.com/data')
// .then(response => response.json())
// .then(data => console.log('Data received:', data));
} catch (error) {
console.error('Fetch error:', error);
}
}, 5000); // 间隔时间:5000毫秒 = 5秒
}
/**
* 停止定时数据获取
* 功能:清除定时器并释放资源
*/
function stopFetching() {
// 检查定时器是否存在
if (intervalId !== null) {
console.log('Stopping fetch interval'); // 日志输出
// 清除定时器
clearInterval(intervalId);
// 清理引用(重要!避免内存泄漏)
intervalId = null;
}
}
// ================= 使用示例 =================
// 在组件挂载/页面加载时启动
startFetching();
// 模拟10秒后停止(实际场景中可能在组件卸载/页面关闭时调用)
setTimeout(() => {
stopFetching();
}, 10000);
十九.事件循环(Event Loop
写文章-CSDN创作中心JavaScript 深入解析:同步、异步、微任务、宏任务、消息队列与事件循环_js消息队列和任务队列-CSDN博客写文章-CSDN创作中心
二十. Promise
JavaScript 异步编程:从 Promise 到 async/await 的优雅之道_promise转await-CSDN博客
二十一. 事件循环(Event Loop)完整解析
js事件循环是单线程语言实现异步的核心机制,简单来说,它通过调用栈,任务队列(Task Queue)和微任务队列的协同工作来处理代码执行顺序
异步:
是指代码代码的执行不会按照书写顺序依次进行,而是允许某些任务在后台执行,等结果准备好后再处理,不会阻塞后续代码运行
1. 为什么需要?
- 因为js是单线程的,没有事件循环,像网络操作或文件读写这样的耗时操作会完全阻塞主线程,导致程序卡死,事件循环允许这些操作在后台进行,完成后再通知主线程处理结果
- 用户界面响应性: 在浏览器环境中,DOM操作,事件监听等都依赖主线程,事件循环确保耗时脚本不会长时间锁定主线程,从而保证用户界面的流畅
- 异步编程模型支持: Promise,async/await,setTimeout,fetch 等异步API的实现基础就是事件循环
2.宏任务和微任务
1.宏任务
- setTimeout: 在指定的延迟后将callback函数放入宏任务队列
- setInterval : 每隔指定的延迟将callback函数放入宏任务队列
- setImmediate(callback)(Node.js特有): 尽快将callback函数放入check阶段任务队列
- UI渲染事(如绘制)
- I/0 操作完成的回调(如读取文件完成)
2. 微任务(Micro tasks):
- Promise.then
- Promise.catch
- Promise.finally(Promise构造函数是同步的,回调函数是异步的,会被推入微任务队列)
- Async/await
- object.observe等等
3.执行顺序
1.同步任务直接进入调入栈执行
2. 异步任务(如setTimeout,fetch) 由WebAPI处理,完成后回调函数进入任务队列
3. 微任务(如Promise.then,MutationObserver)进入微任务队列
4. 事件循环在每次调用栈清空后:
- 先清空所有微任务队列中的任务
- 再取出一个宏任务(如setTimeout) 执行
- 重复此过程
二十二. js中的原型与原型链
1. 原型:
原型是一种机制,每个js对象都有一个隐藏属性__proto__ 或通过Object.getPrototypeof( ) 访问,指向它的原型对象(Prototype)
作用:
- 实现继承,使得对象可以访问父级/原型对象上的属性和方法
- 可以共用,复用原型对象上的方法,节省内存
2. 实例对象
在用构造函数创建一个新实例,实例通过__proto__ 对象的内部属性,指向创建该对象的构造函数的Prototype属性. 每个构造函数会自动获得一个prototype属性,这个属性的值是一个对象
3.相关方法
- Object.getPrototypeof:es6推荐的方法,用于设置一个对象的原型
- Object.setPrototypeOf(obj, prototype) : ES6 推荐的⽅法,⽤于设置⼀个对象的 [[Prototype]] 。不推荐在性能敏感的代码中使⽤,因为它可能导致优化问题。
- Object.create(prototype, propertiesObject) : 创建⼀个新对象,并将该新对象的 [[Prototype]] 指定为 prototype 参数。可以⽤来实现基于原型的纯粹继承。
3.原型链
原型链就一种继承关系形成的链式结构,用于实现属性查找和继承,如果找不到就睡沿着__prototype__指向的原型对象继续查找,知道找到该属性或到达原型链的终点null,如果在原型链的任何位置找到属性,就返回该属性的值,如果整个链条上都找不到,则返回undefined
4.注意点
- this的指向: 原型方法中的this仍然指向该方法的对象实例,而不是原型对象本身
- 不能直接修改__proto__: 尽管可以使用__Proto__来访问或设置对象的原型,但直接修改它的性能开销较大,且可能导致难以预测的行为,ES6提供了Object.getProtoyepeOf( )和Object.setPrototypeof( ) 作为更规范的行为,但setPrototypeOf 在频繁使用时仍需谨慎考虑性能
- 原型链的终点: 所有内置对象和大部分自定义对象的原型链最终都会指向Object.prototype.而Object.Prototype的 [[ Prototype ]] 是null,标志着原型链的结束