一、ES6+ 新特性
ES6(ECMAScript 2015)及后续的 ES7-ES14 被统称为 ES6+,是 JavaScript 语言的重大升级,解决了 ES5 时代的语法冗余、作用域混乱、功能缺失等问题,大幅提升了代码的可读性、可维护性和开发效率。
1. 块级作用域与变量声明
ES5 中只有全局作用域和函数作用域,var 声明的变量存在 “变量提升” 和 “作用域穿透” 问题,极易引发 bug。ES6 新增 let 和 const 关键字,引入块级作用域({} 包裹的区域):
-
let:声明可变变量,仅在当前块级作用域有效,无变量提升,不允许重复声明;
-
const:声明常量,一旦赋值不可修改(引用类型仅保证地址不变),同样遵循块级作用域规则。示例:
// ES5 问题:变量提升+作用域穿透
if (true) {
var a = 10;
}
console.log(a); // 10(全局作用域可访问)
// ES6 解决
if (true) {
let b = 20;
const c = 30;
}
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined
2. 箭头函数
简化函数声明语法,核心特性:
- 语法简洁:单参数可省略括号,单返回语句可省略大括号和
return;
- 无独立
this:箭头函数的 this 继承自外层作用域,解决了 ES5 中 this 指向混乱的问题(如回调函数中 this 丢失);
- 不能作为构造函数:无法使用
new 调用,无 arguments 对象(可改用剩余参数)。示例:
// ES5 函数
const add = function(a, b) {
return a + b;
};
// ES6 箭头函数
const add = (a, b) => a + b;
// this 指向示例
const obj = {
name: "张三",
fn1: function() {
setTimeout(function() {
console.log(this.name); // undefined(this 指向全局)
}, 100);
},
fn2: function() {
setTimeout(() => {
console.log(this.name); // 张三(this 继承自 fn2 的作用域)
}, 100);
}
};
obj.fn1();
obj.fn2();
3. 解构赋值
允许从数组 / 对象中提取值,赋值给变量,简化数据提取逻辑:
- 数组解构:按索引匹配,支持默认值;
- 对象解构:按属性名匹配,支持重命名和默认值。示例:
// 数组解构
const [a, b, c = 30] = [10, 20];
console.log(a, b, c); // 10 20 30
// 对象解构
const { name: userName, age = 18 } = { name: "李四" };
console.log(userName, age); // 李四 18
4. 扩展运算符与剩余参数
- 扩展运算符(
...):将数组 / 对象展开为单个元素,用于合并数据、传递参数;
- 剩余参数(
...):收集剩余的参数,转为数组,替代 arguments。示例:
// 扩展运算符
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [...arr1, ...arr2]; // [1,2,3,4,5,6]
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { ...obj1, ...obj2 }; // {a:1, b:2}
// 剩余参数
const sum = (...args) => args.reduce((total, cur) => total + cur, 0);
console.log(sum(1,2,3)); // 6
5. 模板字符串
用反引号()包裹字符串,支持换行和变量插值(${变量}`),解决 ES5 字符串拼接繁琐的问题:
const name = "王五";
const age = 20;
// ES5 拼接
const str1 = "姓名:" + name + ",年龄:" + age + "岁";
// ES6 模板字符串
const str2 = `姓名:${name},年龄:${age}岁`;
6. 其他核心特性
-
Set/Map 数据结构:Set 用于存储唯一值(数组去重),Map 键值对集合(键可为任意类型,替代对象);
-
Class 类:语法糖,简化原型链继承,提供 constructor、extends、super 等关键字;
- 模块化(
import/export):替代 CommonJS/AMD,实现按需加载,提升代码模块化程度;
- 可选链(
?.)、空值合并(??):ES2020 特性,简化空值判断,避免 Cannot read property 'xxx' of undefined 错误。
ES6+ 新特性的核心价值在于 “语法简化” 和 “功能补全”,让 JavaScript 从 “脚本语言” 向 “工程化语言” 迈进,是现代前端开发(React/Vue/TypeScript)的基础。
二、异步(Promise, async/await)
JavaScript 是单线程语言,默认同步执行代码,但网络请求、定时器、文件操作等场景需要异步处理,否则会阻塞主线程。异步编程经历了 “回调函数 → Promise → async/await” 的演进,核心目标是解决 “回调地狱”,让异步代码更易读、易维护。
1. 异步编程的核心问题:回调地狱
ES5 中异步操作依赖回调函数,多个异步嵌套时会出现 “回调地狱”(代码层级深、可读性差、错误处理繁琐):
// 回调地狱:获取用户信息 → 获取用户订单 → 获取订单详情
$.get("/api/user", (user) => {
$.get(`/api/order?userId=${user.id}`, (order) => {
$.get(`/api/orderDetail?orderId=${order.id}`, (detail) => {
console.log(detail);
}, (err) => {
console.error("获取订单详情失败", err);
});
}, (err) => {
console.error("获取订单失败", err);
});
}, (err) => {
console.error("获取用户失败", err);
});
问题:层级嵌套过深,错误处理分散,代码难以调试和维护。
2. Promise:异步操作的标准化封装
Promise 是 ES6 引入的异步编程解决方案,本质是一个对象,代表异步操作的 “未完成 / 成功 / 失败” 状态,核心特性:
- 三种状态:
pending(进行中)、fulfilled(已成功)、rejected(已失败),状态一旦改变不可逆转;
- 两个回调:
then() 处理成功结果,catch() 处理失败结果,支持链式调用;
- 解决回调地狱:通过链式调用替代嵌套,错误可统一捕获。
(1)Promise 基本用法
// 创建 Promise 对象
const getPromise = (url) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText)); // 成功:调用 resolve
} else {
reject(new Error(xhr.statusText)); // 失败:调用 reject
}
};
xhr.onerror = () => {
reject(new Error("网络请求失败"));
};
xhr.send();
});
};
// 链式调用:解决回调地狱
getPromise("/api/user")
.then((user) => getPromise(`/api/order?userId=${user.id}`))
.then((order) => getPromise(`/api/orderDetail?orderId=${order.id}`))
.then((detail) => console.log(detail))
.catch((err) => console.error("请求失败", err)); // 统一捕获所有错误
(2)Promise 常用方法
-
Promise.all():接收多个 Promise 数组,全部成功才返回结果数组,一个失败则立即失败;
-
Promise.race():接收多个 Promise 数组,返回第一个完成的 Promise 结果(无论成功 / 失败);
-
Promise.resolve()/Promise.reject():快速创建成功 / 失败的 Promise 对象;
-
Promise.allSettled():等待所有 Promise 完成(无论成功 / 失败),返回所有结果(包含状态和值)。
示例(Promise.all):
// 同时请求多个接口,全部完成后处理
const promise1 = getPromise("/api/user");
const promise2 = getPromise("/api/goods");
Promise.all([promise1, promise2])
.then(([user, goods]) => {
console.log("用户信息", user);
console.log("商品信息", goods);
})
.catch((err) => console.error("某个请求失败", err));
3. async/await:异步代码同步化
ES2017 引入的 async/await 是 Promise 的语法糖,允许用 “同步代码的写法” 处理异步操作,核心规则:
-
async 修饰函数:使函数返回一个 Promise 对象;
-
await 修饰 Promise:暂停函数执行,直到 Promise 状态变为成功,返回结果;若 Promise 失败,需用 try/catch 捕获错误。
(1)基本用法(解决回调地狱的终极方案)
// 封装异步请求函数(返回 Promise)
const getUser = () => getPromise("/api/user");
const getOrder = (userId) => getPromise(`/api/order?userId=${userId}`);
const getOrderDetail = (orderId) => getPromise(`/api/orderDetail?orderId=${orderId}`);
// async/await 写法:同步风格的异步代码
const getOrderInfo = async () => {
try {
const user = await getUser(); // 等待 getUser 完成
const order = await getOrder(user.id); // 等待 getOrder 完成
const detail = await getOrderDetail(order.id); // 等待 getOrderDetail 完成
console.log(detail);
} catch (err) {
console.error("请求失败", err); // 统一捕获所有错误
}
};
getOrderInfo();
(2)async/await 优势
- 代码扁平化:无嵌套,可读性接近同步代码;
- 错误处理统一:通过
try/catch 捕获所有异步错误,替代 Promise 的 catch();
- 调试友好:可在
await 处打断点,调试流程与同步代码一致。
4. 异步编程的核心原则
- 避免同步阻塞:异步操作始终不阻塞主线程(如定时器、网络请求由浏览器内核的线程处理);
- 错误处理全覆盖:Promise 需加
catch(),async/await 需包 try/catch,避免未捕获的异步错误;
- 并行处理优化:多个无依赖的异步操作,用
Promise.all() 替代串行 await,提升执行效率。
异步编程是前端开发的核心难点,Promise 解决了 “回调地狱” 的结构问题,async/await 则让异步代码的可读性达到了同步代码的水平,是现代前端处理网络请求、异步数据加载的标配。
三、闭包和原型链
闭包和原型链是 JavaScript 的两大核心特性,也是面试高频考点。闭包关乎作用域和变量生命周期,原型链则是 JavaScript 实现继承的底层机制,理解这两个概念能帮你突破 “语法使用” 到 “原理理解” 的瓶颈。
1. 闭包(Closure)
(1)闭包的定义
闭包是指 “有权访问另一个函数作用域中变量的函数”,本质是函数作用域链的保留:当内部函数被外部引用时,其所在的作用域不会被垃圾回收机制销毁,从而可以持续访问外层函数的变量。
(2)闭包的形成条件
- 存在嵌套函数(内部函数 + 外部函数);
- 内部函数引用外部函数的变量 / 参数;
- 外部函数执行后,内部函数被外部环境引用(如返回、赋值给全局变量)。
(3)基本用法与示例
// 基础闭包:外部函数执行后,内部函数仍能访问其变量
function outer() {
const num = 10; // 外部函数的变量
// 内部函数引用外部变量
function inner() {
console.log(num);
}
return inner; // 返回内部函数,使其被外部引用
}
const fn = outer(); // outer 执行完毕,但其作用域未被销毁
fn(); // 10(inner 仍能访问 num)
(4)闭包的核心应用场景
-
封装私有变量:模拟 “私有属性 / 方法”,避免全局变量污染;
// 封装计数器:count 是私有变量,只能通过方法修改
function createCounter() {
let count = 0;
return {
increment: () => count++,
decrement: () => count--,
getCount: () => count
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined(无法直接访问)
-
防抖 / 节流函数:利用闭包保存定时器 ID、上次执行时间等状态;
// 防抖函数(闭包保存 timer 变量)
function debounce(fn, delay) {
let timer = null; // 闭包保存 timer,多次调用共享同一个 timer
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
-
柯里化函数:将多参数函数转为单参数函数,利用闭包缓存已传入的参数。
(5)闭包的注意事项
- 内存泄漏风险:闭包会保留外层作用域,若长期引用未释放(如赋值给全局变量),会导致变量无法被垃圾回收,占用内存;
- 解决:使用完闭包后,手动解除引用(如
fn = null),让作用域可以被回收。
2. 原型链(Prototype Chain)
JavaScript 是 “基于原型的面向对象语言”,没有类(ES6 Class 是语法糖),所有对象都通过 “原型” 实现属性和方法的继承,原型链是实现继承的核心机制。
(1)核心概念
- 原型(
prototype):函数特有的属性,指向一个对象,该对象是当前函数创建的所有实例的原型;
- 隐式原型(
__proto__):所有对象(包括函数)都有的属性,指向其构造函数的 prototype;
- 原型链:当访问对象的属性 / 方法时,先在自身查找,找不到则通过
__proto__ 向上查找,直到 Object.prototype,这个查找链条就是原型链。
(2)原型链的基本结构
// 构造函数
function Person(name) {
this.name = name;
}
// 给原型添加方法
Person.prototype.sayHello = function() {
console.log(`Hello, ${this.name}`);
};
// 创建实例
const p1 = new Person("张三");
// 原型链查找:p1 → Person.prototype → Object.prototype → null
console.log(p1.name); // 自身属性,直接返回
p1.sayHello(); // p1 自身无 sayHello,查找 p1.__proto__(Person.prototype)找到
console.log(p1.toString()); // p1 和 Person.prototype 无 toString,查找 Object.prototype 找到
console.log(p1.xxx); // 原型链末端为 null,返回 undefined
(3)原型链的核心应用:继承
ES5 中通过修改原型链实现继承(ES6 Class 的 extends 底层仍是原型链):
// 父类
function Parent(name) {
this.name = name;
}
Parent.prototype.eat = function() {
console.log(`${this.name} 吃饭`);
};
// 子类
function Child(name, age) {
Parent.call(this, name); // 继承父类实例属性
this.age = age;
}
// 继承父类原型方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修正构造函数指向
// 子类添加自有方法
Child.prototype.run = function() {
console.log(`${this.name} 跑步,年龄 ${this.age}`);
};
const child = new Child("李四", 10);
child.eat(); // 继承父类方法
child.run(); // 子类自有方法
(4)原型链的关键规则
- 所有对象的最终原型是
Object.prototype,其 __proto__ 为 null;
- 函数的
prototype 是普通对象,Function.prototype 是函数(特殊);
- 修改原型会影响所有实例(原型共享特性)。
3. 闭包与原型链的关联
闭包关注 “作用域和变量保留”,原型链关注 “对象属性继承”,二者共同构成 JavaScript 的核心底层逻辑:闭包让函数可以突破作用域限制访问变量,原型链让对象可以突破自身结构继承方法,是理解 JavaScript 设计思想的关键。
四、DOM 操作和事件处理
DOM(文档对象模型)是浏览器将 HTML 文档解析成的树形结构,前端开发的核心是通过 JavaScript 操作 DOM 实现页面交互,事件处理则是响应用户操作(点击、输入、滚动等)的核心机制。
1. DOM 操作
DOM 操作分为 “查找节点”“创建 / 插入节点”“修改节点”“删除节点” 四类,核心是操作 DOM 树的节点(元素节点、文本节点、属性节点)。
(1)查找 DOM 节点(核心)
查找是 DOM 操作的第一步,常用方法:
- 按 ID 查找:
document.getElementById("id") → 返回单个元素(效率最高);
- 按类名查找:
document.getElementsByClassName("className") → 返回 HTMLCollection(动态集合);
- 按标签名查找:
document.getElementsByTagName("tagName") → 返回 HTMLCollection;
- 按选择器查找:
document.querySelector("selector")(返回第一个匹配元素)、document.querySelectorAll("selector")(返回 NodeList,静态集合)→ 最灵活,支持 CSS 选择器。
示例:
// 按 ID 查找
const box = document.getElementById("box");
// 按选择器查找
const item = document.querySelector(".list .item");
const items = document.querySelectorAll(".list .item"); // NodeList 可通过 forEach 遍历
(2)创建与插入节点
动态生成页面内容的核心,常用方法:
示例:
// 创建元素并插入
const ul = document.querySelector("ul");
const li = document.createElement("li");
li.textContent = "新列表项"; // 设置文本内容(安全,无 XSS)
ul.appendChild(li);
// innerHTML 方式(慎用,避免用户输入内容)
ul.innerHTML += "<li>新列表项</li>";
(3)修改 DOM 节点
-
修改属性:element.setAttribute("attr", "value")(设置属性)、element.getAttribute("attr")(获取属性)、element.removeAttribute("attr")(移除属性);
const img = document.querySelector("img");
img.setAttribute("src", "new.jpg");
console.log(img.getAttribute("src")); // new.jpg
-
修改样式:
- 行内样式:
element.style.cssProperty = "value"(驼峰命名,如 backgroundColor);
- 类名样式:
element.classList.add("className")、element.classList.remove("className")、element.classList.toggle("className")(推荐,分离样式和逻辑)。
const div = document.querySelector(".box");
div.style.width = "200px";
div.classList.add("active"); // 添加类名
div.classList.toggle("show"); // 切换类名
-
修改文本 / HTML:element.textContent(纯文本,安全)、element.innerHTML(HTML 字符串,有 XSS 风险)。
(4)删除 DOM 节点
-
parent.removeChild(child):父节点移除子节点;
-
element.remove():元素自身移除(ES6+ 方法,更简洁)。
示例:
const li = document.querySelector("li");
li.parentElement.removeChild(li); // 传统方式
// 或
li.remove(); // 简洁方式
(5)DOM 操作的性能优化
DOM 操作是 “重操作”,频繁修改会触发浏览器重排(Reflow)/ 重绘(Repaint),导致页面卡顿,优化手段:
-
批量操作:先将节点脱离文档流(如隐藏父节点),操作完成后再恢复;
-
使用文档碎片:document.createDocumentFragment(),批量插入节点仅触发一次重排;
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `项 ${i}`;
fragment.appendChild(li); // 先插入碎片,无重排
}
document.querySelector("ul").appendChild(fragment); // 仅一次重排
-
避免频繁查询 DOM:将查询结果缓存到变量,减少 DOM 遍历。
2. 事件处理
事件是浏览器触发的 “信号”(如点击、输入、加载),事件处理是 JavaScript 响应用户操作的核心,分为 “事件绑定”“事件流”“事件对象”“事件优化” 四部分。
(1)事件绑定方式
-
行内绑定(不推荐):<button onclick="handleClick()">点击</button> → 耦合度高,不利于维护;
-
DOM0 级绑定:element.onclick = function() {} → 简单,但一个事件只能绑定一个处理函数;
-
DOM2 级绑定:element.addEventListener("eventName", handler, useCapture) → 推荐,支持绑定多个处理函数,可控制事件阶段;
-
DOM0 级:浏览器原生支持,无官方规范 → element.onclick = function() {}
-
DOM1 级:仅规范 DOM 结构,未新增事件绑定方式 → 无事件相关内容
-
DOM2 级:W3C 发布标准,新增 addEventListener → 支持多绑定、事件阶段
-
DOM3 级:在 DOM2 基础上新增了更多事件类型(如键盘、鼠标滚轮事件)
const btn = document.querySelector("button");
// DOM0 级
btn.onclick = function() {
console.log("点击1");
};
btn.onclick = function() {
console.log("点击2"); // 覆盖上一个处理函数
};
// DOM2 级
const handleClick = () => console.log("点击1");
btn.addEventListener("click", handleClick);
btn.addEventListener("click", () => console.log("点击2")); // 可绑定多个
btn.removeEventListener("click", handleClick); // 可移除
(2)事件流(事件传播机制)
事件流分为三个阶段:
- 捕获阶段:事件从
document 向下传播到目标元素;
- 目标阶段:事件到达目标元素;
- 冒泡阶段:事件从目标元素向上传播到
document。
addEventListener 的第三个参数 useCapture:true 表示在捕获阶段触发,false(默认)表示在冒泡阶段触发。
(3)事件对象(Event)
事件处理函数的第一个参数是事件对象,包含事件的核心信息:
-
event.target:触发事件的原始元素(事件源);
-
event.currentTarget:绑定事件的元素;
-
event.preventDefault():阻止默认行为(如表单提交、链接跳转);
-
event.stopPropagation():阻止事件传播(冒泡 / 捕获);
-
event.stopImmediatePropagation():阻止事件传播,且阻止当前元素后续的事件处理函数执行。
示例:
// 阻止链接跳转
const a = document.querySelector("a");
a.addEventListener("click", (e) => {
e.preventDefault(); // 阻止默认跳转
console.log("点击链接,不跳转");
});
// 事件委托(利用事件冒泡)
const ul = document.querySelector("ul");
ul.addEventListener("click", (e) => {
if (e.target.tagName === "LI") { // 判断点击的是 li 元素
console.log("点击了列表项", e.target.textContent);
}
});
(4)核心优化:事件委托
利用事件冒泡,将子元素的事件绑定到父元素,减少事件绑定数量,优化性能(尤其适合动态生成的元素):
// 动态生成的 li 无需单独绑定事件,父元素 ul 委托处理
const ul = document.querySelector("ul");
ul.addEventListener("click", (e) => {
if (e.target.classList.contains("item")) {
console.log("点击了动态生成的列表项");
}
});
// 动态添加 li
const li = document.createElement("li");
li.classList.add("item");
li.textContent = "动态项";
ul.appendChild(li);
(5)常见事件类型
- 鼠标事件:
click、dblclick、mouseover、mouseout、mousedown、mouseup;
- 键盘事件:
keydown、keyup、keypress;
- 表单事件:
input、change、submit、focus、blur;
- 页面事件:
load、DOMContentLoaded(DOM 解析完成)、scroll、resize。
DOM 操作和事件处理是前端交互的基础,核心原则是 “减少 DOM 操作次数”“合理利用事件机制”,既保证交互的流畅性,又避免性能问题。
总结
- ES6+ 新特性核心是简化语法、补全功能,是现代前端开发的基础,重点掌握块级作用域、箭头函数、解构、async/await 等高频用法;
- 异步编程从回调地狱演进到 Promise/async/await,核心是让异步代码更易读、易维护,async/await 是当前最优写法;
- 闭包是作用域链的保留,用于封装私有变量、实现防抖节流,需注意内存泄漏;原型链是 JS 继承的底层机制,所有对象通过
__proto__ 形成继承链条;
- DOM 操作需注重性能(批量操作、文档碎片),事件处理核心是事件委托,利用冒泡减少绑定数量,提升页面性能。