大家好,我是鱼樱!!!
关注公众号【鱼樱AI实验室】
持续每天分享更多前端和AI辅助前端编码新知识~~
一个城市淘汰的自由职业-农村前端程序员(虽然不靠代码挣钱,写文章就是为爱发电),兼职远程上班目前!!!热心坚持分享36道有用的JS面试题~~~
数据类型
1. JS 中有哪些基本数据类型?
答案:
-
基本数据类型(Primitive Types) :
Undefined
:声明未赋值的变量Null
:空值Boolean
:true/false
Number
:整数/浮点数(包括Infinity
,NaN
)String
:字符串BigInt
:大整数(ES2020)Symbol
:唯一值(ES6)
-
引用类型(Reference Type) :
Object
(包含Array
,Function
,Date
等)
解析:
- 基本类型存储在栈内存,按值访问。
- 引用类型存储在堆内存,按引用访问。
typeof null
返回"object"
(历史遗留问题)。
2. 如何判断数据类型?
答案:
typeof variable
:返回类型字符串,无法区分数组/对象。variable instanceof Constructor
:检测原型链。Object.prototype.toString.call(variable)
:最准确方式。
示例:
js
console.log(Object.prototype.toString.call([])); // [object Array]
console.log([] instanceof Array); // true
变量与作用域
3. var、let、const 的区别?
答案:
特性 | var | let | const |
---|---|---|---|
作用域 | 函数级 | 块级 | 块级 |
变量提升 | 是 | 否(TDZ) | 否(TDZ) |
重复声明 | 允许 | 禁止 | 禁止 |
重新赋值 | 允许 | 允许 | 不允许 |
解析:
- TDZ(暂时性死区) :声明前访问会报错。
- const 必须初始化,但对象属性可修改。
闭包
4. 什么是闭包?应用场景?
答案: 闭包是能够访问其他函数内部变量的函数。常见场景:
- 封装私有变量
- 模块化开发
- 函数柯里化
示例:
js
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // 1
解析: 闭包会导致外部函数的作用域无法释放,可能引发内存泄漏。
原型与继承
5. 描述原型链机制
答案:
- 每个对象都有
__proto__
指向构造函数的prototype
。 - 访问属性时,若对象本身没有,则沿原型链查找。
- 终点是
Object.prototype.__proto__
(null
)。
示例:
js
function Person(name) { this.name = name; }
Person.prototype.sayName = function() { console.log(this.name); };
const p = new Person('Alice');
p.sayName(); // 通过原型链调用
异步编程
6. 事件循环(Event Loop)机制
答案:
- JS 是单线程,通过事件循环处理异步。
- 调用栈执行同步任务。
- 异步任务进入任务队列 (宏任务:
setTimeout
,微任务:Promise.then
)。 - 每次循环先清空微任务队列,再执行一个宏任务。
示例:
js
setTimeout(() => console.log('宏任务'), 0);
Promise.resolve().then(() => console.log('微任务'));
// 输出顺序:微任务 → 宏任务
ES6+ 新特性
7. 箭头函数与普通函数的区别?
答案:
- 语法更简洁(
() => {}
)。 - 没有自己的
this
,继承外层。 - 不能作为构造函数。
- 没有
arguments
对象,可用剩余参数...args
。
其他重要概念
8. 深拷贝 vs 浅拷贝
答案:
- 浅拷贝 :复制一层属性(
Object.assign
, 展开运算符)。 - 深拷贝 :递归复制所有层级(
JSON.parse(JSON.stringify(obj))
,手动递归,lodash.cloneDeep
)。
注意: JSON
方法会忽略undefined
和函数。
Promise 实现
9. 手写 Promise 的核心实现
答案:
js
class MyPromise {
constructor(executor) {
this.state = 'pending'; // pending/fulfilled/rejected
this.value = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn());
};
const reject = (reason) => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.value = reason;
this.onRejectedCallbacks.forEach(fn => fn());
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handleCallback = (callback, value) => {
try {
const result = callback?.(value);
result instanceof MyPromise ?
result.then(resolve, reject) :
resolve(result);
} catch (err) {
reject(err);
}
};
if (this.state === 'fulfilled') {
setTimeout(() => handleCallback(onFulfilled, this.value));
} else if (this.state === 'rejected') {
setTimeout(() => handleCallback(onRejected, this.value));
} else { // pending
this.onResolvedCallbacks.push(() =>
setTimeout(() => handleCallback(onFulfilled, this.value)));
this.onRejectedCallbacks.push(() =>
setTimeout(() => handleCallback(onRejected, this.value)));
}
});
}
}
解析:
- 状态不可逆 :Promise 状态只能从
pending
变更为fulfilled
或rejected
。 - 链式调用 :
.then
返回新 Promise,支持链式调用。 - 异步执行 :回调函数通过
setTimeout
模拟微任务队列(实际 Promise 使用微任务)。
防抖与节流
10. 防抖(Debounce)与节流(Throttle)的区别与实现
区别:
防抖 | 节流 | |
---|---|---|
核心 | 事件结束后触发 | 固定时间间隔触发 |
场景 | 搜索框输入联想 | 滚动事件、窗口调整 |
实现 | 每次触发重置定时器 | 通过时间戳或定时器控制频率 |
防抖实现:
js
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
节流实现(时间戳版):
js
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
this 指向
11. 如何确定函数中的 this
?
规则优先级:
- new 绑定 :
new Foo()
→this
指向新对象。 - 显式绑定 :
call/apply/bind
→ 指向指定对象。 - 隐式绑定 :
obj.fn()
→ 指向调用者obj
。 - 默认绑定 :全局对象(严格模式下为
undefined
)。 - 箭头函数 :继承外层作用域的
this
。
示例:
js
const obj = {
name: 'Alice',
sayName: function() { console.log(this.name) },
sayNameArrow: () => console.log(this.name)
};
obj.sayName(); // 'Alice'(隐式绑定)
obj.sayNameArrow(); // undefined(箭头函数继承全局 this)
模块化
12. CommonJS 与 ES Module 的区别
特性 | CommonJS | ES Module |
---|---|---|
加载方式 | 同步加载(动态导入) | 异步加载(静态解析) |
输出 | module.exports |
export /export default |
导入 | require() |
import |
执行顺序 | 运行时解析 | 编译时解析(静态) |
适用场景 | Node.js 环境 | 浏览器/现代前端框架 |
CommonJS 示例:
js
// math.js
module.exports = { add: (a, b) => a + b };
// app.js
const math = require('./math.js');
math.add(2, 3); // 5
ES Module 示例:
js
// math.js
export const add = (a, b) => a + b;
// app.js
import { add } from './math.js';
add(2, 3); // 5
POST 与 GET 请求的区别
13. 核心区别
特性 | GET | POST |
---|---|---|
参数位置 | URL 查询字符串(?key=value ) |
请求体(Body) |
安全性 | 参数暴露在 URL 中 | 参数在请求体中,相对安全 |
缓存 | 可被缓存 | 默认不缓存 |
幂等性 | 幂等(多次请求结果相同) | 非幂等(可能修改数据) |
长度限制 | 受浏览器 URL 长度限制 | 无限制 |
使用场景:
- GET:获取数据(如搜索、分页)。
- POST:提交数据(如登录、表单提交)。
重绘(Repaint)与重排(Reflow)的区别
14. 浏览器渲染机制中的关键概念
重绘(Repaint) | 重排(Reflow) | |
---|---|---|
定义 | 元素外观变化(颜色、背景等) | 布局或几何属性变化(尺寸、位置等) |
触发 | color , visibility , background |
width , height , margin , display |
性能 | 开销较小 | 开销较大(可能引发整个文档重排) |
优化建议:
- 避免频繁操作 DOM,使用
DocumentFragment
或虚拟 DOM。 - 使用 CSS3 动画(
transform
、opacity
)代替直接修改布局属性。 - 批量修改样式(通过
class
切换而非逐行修改)。
深入核心概念
15. 异步编程的常见实现方式?
答案:
- 回调函数:传统异步方案,但易导致"回调地狱"。
- Promise:链式调用,解决嵌套问题。
- async/await:基于 Promise 的语法糖,以同步方式写异步代码。
- Generator 函数 :通过
yield
暂停执行(需配合执行器如co
库)。 - 事件监听 :如
EventEmitter
模式。
示例(async/await):
js
async function fetchData() {
try {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
console.log(data);
} catch (err) {
console.error('请求失败:', err);
}
}
16. 原型链的实际应用场景?
答案:
- 继承机制 :通过
prototype
实现对象间属性和方法的共享。 - 内置方法扩展 :如为
Array
添加自定义方法。 - 性能优化:共享方法减少内存占用。
示例(方法扩展):
js
Array.prototype.last = function() {
return this[this.length - 1];
};
console.log([1, 2, 3].last()); // 3
17. 闭包的内存管理问题?
答案:
-
内存泄漏风险:闭包会长期持有外部函数变量的引用,导致无法被垃圾回收。
-
解决方案:
- 及时解除引用:
fn = null
- 避免循环引用
- 使用弱引用(如
WeakMap
)
- 及时解除引用:
JS 延迟加载方式
18. 如何实现 JS 延迟加载?
答案:
方法 | 说明 |
---|---|
defer 属性 | 异步下载,HTML 解析完成后按顺序执行(适用于依赖 DOM 的脚本) |
async 属性 | 异步下载,下载完成后立即执行(适用于独立无依赖的脚本) |
动态脚本插入 | 通过 document.createElement('script') 动态加载 |
setTimeout 延迟 | 延迟执行代码(不推荐,可能阻塞渲染) |
最佳实践:
js
<script defer src="app.js"></script>
<script async src="analytics.js"></script>
call、apply、bind 的区别
19. 三者核心区别与使用场景?
答案:
方法 | 传参方式 | 执行时机 | 使用场景 |
---|---|---|---|
call |
参数逐个传递 (a, b ) |
立即执行 | 明确参数数量时 |
apply |
参数数组传递 ([a, b] ) |
立即执行 | 参数数量不确定(如数组处理) |
bind |
参数逐个传递 | 返回绑定后的函数 | 需要延迟执行或作为回调 |
示例:
js
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const user = { name: 'Alice' };
greet.call(user, 'Hello'); // Hello, Alice
greet.apply(user, ['Hi']); // Hi, Alice
const boundGreet = greet.bind(user, 'Hey');
boundGreet(); // Hey, Alice
new 操作符
20. new 操作符的底层实现步骤?
答案:
- 创建空对象 :
const obj = {}
- 链接原型 :
obj.__proto__ = Constructor.prototype
- 绑定 this :执行构造函数,
this
指向新对象 - 返回对象 :若构造函数返回对象则使用该对象,否则返回
obj
手动实现 new
:
js
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
"use strict" 严格模式
21. 严格模式的主要作用?
答案:
- 消除静默错误 :未声明变量赋值会报错(
a = 10
→ReferenceError
) - 禁止删除不可删除属性 :
delete Object.prototype
→ 报错 - 函数参数不能重名 :
function(a, a) {}
→ 报错 - this 默认不指向全局 :未绑定的
this
为undefined
- 禁用
with
语句:避免作用域混乱
示例:
js
'use strict';
function test() {
console.log(this); // undefined(非严格模式为 window)
}
test();
事件冒泡与事件委托
22. 事件传播机制与委托优化
答案:
- 事件冒泡 :事件从目标元素向上传播到根元素(
<div> → <body> → <html>
)。 - 事件捕获:从根元素向下传播到目标元素(与冒泡相反)。
- 事件委托 :利用冒泡机制,将事件监听器绑定到父元素,通过
event.target
处理子元素事件。
示例(委托优化列表点击):
js
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
console.log('点击了:', e.target.textContent);
}
});
优点:
- 减少内存消耗(无需为每个子元素绑定监听器)
- 动态新增元素无需重新绑定
JavaScript 作用域链
23. 作用域链的查找规则?
答案:
- 词法作用域:函数定义时确定作用域链,与调用位置无关。
- 链式结构:当前作用域 → 外层作用域 → ... → 全局作用域。
- 变量查找 :沿作用域链逐级向上查找,未找到则报
ReferenceError
。
示例:
js
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function inner() {
console.log(outerVar); // 'outer'
console.log(globalVar); // 'global'
}
inner();
}
outer();
堆(Heap)与栈(Stack)的区别
24. 内存管理中的堆与栈
答案:
栈(Stack) | 堆(Heap) | |
---|---|---|
存储内容 | 基本类型值、函数调用上下文(指针) | 引用类型对象(复杂数据结构) |
分配方式 | 系统自动分配/释放(FILO) | 手动分配(开发者申请,GC回收) |
访问速度 | 快(内存连续) | 慢(内存碎片化) |
空间限制 | 固定大小(可能栈溢出) | 灵活(受物理内存限制) |
示例:
js
let a = 10; // 栈内存
let b = { name: 'Bob' }; // 对象存储在堆,栈中存储指针
25. 原型链继承的实现方式与优缺点
答案: 实现方式:
-
原型链继承:子类原型指向父类实例
jsfunction Parent() { this.name = 'Parent'; } Parent.prototype.say = function() { console.log(this.name); }; function Child() {} Child.prototype = new Parent(); // 继承 const child = new Child(); child.say(); // 'Parent'
- 缺点:所有子类实例共享父类引用属性(如数组)。
-
构造函数继承:在子类中调用父类构造函数
jsfunction Child() { Parent.call(this); // 继承实例属性 }
- 缺点:无法继承父类原型方法。
-
组合继承(原型链 + 构造函数):
jsfunction Child() { Parent.call(this); // 实例属性 } Child.prototype = new Parent(); // 原型方法 Child.prototype.constructor = Child;
- 缺点:父类构造函数被调用两次。
-
ES6 Class 继承:
jsclass Parent { constructor() { this.name = 'Parent'; } say() { console.log(this.name); } } class Child extends Parent { constructor() { super(); } } const child = new Child(); child.say(); // 'Parent'
总结 :ES6 class
语法糖最简洁,底层基于原型链。
26. 闭包内存管理的实际案例与解决方案
案例:
js
function createHeavyObject() {
const bigData = new Array(1000000).fill('data');
return function() { console.log(bigData[0]); };
}
const closure = createHeavyObject(); // bigData 无法释放
解决方案:
-
手动释放引用:
jsclosure = null; // 解除引用,触发垃圾回收
-
使用 WeakMap(弱引用):
jsconst wm = new WeakMap(); function createSafeClosure() { const bigData = new Array(1000000).fill('data'); wm.set(this, bigData); return () => console.log(wm.get(this)?.[0]); }
27. 事件循环(Event Loop)的深入解析
执行顺序规则:
- 同步任务:直接进入调用栈执行。
- 微任务 (
Promise.then
,MutationObserver
):在每一个宏任务结束后立即执行。 - 宏任务 (
setTimeout
,setInterval
, I/O):等待下一个事件循环。
示例:
js
console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
console.log('4'); // 同步
// 输出顺序:1 → 4 → 3 → 2
28. Promise 链式调用的错误处理
链式调用规则:
- 每个
.then
返回新 Promise。 - 使用
.catch
捕获链中任意位置的错误。
示例:
js
fetch('url')
.then(res => res.json())
.then(data => processData(data))
.catch(err => console.error('链中任何错误:', err));
async/await 优化:
js
async function fetchData() {
try {
const res = await fetch('url');
const data = await res.json();
return processData(data);
} catch (err) {
console.error('错误捕获:', err);
}
}
29. 渐进增强(Progressive Enhancement)与优雅降级(Graceful Degradation)
概念 | 核心思想 | 适用场景 |
---|---|---|
渐进增强 | 从基础功能开始,逐步增加高级功能(优先保证基础体验) | 面向未来、兼容旧浏览器 |
优雅降级 | 先实现完整功能,再处理兼容性问题(优先高级浏览器,降级适配低版本) | 快速开发、内部系统 |
示例:
- 渐进增强 :先实现
<input type="text">
,再通过 JavaScript 增强为日期选择器。 - 优雅降级:使用 CSS3 动画,低版本浏览器回退为 JavaScript 动画。
30. Web Worker 与 WebSocket 的区别
特性 | Web Worker | WebSocket |
---|---|---|
用途 | 多线程计算,避免阻塞主线程 | 实时双向通信(如聊天室、股票行情) |
通信方式 | 通过 postMessage 传递消息 |
基于 TCP 的全双工通信(ws:// 协议) |
生命周期 | 需手动终止 (worker.terminate() ) |
长连接,持续通信 |
DOM 访问 | 无法直接操作 DOM | 通常由主线程处理数据 |
31. JavaScript 垃圾回收机制
主要算法:
-
标记清除(Mark-Sweep) :
- 从根对象(全局变量、活动函数)出发,标记所有可达对象。
- 清除未标记对象。
-
引用计数(Reference Counting) :
- 记录每个对象的引用次数,次数为0时回收。
- 缺陷:循环引用无法回收(现代浏览器已弃用)。
内存泄漏场景:
- 未清理的定时器:
setInterval
- 未解除的 DOM 引用:
const element = document.getElementById('id')
- 闭包未释放
32. WeakSet 与 WeakMap 的特性
特性 | WeakSet/WeakMap | Set/Map |
---|---|---|
键类型 | 只接受对象作为键 | 任意类型 |
弱引用 | 键是弱引用,不计入垃圾回收机制 | 强引用,阻止垃圾回收 |
可迭代性 | 不可迭代 | 支持 for...of 遍历 |
用例 | 存储 DOM 节点(自动释放) | 通用数据存储 |
示例(WeakMap 缓存):
js
const cache = new WeakMap();
function getData(obj) {
if (cache.has(obj)) return cache.get(obj);
const data = /* 计算 */;
cache.set(obj, data);
return data;
}
33. Shadow DOM 的概念与作用
定义:Shadow DOM 是 Web Components 的核心技术之一,用于创建封装的 DOM 子树,与主文档 DOM 隔离。
特点:
- 样式封装:内部样式不影响外部,外部样式默认不影响内部。
- DOM 隔离 :通过
shadowRoot
访问内部 DOM。 - 自定义元素 :结合
customElements.define
创建可重用组件。
示例:
js
class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>p { color: red; }</style>
<p>Shadow DOM 内容</p>
`;
}
}
customElements.define('my-component', MyComponent);
34. 递归(Recursion)的应用与注意事项
定义:函数直接或间接调用自身,通过分解问题为更小的同类子问题来求解。
示例(阶乘):
js
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 递归调用
}
console.log(factorial(5)); // 120
注意事项:
- 终止条件:必须存在明确的递归结束条件。
- 性能问题:深度递归可能导致栈溢出(可用尾递归优化或循环替代)。
35. 纯函数、高阶函数与睡眠函数
纯函数(Pure Function) :
-
相同输入始终返回相同输出。
-
无副作用(不修改外部状态)。
-
示例:
jsfunction add(a, b) { return a + b; }
高阶函数(Higher-Order Function) :
-
接收函数作为参数或返回函数。
-
示例:
jsfunction map(arr, fn) { return arr.map(fn); }
睡眠函数(Sleep Function) :
-
模拟延迟执行,基于 Promise 实现。
-
示例:
jsfunction sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function demo() { await sleep(2000); console.log('2秒后输出'); }
36. 实现数组随机排序(Fisher-Yates 算法)
问题 :编写 randomSort
函数,实现均匀随机排序。
正确实现(避免 arr.sort(() => Math.random() - 0.5)
的不均匀问题) :
js
function randomSort(arr) {
const result = [...arr];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]]; // 交换
}
return result;
}
// 示例
console.log(randomSort([1, 2, 3, 4, 5])); // 随机顺序如 [3,1,5,2,4]
解析:
- Fisher-Yates 算法:从后向前遍历,保证每个位置的概率均匀。
- 时间复杂度:O(n),效率优于排序算法。