JavaScript 入门
1. 什么是 JavaScript(#%E4%BB%80%E4%B9%88%E6%98%AF-javascript) 2. 如何引入 JavaScript(#%E5%A6%82%E4%BD%95%E5%BC%95%E5%85%A5-javascript) 3. 变量与数据类型(#%E5%8F%98%E9%87%8F%E4%B8%8E%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) 4. 运算符(#%E8%BF%90%E7%AE%97%E7%AC%A6) 5. 字符串操作(#%E5%AD%97%E7%AC%A6%E4%B8%B2%
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
最后更新:2026-03-05
目录
- 什么是 JavaScript
- 如何引入 JavaScript
- 变量与数据类型
- 运算符
- 字符串操作
- 数组方法
- 对象
- 流程控制
- 函数
- DOM 操作
- 事件
- 异步编程
- ES6+ 常用特性
- 综合实战:Todo List
什么是 JavaScript
JavaScript 是运行在浏览器中的编程语言,负责网页的"行为"部分:
- HTML 负责结构(内容是什么)
- CSS 负责样式(内容长什么样)
- JavaScript 负责交互(内容怎么响应用户操作)
JavaScript 也可以运行在服务器端(通过 Node.js),但本文聚焦于浏览器环境。
如何引入 JavaScript
方式一:内联写在 HTML 中
<!DOCTYPE html>
<html>
<body>
<h1>我的页面</h1>
<!-- 推荐放在 body 末尾,确保 HTML 先加载完 -->
<script>
console.log("Hello, JavaScript!");
</script>
</body>
</html>
方式二:外部文件引入(推荐)
<!DOCTYPE html>
<html>
<body>
<h1>我的页面</h1>
<!-- src 指向外部 JS 文件,defer 表示等 HTML 解析完再执行 -->
<script src="main.js" defer></script>
</body>
</html>
// main.js
console.log("Hello from external file!");
查看输出
按 F12 打开浏览器开发者工具,切换到 Console(控制台)标签页,即可看到 console.log() 的输出。
变量与数据类型
var / let / const
JavaScript 有三种声明变量的方式:
// var:旧写法,有变量提升问题,不推荐使用
var oldWay = "旧方式";
// let:可以重新赋值的变量
let count = 0;
count = 1; // 允许
// const:常量,声明后不能重新赋值(推荐优先使用)
const PI = 3.14;
// PI = 3; // 报错:Assignment to constant variable.
推荐原则:默认用 const,需要改变时再用 let,不要用 var。
var / let / const 区别对比
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 有,初始值为 undefined | 有,但有暂时性死区 | 有,但有暂时性死区 |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许 |
| 挂载到 window | 是 | 否 | 否 |
命名规范
// 小驼峰命名法(推荐用于变量和函数)
const userName = "张三";
const totalPrice = 99.9;
// 全大写加下划线(推荐用于真正的常量)
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = "https://api.example.com";
// 布尔值通常以 is / has / can 开头,更易读
const isLoggedIn = true;
const hasPermission = false;
// 不推荐的命名
const a = 1; // 含义不明
const UserName = ""; // 首字母大写通常留给 class
数据类型
JavaScript 共 8 种数据类型,分为基本类型和引用类型两大类:
// 基本类型(Primitive)
// 1. string:字符串
const name = "张三";
const greeting = '你好';
// 2. number:数字(整数和小数都是 number)
const age = 25;
const price = 9.99;
const infinity = Infinity; // 无穷大
const notANumber = NaN; // 非数字,但类型是 number
// 3. boolean:布尔值
const isActive = true;
const isDone = false;
// 4. null:表示"有意为空",需要手动赋值
const selectedUser = null;
// 5. undefined:表示"未定义",变量声明但未赋值时的默认值
let score;
console.log(score); // undefined
// 6. symbol:唯一标识符(ES6 新增,进阶内容)
const id = Symbol("id");
// 7. bigint:超大整数(ES2020 新增)
const bigNumber = 9007199254740991n; // 末尾加 n
// 引用类型
// 8. object:对象(数组、函数等也属于 object)
const person = { name: "张三", age: 25 };
const colors = ["红", "绿", "蓝"];
typeof 检测类型
console.log(typeof "hello"); // "string"
console.log(typeof 42); // "number"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object"(历史遗留 bug,记住这个特例)
console.log(typeof {}); // "object"
console.log(typeof []); // "object"(数组也是 object)
console.log(typeof function(){}); // "function"
console.log(typeof Symbol()); // "symbol"
console.log(typeof 42n); // "bigint"
// 检测数组用 Array.isArray()
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
运算符
算术运算符
const a = 10;
const b = 3;
console.log(a + b); // 13,加
console.log(a - b); // 7,减
console.log(a * b); // 30,乘
console.log(a / b); // 3.3333...,除
console.log(a % b); // 1,取余(模运算)
console.log(a ** b); // 1000,幂运算(10 的 3 次方)
// 自增自减
let count = 5;
count++; // count 变为 6
count--; // count 变为 5
count += 2; // count 变为 7
count *= 3; // count 变为 21
比较运算符:== vs ===
// == 宽松相等:会进行类型转换再比较(不推荐)
console.log(1 == "1"); // true(字符串"1"被转为数字1)
console.log(0 == false); // true(false 被转为 0)
console.log(null == undefined); // true
// === 严格相等:类型和值都必须相同(推荐)
console.log(1 === "1"); // false
console.log(1 === 1); // true
console.log(null === undefined); // false
// 不等
console.log(1 != "1"); // false(宽松,类型转换后相等)
console.log(1 !== "1"); // true(严格,类型不同)
// 大小比较
console.log(5 > 3); // true
console.log(5 >= 5); // true
console.log(3 < 5); // true
console.log(3 <= 2); // false
逻辑运算符
// && 逻辑与:两者都为真才为真
console.log(true && true); // true
console.log(true && false); // false
// || 逻辑或:至少一个为真则为真
console.log(false || true); // true
console.log(false || false); // false
// ! 逻辑非:取反
console.log(!true); // false
console.log(!false); // true
// 短路求值(非常重要)
// && 左边为假时,右边不执行,直接返回左边的值
const result1 = null && "永远不会到这里"; // null
const result2 = "有值" && "到这里了"; // "到这里了"
// || 左边为真时,右边不执行,直接返回左边的值
const result3 = "有值" || "不会到这里"; // "有值"
const result4 = null || "默认值"; // "默认值"
// 常见用法:提供默认值
const userName = inputValue || "匿名用户";
三元运算符
// 语法:条件 ? 真时的值 : 假时的值
const age = 20;
const status = age >= 18 ? "成年人" : "未成年";
console.log(status); // "成年人"
// 等同于
let status2;
if (age >= 18) {
status2 = "成年人";
} else {
status2 = "未成年";
}
可选链运算符 ?.
// 问题:深层访问对象属性时,中间某层为 null/undefined 会报错
const user = null;
// console.log(user.name); // 报错:Cannot read properties of null
// 解决:使用可选链 ?.,遇到 null/undefined 直接返回 undefined
console.log(user?.name); // undefined,不报错
console.log(user?.address?.city); // undefined
const obj = { a: { b: { c: 42 } } };
console.log(obj?.a?.b?.c); // 42
console.log(obj?.x?.y?.z); // undefined
空值合并运算符 ??
// ?? 只在左边为 null 或 undefined 时才使用右边的值
// 与 || 的区别:|| 会对所有假值(0、""、false)使用右边
const score = 0;
console.log(score || 100); // 100,因为 0 是假值(可能不是你想要的)
console.log(score ?? 100); // 0,因为 0 不是 null/undefined
const name = "";
console.log(name || "匿名"); // "匿名"
console.log(name ?? "匿名"); // ""(空字符串不是 null/undefined)
字符串操作
模板字符串(Template Literals)
const name = "张三";
const age = 25;
// 旧方式:字符串拼接,繁琐易出错
const msg1 = "你好,我叫" + name + ",今年" + age + "岁。";
// 新方式:模板字符串(反引号包裹,${}内写表达式)
const msg2 = `你好,我叫${name},今年${age}岁。`;
console.log(msg2); // 你好,我叫张三,今年25岁。
// 可以在 ${} 中写任意表达式
const result = `1 + 1 = ${1 + 1}`;
const isAdult = `${age >= 18 ? "成年" : "未成年"}`;
// 模板字符串支持多行
const html = `
<div>
<h1>${name}</h1>
<p>年龄:${age}</p>
</div>
`;
常用字符串方法
const str = " Hello, JavaScript! ";
// length:字符串长度(属性,不是方法)
console.log(str.length); // 22
// trim():去除首尾空白
console.log(str.trim()); // "Hello, JavaScript!"
console.log(str.trimStart()); // "Hello, JavaScript! "(仅去左边)
console.log(str.trimEnd()); // " Hello, JavaScript!"(仅去右边)
const clean = "Hello, JavaScript!";
// indexOf(searchStr):查找子字符串首次出现的位置,找不到返回 -1
console.log(clean.indexOf("Java")); // 7
console.log(clean.indexOf("Python")); // -1
// includes(searchStr):是否包含某子字符串,返回布尔值
console.log(clean.includes("Java")); // true
console.log(clean.includes("Python")); // false
// startsWith / endsWith:判断开头/结尾
console.log(clean.startsWith("Hello")); // true
console.log(clean.endsWith("!")); // true
console.log(clean.startsWith("Java")); // false
// slice(start, end):截取子字符串,end 不包含
// start/end 可以是负数,表示从末尾倒数
console.log(clean.slice(7)); // "JavaScript!"
console.log(clean.slice(7, 17)); // "JavaScript"
console.log(clean.slice(-1)); // "!"
// split(separator):按分隔符拆分为数组
console.log(clean.split(", ")); // ["Hello", "JavaScript!"]
console.log("a-b-c".split("-")); // ["a", "b", "c"]
console.log("abc".split("")); // ["a", "b", "c"]
// replace(search, replacement):替换第一个匹配
console.log(clean.replace("Hello", "Hi")); // "Hi, JavaScript!"
// replaceAll(search, replacement):替换所有匹配
console.log("a-b-a".replaceAll("a", "x")); // "x-b-x"
// toUpperCase / toLowerCase:大小写转换
console.log(clean.toUpperCase()); // "HELLO, JAVASCRIPT!"
console.log(clean.toLowerCase()); // "hello, javascript!"
// padStart / padEnd:补齐长度
console.log("5".padStart(3, "0")); // "005"
console.log("hi".padEnd(5, ".")); // "hi..."
// repeat:重复
console.log("ha".repeat(3)); // "hahaha"
数组方法
基础增删方法
const fruits = ["苹果", "香蕉", "橙子"];
// push:在末尾添加,返回新长度
fruits.push("葡萄");
console.log(fruits); // ["苹果", "香蕉", "橙子", "葡萄"]
// pop:删除末尾元素,返回被删除的元素
const last = fruits.pop();
console.log(last); // "葡萄"
console.log(fruits); // ["苹果", "香蕉", "橙子"]
// unshift:在开头添加,返回新长度
fruits.unshift("西瓜");
console.log(fruits); // ["西瓜", "苹果", "香蕉", "橙子"]
// shift:删除开头元素,返回被删除的元素
const first = fruits.shift();
console.log(first); // "西瓜"
console.log(fruits); // ["苹果", "香蕉", "橙子"]
遍历方法
const numbers = [1, 2, 3, 4, 5];
// forEach:遍历,无返回值(不能 break)
numbers.forEach((num, index) => {
console.log(`第${index}项:${num}`);
});
// map:遍历并返回新数组(不修改原数组)
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(numbers); // [1, 2, 3, 4, 5](原数组不变)
// filter:过滤,返回满足条件的元素组成的新数组
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]
// find:返回第一个满足条件的元素,找不到返回 undefined
const firstEven = numbers.find(num => num % 2 === 0);
console.log(firstEven); // 2
// findIndex:返回第一个满足条件的元素的索引,找不到返回 -1
const firstEvenIndex = numbers.findIndex(num => num % 2 === 0);
console.log(firstEvenIndex); // 1
// some:是否至少有一个元素满足条件,返回布尔值
console.log(numbers.some(num => num > 4)); // true
console.log(numbers.some(num => num > 10)); // false
// every:是否所有元素都满足条件,返回布尔值
console.log(numbers.every(num => num > 0)); // true
console.log(numbers.every(num => num > 3)); // false
// includes:是否包含某值,返回布尔值
console.log(numbers.includes(3)); // true
console.log(numbers.includes(9)); // false
reduce:累积计算
const numbers = [1, 2, 3, 4, 5];
// reduce(callback, initialValue)
// callback 接收 (accumulator, currentValue, index, array)
// accumulator:累积值(上一次回调的返回值)
// currentValue:当前元素
// 求和
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15
// 求最大值
const max = numbers.reduce((acc, num) => (num > acc ? num : acc), numbers[0]);
console.log(max); // 5
// 统计出现次数
const words = ["apple", "banana", "apple", "cherry", "banana", "apple"];
const count = words.reduce((acc, word) => {
acc[word] = (acc[word] || 0) + 1;
return acc;
}, {});
console.log(count); // { apple: 3, banana: 2, cherry: 1 }
其他常用方法
// sort:排序(默认按字符串排序,数字排序需传比较函数)
const letters = ["c", "a", "b"];
letters.sort();
console.log(letters); // ["a", "b", "c"]
const nums = [10, 1, 5, 2];
nums.sort((a, b) => a - b); // 升序
console.log(nums); // [1, 2, 5, 10]
nums.sort((a, b) => b - a); // 降序
console.log(nums); // [10, 5, 2, 1]
// flat:展平嵌套数组
const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat()); // [1, 2, 3, 4, [5, 6]](只展平一层)
console.log(nested.flat(2)); // [1, 2, 3, 4, 5, 6](展平两层)
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5, 6](展平所有层)
// slice:截取子数组(不修改原数组)
const arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1, 3)); // [2, 3]
console.log(arr.slice(-2)); // [4, 5]
// concat:合并数组
const a = [1, 2];
const b = [3, 4];
console.log(a.concat(b)); // [1, 2, 3, 4]
// join:将数组元素连接为字符串
console.log(["a", "b", "c"].join("-")); // "a-b-c"
console.log(["a", "b", "c"].join("")); // "abc"
// 展开运算符(spread)合并数组
const merged = [...a, ...b, 5];
console.log(merged); // [1, 2, 3, 4, 5]
对象
创建与访问
// 创建对象
const person = {
name: "张三",
age: 25,
isStudent: false,
address: {
city: "北京",
district: "海淀"
},
// 方法
greet() {
return `你好,我叫${this.name}`;
}
};
// 访问属性:点语法
console.log(person.name); // "张三"
console.log(person.address.city); // "北京"
// 访问属性:方括号语法(当属性名是变量时使用)
const key = "age";
console.log(person[key]); // 25
// 调用方法
console.log(person.greet()); // "你好,我叫张三"
// 修改属性
person.age = 26;
// 添加新属性
person.email = "[email protected]";
// 删除属性
delete person.isStudent;
对象解构
const user = {
name: "李四",
age: 30,
city: "上海",
role: "admin"
};
// 基本解构
const { name, age } = user;
console.log(name); // "李四"
console.log(age); // 30
// 重命名
const { name: userName, age: userAge } = user;
console.log(userName); // "李四"
// 默认值(属性不存在时使用默认值)
const { name: n, score = 100 } = user;
console.log(score); // 100(user 中没有 score,使用默认值)
// 剩余属性
const { name: theName, ...rest } = user;
console.log(theName); // "李四"
console.log(rest); // { age: 30, city: "上海", role: "admin" }
// 函数参数中解构(非常常用)
function displayUser({ name, age, city = "未知" }) {
console.log(`${name},${age}岁,来自${city}`);
}
displayUser(user); // "李四,30岁,来自上海"
对象展开与合并
const defaults = { color: "red", size: "medium", weight: 100 };
const custom = { color: "blue", weight: 200 };
// 展开合并(后面的属性覆盖前面的)
const config = { ...defaults, ...custom };
console.log(config);
// { color: "blue", size: "medium", weight: 200 }
// 创建对象副本(浅拷贝)
const original = { a: 1, b: 2 };
const copy = { ...original };
copy.a = 99;
console.log(original.a); // 1(原对象不受影响)
Object 静态方法
const obj = { a: 1, b: 2, c: 3 };
// Object.keys():返回所有键名组成的数组
console.log(Object.keys(obj)); // ["a", "b", "c"]
// Object.values():返回所有值组成的数组
console.log(Object.values(obj)); // [1, 2, 3]
// Object.entries():返回 [键, 值] 对组成的数组
console.log(Object.entries(obj));
// [["a", 1], ["b", 2], ["c", 3]]
// 遍历对象(常用模式)
for (const [key, value] of Object.entries(obj)) {
console.log(`${key}: ${value}`);
}
// Object.fromEntries():将 [键, 值] 数组转为对象
const entries = [["x", 10], ["y", 20]];
console.log(Object.fromEntries(entries)); // { x: 10, y: 20 }
// 实用:将对象的所有值乘以2
const doubled = Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, value * 2])
);
console.log(doubled); // { a: 2, b: 4, c: 6 }
流程控制
if / else
const score = 85;
if (score >= 90) {
console.log("优秀");
} else if (score >= 80) {
console.log("良好");
} else if (score >= 60) {
console.log("及格");
} else {
console.log("不及格");
}
// 输出:"良好"
switch / case
const day = "Monday";
switch (day) {
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
console.log("工作日");
break; // 不加 break 会继续执行下一个 case
case "Saturday":
case "Sunday":
console.log("周末");
break;
default:
console.log("无效日期");
}
// 输出:"工作日"
for 循环
// 基础 for 循环
for (let i = 0; i < 5; i++) {
console.log(i); // 0, 1, 2, 3, 4
}
// for...of:遍历可迭代对象(数组、字符串等)(推荐用于数组)
const fruits = ["苹果", "香蕉", "橙子"];
for (const fruit of fruits) {
console.log(fruit);
}
// for...in:遍历对象的键名(主要用于对象)
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
console.log(`${key}: ${obj[key]}`);
}
// while 循环:先判断条件再执行
let count = 0;
while (count < 3) {
console.log(count);
count++;
}
// do...while:先执行一次再判断条件
let n = 0;
do {
console.log(n);
n++;
} while (n < 3);
break 和 continue
// break:跳出循环
for (let i = 0; i < 10; i++) {
if (i === 5) break;
console.log(i); // 0, 1, 2, 3, 4
}
// continue:跳过当前迭代,继续下一次
for (let i = 0; i < 5; i++) {
if (i === 2) continue;
console.log(i); // 0, 1, 3, 4
}
函数
三种定义方式
// 1. 函数声明(Function Declaration)
// 特点:有变量提升,可在定义前调用
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
// 2. 函数表达式(Function Expression)
// 特点:没有提升,必须先定义再调用
const multiply = function(a, b) {
return a * b;
};
console.log(multiply(2, 3)); // 6
// 3. 箭头函数(Arrow Function)(ES6 新增,推荐)
// 特点:语法简洁,没有自己的 this
const subtract = (a, b) => a - b;
console.log(subtract(5, 3)); // 2
// 箭头函数的多种写法
const square = x => x * x; // 单参数可省略括号
const greet = () => "Hello!"; // 无参数用空括号
const getObj = () => ({ key: "val" }); // 返回对象字面量需加括号
const divide = (a, b) => { // 多行需要花括号和 return
if (b === 0) return null;
return a / b;
};
参数默认值与 rest 参数
// 默认参数值
function createUser(name, role = "普通用户", active = true) {
return { name, role, active };
}
console.log(createUser("张三")); // { name: "张三", role: "普通用户", active: true }
console.log(createUser("李四", "管理员")); // { name: "李四", role: "管理员", active: true }
console.log(createUser("王五", "游客", false)); // { name: "王五", role: "游客", active: false }
// rest 参数:将剩余参数收集为数组(必须放在最后)
function sum(first, ...rest) {
return rest.reduce((acc, num) => acc + num, first);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
作用域与闭包
// 全局作用域:在任何地方都能访问
const globalVar = "我是全局变量";
function outer() {
// 函数作用域
const outerVar = "我是外层变量";
function inner() {
// 块级作用域(可以访问外层变量)
const innerVar = "我是内层变量";
console.log(globalVar); // 可以访问
console.log(outerVar); // 可以访问
console.log(innerVar); // 可以访问
}
inner();
// console.log(innerVar); // 报错:innerVar 在这里不可见
}
// 变量提升(Hoisting)
console.log(hoisted); // undefined(var 声明提升,但赋值不提升)
var hoisted = "我被提升了";
// console.log(notHoisted); // 报错:不能在声明前访问 let/const
let notHoisted = "我没有提升";
// 闭包:函数记住了它被创建时的词法作用域
function makeCounter() {
let count = 0; // 这个变量被闭包"记住"了
return {
increment() { count++; },
decrement() { count--; },
getCount() { return count; }
};
}
const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // 2
// count 变量在外部无法直接访问,被封装在闭包内
高阶函数
接受函数作为参数,或者返回函数的函数,称为高阶函数。
// 接受函数作为参数(map/filter/forEach 都是高阶函数)
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.filter(num => num % 2 === 0); // filter 接收一个函数
// 返回函数(工厂函数)
function createMultiplier(multiplier) {
// 返回一个新函数
return (number) => number * multiplier;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 函数组合
function pipe(...fns) {
return (value) => fns.reduce((acc, fn) => fn(acc), value);
}
const process = pipe(
x => x * 2,
x => x + 1,
x => x ** 2
);
console.log(process(3)); // ((3 * 2) + 1)² = 49
DOM 操作
DOM(Document Object Model,文档对象模型)是浏览器将 HTML 解析为的树形结构,JavaScript 通过操作 DOM 来动态修改页面。
获取元素
// getElementById:通过 id 获取单个元素
const title = document.getElementById("main-title");
// querySelector:通过 CSS 选择器获取第一个匹配元素
const firstButton = document.querySelector("button");
const submitBtn = document.querySelector("#submit-btn");
const firstCard = document.querySelector(".card");
const input = document.querySelector("input[type='text']");
// querySelectorAll:获取所有匹配元素(返回 NodeList,可用 forEach 遍历)
const allButtons = document.querySelectorAll("button");
const allCards = document.querySelectorAll(".card");
allButtons.forEach(btn => {
console.log(btn.textContent);
});
// 检查是否找到了元素
if (title) {
console.log("找到了标题");
} else {
console.log("没有找到 id 为 main-title 的元素");
}
修改内容
const element = document.querySelector("#content");
// innerHTML:读写元素的 HTML 内容(可以包含 HTML 标签)
element.innerHTML = "<strong>加粗文字</strong>";
console.log(element.innerHTML); // "<strong>加粗文字</strong>"
// textContent:读写元素的纯文本内容(不解析 HTML 标签,更安全)
element.textContent = "<strong>这会显示为普通文字</strong>";
// 页面上显示的就是 "<strong>这会显示为普通文字</strong>" 这段文字本身
// innerText:类似 textContent,但会考虑 CSS 样式(比如隐藏元素不会包含)
element.innerText = "普通文字";
// 注意:使用 innerHTML 插入用户输入的内容存在 XSS 安全风险
// 如果只是设置文字,优先用 textContent
修改样式
const box = document.querySelector(".box");
// 通过 style 直接设置行内样式
box.style.color = "red";
box.style.fontSize = "20px"; // CSS 属性名用驼峰命名
box.style.backgroundColor = "blue";
box.style.display = "none"; // 隐藏元素
box.style.display = ""; // 清除行内样式,恢复 CSS 默认值
// 通过 classList 操作 class(推荐,将样式放在 CSS 中管理)
box.classList.add("active"); // 添加 class
box.classList.remove("active"); // 移除 class
box.classList.toggle("active"); // 切换 class(有就删,没有就加)
box.classList.contains("active"); // 检查是否有某 class,返回布尔值
box.classList.replace("old", "new"); // 替换 class
// 读取计算后的样式(考虑了所有 CSS 规则)
const computedStyle = window.getComputedStyle(box);
console.log(computedStyle.fontSize); // 实际字体大小
console.log(computedStyle.display); // 实际显示方式
创建、插入、删除元素
// createElement:创建新元素
const newDiv = document.createElement("div");
newDiv.textContent = "我是新创建的 div";
newDiv.classList.add("card");
const container = document.querySelector("#container");
// appendChild:在末尾插入子元素
container.appendChild(newDiv);
// insertBefore(newElement, referenceElement):在某元素前插入
const firstChild = container.firstElementChild;
container.insertBefore(newDiv, firstChild);
// 更现代的方法(推荐)
container.prepend(newDiv); // 在开头插入
container.append(newDiv); // 在末尾插入(可以插入字符串)
newDiv.before(container); // 在 container 前插入 newDiv
newDiv.after(container); // 在 container 后插入 newDiv
// insertAdjacentHTML:插入 HTML 字符串(效率更高)
container.insertAdjacentHTML("beforeend", "<p>末尾新段落</p>");
container.insertAdjacentHTML("afterbegin", "<p>开头新段落</p>");
// 删除元素
const toRemove = document.querySelector(".old-element");
if (toRemove) {
toRemove.remove(); // 直接删除自身
}
属性操作
const img = document.querySelector("img");
const input = document.querySelector("input");
// getAttribute:获取属性值
console.log(img.getAttribute("src")); // 图片路径
console.log(img.getAttribute("alt")); // 替代文字
// setAttribute:设置属性值
img.setAttribute("src", "/new-image.jpg");
img.setAttribute("alt", "新图片描述");
// removeAttribute:删除属性
input.removeAttribute("disabled");
// 直接通过属性名访问(更简洁,适用于常见属性)
img.src = "/another-image.jpg";
img.alt = "另一张图片";
input.value = "默认文字";
input.disabled = true;
// data-* 自定义属性(用于存储与元素相关的数据)
// HTML: <div data-user-id="123" data-role="admin">
const div = document.querySelector("div");
console.log(div.dataset.userId); // "123"(data-user-id 转为驼峰)
console.log(div.dataset.role); // "admin"
div.dataset.newProp = "value"; // 动态添加 data 属性
事件
addEventListener
const button = document.querySelector("#my-button");
// 基本用法
button.addEventListener("click", function(event) {
console.log("按钮被点击了!");
console.log(event); // 事件对象,包含事件相关信息
});
// 箭头函数写法
button.addEventListener("click", (event) => {
console.log("点击位置:", event.clientX, event.clientY);
});
addEventListener 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| type | string | 是 | 事件类型,如 "click"、"input"、"keydown" |
| callback | Function | 是 | 事件触发时执行的函数,接收 event 对象为参数 |
| options | boolean 或 object | 否 | true 表示在捕获阶段触发;object 可设置 capture、once、passive |
// options 对象的常用配置
button.addEventListener("click", handler, {
capture: false, // 是否在捕获阶段触发(默认 false,即冒泡阶段)
once: true, // 是否只触发一次,触发后自动移除监听器
passive: true // 是否不调用 preventDefault()(用于滚动优化)
});
// 移除事件监听器(需要保存函数引用)
function handleClick() {
console.log("点击");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // 移除
常用事件类型
const input = document.querySelector("input");
const form = document.querySelector("form");
// 鼠标事件
element.addEventListener("click", handler); // 鼠标单击
element.addEventListener("dblclick", handler); // 双击
element.addEventListener("mouseover", handler); // 鼠标移入(包含子元素)
element.addEventListener("mouseout", handler); // 鼠标移出
element.addEventListener("mouseenter", handler); // 鼠标进入(不包含子元素)
element.addEventListener("mouseleave", handler); // 鼠标离开
// 键盘事件
document.addEventListener("keydown", (e) => {
console.log(e.key); // 按键名称,如 "Enter"、"Escape"、"a"
console.log(e.code); // 物理按键代码,如 "KeyA"、"Enter"
console.log(e.ctrlKey); // 是否按住 Ctrl
console.log(e.shiftKey);// 是否按住 Shift
});
document.addEventListener("keyup", handler); // 按键松开
// 表单事件
input.addEventListener("input", (e) => {
console.log("当前值:", e.target.value); // 每次输入都触发
});
input.addEventListener("change", (e) => {
console.log("值改变了:", e.target.value); // 失焦后才触发
});
input.addEventListener("focus", handler); // 获得焦点
input.addEventListener("blur", handler); // 失去焦点
form.addEventListener("submit", (e) => {
e.preventDefault(); // 阻止表单默认提交(页面刷新)
// 处理表单数据
});
// 页面加载事件
window.addEventListener("load", handler); // 页面及所有资源加载完成
document.addEventListener("DOMContentLoaded", handler); // HTML 解析完成(推荐)
window.addEventListener("resize", handler); // 窗口大小变化
window.addEventListener("scroll", handler); // 页面滚动
事件对象
document.addEventListener("click", (event) => {
// 触发事件的原始元素
console.log(event.target);
// 绑定事件监听器的元素
console.log(event.currentTarget);
// 阻止默认行为(如阻止链接跳转、表单提交)
event.preventDefault();
// 阻止事件冒泡(阻止事件继续向父元素传播)
event.stopPropagation();
// 鼠标位置
console.log(event.clientX, event.clientY); // 相对于视口
console.log(event.pageX, event.pageY); // 相对于文档
});
事件委托
将事件监听器绑定到父元素,利用事件冒泡处理子元素的事件。适合动态添加的元素。
// 问题:列表项是动态添加的,无法提前为每个 li 绑定事件
const list = document.querySelector("#todo-list");
// 不用为每个 li 单独绑定,只在父元素上绑定一次
list.addEventListener("click", (event) => {
// 判断点击的是不是 li 元素
if (event.target.tagName === "LI") {
event.target.classList.toggle("done");
}
// 更精确的判断(支持嵌套元素)
const li = event.target.closest("li");
if (li) {
li.classList.toggle("done");
}
});
异步编程
JavaScript 是单线程的,异步操作(如网络请求、定时器)通过事件循环机制处理。
回调函数
// 回调:将函数作为参数传入,等操作完成后调用
setTimeout(() => {
console.log("1秒后执行");
}, 1000);
// 回调地狱(Callback Hell):多层嵌套导致代码难以维护
fetchUser(userId, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
// 越来越深,难以阅读和维护
});
});
});
Promise
Promise 是异步操作的封装,代表一个未来会完成(或失败)的操作。
// 创建 Promise
const promise = new Promise((resolve, reject) => {
// 执行异步操作
const success = true;
if (success) {
resolve("操作成功!"); // 成功时调用 resolve
} else {
reject(new Error("操作失败!")); // 失败时调用 reject
}
});
// 使用 Promise
promise
.then((result) => {
console.log("成功:", result); // "操作成功!"
})
.catch((error) => {
console.log("失败:", error.message);
})
.finally(() => {
console.log("无论成功失败都会执行"); // 常用于关闭 loading 状态
});
// Promise 链式调用
fetch("/api/users")
.then((response) => response.json()) // 解析 JSON,返回新 Promise
.then((users) => {
console.log(users);
return users[0]; // 返回值传给下一个 .then
})
.then((firstUser) => {
console.log(firstUser.name);
})
.catch((error) => {
// 任何一步出错都会跳到这里
console.error("出错了:", error);
});
Promise 静态方法
const p1 = fetch("/api/users");
const p2 = fetch("/api/posts");
const p3 = fetch("/api/comments");
// Promise.all:所有 Promise 都成功才成功,有一个失败就失败
Promise.all([p1, p2, p3])
.then(([users, posts, comments]) => {
console.log("三个请求都成功了");
})
.catch((error) => {
console.log("至少一个请求失败了");
});
// Promise.allSettled:等所有 Promise 结束,不管成功还是失败
Promise.allSettled([p1, p2, p3])
.then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("成功:", result.value);
} else {
console.log("失败:", result.reason);
}
});
});
// Promise.race:第一个完成的 Promise 的结果(不管成功失败)
Promise.race([p1, p2, p3])
.then((firstResult) => {
console.log("最快的请求结果:", firstResult);
});
async / await
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码,更易读。
// async 函数:始终返回 Promise
async function fetchUserData(userId) {
// await 等待 Promise 完成,暂停函数执行
// await 只能在 async 函数内使用
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user; // 等同于 Promise.resolve(user)
}
// 调用 async 函数(它返回 Promise,可以用 .then 或 await)
fetchUserData(1).then(user => console.log(user));
// 更推荐的方式:在另一个 async 函数中 await
async function main() {
const user = await fetchUserData(1);
console.log(user.name);
}
// 错误处理:用 try/catch
async function safeRequest() {
try {
const response = await fetch("/api/data");
// 检查 HTTP 状态码
if (!response.ok) {
throw new Error(`HTTP 错误:${response.status}`);
}
const data = await response.json();
console.log("数据:", data);
return data;
} catch (error) {
// 网络错误或手动 throw 的错误都会在这里捕获
console.error("请求失败:", error.message);
return null; // 返回 null 作为错误时的默认值
} finally {
console.log("请求结束,关闭 loading");
}
}
fetch API
fetch 是浏览器内置的 HTTP 请求函数,返回 Promise。
fetch 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| url | string | 是 | 请求的 URL |
| options | object | 否 | 请求配置,见下表 |
options 配置说明
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| method | string | "GET" | 请求方法:GET、POST、PUT、DELETE 等 |
| headers | object | {} | 请求头 |
| body | string 或 FormData | 无 | 请求体(GET 请求不能有 body) |
| mode | string | "cors" | 跨域模式:cors、no-cors、same-origin |
| credentials | string | "same-origin" | 是否发送 Cookie:omit、same-origin、include |
// GET 请求
async function getUsers() {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const users = await response.json(); // 解析 JSON 响应体
return users;
}
// POST 请求(发送 JSON 数据)
async function createUser(userData) {
const response = await fetch("https://jsonplaceholder.typicode.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json", // 告诉服务器发送的是 JSON
"Authorization": "Bearer token123" // 鉴权 token(如果需要)
},
body: JSON.stringify(userData) // 将对象转为 JSON 字符串
});
const newUser = await response.json();
return newUser;
}
// 使用示例
async function demo() {
try {
const users = await getUsers();
console.log("所有用户:", users);
const newUser = await createUser({ name: "新用户", email: "[email protected]" });
console.log("创建的用户:", newUser);
} catch (error) {
console.error("操作失败:", error.message);
}
}
demo();
ES6+ 常用特性
解构赋值
// 数组解构
const [first, second, third] = [1, 2, 3];
console.log(first); // 1
console.log(second); // 2
// 跳过某些元素
const [a, , c] = [1, 2, 3];
console.log(a, c); // 1 3
// 默认值
const [x = 10, y = 20] = [5];
console.log(x, y); // 5 20
// 剩余元素
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// 交换变量(无需临时变量)
let p = 1, q = 2;
[p, q] = [q, p];
console.log(p, q); // 2 1
// 对象解构(见"对象"章节)
const { name, age } = { name: "张三", age: 25 };
展开运算符(Spread Operator)
// 展开数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
const withExtra = [...arr1, 99, ...arr2]; // [1, 2, 3, 99, 4, 5, 6]
// 复制数组(浅拷贝)
const copy = [...arr1];
copy.push(100);
console.log(arr1); // [1, 2, 3](原数组不变)
// 展开对象
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 3, c: 4 }(obj2 的 b 覆盖 obj1 的 b)
// 将 arguments/NodeList 转为真正的数组
function test() {
const argsArray = [...arguments];
console.log(argsArray.map(x => x * 2));
}
test(1, 2, 3); // [2, 4, 6]
模块:import / export
// utils.js(导出模块)
// 命名导出
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator {
multiply(a, b) {
return a * b;
}
}
// 默认导出(每个文件只能有一个)
export default function main() {
console.log("主函数");
}
// main.js(导入模块)
// 导入命名导出
import { PI, add, Calculator } from "./utils.js";
// 重命名导入
import { add as sum } from "./utils.js";
// 导入所有命名导出到一个命名空间
import * as utils from "./utils.js";
console.log(utils.PI);
console.log(utils.add(1, 2));
// 导入默认导出(名称可以自定义)
import mainFunction from "./utils.js";
// 同时导入默认导出和命名导出
import mainFunction, { PI, add } from "./utils.js";
class
// 定义类
class Animal {
// constructor:构造函数,创建实例时自动调用
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
// 方法
speak() {
return `${this.name} 说:${this.sound}`;
}
// 静态方法:通过类调用,不能通过实例调用
static create(name, sound) {
return new Animal(name, sound);
}
// getter
get info() {
return `动物:${this.name}`;
}
}
// 创建实例
const dog = new Animal("小狗", "汪汪");
console.log(dog.speak()); // "小狗 说:汪汪"
console.log(dog.info); // "动物:小狗"(getter 当属性访问)
console.log(Animal.create("小猫", "喵喵").speak()); // 静态方法
// 继承
class Dog extends Animal {
constructor(name, breed) {
super(name, "汪汪"); // 必须先调用 super() 调用父类构造函数
this.breed = breed;
}
// 重写父类方法
speak() {
return `${super.speak()}(品种:${this.breed})`; // super 调用父类方法
}
fetch() {
return `${this.name} 去捡球了!`;
}
}
const myDog = new Dog("旺财", "金毛");
console.log(myDog.speak()); // "旺财 说:汪汪(品种:金毛)"
console.log(myDog.fetch()); // "旺财 去捡球了!"
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true(继承关系)
Symbol、WeakMap、WeakSet 简介
// Symbol:唯一标识符,常用于对象属性键,避免命名冲突
const id = Symbol("id");
const anotherId = Symbol("id");
console.log(id === anotherId); // false(每个 Symbol 都是唯一的)
const user = {
name: "张三",
[id]: 12345 // 以 Symbol 为键
};
console.log(user[id]); // 12345
// WeakMap:键必须是对象,不阻止垃圾回收(用于存储私有数据或缓存)
const cache = new WeakMap();
const obj = {};
cache.set(obj, { computedValue: 42 });
console.log(cache.get(obj)); // { computedValue: 42 }
// 当 obj 被垃圾回收后,cache 中对应的条目也会自动清理
// WeakSet:存储对象引用,不阻止垃圾回收(用于标记对象)
const seen = new WeakSet();
const element = document.querySelector("div");
seen.add(element);
console.log(seen.has(element)); // true
综合实战:Todo List
一个完整的待办事项应用,涵盖:DOM 操作、事件处理、本地存储。
HTML 结构
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo List</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; max-width: 500px; margin: 40px auto; padding: 0 16px; }
h1 { margin-bottom: 20px; }
.input-area { display: flex; gap: 8px; margin-bottom: 20px; }
.input-area input { flex: 1; padding: 8px 12px; font-size: 16px; border: 1px solid #ccc; border-radius: 4px; }
.input-area button { padding: 8px 16px; background: #333; color: white; border: none; border-radius: 4px; cursor: pointer; }
.todo-list { list-style: none; }
.todo-item { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid #eee; }
.todo-item.done .todo-text { text-decoration: line-through; color: #aaa; }
.todo-text { flex: 1; font-size: 16px; }
.delete-btn { background: none; border: none; color: #f44; cursor: pointer; font-size: 18px; }
.stats { margin-top: 16px; color: #666; font-size: 14px; }
.filter-area { display: flex; gap: 8px; margin-bottom: 16px; }
.filter-area button { padding: 4px 12px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; background: white; }
.filter-area button.active { background: #333; color: white; border-color: #333; }
</style>
</head>
<body>
<h1>待办事项</h1>
<div class="input-area">
<input type="text" id="todo-input" placeholder="输入待办事项..." />
<button id="add-btn">添加</button>
</div>
<div class="filter-area">
<button class="filter-btn active" data-filter="all">全部</button>
<button class="filter-btn" data-filter="active">未完成</button>
<button class="filter-btn" data-filter="done">已完成</button>
</div>
<ul class="todo-list" id="todo-list"></ul>
<div class="stats" id="stats"></div>
<script src="todo.js"></script>
</body>
</html>
JavaScript 逻辑
// todo.js
// ------- 状态管理 -------
// 从 localStorage 读取数据,如果没有则初始化为空数组
let todos = loadFromStorage();
let currentFilter = "all"; // 当前筛选状态
// ------- localStorage 操作 -------
function loadFromStorage() {
try {
const stored = localStorage.getItem("todos");
// 如果 stored 为 null(首次访问),返回空数组;否则解析 JSON
return stored ? JSON.parse(stored) : [];
} catch (error) {
// JSON.parse 出错时(数据损坏),返回空数组
console.error("读取本地数据失败:", error);
return [];
}
}
function saveToStorage() {
// 将 todos 数组转为 JSON 字符串保存
localStorage.setItem("todos", JSON.stringify(todos));
}
// ------- 核心功能 -------
// 生成唯一 ID(用当前时间戳)
function generateId() {
return Date.now().toString();
}
// 添加待办事项
function addTodo(text) {
const trimmed = text.trim();
if (!trimmed) return; // 不允许添加空内容
const todo = {
id: generateId(),
text: trimmed,
done: false,
createdAt: new Date().toISOString()
};
todos.push(todo);
saveToStorage();
render();
}
// 切换完成状态
function toggleTodo(id) {
todos = todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
);
saveToStorage();
render();
}
// 删除待办事项
function deleteTodo(id) {
todos = todos.filter(todo => todo.id !== id);
saveToStorage();
render();
}
// ------- 渲染 -------
// 根据当前筛选条件过滤数据
function getFilteredTodos() {
switch (currentFilter) {
case "active":
return todos.filter(todo => !todo.done);
case "done":
return todos.filter(todo => todo.done);
default: // "all"
return todos;
}
}
// 渲染单个 todo 项的 HTML
function createTodoHTML(todo) {
return `
<li class="todo-item ${todo.done ? "done" : ""}" data-id="${todo.id}">
<input type="checkbox" class="todo-check" ${todo.done ? "checked" : ""} />
<span class="todo-text">${escapeHTML(todo.text)}</span>
<button class="delete-btn" title="删除">×</button>
</li>
`;
}
// 防止 XSS:将用户输入的特殊字符转义
function escapeHTML(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// 渲染统计信息
function renderStats() {
const total = todos.length;
const done = todos.filter(t => t.done).length;
const active = total - done;
const statsEl = document.getElementById("stats");
statsEl.textContent = `共 ${total} 项,已完成 ${done} 项,未完成 ${active} 项`;
}
// 主渲染函数
function render() {
const listEl = document.getElementById("todo-list");
const filtered = getFilteredTodos();
if (filtered.length === 0) {
listEl.innerHTML = "<li style='padding:12px;color:#aaa;'>暂无待办事项</li>";
} else {
listEl.innerHTML = filtered.map(createTodoHTML).join("");
}
renderStats();
}
// ------- 事件绑定 -------
// 添加按钮点击
document.getElementById("add-btn").addEventListener("click", () => {
const input = document.getElementById("todo-input");
addTodo(input.value);
input.value = ""; // 清空输入框
input.focus(); // 焦点回到输入框
});
// 按 Enter 键添加
document.getElementById("todo-input").addEventListener("keydown", (e) => {
if (e.key === "Enter") {
const input = e.target;
addTodo(input.value);
input.value = "";
}
});
// 列表事件委托:处理复选框和删除按钮
document.getElementById("todo-list").addEventListener("click", (e) => {
// 获取最近的 li 元素,取出其 data-id
const li = e.target.closest(".todo-item");
if (!li) return;
const id = li.dataset.id;
if (e.target.classList.contains("todo-check")) {
// 点击了复选框
toggleTodo(id);
} else if (e.target.classList.contains("delete-btn")) {
// 点击了删除按钮
deleteTodo(id);
}
});
// 筛选按钮
document.querySelector(".filter-area").addEventListener("click", (e) => {
const btn = e.target.closest(".filter-btn");
if (!btn) return;
// 更新当前筛选状态
currentFilter = btn.dataset.filter;
// 更新按钮激活样式
document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
render();
});
// ------- 初始化 -------
render();
要点总结
这个 Todo List 综合了以下知识点:
localStorage.getItem/setItem:持久化存储数据,页面刷新后数据不丢失JSON.stringify/JSON.parse:在字符串和对象之间转换(localStorage 只能存字符串)Array.map:更新数组中某个元素(配合展开运算符创建新对象)Array.filter:删除元素或筛选展示insertAdjacentHTML/innerHTML:高效渲染列表- 事件委托:在父元素监听子元素事件,支持动态添加的元素
e.target.closest(selector):向上查找最近的匹配元素dataset:读取data-*属性- XSS 防护:使用
textContent转义用户输入
学习路线建议
掌握本文内容后,建议按以下顺序继续学习:
- HTML与CSS入门 — 巩固 HTML/CSS 基础,理解 DOM 的基础
- 深入学习 TypeScript — 为 JavaScript 添加类型系统
- Vue3入门 — 学习主流前端框架,进一步简化 DOM 操作
- Node.js — 在服务端运行 JavaScript
- 浏览器 API — Web Storage、Web Workers、WebSocket 等
最佳实践
始终使用 const 和 let,不用 var:var 存在变量提升和函数作用域问题,容易产生难以追踪的 bug。const 声明不可重新赋值的变量,let 声明可变量,两者都是块级作用域,语义更清晰。
用 === 严格相等而非 == 宽松相等:== 会进行隐式类型转换(如 0 == false 为 true),产生意料之外的结果。始终用 === 和 !== 进行比较,避免隐式转换陷阱。
异步代码使用 async/await,而非裸 Promise 链:async/await 使异步代码读起来像同步,错误处理用 try/catch 而非 .catch(),代码结构更清晰,调试更容易。
不要用 innerHTML 插入用户输入的内容:innerHTML 会执行 HTML 中的脚本,直接插入用户输入会导致 XSS 攻击。使用 textContent 插入纯文本,或对 HTML 进行转义后再插入。
用 for...of 替代 forEach 进行需要 await 的异步循环:forEach 不支持 async 回调等待,for...of 配合 await 才能顺序执行异步操作。
常见陷阱
陷阱:this 在箭头函数和普通函数中行为不同
现象: 在类方法内用箭头函数的 setTimeout 回调,this 意外丢失;或在对象字面量中用箭头函数,this 指向了 window。
原因: 普通函数的 this 在调用时动态绑定;箭头函数的 this 在定义时捕获外层 this,不会动态改变。
解决: 类方法中需要访问 this 的回调用箭头函数;对象字面量的方法用普通函数以保持正确绑定。
class Timer {
start() {
setTimeout(() => {
this.tick() // 箭头函数:this 正确指向 Timer 实例
}, 1000)
}
}
陷阱:forEach 中用 await 无法等待异步操作
现象: forEach 中的 await 不阻塞循环,所有异步操作同时触发,后续代码在操作完成前就已执行。
原因: forEach 不支持 async 回调的返回 Promise,内部 await 只影响回调本身,forEach 不等待它们。
解决: 用 for...of + await 顺序执行,或 Promise.all + map 并行执行。
// 错误:不等待
items.forEach(async item => { await save(item) })
// 正确:顺序等待
for (const item of items) { await save(item) }
// 正确:并行等待
await Promise.all(items.map(item => save(item)))
陷阱:== 宽松比较产生意外结果
现象: if (value == false) 对 0、""、null、undefined 都成立,逻辑判断出现意外分支。
原因: == 比较不同类型时进行隐式类型转换,规则复杂且违反直觉(null == undefined 为 true,但 null == 0 为 false)。
解决: 始终使用 === 严格相等;需要 null/undefined 兼容检查时用 value == null(这是唯一合理的 == 使用场景)。