一、基础语法类(必问)
1. JavaScript 的数据类型有哪些?基本类型和引用类型的区别?
解答:
-
数据类型分类 :
- 基本类型(值类型):
String、Number、Boolean、Null、Undefined、Symbol(ES6)、BigInt(ES10); - 引用类型:
Object(包含Array、Function、Date、RegExp等)。
- 基本类型(值类型):
-
核心区别 :
维度 基本类型 引用类型 存储位置 栈内存(栈区) 堆内存(堆区),栈存引用地址 赋值方式 赋值 "值本身"(值传递) 赋值 "引用地址"(引用传递) 可变性 不可变(比如 let str = 'a'; str[0] = 'b'无效)可变(比如 let arr = [1]; arr[0] = 2生效)比较方式 比较 "值是否相等" 比较 "地址是否相等"(比如 {} === {}为false)
2.null和undefined的区别:
undefined 是声明了变量但未赋值,表示未定义。
null 是手动赋值为空,表示空值,是一个空对象。
原型链的终端就是null空对象。
2. 什么是 NaN?如何判断一个值是否是 NaN?
- NaN(Not a Number):表示 "非数字",但类型是 Number (
typeof NaN === 'number'); - 特性:
NaN !== NaN(唯一不等于自身的值);
总结:判断一个值能不能当数字用,不能就会返回true,能,返回false
3. 说说 == 和 === 的区别?
解答:
===(严格相等):值 + 类型 都相等才返回 true(无隐式类型转换);==(松散相等):先做隐式类型转换,再比较值是否相等;
javascript
0 == ''; // true(都转成 0)
null == undefined; // true(特殊规则)
[] == false; // true([] 转成 0,false 转成 0)
{} == '[object Object]'; // true({} 转成字符串 "[object Object]")
// 严格相等无坑
0 === ''; // false
null === undefined; // false
开发建议:优先用 ===,避免隐式转换导致的意外结果。
4.数据类型的判断
typeof判基本类型,instanceof判对象、数组。
5.this指向问题
- 普通函数调用 → this 指向 window
- 对象方法调用 → this 指向 这个对象
- 构造函数 new → this 指向 新创建的实例对象
- 箭头函数 → 没有自己的 this ,指向外层作用域的 this
3 个改变this指向的方法:
- call
- apply
- bind
二、作用域与闭包(重点)
1. 什么是作用域?JS 有哪些作用域?
解答:
- 作用域:变量 / 函数的可访问范围,控制变量的可见性和生命周期,避免命名冲突;
- 分类:
- 全局作用域 :在代码任何地方可访问(比如
window上的变量),页面关闭才销毁; - 函数作用域:仅在函数内部可访问(ES5 仅函数有作用域);
- 块级作用域 :ES6 新增,由
let/const+{}产生(if/for/while等块内),块外不可访问;
- 全局作用域 :在代码任何地方可访问(比如
2. 什么是闭包?闭包的用途和缺点?
闭包定义 :函数嵌套时,内部函数引用外部函数的变量 / 函数,导致外部函数执行完后,其作用域不会被销毁,内部函数依然能访问这些变量(核心:保留外部作用域的变量);
javascript
function outer() {
let num = 0; // 外部函数变量
// 内部函数引用外部变量,形成闭包
return function inner() {
num++;
console.log(num);
};
}
const fn = outer();
fn(); // 1(outer 执行完,但 num 未销毁)
fn(); // 2
- 用途 :
- 封装私有变量(比如模拟类的私有属性);
- 缓存数据(比如计算结果缓存,避免重复计算);
- 防抖 / 节流函数的实现;
- 缺点 :
- 闭包会保留外部作用域的变量,导致变量无法被垃圾回收,内存占用增加,滥用可能导致内存泄漏;
- 解决:不用时手动解除引用(
fn = null)。
3. 什么是变量提升?var/let/const 的区别?
变量提升 :JS 引擎在执行代码前,会把 var 声明的变量和函数声明 "提升" 到当前作用域顶部(赋值不会提升);
变量提升只提升变量的声明,不提升赋值,声明前使用的话会得到undefined,而不会报错。
const/let也有变量提升,但是不让用,不能像var一样提前用,所以一般说let和const没有变量提升。
javascript
console.log(a); // undefined(var 提升,未赋值)
var a = 1;
fn(); // 1(函数声明整体提升)
function fn() {
console.log(1);
}
总结:var有变量提升,而,const和let没有变量提升。
| 特性 | var | let | const |
|---|---|---|---|
| 变量提升 | 有(提升后 undefined) | 有(暂时性死区) | 有(暂时性死区) |
| 块级作用域 | 无 | 有 | 有 |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 赋值 | 可多次赋值 | 可多次赋值 | 必须初始化,不可修改引用(基本类型不可改,引用类型可改属性) |
暂时性死区 :let/const 声明的变量,在声明前访问会报错(比如 console.log(b); let b = 2; 报错),避免变量提升的坑。
4.作用域,闭包的用途
函数嵌套函数,内部函数访问外部函数变量
私有化变量、模块化、缓存数据。
三、异步编程(高频)
1. JS 为什么是单线程?异步的解决方案有哪些?
解答:
- 单线程原因:JS 最初设计用于浏览器交互(比如 DOM 操作),如果多线程同时操作 DOM 会导致冲突(比如一个线程加样式,一个线程删节点),所以设计为单线程;
- 异步解决方案(演进) :
- 回调函数 :基础(比如
setTimeout、AJAX 回调),缺点:回调地狱(多层嵌套); - Promise :解决回调地狱,支持链式调用(
then/catch),三种状态:pending(进行中)、fulfilled(成功)、rejected(失败); - async/await:ES8 新增,Promise 的语法糖,用同步写法实现异步,更易读;
- 回调函数 :基础(比如
- 示例(Promise 解决回调地狱):
javascript
// 回调地狱(多层嵌套)
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000);
}, 1000);
// Promise 链式调用
new Promise(resolve => {
setTimeout(() => {
console.log(1);
resolve();
}, 1000);
}).then(() => {
setTimeout(() => {
console.log(2);
}, 1000);
});
// async/await(更简洁)
async function fn() {
await new Promise(resolve => setTimeout(() => {
console.log(1);
resolve();
}, 1000));
await new Promise(resolve => setTimeout(() => {
console.log(2);
resolve();
}, 1000));
}
fn();
2. Promise 的常用方法有哪些?(all/race/allSettled)
解答:
Promise.all([p1, p2, p3]):- 所有 Promise 都成功才返回成功结果数组;
- 只要有一个失败,立即返回失败结果;
- 场景:并行请求多个接口,全部成功后再处理(比如同时加载图片 + 数据)。
Promise.race([p1, p2, p3]):- 只要有一个 Promise 完成(成功 / 失败),就返回该结果;
- 场景:请求超时处理(比如接口 5 秒没返回就提示超时)。
Promise.allSettled([p1, p2, p3]):- 所有 Promise 都完成(无论成功 / 失败),返回所有结果(包含状态和值);
- 场景:需要知道所有请求的结果(比如批量提交表单,不管成功失败都要统计)。
3. 什么是事件循环(Event Loop)?宏任务和微任务的区别?
事件循环:JS 单线程处理异步任务的一套执行机制,核心流程:
- 执行同步代码(主线程);
- 遇到异步任务,分 "宏任务" 和 "微任务" 加入对应队列;
- 同步代码执行完后,先清空所有微任务队列;
- 执行一次宏任务,再清空所有微任务队列;
- 重复步骤 3-4(循环)。
- 宏任务(Macrotask) :
- 常见:
setTimeout、setInterval、DOM 事件、AJAX 请求、script 整体代码;
- 常见:
- 微任务(Microtask) :
- 常见:
Promise.then/catch/finally、async/await(本质是 Promise)、queueMicrotask;
- 常见:
四、原型与继承(核心原理)
1. 什么是原型和原型链?
解答:
-
原型(prototype) :
- 每个函数(除箭头函数)都有
prototype属性(原型对象),包含该函数实例共享的属性 / 方法; - 每个实例对象都有
__proto__属性,指向其构造函数的prototype;
- 每个函数(除箭头函数)都有
-
原型链 :
- 当访问对象的属性 / 方法时,先找自身,找不到就通过
__proto__找构造函数的原型,再找不到就找原型的原型,直到Object.prototype(顶端),找不到则返回undefined;
- 当访问对象的属性 / 方法时,先找自身,找不到就通过
-
示例:
javascriptfunction Person(name) { this.name = name; } Person.prototype.sayHi = function() { console.log(`Hi, ${this.name}`); }; const p = new Person('张三'); p.sayHi(); // Hi, 张三(p 自身没有 sayHi,通过 __proto__ 找到 Person.prototype) p.toString(); // 找到 Object.prototype.toString(原型链顶端)
2. ES6 的 class 继承和 ES5 的原型继承有什么区别?
ES5 原型继承 :通过修改 __proto__ 或 prototype 实现,代码繁琐,易出错;
ES6 class 继承:
- 语法糖(底层还是原型),更简洁,接近传统面向对象;
- 用
class定义类,extends继承,super()调用父类构造函数;
核心区别:ES6 class 更规范、易读,避免了 ES5 手动修改原型的坑(比如忘记修正 constructor)
五、DOM/BOM 操作(实战)
1. 如何获取 / 操作 DOM 元素?常用的 DOM 操作方法有哪些?
解答:
获取 DOM 元素:
javascript
// 常用方法
document.getElementById('id'); // 通过 ID(唯一)
document.getElementsByClassName('class'); // 通过类名(伪数组)
document.getElementsByTagName('div'); // 通过标签名(伪数组)
document.querySelector('.class'); // 通过选择器(返回第一个)
document.querySelectorAll('#id'); // 通过选择器(伪数组)
- 操作 DOM 元素:
- 修改内容:
element.innerText(纯文本)、element.innerHTML(可解析 HTML); - 修改样式:
element.style.color = 'red'(行内样式)、element.classList.add('active')(类名);
2. 事件委托(事件代理)是什么?有什么好处?
解答:
- 事件委托 :利用事件冒泡 ,把子元素的事件绑定到父元素上,通过
event.target判断触发事件的子元素; - 好处:
- 减少事件绑定数量,提升性能(比如 1000 个 li 只绑定 1 个事件);
- 新增子元素无需重新绑定事件(动态渲染的列表尤其适用)。
六、性能优化(加分项)
1. JS 常用的性能优化手段有哪些?
解答:
- 代码层面 :
- 减少不必要的 DOM 操作(DOM 操作是性能瓶颈,缓存 DOM 节点);
- 避免频繁重排 / 重绘(比如批量修改样式,先隐藏元素再修改);
- 防抖(debounce)/ 节流(throttle)处理高频事件(比如 resize、scroll、输入框输入);
- 加载层面:
- 懒加载(图片、组件、JS 文件):可视区域内再加载;
- 代码分割(Code Splitting):把大文件拆分成小文件,按需加载;
- 缓存(localStorage/sessionStorage 缓存数据,避免重复请求)。
2. 什么是内存泄漏?JS 中常见的内存泄漏场景?
- 内存泄漏:不再使用的变量 / 对象无法被垃圾回收,持续占用内存,导致页面卡顿、崩溃;
- 常见场景 :
- 未清理的定时器 / 事件监听(比如
setInterval未清除,DOM 元素删除后事件未解绑); - 闭包滥用(保留不必要的变量);
- 全局变量(比如
a = 1未声明,挂载到 window 上,不会被回收); - 未清理的 DOM 引用(比如
let div = document.getElementById('div'); div.remove();,但变量 div 仍引用该节点);
- 未清理的定时器 / 事件监听(比如
- 解决:不用的变量手动赋值
null,清除定时器 / 事件监听。