Skip to content

第 03 章 · JavaScript 入门面试题集


Q1:varletconst 的区别?

考察点:变量声明、作用域、变量提升

标准答案

维度varletconst
作用域函数级 / 全局块级 {}块级 {}
变量提升提升且初始化为 undefined提升但 TDZ(暂时性死区)同 let
重复声明允许不允许不允许
重新赋值允许允许不允许(基本类型)
是否绑定到全局对象是(var xwindow.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 种引用类型

原始类型引用类型
numberobject
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:什么是闭包?有什么用?

考察点:作用域、闭包

标准答案闭包:函数 + 它定义时所处的词法环境。即使函数在外部被调用,它仍能访问"老家"的变量。

形成条件

  1. 函数嵌套函数;
  2. 内层函数引用了外层函数的变量;
  3. 内层函数被外部"带走"(return / 赋值给全局变量 / 当回调)。

用途

  1. 数据私有化(变量"藏起来");
  2. 函数工厂(生成定制函数);
  3. 保留状态(计数器、防抖节流的 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:forEachmapfilterreduce 区别和用途?

考察点:数组方法

标准答案

方法用途返回能否 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 个方法都不修改原数组(纯函数风格)。
  • 对应改原数组的方法:splicesortreversefillcopyWithin
  • for...offor 能 break,性能也更好;要"早退"就别用 forEach。
  • React/Vue 列表渲染常用 map 把数据转 JSX/VNode。

追问

  • reduce 不传初始值会怎样?
  • mapforEach 性能差异?
  • forEach 能改原数组吗?(能,回调里改原数组的引用元素)

Q7:nullundefined 区别?

考察点:基础概念

标准答案

维度undefinednull
含义声明了但没赋值主动赋值的"空"
是谁给的JS 引擎默认程序员显式
typeof"undefined""object"(历史 bug)
==null == undefined 是 true同上
===不等不等
转 numberNaN0

通俗理解

  • 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 是单线程——同一时刻只能做一件事。但浏览器需要同时处理用户输入、定时器、网络请求……怎么办?

答案就是事件循环

  1. 主线程同步执行**调用栈(Call Stack)**里的代码。
  2. 异步任务(setTimeout、fetch、点击事件等)由浏览器另起线程处理,完成后把回调放进任务队列
  3. 调用栈空了,事件循环从队列里取一个回调放回栈执行。
  4. 任务队列分两种:
    • 宏任务(MacroTask):setTimeout、setInterval、I/O、UI 渲染
    • 微任务(MicroTask):Promise.then、queueMicrotask、MutationObserver
  5. 每次执行完一个宏任务后,清空所有微任务,再执行下一个宏任务。

通俗理解: 你(主线程)一个人开奶茶店——

  • 同步任务:你正在做的奶茶。
  • 异步任务(宏任务):交给"自动煮茶机"煮,煮好它把订单放进普通取餐架
  • 微任务:紧急加急订单,专门一个台子,每做完一杯就要看一下。
  • 事件循环:你的工作流程——做完手上的一杯,先清光"加急台",再去普通取餐架取一杯继续做。

代码示例

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 区别?
  • 重排和重绘有什么区别?
  • 事件冒泡和事件捕获是什么?怎么阻止?