1. 事件循环的理解
JavaScript 是单线程的,它通过 事件循环(Event Loop) 来管理同步和异步任务的执行。任务通常分为 同步任务 和 异步任务。同步任务会在主线程中立即执行,而异步任务则会进入不同的队列,等待合适的时机再执行。
- 宏任务(Macro Task):大的任务单位,例如整个脚本代码块、
setTimeout
、setInterval
、DOM 操作等。 - 微任务(Micro Task):较小的任务单位,例如
Promise
的回调、queueMicrotask
、MutationObserver
等。
执行顺序规则
- 执行主线程上的所有同步任务。
- 检查微任务队列,执行所有微任务,直到清空。
- 如果微任务队列为空,则执行下一个宏任务。
- 重复以上步骤。
常见场景题
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2
2. 判断数据类型的方法
typeof
:适用于原始类型和函数,缺点是不能准确判断null
和复杂对象类型。instanceof
:适用于判断对象是否为某个构造函数的实例,但在跨作用域时有局限性。Object.prototype.toString.call()
:非常可靠,几乎可以用于所有类型判断,推荐使用。Array.isArray()
:判断数组的最佳方式。constructor
:可以用于判断对象类型,但可能会被修改,稳定性不如其他方法。
3.闭包
定义:当函数调用的时候,导致内部新函数被定义,并抛出内部被定义的新函数,即为闭包。
内层函数可以访问外层函数的作用域的变量
function createCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return increment;
}
const counter = createCounter(); // 创建闭包
counter(); // 输出 1
counter(); // 输出 2
// 清除闭包引用
counter = null; // 手动解除对闭包的引用,count 变量将被垃圾回收
4.防抖节流
防抖 是指在某个事件被触发一定时间后再执行回调,如果在这个时间内事件又被触发,则重新计时。这意味着函数在连续触发的情况下只会在最后一次触发后执行一次。
function debounce(func, delay) {
let timeout; // 定义一个变量用于存储定时器
return function(...args) {
const context = this; // 保存当前的上下文
clearTimeout(timeout); // 清除上一次的定时器
timeout = setTimeout(() => {
func.apply(context, args); // 在 delay 后执行目标函数
}, delay);
};
}
// 使用示例
const handleResize = debounce(() => {
console.log('Window resized');
}, 300);
window.addEventListener('resize', handleResize);
节流 是指在一定时间间隔内,无论事件被触发多少次,回调函数只会执行一次。节流可以确保在规定的时间内不会重复执行同一个函数,从而有效控制函数的执行频率。
function throttle(func, delay) {
let lastTime = 0; // 记录上次执行的时间
return function(...args) {
const context = this; // 保存当前的上下文
const now = Date.now(); // 获取当前时间
if (now - lastTime >= delay) {
lastTime = now; // 更新上次执行时间
func.apply(context, args); // 执行目标函数
}
};
}
// 使用示例
const handleScroll = throttle(() => {
console.log('Scrolled');
}, 300);
window.addEventListener('scroll', handleScroll);
5. call,apply,bind三者的区别
call、bind、apply 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法。 call 和 apply 的作用是一样的,都是用来调用函数并且改变函数内部的 this 指向。区别在于传参的方式不同,call 的参数是一个一个传递的,而 apply 的参数是以数组的形式传递的。
既然两者功能一样,那该用哪个呢? 在JavaScript 中,某个函数的参数数量是不固定的,因此要说适用条件的话,当你的参数是明确知道数量时用 call ;而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。
bind 方法不会立即执行函数,而是返回一个新的函数,这个新的函数的 this 值被绑定到了指定的对象,调用时也可以传入参数。