1. JavaScript 数据类型与类型判断实战指南
JavaScript 作为一门动态类型语言,其数据类型系统是面试中最常被考察的基础知识点。在实际开发中,准确理解类型系统能避免90%的类型相关bug。
1.1 基本类型与引用类型的本质区别
基本类型(Primitive Types)在内存中占据固定大小空间,直接存储在栈内存中。包括:
number:IEEE 754标准的双精度浮点数,这也是为什么0.1 + 0.2 !== 0.3string:UTF-16编码的不可变字符序列boolean:仅包含true和falsenull:表示故意设置的空值(注意typeof null返回"object"是历史遗留bug)undefined:表示未定义的值symbol(ES6):唯一且不可变的标识符
引用类型(Reference Types)在栈中存储指针,实际数据存储在堆内存中。当复制引用类型变量时,复制的只是指针而非实际数据。
关键区别:基本类型通过值比较,引用类型通过引用(内存地址)比较。
{} === {}返回false就是因为创建了两个独立对象。
1.2 类型判断的十八般武艺
typeof操作符能快速区分基本类型,但有两大陷阱:
typeof null返回"object"- 所有引用类型(除函数)都返回
"object"
更精确的类型判断方案:
// 判断数组 Array.isArray([]); // true // 判断null const isNull = val => val === null; // 判断纯对象 const isPlainObject = obj => Object.prototype.toString.call(obj) === '[object Object]'; // 综合类型判断 function getType(val) { return val === null ? 'null' : typeof val === 'object' ? Object.prototype.toString.call(val).slice(8, -1).toLowerCase() : typeof val; }1.3 类型转换的暗坑与最佳实践
JavaScript 的隐式类型转换是许多bug的源头。常见陷阱:
==会进行类型转换('1' == 1=> true)+运算符优先字符串拼接(1 + '1'=> '11')- 布尔值转换:
!!''=> false,!!'0'=> true
防御性编程建议:
- 始终使用
===严格相等 - 显式转换类型:
Number(),String(),Boolean() - 使用模板字符串代替字符串拼接
2. 作用域与闭包深度解析
2.1 作用域链的构建过程
JavaScript 采用词法作用域(Lexical Scope),作用域在代码编写阶段就已确定。现代JS中有三种作用域:
- 全局作用域:不在任何函数内声明的变量
- 函数作用域:
var声明的变量存在函数提升 - 块级作用域:
let/const声明的变量(ES6+)
function outer() { var a = 1; // 函数作用域 let b = 2; // 块级作用域 if (true) { var c = 3; // 提升到函数作用域 let d = 4; // 仅在此块内有效 } console.log(c); // 3 console.log(d); // ReferenceError }2.2 闭包的形成原理与应用场景
闭包的本质是函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。V8引擎通过[[Scopes]]内部属性实现这一机制。
经典闭包案例:
function createCounter() { let count = 0; // 闭包保护的变量 return { increment() { count++ }, get() { return count } }; } const counter = createCounter(); counter.increment(); console.log(counter.get()); // 1实际应用场景:
- 模块模式(早期模块化方案)
- 私有变量模拟
- 函数工厂(如React Hooks)
- 防抖/节流函数
内存泄漏警告:闭包会阻止变量被GC回收。在不需要时应手动解除引用,如
elem.onclick = null。
3. 原型与继承的现代实践
3.1 原型链的运作机制
每个JS对象都有一个隐藏的[[Prototype]]属性(通过__proto__访问),指向其原型对象。查找属性时会沿原型链向上查找,直到Object.prototype(其__proto__为null)。
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log(`Hello, ${this.name}`); }; const john = new Person('John'); john.sayHello(); // 通过原型链访问方法3.2 ES6 class 语法糖解析
class语法本质仍是基于原型的继承,但提供了更直观的语法:
class Person { constructor(name) { this.name = name; } // 实例方法(添加到原型) sayHello() { console.log(`Hello, ${this.name}`); } // 静态方法 static create(name) { return new Person(name); } } class Student extends Person { constructor(name, grade) { super(name); // 必须首先调用 this.grade = grade; } }关键点:
super在构造函数中指向父类构造函数super在方法中指向父类原型- 静态方法也被继承
4. 异步编程的演进与最佳实践
4.1 从回调地狱到Promise
回调地狱(Callback Hell)是早期JS异步编程的痛点:
getData(function(a) { getMoreData(a, function(b) { getMoreData(b, function(c) { // 无限嵌套... }); }); });Promise 的解决方案:
getData() .then(a => getMoreData(a)) .then(b => getMoreData(b)) .then(c => { // 链式调用 }) .catch(err => { // 统一错误处理 });4.2 async/await 的工程化应用
async/await 让异步代码看起来像同步代码:
async function fetchData() { try { const a = await getData(); const b = await getMoreData(a); const c = await getMoreData(b); return processData(c); } catch (err) { console.error('处理失败:', err); throw err; // 继续向上传递 } }性能优化技巧:
- 并行请求:
await Promise.all([task1, task2]) - 错误边界:合理使用try-catch
- 取消机制:使用AbortController
5. 事件循环与浏览器渲染机制
5.1 宏任务与微任务的执行顺序
事件循环的完整流程:
- 执行一个宏任务(如script整体代码)
- 执行所有微任务(Promise回调、MutationObserver)
- 渲染(如果有必要)
- 执行下一个宏任务(setTimeout等)
console.log('script start'); // 宏任务 setTimeout(() => { console.log('setTimeout'); // 宏任务 }, 0); Promise.resolve().then(() => { console.log('promise1'); // 微任务 }).then(() => { console.log('promise2'); // 微任务 }); console.log('script end'); // 宏任务 // 输出顺序: // script start -> script end -> promise1 -> promise2 -> setTimeout5.2 requestAnimationFrame 与性能优化
requestAnimationFrame在每次重绘前执行,是动画处理的理想选择:
function animate() { // 动画逻辑 element.style.left = `${pos++}px`; if (pos < 100) { requestAnimationFrame(animate); } } requestAnimationFrame(animate);对比setTimeout的优势:
- 自动匹配浏览器刷新率(通常60fps)
- 后台标签页中自动暂停
- 浏览器会优化并行动画
6. 函数式编程技巧实战
6.1 柯里化的高级应用
柯里化(Currying)的增强实现:
function curry(fn, arity = fn.length) { return function curried(...args) { // 如果参数不足,返回新函数等待剩余参数 if (args.length < arity) { return (...moreArgs) => curried(...args, ...moreArgs); } // 参数足够,执行原函数 return fn(...args); }; } // 使用案例 const add = curry((a, b, c) => a + b + c); add(1)(2)(3); // 6 add(1, 2)(3); // 6实际应用场景:
- 参数复用(创建特定配置的函数)
- 延迟执行(直到所有参数收集完成)
- React高阶组件
6.2 防抖与节流的工程实现
增强版防抖(支持立即执行):
function debounce(fn, delay, immediate = false) { let timer; return function(...args) { const context = this; const later = () => { timer = null; if (!immediate) fn.apply(context, args); }; const shouldCallNow = immediate && !timer; clearTimeout(timer); timer = setTimeout(later, delay); if (shouldCallNow) fn.apply(context, args); }; }节流的时间戳+定时器双保险版:
function throttle(fn, delay) { let lastTime = 0; let timer; return function(...args) { const context = this; const now = Date.now(); const remaining = delay - (now - lastTime); clearTimeout(timer); if (remaining <= 0) { fn.apply(context, args); lastTime = now; } else { timer = setTimeout(() => { fn.apply(context, args); lastTime = Date.now(); }, remaining); } }; }7. 现代JavaScript特性解析
7.1 ES6+ 核心特性实战
箭头函数的特性:
const obj = { name: 'obj', regularFunc: function() { console.log(this.name); // 'obj' }, arrowFunc: () => { console.log(this.name); // undefined(继承自外层作用域) } };解构赋值的妙用:
// 嵌套解构 const { name, address: { city } } = getUser(); // 函数参数解构 function drawChart({ size = 'big', coords = { x: 0, y: 0 } }) { // ... }7.2 模块化与Tree Shaking
现代模块化方案:
// lib.js export const PI = 3.14; export function circleArea(r) { return PI * r * r } // app.js import { circleArea } from './lib.js'; console.log(circleArea(5));Tree Shaking优化原理:
- 使用ES6模块语法(
import/export) - 避免副作用代码
- 配合Webpack/Rollup等打包工具
8. 内存管理与性能优化
8.1 常见内存泄漏场景
- 意外的全局变量:
function leak() { leakedVar = '这是一个全局变量'; // 忘记var/let/const }- 遗忘的定时器:
const intervalId = setInterval(() => { // 如果忘记clearInterval(intervalId)... }, 1000);- 游离的DOM引用:
const elements = { button: document.getElementById('button') }; // 即使从DOM移除,elements.button仍保留引用 document.body.removeChild(document.getElementById('button'));8.2 性能优化黄金法则
- 减少重排(Reflow):
- 使用
transform代替top/left动画 - 批量DOM操作(使用文档片段)
- 事件委托:
// 单个监听器处理多个元素 document.getElementById('parent').addEventListener('click', (e) => { if (e.target.matches('.child')) { // 处理子元素点击 } });- 虚拟列表优化长列表渲染:
- 只渲染可视区域内的项目
- 使用React Window或Vue Virtual Scroller等库
9. 安全防御实战指南
9.1 XSS防御体系
内容安全策略(CSP)配置示例:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *; connect-src https://api.example.com;输入过滤与输出编码:
// 使用DOMPurify清理HTML const clean = DOMPurify.sanitize(userInput); // 文本内容编码 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }9.2 CSRF防护方案
双重Cookie验证:
// 后端设置SameSite=Strict的Cookie // 前端在请求头中添加自定义头 fetch('/api', { headers: { 'X-CSRF-Token': getCookie('csrfToken') } });SameSite Cookie属性:
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure10. 面试实战技巧与高频问题
10.1 手写代码常见题型
实现Promise.all:
Promise.myAll = function(promises) { return new Promise((resolve, reject) => { const results = []; let completed = 0; if (promises.length === 0) { resolve(results); return; } promises.forEach((promise, index) => { Promise.resolve(promise) .then(value => { results[index] = value; completed++; if (completed === promises.length) { resolve(results); } }) .catch(reject); }); }); };深拷贝实现:
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; }10.2 系统设计类问题
"设计一个前端缓存系统"的参考思路:
- 存储策略:内存(Map) + 持久化(localStorage)
- 缓存淘汰:LRU算法实现
- 缓存更新:过期时间 + 版本控制
- 大小限制:按字节计算存储量
- 线程安全:Web Worker处理复杂操作
class CacheSystem { constructor(maxSize = 1024 * 1024) { this.memoryCache = new Map(); this.currentSize = 0; this.maxSize = maxSize; } set(key, value, ttl = 60 * 1000) { // 实现存储逻辑 } get(key) { // 实现读取逻辑 } #checkExpired(entry) { // 私有方法检查过期 } }在实际面试中,除了代码实现,更重要的是能够清晰地阐述设计决策背后的权衡考量。比如选择LRU而不是LFU的原因,内存与持久化存储的分配策略等。