Skip to content

1. let 和 const 的声明时的区别

1.1 变量声明的发展历程

在 JavaScript 发展过程中,变量的声明方式经历了三个阶段:

javascript
// ES5 之前: 使用 var
var name = '张三';
var age = 18;

// ES6 (2015): 新增 let 和 const
let name = '张三';
const age = 18;

// 现代开发: 推荐使用 let 和 const,避免使用 var

1.2 let 和 const 的基本区别

特性letconst
可变性可以重新赋值声明后不可重新赋值
初始化可以不赋初始值必须赋初始值
块级作用域
暂时性死区
重复声明❌ 不允许❌ 不允许
使用场景需要变化的变量不变的常量

1.3 const 的特点

1.3.1 必须初始化

const 声明的变量必须立即初始化,否则会报错:

javascript
// ❌ 错误: 缺少初始值
const name;
// SyntaxError: Missing initializer in const declaration

// ✅ 正确: 声明时必须赋值
const name = '张三';
const age = 18;
const PI = 3.141592653;

1.3.2 不可重新赋值

const 声明的变量不能重新赋值:

javascript
const name = '张三';

// ❌ 错误: 不能重新赋值
name = '李四';
// TypeError: Assignment to constant variable.

const age = 18;
age = 19;
// TypeError: Assignment to constant variable.

1.3.3 const 声明对象的注意事项

重要: const 声明的对象本身不能被重新赋值,但对象内部的属性可以被修改:

javascript
// 声明对象
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.

数组同理:

javascript
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 声明的变量可以不立即赋值:

javascript
// ✅ 正确: 可以不赋初始值
let name;

name = '张三';
console.log(name); // 张三

let age;
console.log(age); // undefined

age = 18;
console.log(age); // 18

1.4.2 可以重新赋值

let 声明的变量可以被重新赋值:

javascript
let name = '张三';

// ✅ 正确: 可以重新赋值
name = '李四';
console.log(name); // 李四

name = '王五';
console.log(name); // 王五

let count = 0;
count++;
count += 1;
console.log(count); // 2

1.5 块级作用域

letconst 都具有块级作用域,而 var 只有函数作用域:

1.5.1 let 和 const 的块级作用域

javascript
// 块级作用域示例
{
    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 defined

1.5.2 var 和 let/const 的区别

javascript
// 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 循环中的变量声明

javascript
// 使用 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)

letconst 声明的变量存在暂时性死区(Temporal Dead Zone),在声明之前无法访问:

javascript
// ❌ 错误: 在声明前访问
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 暂时性死区的示例

javascript
function test() {
    // 暂时性死区开始
    console.log(x); // ReferenceError: Cannot access 'x' before initialization

    let x = 10;
    // 暂时性死区结束

    console.log(x); // ✅ 10
}

test();

1.7 重复声明

letconst 不允许重复声明:

javascript
// ❌ 错误: 使用 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 的对比:

javascript
// var 可以重复声明(但不推荐)
var name = '张三';
var name = '李四';
console.log(name); // 李四

// 但 let 和 const 不允许
let name = '张三';
let name = '李四';
// SyntaxError: Identifier 'name' has already been declared

1.8 实际应用场景

1.8.1 何时使用 const

javascript
// 场景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

javascript
// 场景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 变量声明原则

javascript
// ✅ 默认使用 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 命名规范

javascript
// 常量: 使用全大写
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 代码示例对比

javascript
// ❌ 不好的写法
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 总结对比表

特性letconst
是否必须初始化❌ 不必须✅ 必须
是否可以重新赋值✅ 可以❌ 不可以
是否具有块级作用域✅ 是✅ 是
是否存在暂时性死区✅ 是✅ 是
是否可以重复声明❌ 不可以❌ 不可以
声明对象的属性可修改✅ 可以✅ 可以
声明数组的元素可修改✅ 可以✅ 可以
推荐使用场景需要变化的变量常量和不变的数据

1.11 常见问题

1.11.1 const 声明的对象可以修改吗?

答案: 可以修改对象的属性,但不能重新赋值整个对象。

javascript
const person = { name: '张三', age: 18 };
person.name = '李四'; // ✅ 可以
person.age = 19;      // ✅ 可以
person = {};          // ❌ 不可以

1.11.2 应该优先使用 let 还是 const?

答案: 优先使用 const,只有在需要重新赋值时才使用 let。

javascript
// ✅ 默认使用 const
const name = '张三';
const age = 18;

// 需要重新赋值时使用 let
let count = 0;
count++;

1.11.3 什么时候使用 var?

答案: 现代开发中几乎不使用 var,统一使用 let 和 const。只有在维护旧代码时可能会遇到 var。

1.11.4 const 声明的数组可以修改吗?

答案: 可以修改数组的内容,但不能重新赋值整个数组。

javascript
const arr = [1, 2, 3];
arr.push(4); // ✅ 可以
arr[0] = 10; // ✅ 可以
arr = [];     // ❌ 不可以

1.11.5 为什么 const 更推荐使用?

答案:

  1. 代码更安全: 防止意外重新赋值
  2. 更易维护: 一旦声明就知道值不会改变
  3. 更好的性能: 引擎可以进行优化
  4. 更清晰的意图: 明确表示这是常量


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 APIdocumentquerySelectorcreateElement操作网页文档结构
BOM APIwindowlocationnavigatorhistory操作浏览器窗口和导航
事件 APIaddEventListenerremoveEventListener处理用户交互和浏览器事件
网络 APIfetchXMLHttpRequestWebSocket发送网络请求
存储 APIlocalStoragesessionStorageIndexedDB在浏览器中存储数据
定时器 APIsetTimeoutsetIntervalrequestAnimationFrame延迟和周期性执行任务
Canvas APIgetContext('2d')getImageData2D/3D 图形绘制
地理 APInavigator.geolocation获取用户地理位置
多媒体 APIMediaRecorderAudioContext处理音频、视频
通知 APINotificationService Worker显示通知和消息

分类方式二: W3C 标准分类

javascript
// 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 的特点

javascript
// 特点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 访问和操作。

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 的主要作用:

  1. 访问页面元素: 查找、选择 HTML 元素
  2. 修改元素内容: 改变文本、HTML 内容
  3. 修改元素样式: 修改 CSS 样式
  4. 创建和删除元素: 动态添加或移除元素
  5. 响应事件: 处理用户的点击、输入等操作

示例:

javascript
// 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 的类型

javascript
// 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); // true

2.3 DOM 树

DOM 将 HTML 文档表示为树形结构,称为 DOM 树。

2.3.1 DOM 树的结构

img_18.pngimg_19.png

html
<!-- 示例 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>&copy; 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 节点关系

javascript
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 树的遍历

javascript
// 递归遍历 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 树的操作

javascript
// 添加子节点
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 提供了多个对象来操作文档的不同部分。

html
<!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>

img_20.png

2.4.1 document 对象

document 对象代表整个 HTML 文档,是 DOM 树的根节点。

常用属性:

javascript
// 文档信息
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

常用方法:

javascript
// 查找元素
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();                         // 关闭文档流

示例:

javascript
// 查找元素
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 元素的基类。

常用属性:

javascript
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');        // 替换类

示例:

javascript
// 操作类名
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 是节点的集合,类似数组但不是真正的数组。

javascript
// 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 元素的集合,也是类数组对象。

javascript
// 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 操作最佳实践

javascript
// 最佳实践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: 动态创建列表

javascript
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: 表单验证

javascript
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: 图片懒加载

javascript
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);

Released under the MIT License.