主题
第 03 章 · JavaScript 入门面试题集
Q1:var、let、const 的区别?
考察点:变量声明、作用域、变量提升
标准答案:
| 维度 | var | let | const |
|---|---|---|---|
| 作用域 | 函数级 / 全局 | 块级 {} | 块级 {} |
| 变量提升 | 提升且初始化为 undefined | 提升但 TDZ(暂时性死区) | 同 let |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许(基本类型) |
| 是否绑定到全局对象 | 是(var x → window.x) | 否 | 否 |
通俗理解:
var像 90 年代的开放式办公室——大家都能看到、都能改、还能重复挂同一个名牌。let像现代办公室——你只能在自己房间里看到自己的东西。const像挂上去就拧死的门牌——可以装修房间内部,但门牌不能换。
代码示例:
js
// var:函数作用域
function f() {
if (true) { var x = 1; }
console.log(x); // 1
}
// let:块作用域
function g() {
if (true) { let y = 1; }
console.log(y); // ❌ ReferenceError
}
// const:不可重新赋值
const arr = [1, 2];
arr.push(3); // ✅ 修改内部可以
arr = [4]; // ❌ 不能重新指向加分回答:
const锁的是引用,不是值。对象/数组的内部还能改。- 暂时性死区(TDZ):声明前访问会报错,让你"早点暴露 bug"。
- 现代项目几乎不用
var,全用const+let,配合 ESLint 规则。
追问:
- 什么是变量提升?
- 什么是暂时性死区?
- 为什么
for (let i)在异步循环里能正确输出?
Q2:== 和 === 有什么区别?
考察点:类型转换、相等判断
标准答案:
==(宽松相等):比较前先做类型转换,规则非常复杂(有专门的 ToPrimitive、ToNumber 流程)。===(严格相等):不做类型转换,类型不同直接 false。
通俗理解:
==像"模糊匹配",'5' == 5是 true。===像"精确匹配",'5' === 5是 false。
代码示例:
js
1 == '1' // true
1 === '1' // false
null == undefined // true
null === undefined // false
NaN == NaN // false(NaN 和谁都不等)
NaN === NaN // false
[] == false // true
[] === false // false加分回答:
==唯一合理的用法是判断 null/undefined:x == null等价于x === null || x === undefined。- 判断 NaN 用
Number.isNaN(x)或Object.is(NaN, NaN)(注意不是全局的isNaN,那个会先转类型)。 Object.is是更"严格"的相等:Object.is(NaN, NaN)是 true,Object.is(+0, -0)是 false。
追问:
[] == ![]是 true 还是 false?为什么?- 怎么准确判断 NaN?
Q3:JavaScript 的数据类型有哪些?怎么判断?
考察点:数据类型基础
标准答案: 8 种:7 种原始类型 + 1 种引用类型。
| 原始类型 | 引用类型 |
|---|---|
| number | object |
| string | (function、array 都是 object 的子集) |
| boolean | |
| undefined | |
| null | |
| bigint | |
| symbol |
判断方式:
js
typeof 42 // "number"
typeof "hi" // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ⚠️
typeof [] // "object" ⚠️
typeof {} // "object"
typeof function(){} // "function"
// 更精准的判断(数组、Date、RegExp 等)
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
Array.isArray([]); // true通俗理解: 原始类型像现金(直接拿走就是值),引用类型像支票(拿走的只是张纸,钱在银行)。
代码示例:
js
function getType(x) {
return Object.prototype.toString.call(x).slice(8, -1).toLowerCase();
}
getType([]); // 'array'
getType(null); // 'null'
getType(/x/); // 'regexp'加分回答:
typeof null === 'object'是历史遗留 bug(1995 年的 JS 内部用低 3 位表示类型,object 是 000,null 全是 0)。- BigInt 用于超过
Number.MAX_SAFE_INTEGER(2^53 - 1)的整数。 - Symbol 主要用于"独一无二的属性 key",避免冲突。
追问:
- 怎么判断一个变量是数组?
- 原始类型和引用类型在内存中的存储区别?
- 什么是装箱(boxing)?
Q4:什么是闭包?有什么用?
考察点:作用域、闭包
标准答案: 闭包:函数 + 它定义时所处的词法环境。即使函数在外部被调用,它仍能访问"老家"的变量。
形成条件:
- 函数嵌套函数;
- 内层函数引用了外层函数的变量;
- 内层函数被外部"带走"(return / 赋值给全局变量 / 当回调)。
用途:
- 数据私有化(变量"藏起来");
- 函数工厂(生成定制函数);
- 保留状态(计数器、防抖节流的 timer、模块模式)。
通俗理解: 你出国留学,但你的"家"(妈妈做的小笼包)还在记忆里——不管走多远都带着。
代码示例:
js
// 1. 计数器
function makeCounter() {
let count = 0; // 私有变量
return {
inc: () => ++count,
get: () => count
};
}
const c = makeCounter();
c.inc(); c.inc();
c.get(); // 2
// c.count // undefined(外部访问不到)
// 2. 防抖(闭包保存 timer)
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}加分回答:
- 闭包让局部变量无法被垃圾回收,用得不当会内存泄漏。
- React 的 Hook(
useState)底层就是闭包:每次 render 都形成新的闭包,函数组件能"记住"状态。 - 经典坑:
for (var i)+ setTimeout 会输出多个相同的 i,因为所有回调共享同一个 i;let形成块级作用域,每轮一个新 i,等价于自动闭包。
追问:
- 闭包会引起什么问题?
- 如何避免闭包导致的内存泄漏?
- React 的 useState 为什么是闭包?
Q5:箭头函数和普通函数有什么区别?
考察点:函数、this 绑定
标准答案:
| 维度 | 普通函数 | 箭头函数 |
|---|---|---|
this | 调用时决定(动态) | 词法绑定(继承外层) |
arguments | 有 | 无(用 ...args) |
| 能否当构造函数 | 能(new) | 不能 |
能否用 call/apply/bind 改 this | 能 | 不能(this 已锁定) |
是否有 prototype | 有 | 无 |
| 写法 | function(){...} | () => {...} |
通俗理解:
- 普通函数:到哪一桌就喊"老板"——
this是当前这一桌。 - 箭头函数:从家里出来时已经认准了"妈妈"——不管去哪桌,
this永远是当时定义时的那个。
代码示例:
js
const obj = {
name: '张三',
// 普通函数:this 是 obj
greet1: function() { return this.name; },
// 箭头函数:this 是外层(全局),name 找不到
greet2: () => this.name
};
obj.greet1(); // '张三'
obj.greet2(); // undefined
// 实战:定时器内回调要拿外层 this,用箭头函数
class Timer {
constructor() { this.count = 0; }
start() {
setInterval(() => { this.count++; }, 1000); // ✅ this 是实例
// setInterval(function() { this.count++ }, 1000); // ❌ this 是 undefined/window
}
}加分回答:
- 对象方法不要写成箭头函数(this 错乱)。
- 想要"this 永远指向定义环境"才用箭头函数(如回调、定时器)。
- 箭头函数没有
prototype,不能 new;用new ArrowFn()直接报错。 - React Class 组件以前要 bind 方法,改写成箭头函数(class field)就能省掉 bind。
追问:
- 怎么改变普通函数的 this?(call / apply / bind)
- 箭头函数能用 yield 吗?(不能,箭头函数不能当生成器)
Q6:forEach、map、filter、reduce 区别和用途?
考察点:数组方法
标准答案:
| 方法 | 用途 | 返回 | 能否 break |
|---|---|---|---|
forEach(cb) | 单纯遍历执行副作用 | undefined | ❌ |
map(cb) | 一对一映射成新数组 | 新数组(同长度) | ❌ |
filter(cb) | 筛选满足条件的项 | 新数组(短或同长) | ❌ |
reduce(cb, init) | 累计成单个值 | 任意类型 | ❌ |
通俗理解:
forEach:挨个看一遍,啥也不返回。map:流水线打包——一个零件一个新成品。filter:过滤网——只留合格品。reduce:累加器——把全部零件组装成最终产品。
代码示例:
js
const nums = [1, 2, 3, 4, 5];
nums.forEach(n => console.log(n)); // 打印
const doubled = nums.map(n => n * 2); // [2,4,6,8,10]
const evens = nums.filter(n => n % 2 === 0); // [2, 4]
const sum = nums.reduce((s, n) => s + n, 0); // 15
// 链式:偶数 → 翻倍 → 求和
const result = nums.filter(n => n % 2 === 0).map(n => n * 2).reduce((s, n) => s + n, 0);
// 4 + 8 = 12
// reduce 高级用法:分组
const list = [{ type: 'a', v: 1 }, { type: 'b', v: 2 }, { type: 'a', v: 3 }];
const grouped = list.reduce((g, item) => {
(g[item.type] ??= []).push(item);
return g;
}, {});
// { a: [...], b: [...] }加分回答:
- 这 4 个方法都不修改原数组(纯函数风格)。
- 对应改原数组的方法:
splice、sort、reverse、fill、copyWithin。 for...of和for能 break,性能也更好;要"早退"就别用 forEach。- React/Vue 列表渲染常用
map把数据转 JSX/VNode。
追问:
reduce不传初始值会怎样?map和forEach性能差异?forEach能改原数组吗?(能,回调里改原数组的引用元素)
Q7:null 和 undefined 区别?
考察点:基础概念
标准答案:
| 维度 | undefined | null |
|---|---|---|
| 含义 | 声明了但没赋值 | 主动赋值的"空" |
| 是谁给的 | JS 引擎默认 | 程序员显式 |
| typeof | "undefined" | "object"(历史 bug) |
== | null == undefined 是 true | 同上 |
=== | 不等 | 不等 |
| 转 number | NaN | 0 |
通俗理解:
undefined像"空盒子上没贴标签"——还没人定义里面是啥。null像"空盒子上贴了'空'标签"——有人特意说"这里啥都没有"。
代码示例:
js
let x;
console.log(x); // undefined(声明未赋值)
const y = null; // 显式空值
function f(a) {
console.log(a); // undefined(参数没传)
}
f();
const obj = { a: 1 };
console.log(obj.b); // undefined(属性不存在)加分回答:
undefined在 ES5 之前可以被赋值,ES5 后变成只读。- API 设计上"找不到东西"建议返回
null(明确);变量未初始化是undefined(自然)。 - JSON 不支持 undefined:
JSON.stringify({ a: undefined })得到"{}",null 会保留。 - 可选链
?.在遇到 null/undefined 时会短路返回 undefined。
追问:
- 为什么
typeof null === 'object'? void 0是干嘛的?(永远返回 undefined,比直接写 undefined 安全)
Q8:什么是事件循环(Event Loop)?
考察点:异步、单线程模型(入门级)
标准答案: JS 是单线程——同一时刻只能做一件事。但浏览器需要同时处理用户输入、定时器、网络请求……怎么办?
答案就是事件循环:
- 主线程同步执行**调用栈(Call Stack)**里的代码。
- 异步任务(setTimeout、fetch、点击事件等)由浏览器另起线程处理,完成后把回调放进任务队列。
- 调用栈空了,事件循环从队列里取一个回调放回栈执行。
- 任务队列分两种:
- 宏任务(MacroTask):setTimeout、setInterval、I/O、UI 渲染
- 微任务(MicroTask):Promise.then、queueMicrotask、MutationObserver
- 每次执行完一个宏任务后,清空所有微任务,再执行下一个宏任务。
通俗理解: 你(主线程)一个人开奶茶店——
- 同步任务:你正在做的奶茶。
- 异步任务(宏任务):交给"自动煮茶机"煮,煮好它把订单放进普通取餐架。
- 微任务:紧急加急订单,专门一个台子,每做完一杯就要看一下。
- 事件循环:你的工作流程——做完手上的一杯,先清光"加急台",再去普通取餐架取一杯继续做。
代码示例:
js
console.log('1');
setTimeout(() => console.log('2'), 0); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
console.log('4');
// 输出顺序:1 4 3 2
// 原因:1、4 同步先;微任务 3 在主任务结束后立刻执行;2 是宏任务最后加分回答:
- Node.js 的事件循环和浏览器有差异(process.nextTick、setImmediate)。
async/await本质就是 Promise + 微任务。- 长时间的同步代码会阻塞渲染(页面卡住),要拆成宏任务(setTimeout 0)让出主线程。
requestAnimationFrame在每一帧绘制前执行,不属于宏/微任务,单独的渲染时机。
追问:
- async/await 的执行顺序怎么算?
- 怎么把一个耗时操作"切片"避免卡顿?
- Web Worker 解决了什么问题?
Q9:什么是浅拷贝和深拷贝?怎么实现?
考察点:引用类型、内存
标准答案:
- 浅拷贝:只复制第一层;嵌套对象/数组仍共享引用。
- 深拷贝:完全独立的副本,所有层级递归复制,互不影响。
浅拷贝方法:
js
const a = { x: 1, nested: { y: 2 } };
const b1 = { ...a }; // 展开
const b2 = Object.assign({}, a); // assign
const b3 = a.slice(); // 数组用 slice
b1.nested.y = 999;
console.log(a.nested.y); // 999 ⚠️ 受影响深拷贝方法:
js
// 方法 1:JSON(最常用,但有局限)
const c = JSON.parse(JSON.stringify(a));
// ⚠️ 丢失:function、undefined、Symbol;
// ⚠️ 转换:Date → 字符串、RegExp → {}、循环引用报错
// 方法 2:structuredClone(推荐,浏览器 / Node 17+ 内置)
const d = structuredClone(a);
// 方法 3:手写递归
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (map.has(obj)) return map.get(obj); // 处理循环引用
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}
return clone;
}
// 方法 4:第三方库 lodash.cloneDeep通俗理解:
- 浅拷贝:复印一张菜单,但价格表还共享一份——你改原件价格,复印件也跟着变。
- 深拷贝:连价格表都重印一份——两边互不干扰。
加分回答:
structuredClone是 2022 年才内置的"标准答案",支持 Date、Map、Set、Blob、循环引用,但不支持 function 和 DOM 节点。- React 的状态更新要"不可变",所以经常用展开
{...state, x: 1}做浅拷贝。 - 深拷贝大对象有性能成本,能浅拷贝就别深拷贝。
追问:
- JSON 深拷贝有什么坑?
- 如何处理循环引用?
- 不可变数据结构(Immutable.js)解决了什么?
Q10:DOM 是什么?怎么操作 DOM?
考察点:DOM API 基础
标准答案: DOM(Document Object Model,文档对象模型):浏览器把 HTML 解析成一棵树形对象结构,每个标签是一个节点(Node),JS 通过 DOM API 操作页面。
树形结构:
document
└── html
├── head
│ └── title
└── body
├── h1
└── p常用 API:
js
// 获取元素
document.getElementById('id')
document.querySelector('.class') // 第一个
document.querySelectorAll('div') // NodeList(所有)
// 创建 / 插入
const div = document.createElement('div');
div.textContent = '你好'; // 安全(不解析 HTML)
div.innerHTML = '<b>粗体</b>'; // 解析 HTML(注意 XSS)
document.body.appendChild(div);
parent.insertBefore(newNode, refNode);
parent.append(node1, node2); // 现代 API
// 修改
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('selected');
el.setAttribute('data-id', '5');
el.style.color = 'red';
// 删除
el.remove(); // 现代
parent.removeChild(child); // 老写法
// 事件
el.addEventListener('click', e => {
console.log('clicked', e.target);
});
el.removeEventListener('click', handler);通俗理解: HTML 是"装修图纸",DOM 是"3D 模型"——JS 拿着 3D 模型可以拆墙、加家具、刷漆,浏览器看到模型变了就重新画给用户。
代码示例:
js
// 给所有按钮加点击事件,记录点击次数
document.querySelectorAll('button').forEach(btn => {
let count = 0;
btn.addEventListener('click', () => {
count++;
btn.textContent = `已点击 ${count} 次`;
});
});加分回答:
innerHTML会解析为 HTML,有 XSS 风险——展示用户输入要用textContent。- 频繁操作 DOM 会触发重排(reflow)和重绘(repaint),性能差,可批量修改或用
documentFragment。 - 现代框架(React/Vue)用虚拟 DOM 隔离真实 DOM 操作:先在内存里 diff,最后批量应用。
- 事件委托:父元素监听子元素事件,减少监听器数量。
追问:
- innerHTML 和 textContent 区别?
- 重排和重绘有什么区别?
- 事件冒泡和事件捕获是什么?怎么阻止?