js面试题 高频(12-22题)

十二. 高阶函数和柯里化应用场景 

1.高阶函数 

高阶函数是接收函数作为参数或返回一个函数的函数

2.应用场景

  1. 回调函数  Array.prototype.map/filter  接收一个函数处理每一项 

  2. 事件处理  element.addEventListener(fn) 传入一个函数作为回调

  3. 封装通用逻辑

  4. 函数节流/防抖 

经典高阶函数示例

// 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 创建一个新的函数 boundSayHellow 

const 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

web前端渡一大师课 01 事件循环 -CSDN博客    

写文章-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,标志着原型链的结束


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值