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


目录

  1. 什么是 JavaScript
  2. 如何引入 JavaScript
  3. 变量与数据类型
  4. 运算符
  5. 字符串操作
  6. 数组方法
  7. 对象
  8. 流程控制
  9. 函数
  10. DOM 操作
  11. 事件
  12. 异步编程
  13. ES6+ 常用特性
  14. 综合实战: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 转义用户输入

学习路线建议

掌握本文内容后,建议按以下顺序继续学习:

  1. HTML与CSS入门 — 巩固 HTML/CSS 基础,理解 DOM 的基础
  2. 深入学习 TypeScript — 为 JavaScript 添加类型系统
  3. Vue3入门 — 学习主流前端框架,进一步简化 DOM 操作
  4. Node.js — 在服务端运行 JavaScript
  5. 浏览器 API — Web Storage、Web Workers、WebSocket 等

最佳实践

始终使用 constlet,不用 varvar 存在变量提升和函数作用域问题,容易产生难以追踪的 bug。const 声明不可重新赋值的变量,let 声明可变量,两者都是块级作用域,语义更清晰。

=== 严格相等而非 == 宽松相等== 会进行隐式类型转换(如 0 == falsetrue),产生意料之外的结果。始终用 ===!== 进行比较,避免隐式转换陷阱。

异步代码使用 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""nullundefined 都成立,逻辑判断出现意外分支。

原因: == 比较不同类型时进行隐式类型转换,规则复杂且违反直觉(null == undefinedtrue,但 null == 0false)。

解决: 始终使用 === 严格相等;需要 null/undefined 兼容检查时用 value == null(这是唯一合理的 == 使用场景)。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog