1、this关键字在不同上下文中是如何绑定的?
答:在JS中,this的绑定取决于函数的调用方式:
- 默认绑定:函数直接调用;绑定对象。
- 非严格模式 →
Window(浏览器)或 global(Node) - 严格模式 →
undefined
- 非严格模式 →
- 隐式绑定:函数作为对象的方法被调用;绑定对象:调用它的对象。
- 显式绑定:用
call、bind、apply手动绑定;绑定对象:传入的第一个参数。 - 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
- 如果有 布尔值 → 转成数字(
null和undefinednull == undefined→true- 和其他值比较 →
false(即null == 0是false)
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()代码执行时创建,实际开发中基本不建议用
- 全局执行上下文(Global EC):代码刚开始执行时创建,只会有一个,绑定全局对象(浏览器是
- 执行上下文的生命周期
- 创建阶段:
- 创建变量环境(Variable Environment):存放
var声明的变量、函数声明(提升),并初始化值 - 创建词法环境(Lexical Environment):存放
let/const声明的变量(不提升,暂时性死区) - 建立作用域链
- 确定 this 绑定
- 创建变量环境(Variable Environment):存放
- 执行阶段
- 按代码顺序执行,读取和赋值变量,执行函数调用
- 销毁阶段
- 函数执行结束后,其执行上下文会从栈中弹出,内存回收(闭包除外)
- 创建阶段:
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(浏览器环境)。
- 回到第一步,继续循环。
- 执行宏任务队列(Macro Task Queue) 中的一个任务(例如: