JavaScript 面试题汇总
1. 说说你对闭包的理解,以及闭包使用场景
参考答案:
什么是闭包
闭包是指有权访问另一个函数作用域中变量的函数。创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包的特性
- 函数内再嵌套函数
- 内部函数可以引用外层的参数和变量
- 参数和变量不会被垃圾回收机制回收
使用场景
- 创建私有变量
jsfunction createCounter() { let count = 0 return { increment: function() { count++ return count } } }
- 延长变量的生命周期
jsfunction makeAdder(x) { return function(y) { return x + y; }; }
- 柯里化函数
jsfunction curry(fn) { return function curried(...args) { if(args.length >= fn.length) { return fn.apply(this, args) } return function(...args2) { return curried.apply(this, args.concat(args2)) } } }
2. JavaScript 中的数据类型有哪些?如何判断?
参考答案:
基本数据类型
- Number
- String
- Boolean
- Undefined
- Null
- Symbol (ES6新增)
- BigInt (ES2020新增)
引用数据类型
- Object
- Array
- Function
- Date
- RegExp
判断方法
- typeof 操作符
jstypeof 123 // "number" typeof '123' // "string" typeof true // "boolean" typeof undefined // "undefined" typeof null // "object" typeof Symbol() // "symbol" typeof {} // "object" typeof [] // "object" typeof function(){} // "function"
- instanceof 操作符
js[] instanceof Array // true {} instanceof Object // true
- Object.prototype.toString.call()
jsObject.prototype.toString.call([]) // "[object Array]" Object.prototype.toString.call({}) // "[object Object]"
3. Promise 的理解和使用
参考答案:
Promise 是什么
Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。Promise 对象代表一个异步操作,三种状态:
- pending(进行中)
- fulfilled(已成功)
- rejected(已失败)
基本用法
jsconst promise = new Promise((resolve, reject) => { // 异步操作 if(/* 异步操作成功 */) { resolve(value) } else { reject(error) } }) promise.then(value => { // success }).catch(error => { // error })
Promise 的特点
- 对象的状态不受外界影响
- 一旦状态改变就不会再变
Promise 的方法
- Promise.all()
- Promise.race()
- Promise.resolve()
- Promise.reject()
- Promise.allSettled()
4. ES6 新特性有哪些?
参考答案:
- let 和 const
- 块级作用域
- 不存在变量提升
- 暂时性死区
- 不允许重复声明
- 箭头函数
- 更简洁的函数写法
- 不绑定this
- 不能用作构造函数
- 解构赋值
jslet [a, b, c] = [1, 2, 3] let {foo, bar} = {foo: 'aaa', bar: 'bbb'}
- 模板字符串
jslet name = 'Bob' `Hello ${name}!`
Promise 对象
Class 类
Module 模块化
扩展运算符
js[...arr1, ...arr2]
Symbol 新增数据型
Set 和 Map 数据结构
5. 说说对原型链的理解
参考答案:
什么是原型链
原型链是JavaScript实现继承的主要方法。它的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
原型链的组成
- 每个对象都有一个私有属性 proto 指向它的构造函数的原型对象(prototype)
- 该原型对象也有一个自己的原型对象,层层向上直到一个对象的原型对象为 null
示例
jsfunction Person() {} const person = new Person() console.log(person.__proto__ === Person.prototype) // true console.log(Person.prototype.__proto__ === Object.prototype) // true console.log(Object.prototype.__proto__ === null) // true
原型链的作用
- 属性查找机制
- 实现继承
jsfunction Animal() {} function Dog() {} // 继承 Dog.prototype = new Animal()
6. 说说对 Event Loop 的理解
参考答案:
什么是Event Loop
Event Loop是JavaScript实现异步的核心机制,它维护了一个或多个任务队列,不断地检查并执行队列中的任务。
执行过程
- 执行同步代码(主线程)
- 执行微任队列(micro task)
- 执行宏任务队列(macro task)
任务类型
微任务:
- Promise.then/catch/finally
- process.nextTick
- MutationObserver
宏任务:
- setTimeout/setInterval
- script(整体代码)
- I/O
- UI rendering
示例
jsconsole.log('1'); // 同步 setTimeout(() => { console.log('2'); // 宏任务 }, 0); Promise.resolve().then(() => { console.log('3'); // 微任务 }); console.log('4'); // 同步 // 输出: 1 4 3 2
7. this 指向问题
参考答案:
this 的指向规则
- 默认绑定 - 独立函数调用时,this指向全局对象(非严格模式)或undefined(严格模式)
jsfunction foo() { console.log(this); } foo(); // window or undefined
- 隐式绑定 - 作为对象方法调用时,this指向调用该方法的对象
jsconst obj = { foo: function() { console.log(this); } } obj.foo(); // obj
- 显式绑定 - 使用call、apply、bind时,this指向指定的对象
jsfunction foo() { console.log(this); } const obj = { name: 'obj' }; foo.call(obj); // obj
- new绑定 - 使用new调用构造函数时,this指向新创建对象
jsfunction Person(name) { this.name = name; console.log(this); } new Person('Tom'); // Person {name: "Tom"}
- 箭头函数 - this由外层作用域决定
jsconst obj = { foo: () => { console.log(this); } } obj.foo(); // window
8. 深拷贝与浅拷贝的区别及实现
参考答案:
区别
- 浅拷贝: 只复制一层对象的属性,如果属性是引用类型,拷贝的是引用地址
- 深拷贝: 递归复制所有层级的属性,完全独立的复制
浅拷贝实现方式
- Object.assign()
jsconst obj = { a: 1, b: { c: 2 } }; const clone = Object.assign({}, obj);
- 展开运算符
jsconst clone = { ...obj };
深拷贝实现方式
- JSON.parse(JSON.stringify())
jsconst clone = JSON.parse(JSON.stringify(obj)); // 缺点: 无法处理函数、undefined、循环引用等
- 递归实现
jsfunction deepClone(obj, hash = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (hash.has(obj)) return hash.get(obj); let clone = Array.isArray(obj) ? [] : {}; hash.set(obj, clone); Reflect.ownKeys(obj).forEach(key => { clone[key] = deepClone(obj[key], hash); }); return clone; }
9. 说说对 async/await 的理解
参考答案:
概念
async/await 是 ES2017 引入的异步编程解决方案,是 Generator 函数的语法糖,使异步代码更易于理解和维护。
特点
- async 函数返回 Promise 对象
- await 只能在 async 函数内使用
- await 后面可以是 Promise 或其他值
- 可以通过 try/catch 捕获异常
基本用法
jsasync function fetchData() { try { const response = await fetch('api/data'); const data = await response.json(); return data; } catch (error) { console.error(error); } }
错误处理
js// 方式一: try/catch async function foo() { try { await Promise.reject('error'); } catch(e) { console.log(e); } } // 方式二: catch方法 async function foo() { await Promise.reject('error') .catch(e => console.log(e)); }
10. JavaScript 中的继承方式有哪些?
参考答案:
1. 原型链继承
jsfunction Parent() {} function Child() {} Child.prototype = new Parent();
2. 构造函数继承
jsfunction Parent() {} function Child() { Parent.call(this); }
3. 组合继承
jsfunction Parent() {} function Child() { Parent.call(this); } Child.prototype = new Parent(); Child.prototype.constructor = Child;
4. 原型式继承
jsconst parent = { name: 'parent' }; const child = Object.create(parent);
5. 寄生式继承
jsfunction createObj(o) { const clone = Object.create(o); clone.sayName = function() {}; return clone; }
6. 寄生组合式继承(最优)
jsfunction Parent() {} function Child() { Parent.call(this); } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;
7. ES6 Class继承
jsclass Parent {} class Child extends Parent { constructor() { super(); } }
11. 说说 JavaScript 中的事件机制
参考答案:
事件流
事件流描述的是从页面中接收事件的顺序,DOM2级事件流包括三个阶段:
- 事件捕获阶段
- 目标阶段
- 事件冒泡阶段
事件处理程序
js// DOM0级事件处理程序 element.onclick = function() {} // DOM2级事件处理程序 element.addEventListener('click', function() {}, false) element.removeEventListener('click', function() {}, false) // IE事件处理程序 element.attachEvent('onclick', function() {}) element.detachEvent('onclick', function() {})
事件委托(代理)
利用事件冒泡,指定一个事件处理程序,就可以管理某一类型的所有事件。
jsdocument.getElementById('parent').addEventListener('click', function(e) { if(e.target.tagName.toLowerCase() === 'li') { console.log('Li element clicked'); } });
阻止事件传播
js// 阻止冒泡 e.stopPropagation() // 阻止默认行为 e.preventDefault()
12. 说说 JavaScript 中的垃圾回收机制
参考答案:
垃圾回收基本原理
JavaScript 中的内存管理是自动执行的,主要有两种垃圾回收算法:
- 标记清除算法
- 标记阶段:遍历所有对象,标记活动对象
- 清除阶段:清除没有标记的对象
- 引用计数算法
- 跟踪记录每个值被引用的次数
- 当引用次数为0时,将其回收
内存泄漏的常见情况
js// 1. 意外的全局变量 function foo() { bar = "全局变量"; // 没有声明就使用,会成为全局变量 } // 2. 闭包 function outer() { const someResource = {}; return function inner() { // someResource 一直被引用,无法回收 }; } // 3. 被遗忘的定时器或回调函数 setInterval(() => { // 如果不清除定时器,其中引用的变量都无法被回收 }, 1000); // 4. DOM引用 const elements = { button: document.getElementById('button') }; document.body.removeChild(document.getElementById('button')); // elements.button 仍然引用着那个 DOM 元素
最佳实践
- 及时清除引用
- 使用 WeakMap/WeakSet
- 及时清除定时器
- 避免过多的闭包
- 手动解除DOM引用
13. 说说 JavaScript 中的设计模式
参考答案:
常见设计模式
- 单例模式
jsconst Singleton = (function() { let instance; function createInstance() { return new Object("I am the instance"); } return { getInstance: function() { if (!instance) { instance = createInstance(); } return instance; } }; })();
- 观察者模式
jsclass EventEmitter { constructor() { this.events = {}; } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); } emit(event, ...args) { if (this.events[event]) { this.events[event].forEach(callback => callback(...args)); } } }
- 工厂模式
jsclass Factory { createProduct(type) { switch(type) { case 'A': return new ProductA(); case 'B': return new ProductB(); default: throw new Error('Invalid product type'); } } }
- 装饰器模式
jsfunction readonly(target, key, descriptor) { descriptor.writable = false; return descriptor; } class Example { @readonly pi() { return 3.14; } }
14. 说说浏览器的渲染原理
参考答案:
渲染过程
- 解析HTML生成DOM树
- 解析CSS生成CSSOM树
- 将DOM和CSSOM合并成渲染树
- 布局(Layout):计算元素的位置和大小
- 绘制(Paint):将渲染树绘制到屏幕上
性能优化
js// 1. 避免重排(reflow) const el = document.getElementById('app'); el.style.width = '100px'; el.style.height = '100px'; // 优化后 el.style.cssText = 'width: 100px; height: 100px;'; // 2. 使用文档片段 const fragment = document.createDocumentFragment(); for(let i = 0; i < 10; i++) { const li = document.createElement('li'); fragment.appendChild(li); } document.getElementById('list').appendChild(fragment); // 3. 使用 transform 代替位置调整 // 不推荐 element.style.left = '10px'; // 推荐 element.style.transform = 'translateX(10px)';
15. 说说 JavaScript 中的模块化
参考答案:
模块化的发展
- 全局函数模式
jsfunction foo() {} function bar() {}
- 命名空间模式
jsvar MyModule = { foo: function() {}, bar: function() {} };
- IIFE模式
jsvar Module = (function(){ var private = 'private'; return { public: function() { console.log(private); } }; })();
- CommonJS
js// 导出 module.exports = { foo: function() {} }; // 导入 const module = require('./module');
- AMD
jsdefine(['dependency'], function(dependency) { return { foo: function() {} }; });
- ES6 Module
js// 导出 export function foo() {} export default class {} // 导入 import { foo } from './module'; import DefaultExport from './module';
16. 说说 JavaScript 中的防抖和节流
参考答案:
防抖(Debounce)
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
jsfunction debounce(fn, delay) { let timer = null; return function(...args) { if(timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); } } // 使用示例 const handleSearch = debounce(function(e) { console.log(e.target.value); }, 500);
节流(Throttle)
规定在一个单位时间内,只能触发一次函数。如果��个单位时间内触发多次函数,只有一次生效。
jsfunction throttle(fn, delay) { let timer = null; return function(...args) { if(!timer) { timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); } } } // 使用示例 const handleScroll = throttle(function() { console.log(window.scrollY); }, 200);
17. 说说 JavaScript 中的性能优化
参考答案:
代码层面
- 减少DOM操作
js// 不推荐 for(let i = 0; i < 1000; i++) { document.body.innerHTML += '<span>' + i + '</span>' } // 推荐 const fragment = document.createDocumentFragment(); for(let i = 0; i < 1000; i++) { const span = document.createElement('span'); span.textContent = i; fragment.appendChild(span); } document.body.appendChild(fragment);
- 事件委托
js// 不推荐 document.querySelectorAll('li').forEach(item => { item.onclick = function() {} }); // 推荐 document.querySelector('ul').onclick = function(e) { if(e.target.tagName.toLowerCase() === 'li') {} }
防抖节流
避免内存泄漏
js// 清除定时器 let timer = setInterval(() => {}, 1000); clearInterval(timer); // 解除引用 let element = document.getElementById('button'); element.remove(); element = null;
加载性能
- 资源懒加载
jsconst img = document.createElement('img'); img.src = 'placeholder.jpg'; img.dataset.src = 'actual.jpg'; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if(entry.isIntersecting) { entry.target.src = entry.target.dataset.src; } }); }); observer.observe(img);
- 代码分割
js// 动态import button.onclick = async () => { const module = await import('./module.js'); module.default(); }
18. 说说 JavaScript 中的设计模式进阶
参考答案:
发布订阅模式
jsclass PubSub { constructor() { this.subscribers = {}; } subscribe(event, callback) { if (!this.subscribers[event]) { this.subscribers[event] = []; } this.subscribers[event].push(callback); return () => this.unsubscribe(event, callback); } publish(event, data) { if (!this.subscribers[event]) return; this.subscribers[event].forEach(callback => callback(data)); } unsubscribe(event, callback) { if (!this.subscribers[event]) return; this.subscribers[event] = this.subscribers[event] .filter(cb => cb !== callback); } }
代理模式
jsconst target = { name: 'target' }; const handler = { get: function(target, prop) { console.log(`Accessing property: ${prop}`); return target[prop]; }, set: function(target, prop, value) { console.log(`Setting property: ${prop} = ${value}`); target[prop] = value; return true; } }; const proxy = new Proxy(target, handler);
策略模式
jsconst strategies = { A: function(salary) { return salary * 4; }, B: function(salary) { return salary * 3; }, C: function(salary) { return salary * 2; } }; const calculateBonus = function(level, salary) { return strategies[level](salary); };
19. 说说 JavaScript 中的错误处理机制
参考答案:
错误类型
- SyntaxError: 语法错误
- ReferenceError: 引用错误
- TypeError: 类型错误
- RangeError: 范围错误
- URIError: URI相关错误
- EvalError: eval()相关错误
错误处理
js// try...catch try { throw new Error('出错了!'); } catch(e) { console.log(e.name + ': ' + e.message); } finally { console.log('finally'); } // 自定义错误 class CustomError extends Error { constructor(message) { super(message); this.name = 'CustomError'; } } // Promise错误处理 promise .then(result => {}) .catch(error => { console.log(error); }) .finally(() => {}); // async/await错误处理 async function foo() { try { await Promise.reject('error'); } catch(e) { console.log(e); } }
20. 说说 JavaScript 中的函数式编程
参考答案:
基本概念
- 纯函数
js// 纯函数 function add(a, b) { return a + b; } // 非纯函数 let count = 0; function increment() { count++; }
- 不可变性
js// 不推荐 const arr = [1, 2, 3]; arr.push(4); // 推荐 const arr = [1, 2, 3]; const newArr = [...arr, 4];
- 函数组合
jsconst compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x); const addOne = x => x + 1; const double = x => x * 2; const addOneThenDouble = compose(double, addOne); console.log(addOneThenDouble(2)); // 6
21. 如何实现一个支持过期时间的 localStorage?
参考答案:
实现思路
- 封装 localStorage 的操作
- 存储数据时添加时间戳
- 获取数据时进行过期判断
jsclass ExpiresStorage { constructor() { this.storage = window.localStorage; } // 设置数据 setItem(key, value, expires) { const data = { value, expires: expires ? new Date().getTime() + expires * 1000 : null }; this.storage.setItem(key, JSON.stringify(data)); } // 获取数据 getItem(key) { const data = JSON.parse(this.storage.getItem(key)); if (!data) return null; // 判断是否过期 if (data.expires && data.expires < new Date().getTime()) { this.removeItem(key); return null; } return data.value; } // 删除数据 removeItem(key) { this.storage.removeItem(key); } // 清空数据 clear() { this.storage.clear(); } } // 使用示例 const storage = new ExpiresStorage(); // 设置数据,10秒后过期 storage.setItem('token', 'abc123', 10); // 获取数据 console.log(storage.getItem('token')); // 10秒后获取数据 setTimeout(() => { console.log(storage.getItem('token')); // null }, 11000);
22. 如何实现一个支持并发限制的异步请求函数?
参考答案:
实现思路
- 维护一个请求队列
- 控制同时执行的请求数量
- 请求完成后自动执行下一个请求
jsclass RequestQueue { constructor(limit = 2) { this.limit = limit; // 最大并发数 this.queue = []; // 请求队列 this.running = 0; // 正在运行的请求数 } // 添加请求任务 add(requestFn) { return new Promise((resolve, reject) => { this.queue.push({ requestFn, resolve, reject }); this.run(); }); } // 执行请求任务 async run() { // 队列为空或正在运行的请求达到上限则返回 if (!this.queue.length || this.running >= this.limit) { return; } this.running++; const { requestFn, resolve, reject } = this.queue.shift(); try { const result = await requestFn(); resolve(result); } catch (err) { reject(err); } finally { this.running--; this.run(); } } } // 使用示例 const queue = new RequestQueue(2); // 最多同时执行2个请求 // 模拟请求函数 const createRequest = (id, delay) => { return () => new Promise(resolve => { setTimeout(() => { resolve(`请求${id}完成`); }, delay); }); }; // 添加多个请求 async function test() { const requests = [ createRequest(1, 1000), createRequest(2, 2000), createRequest(3, 2000), createRequest(4, 1000) ]; const promises = requests.map(req => queue.add(req)); const results = await Promise.all(promises); console.log(results); } test();
23. 如何实现一个支持批量操作的前端缓存系统?
参考答案:
实现思路
- 支持内存缓存和持久化存储
- 支持数据过期和容量限制
- 支持批量操作和事务
jsclass Cache { constructor(options = {}) { this.maxSize = options.maxSize || 100; // 最大缓存数量 this.timeout = options.timeout || 0; // 默认过期时间 this.storage = options.storage || new Map(); // 存储器 this.queue = []; // 操作队列 } // 开始批量操作 startBatch() { this.queue = []; return this; } // 提交批量操作 async commit() { const results = []; for (const operation of this.queue) { try { const result = await operation(); results.push(result); } catch (err) { // 回滚操作 this.rollback(); throw err; } } this.queue = []; return results; } // 回滚操作 rollback() { this.queue = []; } // 设置缓存 set(key, value, timeout = this.timeout) { const operation = () => { // 检查容量 if (this.storage.size >= this.maxSize) { const firstKey = this.storage.keys().next().value; this.storage.delete(firstKey); } const item = { value, expires: timeout ? Date.now() + timeout * 1000 : null }; this.storage.set(key, item); return item; }; if (this.queue.length > 0) { this.queue.push(operation); return this; } return operation(); } // 获取缓存 get(key) { const item = this.storage.get(key); if (!item) return null; // 检查是否过期 if (item.expires && item.expires < Date.now()) { this.storage.delete(key); return null; } return item.value; } // 批量设置 mset(items) { this.startBatch(); items.forEach(([key, value, timeout]) => { this.set(key, value, timeout); }); return this.commit(); } // 批量获取 mget(keys) { return keys.map(key => this.get(key)); } // 删除缓存 delete(key) { const operation = () => this.storage.delete(key); if (this.queue.length > 0) { this.queue.push(operation); return this; } return operation(); } // 清空缓存 clear() { const operation = () => this.storage.clear(); if (this.queue.length > 0) { this.queue.push(operation); return this; } return operation(); } } // 使用示例 const cache = new Cache({ maxSize: 100, timeout: 3600 // 默认1小时过期 }); // 单个操作 cache.set('key1', 'value1', 1800); // 30分钟过期 console.log(cache.get('key1')); // 批量操作 cache.mset([ ['key2', 'value2', 3600], ['key3', 'value3', 7200] ]).then(() => { console.log(cache.mget(['key2', 'key3'])); }); // 事务操作 cache .startBatch() .set('key4', 'value4') .set('key5', 'value5') .commit() .then(() => { console.log(cache.mget(['key4', 'key5'])); });
24. 如何实现一个支持撤销/重做的编辑器操作历史?
参考答案:
实现思路
- 维护操作历史栈
- 支持撤销和重做操作
- 限制历史记录数量
jsclass History { constructor(maxLength = 50) { this.maxLength = maxLength; // 最大历史记录数 this.undoStack = []; // 撤销栈 this.redoStack = []; // 重做栈 } // 执行操作 execute(action) { // 执行操作 action.do(); // 添加到撤销栈 this.undoStack.push(action); // 清空重做栈 this.redoStack = []; // 限制历史记录数量 if (this.undoStack.length > this.maxLength) { this.undoStack.shift(); } } // 撤销操作 undo() { if (this.undoStack.length === 0) return false; const action = this.undoStack.pop(); action.undo(); this.redoStack.push(action); return true; } // 重做操作 redo() { if (this.redoStack.length === 0) return false; const action = this.redoStack.pop(); action.do(); this.undoStack.push(action); return true; } // 清空历史 clear() { this.undoStack = []; this.redoStack = []; } // 是否可以撤销 canUndo() { return this.undoStack.length > 0; } // 是否可以重做 canRedo() { return this.redoStack.length > 0; } } // 文本编辑操作示例 class TextAction { constructor(editor, start, end, text, oldText) { this.editor = editor; this.start = start; this.end = end; this.text = text; this.oldText = oldText; } do() { this.editor.replace(this.start, this.end, this.text); } undo() { this.editor.replace(this.start, this.start + this.text.length, this.oldText); } } // 使用示例 class Editor { constructor() { this.content = ''; this.history = new History(50); } // 替换文本 replace(start, end, text) { this.content = this.content.slice(0, start) + text + this.content.slice(end); } // 插入文本 insert(position, text) { const action = new TextAction( this, position, position, text, '' ); this.history.execute(action); } // 删除文本 delete(start, end) { const action = new TextAction( this, start, end, '', this.content.slice(start, end) ); this.history.execute(action); } // 撤销 undo() { return this.history.undo(); } // 重做 redo() { return this.history.redo(); } // 获取内容 getContent() { return this.content; } } // 使用示例 const editor = new Editor(); editor.insert(0, 'Hello'); // Hello editor.insert(5, ' World'); // Hello World editor.delete(5, 11); // Hello console.log(editor.getContent()); // Hello editor.undo(); // Hello World console.log(editor.getContent()); // Hello World editor.redo(); // Hello console.log(editor.getContent()); // Hello
25. 如何实现一个简单的图片懒加载指令?
参考答案:
实现思路
- 使用 IntersectionObserver 监听图片是否进入视口
- 图片进入视口时加载真实图片
js// Vue自定义指令实现 const lazyLoad = { mounted(el, binding) { const observer = new IntersectionObserver(([{isIntersecting}]) => { if (isIntersecting) { el.src = binding.value; observer.unobserve(el); } }); observer.observe(el); } } // 使用示例 app.directive('lazy', lazyLoad); // 在模板中使用 <img v-lazy="imageUrl" src="placeholder.jpg">
26. 如何实现一个简单的虚拟列表?
参考答案:
实现思路
- 只渲染可视区域的列表项
- 监听滚动更新可视区域
jsclass VirtualList { constructor(options) { this.itemHeight = options.itemHeight; this.container = options.container; this.items = options.items; this.visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight); this.init(); } init() { this.container.addEventListener('scroll', this.onScroll.bind(this)); this.render(); } getVisibleRange() { const scrollTop = this.container.scrollTop; const start = Math.floor(scrollTop / this.itemHeight); const end = start + this.visibleCount; return { start, end }; } render() { const { start, end } = this.getVisibleRange(); const visibleItems = this.items.slice(start, end); this.container.innerHTML = ` <div style="height: ${this.items.length * this.itemHeight}px; position: relative;"> <div style="position: absolute; top: ${start * this.itemHeight}px;"> ${visibleItems.map(item => `<div style="height: ${this.itemHeight}px;">${item}</div>`).join('')} </div> </div> `; } onScroll() { requestAnimationFrame(() => this.render()); } } // 使用示例 const list = new VirtualList({ container: document.querySelector('.container'), itemHeight: 30, items: Array.from({length: 10000}, (_, i) => `Item ${i}`) });
27. 如何实现一个简单的前端路由?
参考答案:
实现思路
- 支持 hash 和 history 两种模式
- 监听路由变化并更新视图
jsclass Router { constructor(options) { this.mode = options.mode || 'hash'; this.routes = options.routes || []; this.currentComponent = null; this.init(); } init() { if (this.mode === 'hash') { window.addEventListener('hashchange', this.onHashChange.bind(this)); window.addEventListener('load', this.onHashChange.bind(this)); } else { window.addEventListener('popstate', this.onPopState.bind(this)); window.addEventListener('load', this.onPopState.bind(this)); } } getPath() { if (this.mode === 'hash') { return window.location.hash.slice(1) || '/'; } return window.location.pathname || '/'; } push(path) { if (this.mode === 'hash') { window.location.hash = path; } else { window.history.pushState(null, '', path); this.onPopState(); } } onHashChange() { const path = this.getPath(); this.updateView(path); } onPopState() { const path = this.getPath(); this.updateView(path); } updateView(path) { const route = this.routes.find(route => route.path === path); if (route) { this.currentComponent = route.component; // 更新视图的逻辑 } } } // 使用示例 const router = new Router({ mode: 'hash', routes: [ { path: '/', component: Home }, { path: '/about', component: About } ] });
28. 如何实现一个简单的状态管理器?
参考答案:
实现思路
- 维护一个全局状态
- 支持订阅状态变化
- 支持修改状态并通知更新
jsclass Store { constructor(options) { this.state = options.state || {}; this.mutations = options.mutations || {}; this.subscribers = []; } get state() { return this._state; } set state(value) { this._state = value; } commit(type, payload) { if (this.mutations[type]) { this.mutations[type](this.state, payload); this.notify(); } } subscribe(fn) { this.subscribers.push(fn); return () => { const index = this.subscribers.indexOf(fn); if (index > -1) { this.subscribers.splice(index, 1); } }; } notify() { this.subscribers.forEach(fn => fn(this.state)); } } // 使用示例 const store = new Store({ state: { count: 0 }, mutations: { increment(state) { state.count++; }, decrement(state) { state.count--; } } }); // 订阅状态变化 store.subscribe(state => { console.log('state changed:', state); }); // 修改状态 store.commit('increment');
29. 如何实现一个简单的文件上传进度监控?
参考答案:
实现思路
- 使用 XMLHttpRequest 监听上传进度
- 支持单文件和多文件上传
- 支持进度回调
jsclass FileUploader { constructor(options = {}) { this.url = options.url; this.onProgress = options.onProgress; this.onSuccess = options.onSuccess; this.onError = options.onError; } upload(file) { const formData = new FormData(); formData.append('file', file); const xhr = new XMLHttpRequest(); // 监听上传进度 xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded * 100) / e.total); this.onProgress?.(percent, file); } }); // 处理成功和失败 xhr.addEventListener('load', () => { if (xhr.status === 200) { this.onSuccess?.(xhr.response, file); } else { this.onError?.(new Error('Upload failed'), file); } }); xhr.addEventListener('error', () => { this.onError?.(new Error('Network error'), file); }); xhr.open('POST', this.url, true); xhr.send(formData); return xhr; } } // 使用示例 const uploader = new FileUploader({ url: '/api/upload', onProgress: (percent, file) => { console.log(`${file.name} uploaded ${percent}%`); }, onSuccess: (response, file) => { console.log(`${file.name} upload complete`); }, onError: (error, file) => { console.error(`${file.name} upload failed:`, error); } }); // 上传文件 inputElement.addEventListener('change', (e) => { const file = e.target.files[0]; uploader.upload(file); });
30. 如何实现一个简单的图片预览和裁剪功能?
参考答案:
实现思路
- 使用 Canvas 实现图片预览
- 支持拖拽和缩放
- 输出裁剪后的图片
jsclass ImageCropper { constructor(options) { this.canvas = options.canvas; this.ctx = this.canvas.getContext('2d'); this.image = null; this.cropArea = { x: 0, y: 0, width: 100, height: 100 }; this.isDragging = false; this.initEvents(); } loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { this.image = img; this.render(); resolve(img); }; img.onerror = reject; img.src = url; }); } initEvents() { this.canvas.addEventListener('mousedown', this.startDrag.bind(this)); this.canvas.addEventListener('mousemove', this.drag.bind(this)); this.canvas.addEventListener('mouseup', this.stopDrag.bind(this)); } startDrag(e) { const { offsetX, offsetY } = e; if (this.isInCropArea(offsetX, offsetY)) { this.isDragging = true; } } drag(e) { if (!this.isDragging) return; this.cropArea.x = Math.max(0, Math.min(e.offsetX, this.canvas.width - this.cropArea.width)); this.cropArea.y = Math.max(0, Math.min(e.offsetY, this.canvas.height - this.cropArea.height)); this.render(); } stopDrag() { this.isDragging = false; } isInCropArea(x, y) { return x >= this.cropArea.x && x <= this.cropArea.x + this.cropArea.width && y >= this.cropArea.y && y <= this.cropArea.y + this.cropArea.height; } render() { if (!this.image) return; // 清空画布 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 绘制图片 this.ctx.drawImage(this.image, 0, 0, this.canvas.width, this.canvas.height); // 绘制裁剪区域 this.ctx.strokeStyle = '#fff'; this.ctx.strokeRect( this.cropArea.x, this.cropArea.y, this.cropArea.width, this.cropArea.height ); } crop() { const canvas = document.createElement('canvas'); canvas.width = this.cropArea.width; canvas.height = this.cropArea.height; const ctx = canvas.getContext('2d'); ctx.drawImage( this.canvas, this.cropArea.x, this.cropArea.y, this.cropArea.width, this.cropArea.height, 0, 0, this.cropArea.width, this.cropArea.height ); return canvas.toDataURL(); } } // 使用示例 const cropper = new ImageCropper({ canvas: document.querySelector('#cropCanvas') }); // 加载图片 cropper.loadImage('image.jpg'); // 获取裁剪结果 const cropButton = document.querySelector('#cropButton'); cropButton.addEventListener('click', () => { const croppedImage = cropper.crop(); console.log(croppedImage); // base64格式的裁剪后图片 });
31. 如何实现一个简单的无限滚动加载?
参考答案:
实现思路
- 监听滚动位置
- 在接近底部时加载更多数据
- 防抖处理滚动事件
jsclass InfiniteScroll { constructor(options) { this.container = options.container; this.loadMore = options.loadMore; this.threshold = options.threshold || 100; this.loading = false; this.init(); } init() { this.container.addEventListener('scroll', this.debounce(this.handleScroll.bind(this), 200) ); } async handleScroll() { if (this.loading) return; const { scrollTop, scrollHeight, clientHeight } = this.container; if (scrollHeight - scrollTop - clientHeight < this.threshold) { this.loading = true; try { await this.loadMore(); } finally { this.loading = false; } } } debounce(fn, delay) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); } } } // 使用示例 const scroller = new InfiniteScroll({ container: document.querySelector('.scroll-container'), threshold: 100, async loadMore() { const data = await fetchMoreData(); renderItems(data); } });
32. 如何实现一个支持拖拽排序的列表组件?
参考答案:
实现思路
- 监听拖拽事件
- 计算拖拽位置
- 更新列表顺序
jsclass DraggableList { constructor(container) { this.container = container; this.items = []; this.dragItem = null; this.placeholder = null; this.init(); } init() { this.container.addEventListener('dragstart', this.handleDragStart.bind(this)); this.container.addEventListener('dragover', this.handleDragOver.bind(this)); this.container.addEventListener('drop', this.handleDrop.bind(this)); } handleDragStart(e) { this.dragItem = e.target; e.dataTransfer.effectAllowed = 'move'; // 创建占位元素 this.placeholder = this.dragItem.cloneNode(true); this.placeholder.style.opacity = '0.5'; } handleDragOver(e) { e.preventDefault(); const target = e.target.closest('.list-item'); if (target && target !== this.dragItem) { const targetRect = target.getBoundingClientRect(); const targetCenter = targetRect.top + targetRect.height / 2; if (e.clientY < targetCenter) { target.parentNode.insertBefore(this.placeholder, target); } else { target.parentNode.insertBefore(this.placeholder, target.nextSibling); } } } handleDrop(e) { e.preventDefault(); this.placeholder.parentNode.replaceChild(this.dragItem, this.placeholder); this.dragItem = null; this.placeholder = null; // 触发排序更新事件 this.container.dispatchEvent(new CustomEvent('sort-update')); } getOrder() { return Array.from(this.container.children).map(item => item.dataset.id); } } // 使用示例 const list = new DraggableList(document.querySelector('.list-container')); // 监听排序更新 list.container.addEventListener('sort-update', () => { const newOrder = list.getOrder(); console.log('New order:', newOrder); });
33. 如何实现一个简单的前端数据导出功能?
参考答案:
实现思路
- 支持多种格式导出(CSV/Excel)
- 处理大数据量分片导出
- 支持自定义导出字段
jsclass DataExporter { constructor(options = {}) { this.fileName = options.fileName || 'export'; this.fields = options.fields || []; this.chunkSize = options.chunkSize || 1000; } // 导出CSV exportCSV(data) { const header = this.fields.map(field => field.label).join(','); const rows = data.map(item => this.fields.map(field => this.formatValue(item[field.key])).join(',') ); const csv = [header, ...rows].join('\n'); this.download(csv, 'text/csv'); } // 分片导出大数据量 async exportLargeData(getData) { let offset = 0; const chunks = []; while (true) { const data = await getData(offset, this.chunkSize); if (!data.length) break; chunks.push(data); offset += data.length; // 触发进度回调 this.onProgress?.(offset); } this.exportCSV([].concat(...chunks)); } // 格式化单元格值 formatValue(value) { if (value == null) return ''; value = String(value); // 处理包含逗号的值 return value.includes(',') ? `"${value}"` : value; } // 下载文件 download(content, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${this.fileName}.csv`; link.click(); URL.revokeObjectURL(url); } } // 使用示例 const exporter = new DataExporter({ fileName: 'users', fields: [ { key: 'id', label: 'ID' }, { key: 'name', label: '姓名' }, { key: 'email', label: '邮箱' } ] }); // 导出小数据量 exporter.exportCSV([ { id: 1, name: '张三', email: 'zhangsan@example.com' }, { id: 2, name: '李四', email: 'lisi@example.com' } ]); // 导出大数据量 exporter.exportLargeData(async (offset, limit) => { const response = await fetch(`/api/users?offset=${offset}&limit=${limit}`); return response.json(); });
34. 如何实现一个简单的前端日志收集系统?
参考答案:
实现思路
- 收集错误和性能数据
- 批量上报和本地缓存
- 支持采样率控制
jsclass Logger { constructor(options = {}) { this.url = options.url; this.app = options.app; this.queue = []; this.timer = null; this.maxCache = options.maxCache || 100; this.delay = options.delay || 1000; this.sampling = options.sampling || 1; this.init(); } init() { // 错误监听 window.addEventListener('error', this.onError.bind(this)); window.addEventListener('unhandledrejection', this.onUnhandledRejection.bind(this)); // 性能监听 if (window.PerformanceObserver) { const observer = new PerformanceObserver(this.onPerformanceEntry.bind(this)); observer.observe({ entryTypes: ['navigation', 'resource', 'largest-contentful-paint'] }); } } log(type, data) { // 采样控制 if (Math.random() > this.sampling) return; const log = { type, time: Date.now(), app: this.app, data, url: location.href, ua: navigator.userAgent }; this.queue.push(log); this.checkQueue(); } onError(e) { this.log('error', { message: e.message, filename: e.filename, lineno: e.lineno, colno: e.colno, stack: e.error?.stack }); } onUnhandledRejection(e) { this.log('promise', { message: e.reason?.message, stack: e.reason?.stack }); } onPerformanceEntry(list) { list.getEntries().forEach(entry => { this.log('performance', { name: entry.name, type: entry.entryType, duration: entry.duration, startTime: entry.startTime }); }); } checkQueue() { if (this.queue.length >= this.maxCache) { this.flush(); } else if (!this.timer) { this.timer = setTimeout(() => this.flush(), this.delay); } } async flush() { if (!this.queue.length) return; const logs = this.queue.slice(); this.queue = []; this.timer = null; try { await fetch(this.url, { method: 'POST', body: JSON.stringify(logs) }); } catch (err) { // 上报失败,回写队列 this.queue.unshift(...logs); } } } // 使用示例 const logger = new Logger({ url: '/api/logs', app: 'my-app', sampling: 0.1, // 采样率10% maxCache: 100, delay: 1000 }); // 手动记录日志 logger.log('custom', { action: 'click', target: 'button' });