面试备战录

36 阅读7分钟

1、this关键字在不同上下文中是如何绑定的?

答:在JS中,this的绑定取决于函数的调用方式:

  • 默认绑定:函数直接调用;绑定对象。
    • 非严格模式 → Window(浏览器)或 global(Node)
    • 严格模式 → undefined
  • 隐式绑定:函数作为对象的方法被调用;绑定对象:调用它的对象。
  • 显式绑定:用callbindapply手动绑定;绑定对象:传入的第一个参数。
  • new 绑定:构造函数调用;绑定对象:由new创建的新对象。
  • 箭头函数绑定:箭头函数 不会创建自己的 this,它的this来自定义时所在的作用域
  • DOM 事件绑定:
    • 普通函数作为事件处理器:→ this指向触发事件的元素;
    • 箭头函数作为事件处理器 → this 来自外层作用域(可能是window

注:

  • new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
  • 箭头函数绑定一旦确定,不会被改变(call/apply无法修改它的this

2、谈谈你对原型、原型链的理解?

答:在JS中,每一个函数(构造函数)都有prototype属性,用于存放共享方法;每个对象都有__proto__属性,指向其构造函数的prototype。当访问对象属性时,会沿着__proto__形成的链条向上查找,这条链就是原型链,最终指向Object.prototype,再到null结束。

3、什么是闭包?闭包有哪些典型的应用场景?

答:闭包(Closure)是指函数能够记住并访问它所在的词法作用域(Lexical Scope),即使这个函数是在它的作用域之外执行的。简单说:函数 + 被函数引用的外部变量 = 闭包

  • 形成条件:
    • 函数内部引用了函数外部的变量
    • 返回或传递了这个函数,使它在原作用域之外执行
  • 作用:让函数“记住”它出生时的作用域环境

闭包的典型应用场景:

  • 数据私有化 / 模拟私有变量。闭包让变量对外部不可直接访问,只能通过指定方法操作:
function createCounter() {
  let value = 0;
  return {
    inc: () => ++value,
    dec: () => --value,
    get: () => value
  };
}
const counter = createCounter();
console.log(counter.inc()); // 1
console.log(counter.get()); // 1
console.log(counter.value); // undefined
  • 函数柯里化
function add(a) {
  return function (b) {
    return a + b; // a 被记住
  };
}
const add5 = add(5);
console.log(add5(10)); // 15
  • 事件回调 / 异步操作
function bindEvent(id) {
  let count = 0;
  document.getElementById(id).addEventListener('click', function () {
    count++;
    console.log(`按钮被点击了 ${count} 次`);
  });
}
  • 节流 / 防抖
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

闭包的优缺点:

  • 优点:
    • 变量持久化(状态记忆)
    • 封装性好(模拟私有成员)
  • 缺点:
    • 占用内存,可能导致内存泄漏(闭包引用的变量不会被 GC 释放)
    • 过度使用会降低性能

个人理解:闭包就是一个函数引用了外部的变量,然后这个函数没有被回收,所以外部变量声明周期就被延长了,就形成了闭包。比如 outer()虽然执行了,但内部返回了inner,inner引用了外部的x,按理来说当outer()执行完毕后let x = 10就会被回收,但inner又用到了,就没有被回收,就形成了闭包

function outer() {
  let x = 10;
  return function inner() {
    console.log(x);
  };
}
const fn = outer(); // fn 持有 x 的引用
fn(); // 输出 10

4、=====的区别?有哪些隐式类型转换的坑?

答:===不做类型转换,类型不同直接 false==会进行隐式类型转换后在比较。转换规则包括:布尔转数字、字符串转数字、对象转原始值;null只等于undefined,NaN不等于任何值。

常见转换规则:

  • 不同类型比较:
    • 如果有 布尔值 → 转成数字(true → 1, false → 0)
    • 如果有 字符串和数字 → 字符串转数字
    • 如果有 对象和原始值 → 对象先调用 valueOf,再调用 toString
  • nullundefined
    • null == undefinedtrue
    • 和其他值比较 → false(即 null == 0false
  • NaN 任何比较都是
NaN === NaN; // false
NaN == NaN; // false
// 常见隐式类型转换坑
0 == false; // true (false → 0)
'' == false; // true   ('' → 0)
[] == false; // true   ([] → '' → 0)
[] == ![]; // true   (![] → false → 0, [] → '' → 0)
[1] == '1'; // true   ([1] → '1')
null == 0; // false  (特例)
undefined == 0; // false  (特例)
' \t\n' == 0; // true   (空白字符串 → 0)

注:建议实际开发中优先使用=== 避免隐式类型转换带来的坑。

5、JavaScript 中的执行上下文(Execution Context)是怎样工作的?

答:JavaScript在运行时会维护一个执行上下文栈,来管理代码的运行环境。执行上下文分为全局、函数和eval三种,每个上下文在创建时会生成变量环境、词法环境、作用域链和this绑定,然后进入执行阶段,执行完毕后出栈销毁(闭包可能延迟销毁)。

  • 执行上下文:执行上下文(Execution Context, EC)JavaScript代码执行时的运行环境,它定义了变量、函数、作用域链和this的值。JS 执行时,会创建一个执行上下文栈(EC Stack),用来管理所有的执行上下文。
  • 执行上下文的类型:
    • 全局执行上下文(Global EC):代码刚开始执行时创建,只会有一个,绑定全局对象(浏览器是window),this指向全局对象(严格模式下是 undefined
    • 函数执行上下文(Function EC):每调用一次函数就创建一个新的执行上下文,拥有自己独立的变量环境、作用域链、this
    • Eval 执行上下文(几乎不用)eval()代码执行时创建,实际开发中基本不建议用
  • 执行上下文的生命周期
    • 创建阶段:
      • 创建变量环境(Variable Environment):存放var声明的变量、函数声明(提升),并初始化值
      • 创建词法环境(Lexical Environment):存放let/const声明的变量(不提升,暂时性死区)
      • 建立作用域链
      • 确定 this 绑定
    • 执行阶段
      • 按代码顺序执行,读取和赋值变量,执行函数调用
    • 销毁阶段
      • 函数执行结束后,其执行上下文会从栈中弹出,内存回收(闭包除外)

6、为什么 JS 是单线程的?异步任务是怎么调度的?

答:JavaScript是单线程的,为了避免多线程操作DOM导致的不一致和复杂性。异步由运行环境提供(浏览器 / Node.js),通过事件循环机制调度任务。任务分为宏任务和微任务:每次事件循环先执行一个宏任务,然后清空所有微任务,再渲染UI

为什么JavaScript是单线程的?

  • 历史原因:JS最初是为浏览器做表单验证和DOM操作设计的,如果多线程同时修改DOM,可能会导致DOM 状态不一致操作冲突(比如两个线程同时删除同一个节点)。
  • 运行环境限制:浏览器中的 JS 引擎和渲染引擎是紧密协作的,单线程可以简化实现,避免锁和同步问题。
  • 可预测性:单线程让代码按顺序执行,避免多线程编程中的复杂竞态条件(race condition)

异步是怎么做到的?

  • JS自身是单线程的,但运行环境(浏览器/Node.js)提供了异步能力:
    • 浏览器提供Web APIs(setTimeout、fetch、DOM 事件等)
    • Node.js 提供libuv线程池异步IO
  • 执行流程:
    • JS主线程遇到异步任务(如setTimeout/fetch),交给运行环境处理。
    • 运行环境在任务完成时,把回调函数推入任务队列(Task Queue)
    • JS引擎空闲时通过 事件循环(Event Loop)从队列取出任务执行。
  • 事件循环(Event Loop) & 任务调度;事件循环会不断执行以下步骤:
    • 执行宏任务队列(Macro Task Queue) 中的一个任务(例如:setTimeout 回调、script 整体代码)。
    • 执行完宏任务后,立即清空 微任务队列(Micro Task Queue)(例如:Promise.then、queueMicrotask)。
    • 渲染 UI(浏览器环境)。
    • 回到第一步,继续循环。