1. let 和 const 的声明时的区别
1.1 变量声明的发展历程
在 JavaScript 发展过程中,变量的声明方式经历了三个阶段:
// ES5 之前: 使用 var
var name = '张三';
var age = 18;
// ES6 (2015): 新增 let 和 const
let name = '张三';
const age = 18;
// 现代开发: 推荐使用 let 和 const,避免使用 var1.2 let 和 const 的基本区别
| 特性 | let | const |
|---|---|---|
| 可变性 | 可以重新赋值 | 声明后不可重新赋值 |
| 初始化 | 可以不赋初始值 | 必须赋初始值 |
| 块级作用域 | ✅ | ✅ |
| 暂时性死区 | ✅ | ✅ |
| 重复声明 | ❌ 不允许 | ❌ 不允许 |
| 使用场景 | 需要变化的变量 | 不变的常量 |
1.3 const 的特点
1.3.1 必须初始化
const 声明的变量必须立即初始化,否则会报错:
// ❌ 错误: 缺少初始值
const name;
// SyntaxError: Missing initializer in const declaration
// ✅ 正确: 声明时必须赋值
const name = '张三';
const age = 18;
const PI = 3.141592653;1.3.2 不可重新赋值
const 声明的变量不能重新赋值:
const name = '张三';
// ❌ 错误: 不能重新赋值
name = '李四';
// TypeError: Assignment to constant variable.
const age = 18;
age = 19;
// TypeError: Assignment to constant variable.1.3.3 const 声明对象的注意事项
重要: const 声明的对象本身不能被重新赋值,但对象内部的属性可以被修改:
// 声明对象
const person = {
name: '张三',
age: 18
};
// ✅ 正确: 可以修改对象的属性
person.name = '李四';
person.age = 19;
console.log(person); // { name: '李四', age: 19 }
// ✅ 正确: 可以添加新属性
person.city = '北京';
console.log(person); // { name: '李四', age: 19, city: '北京' }
// ✅ 正确: 可以删除属性
delete person.city;
console.log(person); // { name: '李四', age: 19 }
// ❌ 错误: 不能重新赋值整个对象
person = { name: '王五', age: 20 };
// TypeError: Assignment to constant variable.数组同理:
const numbers = [1, 2, 3, 4, 5];
// ✅ 正确: 可以修改数组内容
numbers.push(6);
numbers[0] = 10;
numbers.pop();
console.log(numbers); // [10, 2, 3, 4]
// ❌ 错误: 不能重新赋值整个数组
numbers = [1, 2, 3];
// TypeError: Assignment to constant variable.1.4 let 的特点
1.4.1 可以不初始化
let 声明的变量可以不立即赋值:
// ✅ 正确: 可以不赋初始值
let name;
name = '张三';
console.log(name); // 张三
let age;
console.log(age); // undefined
age = 18;
console.log(age); // 181.4.2 可以重新赋值
let 声明的变量可以被重新赋值:
let name = '张三';
// ✅ 正确: 可以重新赋值
name = '李四';
console.log(name); // 李四
name = '王五';
console.log(name); // 王五
let count = 0;
count++;
count += 1;
console.log(count); // 21.5 块级作用域
let 和 const 都具有块级作用域,而 var 只有函数作用域:
1.5.1 let 和 const 的块级作用域
// 块级作用域示例
{
let name = '张三';
const age = 18;
console.log(name); // 张三
console.log(age); // 18
}
// ❌ 错误: 超出作用域,无法访问
console.log(name); // ReferenceError: name is not defined
console.log(age); // ReferenceError: age is not defined1.5.2 var 和 let/const 的区别
// var 只有函数作用域
function testVar() {
if (true) {
var name = '张三';
}
console.log(name); // ✅ 可以访问: 张三
}
testVar();
// let 和 const 有块级作用域
function testLet() {
if (true) {
let name = '李四';
const age = 18;
}
console.log(name); // ❌ 错误: ReferenceError
console.log(age); // ❌ 错误: ReferenceError
}
testLet();1.5.3 循环中的变量声明
// 使用 let: 每次循环都会创建新的变量
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
// 使用 var: 所有循环共享同一个变量
for (var j = 0; j < 3; j++) {
setTimeout(function() {
console.log(j); // 3, 3, 3
}, 100);
}1.6 暂时性死区 (TDZ)
let 和 const 声明的变量存在暂时性死区(Temporal Dead Zone),在声明之前无法访问:
// ❌ 错误: 在声明前访问
console.log(name); // ReferenceError: Cannot access 'name' before initialization
const name = '张三';
// ❌ 错误: 在声明前访问
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 18;
// ✅ 正确: 声明后可以访问
console.log(name); // 张三1.6.1 暂时性死区的示例
function test() {
// 暂时性死区开始
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;
// 暂时性死区结束
console.log(x); // ✅ 10
}
test();1.7 重复声明
let 和 const 不允许重复声明:
// ❌ 错误: 使用 let 重复声明
let name = '张三';
let name = '李四';
// SyntaxError: Identifier 'name' has already been declared
// ❌ 错误: 使用 const 重复声明
const age = 18;
const age = 19;
// SyntaxError: Identifier 'age' has already been declared
// ❌ 错误: let 和 const 也不能混合重复声明
let count = 0;
const count = 1;
// SyntaxError: Identifier 'count' has already been declared与 var 的对比:
// var 可以重复声明(但不推荐)
var name = '张三';
var name = '李四';
console.log(name); // 李四
// 但 let 和 const 不允许
let name = '张三';
let name = '李四';
// SyntaxError: Identifier 'name' has already been declared1.8 实际应用场景
1.8.1 何时使用 const
// 场景1: 声明常量
const PI = 3.141592653;
const MAX_SIZE = 100;
const API_URL = 'https://api.example.com';
// 场景2: 声明不会改变的配置
const config = {
theme: 'dark',
language: 'zh-CN',
timeout: 5000
};
// 场景3: 声明函数
function greet(name) {
console.log(`你好, ${name}`);
}
// 场景4: 声明不会重新赋值的对象
const user = {
id: 1,
name: '张三'
};
// 可以修改属性,但不能重新赋值整个对象
user.name = '李四'; // ✅
// user = {}; // ❌
// 场景5: 声明数组
const fruits = ['苹果', '香蕉', '橙子'];
fruits.push('葡萄'); // ✅ 可以修改数组内容
// fruits = []; // ❌ 不能重新赋值1.8.2 何时使用 let
// 场景1: 需要重新赋值的计数器
let count = 0;
count++;
count += 10;
// 场景2: 循环变量
for (let i = 0; i < 10; i++) {
console.log(i);
}
// 场景3: 需要改变值的变量
let score = 0;
score += 10;
score = 100;
// 场景4: 条件赋值
let result;
if (condition) {
result = 'yes';
} else {
result = 'no';
}
// 场景5: 累加计算
let sum = 0;
for (let num of numbers) {
sum += num;
}1.9 最佳实践
1.9.1 变量声明原则
// ✅ 默认使用 const
const name = '张三';
const age = 18;
const user = { id: 1, name: '张三' };
// ✅ 需要重新赋值时使用 let
let count = 0;
let score = 0;
let isValid = false;
// ❌ 避免使用 var (现代开发不推荐)
var name = '张三';1.9.2 命名规范
// 常量: 使用全大写
const MAX_SIZE = 100;
const API_URL = 'https://api.example.com';
const DEFAULT_CONFIG = {
theme: 'dark'
};
// 普通变量: 使用小驼峰
let userName = '张三';
let userId = 1;
let isLoggedIn = false;
// 布尔值: 使用 is/has/can 前缀
let isActive = true;
let hasPermission = false;
let canEdit = true;1.9.3 代码示例对比
// ❌ 不好的写法
var name = '张三';
var age = 18;
var isStudent = true;
// ✅ 好的写法: 默认使用 const
const name = '张三';
const age = 18;
const isStudent = true;
// ❌ 不好的写法: 不必要的 var
function process() {
var i = 0;
for (var i = 0; i < 10; i++) {
console.log(i);
}
}
// ✅ 好的写法: 使用 let
function process() {
for (let i = 0; i < 10; i++) {
console.log(i);
}
}
// ❌ 不好的写法: const 重新赋值
const count = 0;
count++; // 错误
// ✅ 好的写法: 使用 let
let count = 0;
count++; // 正确
// ❌ 不好的写法: let 用于不变的数据
let config = {
theme: 'dark',
language: 'zh-CN'
};
// ✅ 好的写法: 使用 const
const config = {
theme: 'dark',
language: 'zh-CN'
};
// 可以修改属性
config.theme = 'light'; // 正确1.10 总结对比表
| 特性 | let | const |
|---|---|---|
| 是否必须初始化 | ❌ 不必须 | ✅ 必须 |
| 是否可以重新赋值 | ✅ 可以 | ❌ 不可以 |
| 是否具有块级作用域 | ✅ 是 | ✅ 是 |
| 是否存在暂时性死区 | ✅ 是 | ✅ 是 |
| 是否可以重复声明 | ❌ 不可以 | ❌ 不可以 |
| 声明对象的属性可修改 | ✅ 可以 | ✅ 可以 |
| 声明数组的元素可修改 | ✅ 可以 | ✅ 可以 |
| 推荐使用场景 | 需要变化的变量 | 常量和不变的数据 |
1.11 常见问题
1.11.1 const 声明的对象可以修改吗?
答案: 可以修改对象的属性,但不能重新赋值整个对象。
const person = { name: '张三', age: 18 };
person.name = '李四'; // ✅ 可以
person.age = 19; // ✅ 可以
person = {}; // ❌ 不可以1.11.2 应该优先使用 let 还是 const?
答案: 优先使用 const,只有在需要重新赋值时才使用 let。
// ✅ 默认使用 const
const name = '张三';
const age = 18;
// 需要重新赋值时使用 let
let count = 0;
count++;1.11.3 什么时候使用 var?
答案: 现代开发中几乎不使用 var,统一使用 let 和 const。只有在维护旧代码时可能会遇到 var。
1.11.4 const 声明的数组可以修改吗?
答案: 可以修改数组的内容,但不能重新赋值整个数组。
const arr = [1, 2, 3];
arr.push(4); // ✅ 可以
arr[0] = 10; // ✅ 可以
arr = []; // ❌ 不可以1.11.5 为什么 const 更推荐使用?
答案:
- 代码更安全: 防止意外重新赋值
- 更易维护: 一旦声明就知道值不会改变
- 更好的性能: 引擎可以进行优化
- 更清晰的意图: 明确表示这是常量
2. Web API 基本认知
2.1 作用和分类
Web API 是浏览器提供的 API(应用程序编程接口),允许 JavaScript 与浏览器功能进行交互。
2.1.1 Web API 的作用
Web API 的主要作用包括:
- 操作网页内容: 通过 DOM API 动态修改网页的 HTML、CSS 和内容
- 响应用户交互: 处理点击、键盘输入、鼠标移动等事件
- 网络通信: 发送 HTTP 请求,与服务器进行数据交换
- 存储数据: 在浏览器中保存和读取数据
- 多媒体操作: 处理音频、视频、Canvas 绘图等
- 获取设备信息: 获取地理位置、设备方向、摄像头等信息
JavaScript 与 Web API 的关系:
JavaScript (编程语言)
↓ 调用
Web API (浏览器提供的接口)
↓ 访问
浏览器功能 (DOM、存储、网络等)2.1.2 Web API 的分类
分类方式一: 按功能分类
| API 类别 | 常用 API | 功能描述 |
|---|---|---|
| DOM API | document、querySelector、createElement | 操作网页文档结构 |
| BOM API | window、location、navigator、history | 操作浏览器窗口和导航 |
| 事件 API | addEventListener、removeEventListener | 处理用户交互和浏览器事件 |
| 网络 API | fetch、XMLHttpRequest、WebSocket | 发送网络请求 |
| 存储 API | localStorage、sessionStorage、IndexedDB | 在浏览器中存储数据 |
| 定时器 API | setTimeout、setInterval、requestAnimationFrame | 延迟和周期性执行任务 |
| Canvas API | getContext('2d')、getImageData | 2D/3D 图形绘制 |
| 地理 API | navigator.geolocation | 获取用户地理位置 |
| 多媒体 API | MediaRecorder、AudioContext | 处理音频、视频 |
| 通知 API | Notification、Service Worker | 显示通知和消息 |
分类方式二: W3C 标准分类
// 1. DOM (Document Object Model) - 文档对象模型
document.getElementById('id');
document.querySelector('.class');
// 2. BOM (Browser Object Model) - 浏览器对象模型
window.location.href;
window.navigator.userAgent;
window.history.back();
// 3. Web Storage - Web 存储
localStorage.setItem('key', 'value');
sessionStorage.getItem('key');
// 4. Web Workers - Web 工作线程
const worker = new Worker('worker.js');
// 5. Web Sockets - WebSocket 通信
const socket = new WebSocket('ws://example.com');2.1.3 Web API 的特点
// 特点1: 浏览器内置,无需额外安装
document.getElementById('app'); // 直接使用,无需导入
// 特点2: 由各大浏览器厂商实现(遵循 W3C 标准)
// Chrome、Firefox、Safari、Edge 都实现了标准 API
// 特点3: 可能存在兼容性问题
// 需要检查 API 是否支持
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(successCallback);
}
// 特点4: 异步 API 使用回调或 Promise
// Promise 形式
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data));
// 回调形式
setTimeout(() => {
console.log('1秒后执行');
}, 1000);2.2 什么是 DOM
DOM (Document Object Model,文档对象模型) 是 HTML 和 XML 文档的编程接口。
2.2.1 DOM 的定义
DOM 将 HTML 文档表示为树形结构,每个节点都是对象,可以通过 JavaScript 访问和操作。
// HTML 文档
/*
<!DOCTYPE html>
<html>
<head>
<title>我的网页</title>
</head>
<body>
<div id="app">
<h1>欢迎</h1>
<p class="content">这是内容</p>
</div>
</body>
</html>
*/
// 通过 DOM 访问文档
console.log(document.title); // "我的网页"
console.log(document.body.innerHTML); // 获取 body 内容2.2.2 DOM 的作用
DOM 的主要作用:
- 访问页面元素: 查找、选择 HTML 元素
- 修改元素内容: 改变文本、HTML 内容
- 修改元素样式: 修改 CSS 样式
- 创建和删除元素: 动态添加或移除元素
- 响应事件: 处理用户的点击、输入等操作
示例:
// 1. 访问页面元素
const title = document.getElementById('app');
const paragraphs = document.querySelectorAll('p');
// 2. 修改元素内容
title.textContent = '新标题';
paragraphs[0].innerHTML = '<strong>加粗内容</strong>';
// 3. 修改元素样式
title.style.color = 'red';
title.style.fontSize = '24px';
// 4. 创建和添加元素
const newElement = document.createElement('div');
newElement.textContent = '新元素';
document.body.appendChild(newElement);
// 5. 响应事件
title.addEventListener('click', function() {
alert('被点击了!');
});2.2.3 DOM 的类型
// DOM Node 节点类型
const Node = {
ELEMENT_NODE: 1, // 元素节点
ATTRIBUTE_NODE: 2, // 属性节点
TEXT_NODE: 3, // 文本节点
COMMENT_NODE: 8, // 注释节点
DOCUMENT_NODE: 9, // 文档节点
DOCUMENT_FRAGMENT_NODE: 11 // 文档片段节点
};
// 检查节点类型
const element = document.getElementById('app');
console.log(element.nodeType === Node.ELEMENT_NODE); // true
const textNode = element.firstChild;
console.log(textNode.nodeType === Node.TEXT_NODE); // true2.3 DOM 树
DOM 将 HTML 文档表示为树形结构,称为 DOM 树。
2.3.1 DOM 树的结构


