全栈学习 —— 前端(二)JavaScript
本文系统介绍了JavaScript在前端开发中的核心作用。主要内容包括:1) JavaScript基础语法(变量、数据类型、运算符、流程控制等);2) DOM操作技术(元素选择、内容修改、样式调整、事件处理);3) 异步编程方法(回调函数、Promise、async/await);4) 本地存储机制(localStorage、sessionStorage);5) 通过TodoList项目实战演示综
目录
JavaScript(简称 JS)是前端开发的核心编程语言,它赋予网页交互能力,让静态的 HTML/CSS “动” 起来。从简单的表单验证到复杂的单页应用(SPA),JavaScript 都是实现交互逻辑的基石。本文将系统讲解 JavaScript 的核心语法、运行原理、DOM 操作、异步编程等内容,并通过实战项目巩固知识,帮助你掌握这门 “前端灵魂” 语言。
一、JavaScript 的核心价值与运行环境
1. 为什么需要 JavaScript?
HTML 定义网页结构,CSS 负责样式美化,而 JavaScript 则实现交互逻辑,三者共同构成现代网页的三大支柱。例如:
- 表单提交前验证用户输入(如手机号格式);
- 动态更新页面内容(如实时显示购物车数量);
- 响应用户操作(如点击按钮弹出菜单);
- 与服务器交互获取数据(如刷新页面无刷新加载新内容)。
没有 JavaScript,网页只能是静态的 “电子海报”;有了 JavaScript,才能实现如在线编辑器、视频播放器、游戏等复杂交互功能。
2. 运行环境
- 浏览器:这是 JavaScript 最主要的运行环境,浏览器内置 JavaScript 引擎(如 Chrome 的 V8 引擎)解析执行代码。
- 服务器端:通过 Node.js(基于 V8 引擎的运行环境),JavaScript 可用于开发后端服务。
- 其他环境:如桌面应用(Electron)、移动端应用(React Native)等。
本文聚焦浏览器端 JavaScript,重点讲解与网页交互相关的核心功能。
3. 引入方式
JavaScript 代码可通过以下方式嵌入网页:
(1)内联脚本(不推荐)
直接在 HTML 标签的事件属性中编写代码:
<button onclick="alert('Hello World')">点击我</button>
缺点:代码与 HTML 耦合,不利于维护,仅适合简单调试。
(2)内部脚本
在 HTML 的<script>标签中编写代码:
<!DOCTYPE html>
<html>
<head>
<title>内部脚本示例</title>
</head>
<body>
<script>
// JavaScript代码
console.log("这是内部脚本");
let message = "Hello";
alert(message); // 弹出对话框
</script>
</body>
</html>
特点:作用于当前页面,适合中小型项目。
(3)外部脚本(推荐)
将代码写入独立的.js文件,通过<script>标签引入:
创建script.js文件:
// script.js
console.log("这是外部脚本");
function greet() {
alert("Hello from external script!");
}
在 HTML 中引入:
<!DOCTYPE html>
<html>
<head>
<title>外部脚本示例</title>
<!-- 引入外部JS文件 -->
<script src="script.js"></script>
</head>
<body>
<button onclick="greet()">调用函数</button>
</body>
</html>
优势:代码复用性高,便于维护,适合大型项目。
注意:<script>标签的defer和async属性可控制脚本加载时机,避免阻塞页面渲染(详见 “DOM 加载与脚本执行” 部分)。
二、JavaScript 基础语法
1. 变量与数据类型
(1)变量声明
JavaScript 使用let、const、var声明变量(推荐优先使用let和const):
// let:声明可修改的变量
let age = 20;
age = 21; // 允许重新赋值
// const:声明不可修改的常量(必须初始化)
const PI = 3.14159;
// PI = 3.14; // 报错:常量不能重新赋值
// var:旧版语法,存在作用域问题,不推荐使用
var name = "张三";
- let和const是 ES6(2015 年)新增语法,支持块级作用域({}内有效);
- const声明的对象 / 数组,其引用不可变,但内部属性可修改:
(2)数据类型
JavaScript 有 7 种基本数据类型和 1 种引用数据类型:
类型 |
说明 |
示例 |
number |
数字(整数 / 浮点数) |
42、3.14 |
string |
字符串(单 / 双引号包裹) |
"Hello"、'World' |
boolean |
布尔值 |
true、false |
null |
空值(表示无值) |
null |
undefined |
未定义(变量声明未赋值) |
let x;(x 为 undefined) |
symbol |
唯一标识符(ES6 新增) |
Symbol("id") |
bigint |
大整数(ES2020 新增) |
123n |
引用类型 |
||
object |
对象(包括数组、函数等) |
{name: "张三"}、[1,2] |
类型检测:使用typeof运算符(注意null的特殊性):
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof null); // "object"(历史遗留bug)
console.log(typeof undefined); // "undefined"
console.log(typeof {}); // "object"
console.log(typeof []); // "object"(数组是特殊对象)
console.log(typeof function(){}); // "function"
(3)类型转换
JavaScript 是弱类型语言,会自动转换类型,也可手动转换:
// 自动转换
console.log(10 + "20"); // "1020"(数字转字符串)
console.log("10" - 5); // 5(字符串转数字)
console.log(!"hello"); // false(字符串转布尔值,非空字符串为true)
// 手动转换
let str = "123";
let num = Number(str); // 123(字符串转数字)
let bool = Boolean(0); // false(0转布尔值为false)
let str2 = String(456); // "456"(数字转字符串)
2. 运算符与表达式
(1)算术运算符
let a = 10, b = 3;
console.log(a + b); // 13(加)
console.log(a - b); // 7(减)
console.log(a * b); // 30(乘)
console.log(a / b); // 3.333...(除)
console.log(a % b); // 1(取余)
console.log(a **b); // 1000(幂运算,等价于Math.pow(10,3))
(2)赋值运算符
let x = 5;
x += 3; // 等价于x = x + 3 → x=8
x *= 2; // 等价于x = x * 2 → x=16
(3)比较运算符
console.log(5 == "5"); // true(宽松相等,自动转换类型)
console.log(5 === "5"); // false(严格相等,不转换类型)
console.log(10 > 5); // true
console.log(10 <= 5); // false
推荐使用===,避免自动类型转换导致的意外结果。
(4)逻辑运算符
let a = true, b = false;
console.log(a && b); // false(逻辑与:都为true才返回true)
console.log(a || b); // true(逻辑或:有一个为true就返回true)
console.log(!a); // false(逻辑非:取反)
3. 流程控制语句
(1)条件语句
// if-else语句
let score = 85;
if (score >= 90) {
console.log("优秀");
} else if (score >= 60) {
console.log("及格");
} else {
console.log("不及格");
}
// switch语句(适合多条件判断)
let day = 3;
switch (day) {
case 1:
console.log("星期一");
break;
case 2:
console.log("星期二");
break;
case 3:
console.log("星期三");
break;
default:
console.log("其他");
}
(2)循环语句
// for循环
for (let i = 0; i < 5; i++) {
console.log(i); // 输出0-4
}
// while循环
let j = 0;
while (j < 5) {
console.log(j); // 输出0-4
j++;
}
// do-while循环(至少执行一次)
let k = 0;
do {
console.log(k); // 输出0-4
k++;
} while (k < 5);
// for...of循环(遍历数组/字符串)
let arr = [10, 20, 30];
for (let item of arr) {
console.log(item); // 输出10、20、30
}
4. 函数
函数是封装可重用代码的块,通过function关键字定义:
(1)函数声明
// 定义函数
function add(a, b) { // a、b为参数
return a + b; // 返回计算结果
}
// 调用函数
let result = add(3, 5);
console.log(result); // 8
(2)函数表达式
将函数赋值给变量:
const multiply = function(a, b) {
return a * b;
};
console.log(multiply(4, 5)); // 20
(3)箭头函数(ES6 新增)
简化函数写法,适合短函数:
/ 基本语法:(参数) => 表达式
const subtract = (a, b) => a - b;
console.log(subtract(10, 3)); // 7
// 多语句需用{}和return
const divide = (a, b) => {
if (b === 0) {
return "除数不能为0";
}
return a / b;
};
注意:箭头函数没有this绑定,不适合作为对象方法或构造函数。
(4)参数默认值
function greet(name = "Guest") { // 默认参数
console.log(`Hello, ${name}!`);
}
greet(); // "Hello, Guest!"
greet("张三"); // "Hello, 张三!"
5. 对象与数组
(1)对象(Object)
对象用于存储键值对集合,是 JavaScript 中最核心的数据结构:
// 创建对象
const user = {
name: "张三", // 属性
age: 25,
sayHello: function() { // 方法
console.log(`Hello, 我是${this.name}`);
}
};
// 访问属性
console.log(user.name); // "张三"
console.log(user["age"]); // 25
// 调用方法
user.sayHello(); // "Hello, 我是张三"
// 修改属性
user.age = 26;
console.log(user.age); // 26
// 添加新属性/方法
user.gender = "男";
user.getAge = function() {
return this.age;
};
console.log(user.getAge()); // 26
(2)数组(Array)
数组用于存储有序集合,支持多种操作方法:
// 创建数组
const fruits = ["苹果", "香蕉", "橙子"];
// 访问元素
console.log(fruits[0]); // "苹果"(索引从0开始)
// 修改元素
fruits[1] = "葡萄";
console.log(fruits); // ["苹果", "葡萄", "橙子"]
// 常用方法
fruits.push("西瓜"); // 添加到末尾 → ["苹果", "葡萄", "橙子", "西瓜"]
fruits.pop(); // 移除末尾元素 → ["苹果", "葡萄", "橙子"]
fruits.unshift("草莓"); // 添加到开头 → ["草莓", "苹果", "葡萄", "橙子"]
fruits.shift(); // 移除开头元素 → ["苹果", "葡萄", "橙子"]
// 遍历数组
fruits.forEach(function(fruit, index) {
console.log(`${index}: ${fruit}`);
});
// 数组转换(map)
const upperFruits = fruits.map(fruit => fruit.toUpperCase());
console.log(upperFruits); // ["苹果", "葡萄", "橙子"](中文无大小写,此处仅示例)
// 数组过滤(filter)
const longFruits = fruits.filter(fruit => fruit.length > 2); // 假设"葡萄"长度>2
console.log(longFruits); // ["葡萄"]
三、DOM 操作:JavaScript 操作网页的桥梁
DOM(Document Object Model,文档对象模型)是浏览器将 HTML 解析为的树形结构,JavaScript 通过 DOM API 操作这个树,实现网页内容的动态修改。
1. DOM 树结构
HTML 文档被解析为一个树形结构,每个节点代表一个 HTML 元素、文本或属性:
<html>
<head>
<title>DOM示例</title>
</head>
<body>
<h1>标题</h1>
<p>段落</p>
</body>
</html>
对应的 DOM 树:
document
└── html
├── head
│ └── title
│ └── 文本节点:"DOM示例"
└── body
├── h1
│ └── 文本节点:"标题"
└── p
└── 文本节点:"段落"
2. 选择 DOM 元素
要操作元素,首先需要通过选择器获取元素:
// 通过ID选择(唯一元素)
const title = document.getElementById("myTitle");
// 通过类名选择(返回集合)
const items = document.getElementsByClassName("item");
// 通过标签名选择(返回集合)
const paragraphs = document.getElementsByTagName("p");
// 通过CSS选择器选择(返回第一个匹配元素)
const firstBox = document.querySelector(".box");
// 通过CSS选择器选择(返回所有匹配元素)
const allBoxes = document.querySelectorAll(".box");
示例:
<div class="box">盒子1</div>
<div class="box">盒子2</div>
<script>
const boxes = document.querySelectorAll(".box");
boxes.forEach(box => {
console.log(box.textContent); // 输出"盒子1"、"盒子2"
});
</script>
3. 修改 DOM 元素
(1)修改内容
// 获取元素
const heading = document.querySelector("h1");
// 修改文本内容
heading.textContent = "新标题"; // 纯文本,不解析HTML
// 修改HTML内容(会解析HTML标签)
heading.innerHTML = "<span>带<span style='color:red'>颜色</span>的标题</span>";
(2)修改样式
const para = document.querySelector("p");
// 修改行内样式(style属性)
para.style.color = "blue"; // 字体颜色
para.style.fontSize = "18px"; // 字体大小(注意驼峰命名)
para.style.backgroundColor = "#f0f0f0"; // 背景色
// 添加/移除类(推荐:通过CSS类控制样式)
para.classList.add("highlight"); // 添加类
para.classList.remove("highlight"); // 移除类
para.classList.toggle("active"); // 切换类(存在则移除,不存在则添加)
配合 CSS:
.highlight {
color: red;
font-weight: bold;
}
.active {
background-color: yellow;
}
(3)修改属性
const img = document.querySelector("img");
// 修改属性
img.src = "new-image.jpg";
img.alt = "新图片";
// 获取属性
console.log(img.src); // 输出图片URL
// 移除属性
img.removeAttribute("title");
4. 事件处理:响应用户操作
事件是用户或浏览器的行为(如点击、输入、加载等),JavaScript 通过事件监听实现交互。
(1)常用事件类型
- click:鼠标点击
- mouseover:鼠标悬停
- mouseout:鼠标离开
- input:输入框内容变化
- submit:表单提交
- load:页面加载完成
(2)事件监听方式
// 获取按钮元素
const btn = document.querySelector("#myBtn");
// 方式1:通过onclick属性(类似内联脚本,不推荐)
btn.onclick = function() {
alert("按钮被点击了");
};
// 方式2:addEventListener(推荐,可添加多个监听器)
btn.addEventListener("click", function() {
console.log("点击事件1");
});
btn.addEventListener("click", function() {
console.log("点击事件2"); // 点击时会同时执行两个监听器
});
// 移除事件监听(需保存函数引用)
function handleClick() {
console.log("可移除的事件");
}
btn.addEventListener("click", handleClick);
// 移除
btn.removeEventListener("click", handleClick);
(3)事件对象
事件处理函数会接收一个事件对象(event),包含事件相关信息:
document.querySelector("a").addEventListener("click", function(event) {
event.preventDefault(); // 阻止默认行为(此处阻止链接跳转)
console.log("链接被点击,href:" + this.href); // this指向触发事件的元素
console.log("点击位置:x=" + event.clientX + ", y=" + event.clientY);
});
(4)事件冒泡与委托
- 事件冒泡:事件从触发元素向上传播到父元素的现象。
// 父元素也会触发点击事件(冒泡导致)
document.querySelector(".parent").addEventListener("click", function() {
console.log("父元素被点击");
});
document.querySelector(".child").addEventListener("click", function(event) {
console.log("子元素被点击");
// event.stopPropagation(); // 阻止冒泡
});
- 事件委托:利用冒泡原理,将子元素的事件委托给父元素处理(优化性能)。
<ul id="myList">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
<script>
// 委托给父元素ul,无需为每个li添加监听器
document.getElementById("myList").addEventListener("click", function(event) {
// 判断点击的是li元素
if (event.target.tagName === "LI") {
console.log("点击了:" + event.target.textContent);
}
});
</script>
四、异步编程:处理非阻塞操作
JavaScript 是单线程语言(同一时间只能执行一段代码),为避免长时间操作(如网络请求)阻塞页面,需要使用异步编程。
1. 同步与异步的区别
- 同步:代码按顺序执行,前一个操作完成后才执行下一个。
console.log("1");
console.log("2"); // 一定在"1"后输出
- 异步:操作发起后不等待结果,继续执行后续代码,结果通过回调处理。
console.log("1");
// 异步操作(1秒后执行)
setTimeout(function() {
console.log("2");
}, 1000);
console.log("3"); // 先输出"3",1秒后输出"2"
2. 回调函数(Callback)
回调函数是异步操作完成后执行的函数,是最基础的异步处理方式:
// 模拟网络请求(异步)
function fetchData(callback) {
setTimeout(function() {
const data = { name: "张三", age: 25 };
callback(data); // 操作完成后调用回调
}, 1000);
}
// 调用异步函数
fetchData(function(user) {
console.log("获取到数据:", user);
});
问题:多个异步操作嵌套会导致 “回调地狱”:
// 回调地狱示例(不推荐)
fetchUser(function(user) {
fetchOrders(user.id, function(orders) {
fetchDetails(orders[0].id, function(detail) {
// 多层嵌套,代码可读性差
});
});
});
3. Promise(ES6 新增)
Promise 用于解决回调地狱,将异步操作包装为对象,支持链式调用:
(1)创建 Promise
// 创建Promise对象
const promise = new Promise(function(resolve, reject) {
// 异步操作
setTimeout(function() {
const success = true;
if (success) {
resolve("操作成功"); // 成功时调用resolve
} else {
reject(new Error("操作失败")); // 失败时调用reject
}
}, 1000);
});
(2)使用 Promise
// 调用Promise
promise
.then(function(result) { // 成功回调
console.log(result); // "操作成功"
return "继续处理";
})
.then(function(data) { // 链式调用
console.log(data); // "继续处理"
})
.catch(function(error) { // 失败回调
console.error(error.message);
})
.finally(function() { // 无论成功失败都会执行
console.log("操作完成");
});
(3)解决回调地狱
// 用Promise重构异步函数
function fetchUser() {
return new Promise(resolve => {
setTimeout(() => resolve({ id: 1, name: "张三" }), 500);
});
}
function fetchOrders(userId) {
return new Promise(resolve => {
setTimeout(() => resolve([{ id: 101, userId: userId }]), 500);
});
}
// 链式调用,避免嵌套
fetchUser()
.then(user => fetchOrders(user.id))
.then(orders => console.log("订单:", orders))
.catch(error => console.error(error));
4. async/await(ES2017 新增)
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:
/
/ 异步函数前加async
async function getUserOrders() {
try {
// 用await等待Promise完成
const user = await fetchUser();
const orders = await fetchOrders(user.id);
console.log("用户订单:", orders);
return orders;
} catch (error) {
console.error("错误:", error);
}
}
// 调用异步函数(返回Promise)
getUserOrders();
五、本地存储:持久化数据
浏览器提供本地存储机制,可在客户端保存数据(如用户偏好、购物车等),常用的有localStorage和sessionStorage。
1. localStorage
- 数据永久保存(除非手动删除);
- 容量约 5MB;
- 同一域名下的页面共享数据。
// 保存数据(只能存字符串,对象需序列化)
localStorage.setItem("username", "张三");
localStorage.setItem("user", JSON.stringify({ name: "张三", age: 25 }));
// 获取数据
const username = localStorage.getItem("username");
const user = JSON.parse(localStorage.getItem("user")); // 反序列化
// 删除数据
localStorage.removeItem("username");
// 清空所有数据
// localStorage.clear();
2. sessionStorage
- 数据仅在当前会话(标签页)有效,关闭标签页后删除;
- 用法与localStorage相同:
sessionStorage.setItem("tempData", "临时数据");
const temp = sessionStorage.getItem("tempData");
六、项目实战:待办事项(Todo List)应用
1. 项目需求
实现一个简单的待办事项应用,功能包括:
- 添加待办事项;
- 标记待办事项为已完成;
- 删除待办事项;
- 本地存储待办数据(刷新页面不丢失)。
2. 实现代码
(1)HTML 结构(index.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>
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
#todoInput {
width: 70%;
padding: 8px;
font-size: 16px;
}
#addBtn {
width: 25%;
padding: 8px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}
.todo-item {
display: flex;
align-items: center;
margin: 10px 0;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
.todo-item input[type="checkbox"] {
margin-right: 10px;
}
.todo-item .text {
flex: 1;
}
.todo-item .delete {
color: red;
cursor: pointer;
margin-left: 10px;
}
.completed .text {
text-decoration: line-through;
color: #888;
}
</style>
</head>
<body>
<div class="container">
<h1>待办事项</h1>
<div>
<input type="text" id="todoInput" placeholder="输入待办事项...">
<button id="addBtn">添加</button>
</div>
<div id="todoList"></div>
</div>
<script src="todo.js"></script>
</body>
</html>
(2)JavaScript 逻辑(todo.js)
// 从本地存储加载待办事项
function loadTodos() {
const todos = localStorage.getItem("todos");
return todos ? JSON.parse(todos) : [];
}
// 保存待办事项到本地存储
function saveTodos(todos) {
localStorage.setItem("todos", JSON.stringify(todos));
}
// 渲染待办事项列表
function renderTodos() {
const todoList = document.getElementById("todoList");
todoList.innerHTML = ""; // 清空列表
const todos = loadTodos();
todos.forEach((todo, index) => {
// 创建待办项元素
const item = document.createElement("div");
item.className = `todo-item ${todo.completed ? "completed" : ""}`;
item.innerHTML = `
<input type="checkbox" ${todo.completed ? "checked" : ""}>
<span class="text">${todo.text}</span>
<span class="delete">删除</span>
`;
// 绑定复选框事件(标记完成/未完成)
const checkbox = item.querySelector("input");
checkbox.addEventListener("change", () => {
todos[index].completed = !todos[index].completed;
saveTodos(todos);
renderTodos(); // 重新渲染
});
// 绑定删除事件
const deleteBtn = item.querySelector(".delete");
deleteBtn.addEventListener("click", () => {
todos.splice(index, 1); // 删除当前项
saveTodos(todos);
renderTodos(); // 重新渲染
});
todoList.appendChild(item);
});
}
// 添加新待办事项
document.getElementById("addBtn").addEventListener("click", addTodo);
document.getElementById("todoInput").addEventListener("keypress", (e) => {
if (e.key === "Enter") { // 按Enter键也能添加
addTodo();
}
});
function addTodo() {
const input = document.getElementById("todoInput");
const text = input.value.trim();
if (text) {
const todos = loadTodos();
todos.push({ text: text, completed: false }); // 添加新项
saveTodos(todos);
input.value = ""; // 清空输入框
renderTodos(); // 重新渲染
}
}
// 初始渲染
renderTodos();
3. 项目测试与效果
(1)测试步骤
创建index.html和todo.js文件,复制上述代码;
用浏览器打开index.html;
测试功能:
- 在输入框中输入内容,点击 “添加” 或按 Enter 键添加待办事项;
- 勾选复选框,待办事项变为已完成(文字加删除线);
- 点击 “删除” 可移除待办事项;
- 刷新页面,数据仍保留(本地存储生效)。
(2)核心技术点回顾
- DOM 操作:动态创建、修改和删除元素;
- 事件处理:监听点击、输入事件,实现交互;
- 本地存储:用localStorage持久化数据;
- 数组操作:添加、删除数组元素,配合渲染列表;
- 函数封装:将加载、保存、渲染等功能封装为函数,提高代码复用性。
七、总结与底层原理
1. JavaScript 核心特性
- 单线程:同一时间只能执行一段代码,避免 DOM 操作冲突;
- 非阻塞 I/O:通过异步编程(回调、Promise、async/await)处理耗时操作;
- 动态类型:变量类型可随时改变,灵活但需注意类型转换问题;
- 原型继承:不同于类继承,通过原型链实现对象间的属性共享。
2. 事件循环(Event Loop)
JavaScript 的异步机制依赖事件循环:
(1)同步代码直接执行,异步操作(如setTimeout、网络请求)放入任务队列;
(2)同步代码执行完毕后,从任务队列中取出异步任务执行;
(3)重复步骤 2,形成循环。
这一机制保证了单线程下非阻塞的执行模式。
更多推荐
所有评论(0)