<!-- 示例 HTML -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>DOM 树示例</title>
<style>
body { font-family: Arial; }
</style>
</head>
<body>
<div id="container">
<header>
<h1>网站标题</h1>
<nav>
<a href="#">首页</a>
<a href="#">关于</a>
</nav>
</header>
<main>
<section>
<h2>文章标题</h2>
<p>这是第一段内容</p>
<p>这是第二段内容</p>
</section>
<aside>
<h3>侧边栏</h3>
<ul>
<li>链接1</li>
<li>链接2</li>
</ul>
</aside>
</main>
<footer>
<p>© 2024 我的网站</p>
</footer>
</div>
</body>
</html>对应的 DOM 树结构:
Document (文档节点)
└── html (元素节点)
├── head (元素节点)
│ ├── meta (元素节点)
│ ├── title (元素节点)
│ │ └── "DOM 树示例" (文本节点)
│ └── style (元素节点)
│ └── "body { font-family: Arial; }" (文本节点)
└── body (元素节点)
└── div#container (元素节点)
├── header (元素节点)
│ ├── h1 (元素节点)
│ │ └── "网站标题" (文本节点)
│ └── nav (元素节点)
│ └── a (元素节点) × 2
│ └── "首页" / "关于" (文本节点)
├── main (元素节点)
│ ├── section (元素节点)
│ │ ├── h2 (元素节点)
│ │ │ └── "文章标题" (文本节点)
│ │ └── p (元素节点) × 2
│ │ └── "这是第一段内容" / "这是第二段内容" (文本节点)
│ └── aside (元素节点)
│ ├── h3 (元素节点)
│ │ └── "侧边栏" (文本节点)
│ └── ul (元素节点)
│ └── li (元素节点) × 2
│ └── "链接1" / "链接2" (文本节点)
└── footer (元素节点)
└── p (元素节点)
└── "© 2024 我的网站" (文本节点)2.3.2 DOM 节点关系
const parent = document.getElementById('parent');
const child = parent.querySelector('.child');
// 节点关系
console.log(parent.parentNode); // 父节点
console.log(parent.parentElement); // 父元素(不包括 Document)
console.log(parent.childNodes); // 所有子节点(包括文本节点)
console.log(parent.children); // 所有子元素(不包括文本节点)
console.log(parent.firstChild); // 第一个子节点
console.log(parent.firstElementChild); // 第一个子元素
console.log(parent.lastChild); // 最后一个子节点
console.log(parent.lastElementChild); // 最后一个子元素
console.log(child.previousSibling); // 上一个兄弟节点
console.log(child.previousElementSibling); // 上一个兄弟元素
console.log(child.nextSibling); // 下一个兄弟节点
console.log(child.nextElementSibling); // 下一个兄弟元素2.3.3 DOM 树的遍历
// 递归遍历 DOM 树
function traverseDOM(node, indent = 0) {
console.log(' '.repeat(indent) + node.nodeName);
// 遍历子节点
for (let i = 0; i < node.childNodes.length; i++) {
traverseDOM(node.childNodes[i], indent + 1);
}
}
traverseDOM(document.body);
// 获取所有文本节点
function getTextNodes(element) {
const textNodes = [];
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
if (node.textContent.trim()) {
textNodes.push(node);
}
}
return textNodes;
}
const texts = getTextNodes(document.body);
console.log(texts.map(t => t.textContent));2.3.4 DOM 树的操作
// 添加子节点
const parent = document.getElementById('parent');
const newChild = document.createElement('div');
newChild.textContent = '新子元素';
parent.appendChild(newChild); // 添加到末尾
parent.insertBefore(newChild, parent.firstChild); // 插入到指定位置
// 替换节点
const oldChild = parent.firstChild;
const replacement = document.createElement('span');
replacement.textContent = '替换的元素';
parent.replaceChild(replacement, oldChild);
// 删除节点
parent.removeChild(oldChild);
// 克隆节点
const cloned = parent.cloneNode(true); // true 表示深度克隆(包括子节点)
const shallowClone = parent.cloneNode(false); // false 表示浅克隆(不包括子节点)2.4 DOM 对象
DOM 提供了多个对象来操作文档的不同部分。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>DOM树</div>
<script>
//div标签会被转换为JS对象(Object)
const divDom = document.querySelector('div')
console.dir(divDom)
console.log(typeof(divDom))
</script>
</body>
</html>
2.4.1 document 对象
document 对象代表整个 HTML 文档,是 DOM 树的根节点。
常用属性:
// 文档信息
document.title; // 页面标题
document.URL; // 页面 URL
document.domain; // 域名
document.referrer; // 来源 URL
document.lastModified; // 最后修改时间
// 文档类型
document.doctype; // 文档类型声明
document.documentElement; // html 元素
document.body; // body 元素
document.head; // head 元素
// 集合
document.all; // 所有元素(不推荐)
document.forms; // 所有表单
document.images; // 所有图片
document.links; // 所有链接
document.scripts; // 所有脚本
// 准备状态
document.readyState; // loading、interactive、complete常用方法:
// 查找元素
document.getElementById('id'); // 通过 ID 查找
document.getElementsByClassName('class'); // 通过类名查找
document.getElementsByName('name'); // 通过 name 属性查找
document.getElementsByTagName('tag'); // 通过标签名查找
document.querySelector('selector'); // 查找第一个匹配元素
document.querySelectorAll('selector'); // 查找所有匹配元素
// 创建元素
document.createElement('div'); // 创建元素
document.createTextNode('text'); // 创建文本节点
document.createDocumentFragment(); // 创建文档片段
document.createAttribute('class'); // 创建属性节点
document.createComment('comment'); // 创建注释节点
// 其他方法
document.write('content'); // 写入内容(不推荐)
document.open(); // 打开文档流
document.close(); // 关闭文档流示例:
// 查找元素
const app = document.getElementById('app');
const buttons = document.getElementsByClassName('btn');
const allDivs = document.querySelectorAll('div');
// 创建并添加元素
const newDiv = document.createElement('div');
newDiv.className = 'new-element';
newDiv.textContent = '这是新创建的元素';
document.body.appendChild(newDiv);
// 文档加载完成
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM 加载完成');
});
// 或使用
document.addEventListener('readystatechange', function() {
if (document.readyState === 'complete') {
console.log('文档完全加载');
}
});2.4.2 Element 对象
Element 对象代表 HTML 元素,是所有 HTML 元素的基类。
常用属性:
const element = document.getElementById('app');
// 标签和属性
element.tagName; // 标签名(大写,如 DIV)
element.id; // ID 属性
element.className; // class 属性
element.classList; // 类名列表(对象)
// 样式
element.style; // 行内样式对象
element.style.color; // 颜色
element.style.display; // 显示方式
// 内容
element.innerHTML; // HTML 内容(可以解析 HTML)
element.textContent; // 纯文本内容(不解析 HTML)
element.innerText; // 类似 textContent
// 属性
element.attributes; // 所有属性
element.getAttribute('data-id'); // 获取属性
element.setAttribute('data-id', '123'); // 设置属性
element.removeAttribute('data-id'); // 删除属性
// 位置和尺寸
element.offsetWidth; // 元素宽度(包括边框和内边距)
element.offsetHeight; // 元素高度(包括边框和内边距)
element.clientWidth; // 元素宽度(不包括边框)
element.clientHeight; // 元素高度(不包括边框)
element.offsetLeft; // 相对父元素左边距
element.offsetTop; // 相对父元素上边距
// 类名操作
element.classList.add('class1', 'class2'); // 添加类
element.classList.remove('class1'); // 删除类
element.classList.toggle('active'); // 切换类
element.classList.contains('active'); // 检查类
element.classList.replace('old', 'new'); // 替换类示例:
// 操作类名
const button = document.getElementById('submit-btn');
button.classList.add('btn', 'btn-primary');
button.classList.contains('btn-primary'); // true
button.classList.toggle('active'); // 切换 active 类
button.classList.remove('btn-primary');
// 操作属性
const link = document.querySelector('a');
link.getAttribute('href'); // 获取 href
link.setAttribute('target', '_blank'); // 设置 target
link.dataset.userId = '123'; // 设置 data-* 属性
// 操作内容
const container = document.getElementById('content');
container.innerHTML = '<p><strong>HTML 内容</strong></p>';
container.textContent = '纯文本内容';2.4.3 NodeList 对象
NodeList 是节点的集合,类似数组但不是真正的数组。
// querySelectorAll 返回 NodeList
const divs = document.querySelectorAll('div');
console.log(divs); // NodeList [div, div, div, ...]
// NodeList 的特点
console.log(divs.length); // 长度
console.log(divs[0]); // 索引访问
console.log(divs.item(0)); // item() 方法
// NodeList 不是数组,没有数组方法
// divs.push() - 不存在
// 转换为数组
const divsArray = Array.from(divs);
const divsArray2 = [...divs];
divsArray.forEach(div => console.log(div));
// forEach 方法(现代浏览器支持)
divs.forEach((div, index) => {
console.log(`索引 ${index}:`, div);
});2.4.4 HTMLCollection 对象
HTMLCollection 是 HTML 元素的集合,也是类数组对象。
// getElementsByClassName 返回 HTMLCollection
const buttons = document.getElementsByClassName('btn');
console.log(buttons); // HTMLCollection [button, button, ...]
// HTMLCollection 的特点
console.log(buttons.length); // 长度
console.log(buttons[0]); // 索引访问
console.log(buttons.item(0)); // item() 方法
console.log(buttons.namedItem('name')); // 通过 name 或 id 访问
// HTMLCollection 是动态的(实时更新)
const container = document.getElementById('container');
const items = container.getElementsByClassName('item');
console.log(items.length); // 例如: 3
// 添加新元素
const newItem = document.createElement('div');
newItem.className = 'item';
container.appendChild(newItem);
console.log(items.length); // 4 (自动更新!)
// 转换为数组(避免动态性)
const itemsArray = Array.from(items);
const itemsArray2 = [...items];2.4.5 DOM 操作最佳实践
// 最佳实践1: 批量操作时使用文档片段
function addMultipleItems(container, items) {
// 不好的做法: 每次添加都触发重排
// items.forEach(item => container.appendChild(item));
// 好的做法: 使用文档片段
const fragment = document.createDocumentFragment();
items.forEach(item => fragment.appendChild(item));
container.appendChild(fragment); // 只触发一次重排
}
// 最佳实践2: 缓存 DOM 查询结果
// 不好的做法: 每次都查询
for (let i = 0; i < 10; i++) {
const element = document.getElementById('app');
element.style.opacity = i / 10;
}
// 好的做法: 缓存查询结果
const element = document.getElementById('app');
for (let i = 0; i < 10; i++) {
element.style.opacity = i / 10;
}
// 最佳实践3: 优先使用 querySelector/querySelectorAll
// 推荐: 选择器语法强大,一致性好
const element = document.querySelector('.class');
const elements = document.querySelectorAll('div.class');
// 其次: 旧的 API 也可用,但功能有限
const element2 = document.getElementById('id');
const elements2 = document.getElementsByClassName('class');
// 最佳实践4: 使用 classList 操作类名
// 推荐: 更高效、更易读
element.classList.add('active');
element.classList.remove('hidden');
element.classList.toggle('active');
// 不推荐: 直接操作 className 字符串
// element.className += ' active hidden';
// 最佳实践5: 使用 dataset 操作 data-* 属性
// 推荐
const userId = element.dataset.userId;
element.dataset.userName = '张三';
// 不推荐
const userId2 = element.getAttribute('data-user-id');
element.setAttribute('data-user-name', '张三');2.4.6 DOM 操作示例
示例1: 动态创建列表
function createList(items) {
const ul = document.createElement('ul');
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
li.className = 'list-item';
li.dataset.index = items.indexOf(item);
fragment.appendChild(li);
});
ul.appendChild(fragment);
ul.className = 'item-list';
return ul;
}
const items = ['苹果', '香蕉', '橙子', '葡萄'];
const list = createList(items);
document.getElementById('app').appendChild(list);示例2: 表单验证
function validateForm(formId) {
const form = document.getElementById(formId);
const inputs = form.querySelectorAll('input, textarea, select');
let isValid = true;
const errors = [];
inputs.forEach(input => {
const name = input.name;
const value = input.value.trim();
const required = input.hasAttribute('required');
// 移除旧的错误提示
const existingError = form.querySelector(`[data-error="${name}"]`);
if (existingError) {
existingError.remove();
}
// 验证必填字段
if (required && !value) {
isValid = false;
errors.push(`${name} 不能为空`);
showError(input, `${name} 不能为空`);
return;
}
// 验证邮箱
if (name === 'email' && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
isValid = false;
errors.push('邮箱格式不正确');
showError(input, '邮箱格式不正确');
}
}
});
return { isValid, errors };
}
function showError(input, message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.dataset.error = input.name;
errorDiv.textContent = message;
errorDiv.style.color = 'red';
errorDiv.style.fontSize = '12px';
input.parentNode.appendChild(errorDiv);
}示例3: 图片懒加载
function lazyLoadImages() {
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
}
// 页面加载时启动懒加载
document.addEventListener('DOMContentLoaded', lazyLoadImages);