一、JavaScript 简介
JavaScript(简称 JS)是一种高级的、解释型的编程语言,由 Netscape 公司的 Brendan Eich 在 1995 年开发,最初命名为 LiveScript,后因与 Java 的合作关系而改名为 JavaScript。作为 Web 开发的三大核心技术之一(其他两个是 HTML 和 CSS),JavaScript 在现代网页中扮演着至关重要的角色。
JS 的主要特点包括:
- 客户端脚本语言:直接在用户的浏览器中执行,无需服务器处理
- 动态类型:变量类型在运行时确定
- 基于原型的面向对象特性
- 函数是一等公民
- 支持异步编程(通过 Promise 和 async/await)
与 HTML(超文本标记语言)负责页面结构、CSS(层叠样式表)负责页面样式不同,JavaScript 主要负责页面的行为和交互功能。典型的应用场景包括:
- 表单验证:检查用户输入是否符合要求(如邮箱格式验证)
- 动态内容更新:无需刷新页面即可加载新内容(如社交媒体信息流)
- 动画效果:创建平滑的页面过渡和交互效果
- 用户交互响应:处理点击、滚动等用户事件
- 与后端 API 通信:通过 AJAX 或 Fetch API 获取数据
现代 JavaScript 已经发展出多个框架和库(如 React、Vue、Angular),使得开发复杂的前端应用变得更加高效。根据 Stack Overflow 2022 开发者调查,JavaScript 已连续10年成为最常用的编程语言。
示例代码展示基本的 DOM 操作:
// 获取页面元素
const button = document.getElementById('myButton');
// 添加点击事件监听器
button.addEventListener('click', function() {// 修改元素内容this.textContent = '已点击';// 改变样式this.style.backgroundColor = 'blue';
});
二、JavaScript 的引入方式
1. 内嵌式 JavaScript
内嵌式是最基础的 JavaScript 引入方式,直接在 HTML 文件中使用 <script>
标签包裹 JavaScript 代码。这种方式的优点是简单直接,适合快速测试和小型脚本。
典型应用场景:
- 快速原型开发
- 小型网站或单页应用
- 需要立即执行的初始化代码
示例代码详解:
<!DOCTYPE html>
<html>
<head><title>内嵌式JS</title><script>// 这里是JavaScript代码function init() {alert("页面加载完成,这是内嵌式JavaScript");console.log("调试信息输出到控制台");}// 当DOM加载完成后执行document.addEventListener('DOMContentLoaded', init);</script>
</head>
<body><!-- 页面内容 -->
</body>
</html>
特性说明:
- 代码会按照在HTML中的出现顺序执行
- 可以放在
<head>
或<body>
中,位置不同会影响执行时机 - 适合少量脚本,但不利于代码维护和复用
2. 外链式 JavaScript
外链式是将 JavaScript 代码写在单独的.js文件中,然后在 HTML 中通过<script>
标签引入。这是现代Web开发推荐的方式。
典型应用场景:
- 大中型项目开发
- 需要复用的组件或库
- 需要代码分离和模块化的项目
示例代码详解:
HTML文件:
<!DOCTYPE html>
<html>
<head><title>外链式JS</title><!-- 推荐放在body结束前,避免阻塞渲染 --><script src="assets/js/myScript.js" defer></script><!-- 或者使用async属性实现异步加载 -->
</head>
<body><!-- 页面内容 -->
</body>
</html>
myScript.js文件内容:
// 使用严格模式
'use strict';// 定义模块
const App = {init: function() {alert("这是外链式JavaScript");this.bindEvents();},bindEvents: function() {// 事件绑定代码}
};// 当DOM加载完成后初始化应用
document.addEventListener('DOMContentLoaded', function() {App.init();
});
最佳实践:
- 文件命名要有意义,如
main.js
、app.js
等 - 使用
defer
或async
属性优化加载性能 - 合理组织目录结构,如
js/
或assets/js/
- 考虑使用模块化开发工具(如Webpack、Rollup)
3. 行内式 JavaScript
行内式是将JavaScript代码直接写在HTML标签的事件属性中,这种方式虽然简单但不推荐大量使用。
典型应用场景:
- 快速测试某个事件处理
- 简单的交互效果
- 传统老式网站维护
示例代码详解:
<!DOCTYPE html>
<html>
<head><title>行内式JS</title><style>.btn {padding: 10px 20px;background-color: #4CAF50;color: white;border: none;cursor: pointer;}</style>
</head>
<body><!-- 简单的事件处理 --><button class="btn" onclick="alert('这是行内式JavaScript'); this.textContent='已点击'">点击我</button><!-- 表单验证示例 --><form onsubmit="return validateForm()"><input type="text" id="username" required><button type="submit">提交</button></form><script>// 可以配合使用的函数function validateForm() {const input = document.getElementById('username');if(input.value.length < 3) {alert('用户名至少3个字符');return false;}return true;}</script>
</body>
</html>
注意事项:
- 这种方式使HTML和JavaScript高度耦合,不利于维护
- 违反了关注点分离(Separation of Concerns)原则
- 现代开发中推荐使用事件监听器而非行内事件
- 在React等框架中的"行内事件"实际上是语法糖,原理不同
三、变量
变量是用于存储数据的容器
在编程中,变量是存储数据的基本单元,相当于一个带标签的盒子,我们可以把数据放进去,需要时再取出来使用。JavaScript 提供了三种声明变量的方式:var
、let
和 const
。
1. 声明和赋值详解
1.1 使用 var
声明变量(ES5及之前的方式)
var
是 JavaScript 最早使用的变量声明方式,具有函数作用域:
// 1. 先声明后赋值
var a; // 声明变量a,此时a的值为undefined
a = 10; // 给变量a赋值为10// 2. 声明同时赋值
var b = 20; // 声明变量b并赋值为20// 3. 变量提升现象
console.log(c); // 输出undefined,不会报错
var c = 30;
1.2 使用 let
声明变量(ES6新增)
let
是 ES6 引入的声明方式,具有块级作用域,解决了 var
的一些问题:
// 1. 基本用法
let x = 100; // 声明并赋值// 2. 块级作用域示例
{let y = 200;console.log(y); // 200
}
// console.log(y); // 报错,y不在作用域内// 3. 不存在变量提升
console.log(z); // 报错
let z = 300;
1.3 使用 const
声明常量(ES6新增)
const
用于声明不可变的常量,必须初始化时就赋值:
// 1. 基本用法
const PI = 3.14159; // 声明常量PI// 2. 不可重新赋值
// PI = 3.14; // 报错,不能修改常量// 3. 对于对象和数组
const colors = ['red', 'green'];
colors.push('blue'); // 允许,因为修改的是数组内容
// colors = ['yellow']; // 报错,不能重新赋值
2. 变量命名规则详解
JavaScript 变量命名需要遵循以下规则:
组成字符:
- 允许使用字母(a-z, A-Z)
- 数字(0-9),但不能以数字开头
- 下划线(_)
- 美元符号($)
命名示例:
- 合法:
userName
,_count
,$price
,num1
- 非法:
1stPlace
(数字开头),my-name
(含连字符)
- 合法:
关键字限制:
- 不能使用 JavaScript 保留的关键字,如:
// let if = 10; // 报错,if是关键字 // let for = 20; // 报错,for是关键字
- 不能使用 JavaScript 保留的关键字,如:
大小写敏感:
firstName
和FirstName
是两个不同的变量age
和AGE
也是不同的变量
命名建议:
- 使用驼峰命名法(如
userAge
) - 常量通常全大写(如
MAX_SIZE
) - 使用有意义的名称(避免单字母变量名)
- 使用驼峰命名法(如
举例说明:
let userAge = 25; // 正确
const MAX_USERS = 100; // 正确
let $price = 99.9; // 正确
// let 2ndPlace = "John"; // 错误:数字开头
// let first-name = "Tom";// 错误:包含连字符
四、数据类型
1. 基本数据类型
字符串(String)
字符串是表示文本的数据类型,可以用单引号('')、双引号("")或反引号(``)定义。ES6引入的模板字符串(反引号)支持多行文本和字符串插值。
let str1 = "Hello"; // 双引号
let str2 = 'World'; // 单引号
let str3 = `Hello ${str2}`; // 模板字符串,输出"Hello World"
let multiLine = `这是
多行
字符串`; // 支持换行
数字(Number)
JavaScript 只有一种数字类型,包括整数和浮点数,采用IEEE 754标准的64位双精度浮点数格式表示。
let num1 = 100; // 整数
let num2 = 3.14; // 浮点数
let num3 = 0.1 + 0.2; // 0.30000000000000004(浮点数精度问题)
let hex = 0xff; // 十六进制
let oct = 0o10; // 八进制
let bigNum = 1e6; // 科学计数法,表示1000000
布尔值(Boolean)
布尔值表示逻辑实体,只有两个值:true和false。常用于条件判断。
let isTrue = true; // 真值
let isFalse = false; // 假值
let isGreater = 10 > 5; // 比较运算返回布尔值
空值(Null)
Null类型只有一个值null,表示一个空对象引用。通常用于表示变量有意为空值。
let nullValue = null; // 明确赋值为空
let element = document.getElementById('not-exist'); // 返回null
未定义(Undefined)
Undefined类型表示变量已声明但未赋值,或访问对象不存在的属性。
let undefinedValue; // 只声明未赋值
let obj = {};
console.log(obj.noProperty); // 访问不存在的属性返回undefined
Symbol(ES6新增)
Symbol表示唯一的、不可变的值,主要用作对象属性的标识符。
let sym1 = Symbol('description');
let sym2 = Symbol('description');
console.log(sym1 === sym2); // false,每个Symbol都是唯一的
BigInt(ES2020新增)
BigInt表示任意精度的整数,可以表示大于2^53的整数。
let bigInt = 9007199254740991n; // 末尾加n表示BigInt
let bigInt2 = BigInt("9007199254740991"); // 使用BigInt函数
2. 引用数据类型
对象(Object)
对象是键值对的集合,用于存储复杂数据结构。对象属性可以是基本类型值或其他对象。
let person = {name: "张三",age: 20,address: {city: "北京",street: "长安街"},sayHello: function() {console.log("你好!");}
};// 访问属性
console.log(person.name); // 点表示法
console.log(person['age']); // 方括号表示法
person.sayHello(); // 调用方法
数组(Array)
数组是有序的值集合,可以包含不同类型的元素,长度动态可变。
let arr = [1, "two", true, {name: "数组元素"}];
arr.push(5); // 添加元素到末尾
arr.unshift(0); // 添加元素到开头
let first = arr[0]; // 通过索引访问
let length = arr.length; // 获取数组长度// 数组遍历
arr.forEach(function(item) {console.log(item);
});
函数(Function)
函数是可执行代码块,可以接收参数并返回值。JavaScript中函数是一等公民。
// 函数声明
function add(a, b) {return a + b;
}// 函数表达式
let multiply = function(x, y) {return x * y;
};// 箭头函数(ES6)
let divide = (x, y) => x / y;// 调用函数
let sum = add(5, 3); // 8
let product = multiply(4, 6); // 24
let quotient = divide(10, 2); // 5
其他引用类型
还包括Date、RegExp、Map、Set等特殊对象类型。
// Date对象
let now = new Date();
console.log(now.getFullYear());// RegExp正则表达式
let pattern = /hello/i;
console.log(pattern.test("Hello World")); // true// Map集合
let map = new Map();
map.set('name', '李四');
console.log(map.get('name'));// Set集合
let set = new Set([1, 2, 3, 3, 4]);
console.log(set.size); // 4(自动去重)
3. 数据类型检测与转换
类型检测
// typeof运算符
console.log(typeof "Hello"); // "string"
console.log(typeof 100); // "number"
console.log(typeof true); // "boolean"
console.log(typeof null); // "object"(历史遗留问题)
console.log(typeof undefined); // "undefined"
console.log(typeof {name: "张三"}); // "object"
console.log(typeof [1, 2, 3]); // "object"(数组也是对象)
console.log(typeof function(){}); // "function"// instanceof运算符(检测对象类型)
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true// Object.prototype.toString方法(更准确的类型检测)
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
类型转换
// 显式类型转换
let numStr = "123";
let num = Number(numStr); // 字符串转数字
let str = String(123); // 数字转字符串
let bool = Boolean(1); // 真值转换// 隐式类型转换
let result = "5" + 2; // "52"(字符串拼接)
let sum = "5" - 2; // 3(数字运算)
let isTrue = !!1; // true(布尔转换)// 特殊转换案例
console.log(Number("")); // 0
console.log(Number("123abc")); // NaN
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
五、运算符
1. 算术运算符
JavaScript 提供了多种算术运算符用于数值计算,包括:
+
加法运算-
减法运算*
乘法运算/
除法运算%
取余运算(求模)++
自增运算(分为前自增和后自增)--
自减运算(分为前自减和后自减)
示例代码
let a = 10;
let b = 5;// 基本运算
console.log(a + b); // 15,加法运算
console.log(a - b); // 5,减法运算
console.log(a * b); // 50,乘法运算
console.log(a / b); // 2,除法运算
console.log(a % b); // 0,取余运算(10除以5余0)// 自增运算(后自增)
a++; // 等价于 a = a + 1
console.log(a); // 11// 自减运算(后自减)
b--; // 等价于 b = b - 1
console.log(b); // 4// 前自增与前自减
let x = 5;
let y = ++x; // 先自增再赋值
console.log(y); // 6let m = 5;
let n = m++; // 先赋值再自增
console.log(n); // 5
console.log(m); // 6
2. 赋值运算符
赋值运算符用于给变量赋值,包括:
=
基本赋值+=
加后赋值-=
减后赋值*=
乘后赋值/=
除后赋值%=
取余后赋值
示例代码
let c = 10; // 基本赋值// 复合赋值运算
c += 5; // 相当于 c = c + 5,结果为15
console.log(c); // 15c -= 3; // 相当于 c = c - 3,结果为12
console.log(c); // 12c *= 2; // 相当于 c = c * 2,结果为24
console.log(c); // 24c /= 4; // 相当于 c = c / 4,结果为6
console.log(c); // 6c %= 2; // 相当于 c = c % 2,结果为0
console.log(c); // 0// 字符串连接赋值
let str = "Hello";
str += " World"; // 相当于 str = str + " World"
console.log(str); // "Hello World"
3. 比较运算符
比较运算符用于比较两个值的大小或相等性,返回布尔值:
==
等于(会自动类型转换)===
严格等于(不进行类型转换)!=
不等于!==
严格不等于>
大于<
小于>=
大于等于<=
小于等于
示例代码
// 松散比较(会进行类型转换)
console.log(10 == "10"); // true,因为字符串"10"会被转换为数字10
console.log(1 == true); // true,true会被转换为1// 严格比较(不进行类型转换)
console.log(10 === "10"); // false,类型不同
console.log(10 === 10); // true,值和类型都相同// 数值比较
console.log(10 > 5); // true
console.log(10 < 5); // false
console.log(10 >= 10); // true
console.log(10 <= 9); // false// 不等于比较
console.log(10 != "10"); // false,因为松散比较会转换类型
console.log(10 !== "10"); // true,因为严格比较类型不同// 特殊比较
console.log(null == undefined); // true
console.log(null === undefined); // false
console.log(NaN == NaN); // false,NaN不等于任何值包括它自己
4. 逻辑运算符
逻辑运算符用于处理布尔值,包括:
&&
逻辑与(两个操作数都为true时返回true)||
逻辑或(至少一个操作数为true时返回true)!
逻辑非(反转操作数的布尔值)
示例代码
// 逻辑与
console.log(true && false); // false
console.log(true && true); // true
console.log(false && false); // false// 逻辑或
console.log(true || false); // true
console.log(false || false); // false
console.log(true || true); // true// 逻辑非
console.log(!true); // false
console.log(!false); // true// 短路求值特性
let x = 10;
(x > 5) && console.log("x大于5"); // 会输出,因为第一个条件为true
(x < 5) && console.log("这不会输出"); // 不会输出,因为第一个条件为false// 实际应用:条件赋值
let name = "";
let displayName = name || "匿名用户";
console.log(displayName); // "匿名用户"let user = { name: "张三" };
let userName = user && user.name;
console.log(userName); // "张三"
六、流程控制
JavaScript 条件语句和循环语句详解
1. 条件语句
if 语句
if 语句是最基本的条件控制结构,用于根据条件执行不同的代码块。
let score = 80;if (score >= 60) {console.log("及格了"); // 当score>=60时执行
} else {console.log("没及格"); // 当score<60时执行
}
应用场景:判断用户是否登录、表单验证、成绩评定等。
if-else if-else 语句
当需要判断多个条件时,可以使用多分支结构。
let grade = 85;if (grade >= 90) {console.log("优秀"); // 90分以上
} else if (grade >= 80) {console.log("良好"); // 80-89分
} else if (grade >= 60) {console.log("及格"); // 60-79分
} else {console.log("不及格"); // 60分以下
}
执行流程:从上到下依次判断条件,当某个条件满足时执行对应代码块并退出整个判断结构。
switch 语句
switch 语句适用于多条件等值判断的场景,比多个if-else更清晰。
let day = 3;switch (day) {case 1:console.log("星期一");break; // 必须使用break退出switch结构case 2:console.log("星期二");break;case 3:console.log("星期三"); // 本例会输出这个结果break;default:console.log("其他星期"); // 当所有case都不匹配时执行
}
注意事项:
- 每个case后面必须加break,否则会继续执行下一个case
- default分支是可选的,用于处理未匹配的情况
- switch使用严格比较(===)
2. 循环语句
for 循环
for循环是最常用的循环结构,适合已知循环次数的场景。
for (let i = 0; i < 5; i++) {console.log(i); // 依次输出0、1、2、3、4
}
循环步骤:
- 初始化变量(let i = 0)
- 检查条件(i < 5)
- 执行循环体
- 更新变量(i++)
- 重复步骤2-4直到条件不满足
while 循环
while循环在不确定循环次数时使用,先判断条件再执行循环体。
let j = 0;
while (j < 5) {console.log(j); // 依次输出0、1、2、3、4j++;
}
应用场景:读取文件直到结束、处理用户输入直到满足条件等。
do-while 循环
do-while循环至少执行一次循环体,然后再判断条件。
let k = 0;
do {console.log(k); // 先输出0,然后输出1、2、3、4k++;
} while (k < 5);
特点:无论条件是否成立,循环体至少执行一次。适合需要先执行操作再检查条件的场景,如菜单显示和用户交互。
七、函数
函数的概念与重要性
函数是编程中的基本构建块,它是一段可重复使用的代码,用于执行特定的任务。合理使用函数可以带来多重好处:
- 提高代码可维护性:将复杂逻辑分解为多个小函数,便于理解和修改
- 增强代码复用性:避免重复代码,一处定义多处调用
- 降低耦合度:函数间通过明确接口通信,减少相互依赖
- 便于调试:可以单独测试每个函数的功能
1. 函数的声明和调用
在JavaScript中,函数可以通过多种方式声明和调用,每种方式都有其适用场景。
基本函数声明
// 声明一个名为sayHello的函数
// 它接受一个name参数,用于个性化问候
function sayHello(name) {// 使用console.log输出问候语// 字符串拼接可以使用+运算符console.log("Hello, " + name + "!");// 也可以使用ES6的模板字符串console.log(`Hello, ${name}!`);
}// 调用函数并传入参数"张三"
sayHello("张三");
// 输出:
// Hello, 张三!
// Hello, 张三!// 再次调用传入不同参数
sayHello("李四");
// 输出:
// Hello, 李四!
// Hello, 李四!
带多个参数的函数
// 声明一个包含多个参数的函数
// 参数可以是任意类型:字符串、数字、布尔值、对象等
function introduce(name, age, job) {// 使用模板字符串构建自我介绍console.log(`大家好,我是${name},今年${age}岁,职业是${job}。`);// 可以添加更多逻辑if (age < 18) {console.log("我还是个未成年人~");}
}// 调用函数并传入三个参数
introduce("王五", 28, "工程师");
// 输出:
// 大家好,我是王五,今年28岁,职业是工程师。introduce("小明", 16, "学生");
// 输出:
// 大家好,我是小明,今年16岁,职业是学生。
// 我还是个未成年人~
2. 函数的返回值
函数可以使用return语句返回一个值,这个值可以被其他代码使用。返回值可以是任何JavaScript数据类型。
简单返回值
// 声明一个求两数之和的函数
function sum(a, b) {// 返回a和b的和// return语句会立即结束函数执行return a + b;// 这行代码永远不会执行console.log("This won't be printed");
}// 调用函数并将返回值赋给result变量
let result = sum(3, 5);
console.log(result); // 输出8// 可以直接在表达式中使用函数调用
console.log(sum(10, 20) * 2); // 输出60// 也可以作为其他函数的参数
console.log(sum(sum(2,3), sum(4,5))); // 输出14
复杂返回值
// 返回复杂数据结构的函数
function createUser(name, age) {// 返回一个对象// 可以根据参数动态计算属性值return {username: name,userAge: age,isAdult: age >= 18,registeredAt: new Date(), // 添加注册时间// 方法也可以作为返回值greet: function() {return `Hi, I'm ${this.username}`;}};
}let user = createUser("赵六", 25);
console.log(user);
/* 输出:
{username: "赵六",userAge: 25,isAdult: true,registeredAt: [当前日期时间],greet: [Function: greet]
}
*/
console.log(user.greet()); // 输出: Hi, I'm 赵六let teen = createUser("小红", 16);
console.log(teen.isAdult); // false
3. 函数表达式
函数也可以作为表达式赋值给变量,这种形式更加灵活。
匿名函数表达式
// 将匿名函数赋值给multiply变量
// 函数表达式不会被提升,必须先定义后使用
let multiply = function(a, b) {return a * b;
};// 调用函数表达式
let product = multiply(4, 5);
console.log(product); // 输出20// 立即调用的函数表达式(IIFE)
// 常用于创建独立作用域
let result = (function(x, y) {return x / y;
})(10, 2);
console.log(result); // 输出5// 带参数的IIFE
(function(config) {console.log(`App starting with ${config.env} mode`);
})({ env: 'development', debug: true });
箭头函数(ES6)
// 使用箭头函数简化语法
// 当只有一个参数和一条语句时最简洁
let square = x => x * x;
console.log(square(5)); // 输出25// 多参数箭头函数
// 需要括号包裹参数
let greet = (name, time) => {return `Good ${time}, ${name}!`;
};
console.log(greet("张三", "morning")); // 输出"Good morning, 张三!"// 箭头函数与this绑定
const counter = {count: 0,increment: function() {setInterval(() => {// 箭头函数不绑定自己的this,继承自外围this.count++;console.log(this.count);}, 1000);}
};
counter.increment();
实际应用场景
数据处理
// 格式化货币金额
function formatCurrency(amount) {// 添加货币符号并保留两位小数return "¥" + amount.toFixed(2);
}
console.log(formatCurrency(29.9)); // 输出"¥29.90"
console.log(formatCurrency(123.456)); // 输出"¥123.46"// 更复杂的格式化函数
function formatPrice(amount, currency = '¥', decimal = 2) {const formatted = amount.toFixed(decimal);if (currency === '¥') {return currency + formatted;} else {return formatted + ' ' + currency;}
}
console.log(formatPrice(29.9)); // "¥29.90"
console.log(formatPrice(99.99, '$')); // "99.99 $"
表单验证
// 验证邮箱格式
function validateEmail(email) {// 使用正则表达式验证基本邮箱格式const re = /\S+@\S+\.\S+/;return re.test(email);
}
console.log(validateEmail("test@example.com")); // true
console.log(validateEmail("invalid.email")); // false// 更全面的验证函数
function validateForm(data) {const errors = {};if (!data.username) {errors.username = '用户名不能为空';}if (!validateEmail(data.email)) {errors.email = '邮箱格式不正确';}if (data.password.length < 6) {errors.password = '密码至少需要6个字符';}return Object.keys(errors).length === 0 ? null : errors;
}const formData = {username: '张三',email: 'test@',password: '123'
};
console.log(validateForm(formData));
/* 输出:
{email: '邮箱格式不正确',password: '密码至少需要6个字符'
}
*/
业务逻辑封装
// 计算订单总金额
function calculateTotal(items, taxRate) {// 计算小计let subtotal = items.reduce((sum, item) => sum + item.price, 0);// 计算税费const tax = subtotal * taxRate;// 返回包含明细的对象return {subtotal: subtotal,tax: tax,total: subtotal + tax,itemsCount: items.length};
}const cartItems = [{ id: 1, name: '商品A', price: 100 },{ id: 2, name: '商品B', price: 200 },{ id: 3, name: '商品C', price: 150 }
];const orderTotal = calculateTotal(cartItems, 0.1);
console.log(orderTotal);
/* 输出:
{subtotal: 450,tax: 45,total: 495,itemsCount: 3
}
*/// 更复杂的业务逻辑函数
function processOrder(order, paymentMethod) {// 验证订单if (!order.items || order.items.length === 0) {throw new Error('订单中没有商品');}// 计算总金额const total = calculateTotal(order.items, order.taxRate || 0.1);// 处理支付const paymentResult = processPayment(total.total, paymentMethod);// 记录订单const orderRecord = createOrderRecord(order, total, paymentResult);// 发送确认邮件sendConfirmationEmail(order.customerEmail, orderRecord);return orderRecord;
}
通过合理使用函数,可以使代码更加模块化、易于维护和测试。
八、数组
数组基础概念
数组是编程中最常用的数据结构之一,它是一种用于存储多个值的有序集合。数组中的每个元素都有一个对应的索引(从0开始的整数),通过这个索引可以快速访问或修改特定位置的元素。数组在内存中是连续存储的,这使得它的访问效率非常高。
数组的特点
- 有序集合:数组中的元素按照插入顺序排列
- 索引访问:每个元素都有对应的从0开始的整数索引
- 连续内存:数组元素在内存中是连续存储的
- 动态大小:在JavaScript中数组长度可以动态变化
- 混合类型:可以存储不同类型的数据
数组的创建方式
1. 字面量方式(推荐)
// 创建数字数组
let numbers = [1, 2, 3, 4, 5]; // 创建字符串数组
let fruits = ['apple', 'banana', 'orange'];// 创建混合类型数组
let mixed = [1, 'text', true, null, {name: 'John'}];// 创建空数组
let empty = [];
2. 构造函数方式
// 创建包含元素的数组
let arr = new Array(6, 7, 8, 9, 10);// 创建指定长度的空数组
let emptyArr = new Array(5); // 长度为5,元素都是undefined// 创建单元素数组时要特别注意
let single = new Array(5); // 创建的是长度为5的空数组
let singleCorrect = [5]; // 这才是包含数字5的数组
数组的访问和修改
let products = ['手机', '电脑', '平板', '耳机', '鼠标'];// 访问元素
console.log(products[0]); // 输出:"手机"
console.log(products[2]); // 输出:"平板"// 访问不存在的索引
console.log(products[10]); // 输出:undefined// 修改元素
products[1] = '笔记本电脑'; // 修改第二个元素
products[3] = '蓝牙耳机'; // 修改第四个元素// 添加新元素
products[5] = '键盘'; // 添加新元素
products[10] = '显示器'; // 这会创建空位(empty items)console.log(products);
// 输出:['手机', '笔记本电脑', '平板', '蓝牙耳机', '鼠标', '键盘', 空 × 4, '显示器']
数组的常用方法和属性
1. 添加/删除元素
// 初始化数组
let tasks = ['学习', '运动'];// push() - 末尾添加
tasks.push('阅读'); // 添加单个
tasks.push('购物', '做饭'); // 添加多个// pop() - 末尾删除
let lastTask = tasks.pop(); // 删除并返回'做饭'// unshift() - 开头添加
tasks.unshift('起床'); // 添加单个
tasks.unshift('刷牙', '洗脸'); // 添加多个// shift() - 开头删除
let firstTask = tasks.shift(); // 删除并返回'刷牙'console.log(tasks); // 输出:['洗脸', '起床', '学习', '运动', '阅读']
2. 数组长度
let colors = ['red', 'green', 'blue'];// 获取长度
console.log(colors.length); // 输出:3// 修改长度
colors.length = 5; // 扩展数组
console.log(colors); // 输出:['red', 'green', 'blue', empty × 2]colors.length = 2; // 截断数组
console.log(colors); // 输出:['red', 'green']// 清空数组
colors.length = 0;
console.log(colors); // 输出:[]
3. 其他常用方法
// 初始化数组
let numbers = [1, 2, 3, 4, 5];// slice() - 提取子数组
let subArr = numbers.slice(1, 4); // [2, 3, 4]
let lastTwo = numbers.slice(-2); // [4, 5]// splice() - 修改数组
// 删除
numbers.splice(2, 1); // 从索引2开始删除1个 → [1, 2, 4, 5]
// 插入
numbers.splice(2, 0, 3); // 在索引2插入3 → [1, 2, 3, 4, 5]
// 替换
numbers.splice(1, 2, 'a', 'b'); // [1, 'a', 'b', 4, 5]// concat() - 合并数组
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = arr1.concat(arr2, [5, 6]); // [1, 2, 3, 4, 5, 6]
实际应用示例
示例1:购物车商品管理
// 初始化购物车
let cart = [];// 添加商品
function addProduct(id, name, price) {cart.push({id, name, price});console.log(`已添加: ${name}`);
}// 移除商品
function removeProduct(index) {if (index >= 0 && index < cart.length) {let removed = cart.splice(index, 1);console.log(`已移除: ${removed[0].name}`);} else {console.log('无效的索引');}
}// 计算总价
function calculateTotal() {return cart.reduce((total, item) => total + item.price, 0);
}// 使用示例
addProduct(1, 'iPhone 13', 5999);
addProduct(2, 'AirPods Pro', 1499);
addProduct(3, 'MacBook Pro', 12999);console.log('当前购物车:');
cart.forEach((item, index) => {console.log(`${index + 1}. ${item.name} - ¥${item.price}`);
});removeProduct(1); // 移除AirPods Proconsole.log(`总价: ¥${calculateTotal()}`);
示例2:成绩处理系统
// 初始化成绩数组
let scores = [85, 90, 78, 92, 88];// 计算统计信息
function calculateStats(scores) {if (scores.length === 0) return null;// 计算总分和平均分let sum = scores.reduce((a, b) => a + b, 0);let average = sum / scores.length;// 计算最高分和最低分let max = Math.max(...scores);let min = Math.min(...scores);// 找出不及格的成绩let failed = scores.filter(score => score < 60);return {count: scores.length,sum,average: average.toFixed(2),max,min,failedCount: failed.length,failedScores: failed};
}// 添加新成绩
function addScore(newScore) {scores.push(newScore);console.log(`已添加成绩: ${newScore}`);
}// 删除最低分
function removeLowest() {let minIndex = scores.indexOf(Math.min(...scores));if (minIndex !== -1) {let removed = scores.splice(minIndex, 1);console.log(`已移除最低分: ${removed[0]}`);}
}// 使用示例
console.log('初始成绩:', scores.join(', '));
addScore(95);
removeLowest();let stats = calculateStats(scores);
console.log('统计信息:');
console.log(`数量: ${stats.count}`);
console.log(`总分: ${stats.sum}`);
console.log(`平均分: ${stats.average}`);
console.log(`最高分: ${stats.max}`);
console.log(`最低分: ${stats.min}`);
console.log(`不及格数量: ${stats.failedCount}`);
九、对象
对象的基本概念
对象是JavaScript中最重要的数据类型之一,它是一个无序的键值对集合,用于存储和表示复杂的数据结构。在对象中:
键(key):称为属性(property),必须是字符串或Symbol类型。属性名遵循标识符命名规则,但也可以使用引号包裹特殊字符作为属性名。
值(value):称为属性值,可以是任意JavaScript数据类型,包括:
- 基本类型:字符串、数字、布尔值、null、undefined
- 复杂类型:数组、函数、其他对象
- 特殊类型:Symbol、BigInt
属性描述符:每个属性还有一组描述其特性的属性描述符,包括:
value
:属性的值writable
:是否可修改(默认true)enumerable
:是否可枚举(默认true)configurable
:是否可配置(默认true)get
/set
:访问器函数
例如,在浏览器环境中,document
对象就是一个典型的JavaScript对象,它包含了大量属性和方法来操作DOM。
对象的创建方式
1. 对象字面量方式
这是最常用的创建对象方式,使用大括号{}语法,适合创建简单的、一次性的对象:
let person = {// 基本类型属性name: "张三", // 字符串属性age: 20, // 数字属性isStudent: true, // 布尔属性// 复杂类型属性courses: ["数学", "语文", "英语"], // 数组属性address: { // 嵌套对象city: "北京",street: "中关村大街",getFullAddress() {return `${this.city} ${this.street}`}},// 方法属性sayHello: function() {console.log("Hello, I'm " + this.name);},// ES6简写方法introduce() {console.log(`我叫${this.name},今年${this.age}岁`);},// 计算属性名["id_" + Math.random().toString(36).substr(2)]: "随机ID"
};// 使用对象
person.sayHello(); // 输出: Hello, I'm 张三
console.log(person.address.getFullAddress()); // 输出: 北京 中关村大街
2. 使用Object构造函数
通过new Object()
创建空对象,再动态添加属性,适合需要动态构建对象的场景:
let student = new Object(); // 创建一个空对象// 添加属性
student.name = "李四";
student.age = 18;// 添加方法
student.study = function(subject) {console.log(`${this.name}正在学习${subject}`);
};// 添加嵌套对象
student.scores = new Object();
student.scores.math = 90;
student.scores.english = 85;// 使用对象
student.study("数学"); // 输出: 李四正在学习数学
console.log(student.scores.math); // 输出: 90
3. 使用Object.create()方法
可以指定原型对象创建新对象,适合需要继承的场景:
let animal = {type: "动物",makeSound() {console.log("发出声音");}
};let dog = Object.create(animal);
dog.type = "狗";
dog.breed = "金毛";
dog.bark = function() {console.log("汪汪!");
};dog.makeSound(); // 继承自animal: 发出声音
dog.bark(); // 输出: 汪汪!
4. 使用构造函数和new操作符
适合需要创建多个相似对象的场景:
function Car(make, model, year) {this.make = make;this.model = model;this.year = year;this.displayInfo = function() {console.log(`${this.year} ${this.make} ${this.model}`);};
}let myCar = new Car("Toyota", "Camry", 2020);
myCar.displayInfo(); // 输出: 2020 Toyota Camry
对象属性的访问和操作
1. 点表示法访问
最常用的属性访问方式,简洁直观:
let person = {name: "张三",age: 20,"first-name": "张" // 包含特殊字符的属性名
};// 访问属性
console.log(person.name); // 输出"张三"// 修改属性
person.age = 21; // 修改age属性
console.log(person.age); // 输出21// 添加新属性
person.gender = "男";
console.log(person.gender); // 输出"男"// 无法访问特殊字符属性
// console.log(person.first-name); // 报错
2. 方括号表示法访问
当属性名包含特殊字符或需要动态计算时使用:
let person = {"first-name": "张","last-name": "三",1: "数字作为属性名",[Symbol("id")]: "symbol作为属性名"
};// 访问特殊字符属性
console.log(person["first-name"]); // 输出"张"// 动态访问属性
let propName = "last-name";
console.log(person[propName]); // 输出"三"// 数字属性名
console.log(person[1]); // 输出"数字作为属性名"
console.log(person["1"]); // 同上// Symbol属性名
let sym = Symbol("id");
console.log(person[sym]); // 输出"symbol作为属性名"// 添加计算属性
let dynamicProp = "score_" + Math.floor(Math.random() * 10);
person[dynamicProp] = 95;
console.log(person[dynamicProp]);
3. 属性操作
删除属性
使用delete
操作符删除对象属性:
let person = {name: "张三",age: 20,gender: "男"
};console.log("age" in person); // 输出true
delete person.age;
console.log(person.age); // 输出undefined
console.log("age" in person); // 输出false
检查属性存在
let person = {name: "张三"};// in操作符检查属性是否存在(包括原型链)
console.log("name" in person); // true
console.log("toString" in person); // true (继承自Object.prototype)// hasOwnProperty检查自有属性
console.log(person.hasOwnProperty("name")); // true
console.log(person.hasOwnProperty("toString")); // false
遍历属性
let person = {name: "张三",age: 20,gender: "男"
};// for...in循环(包含继承的可枚举属性)
for (let key in person) {console.log(key + ": " + person[key]);
}// Object.keys()获取自有可枚举属性
let keys = Object.keys(person);
console.log(keys); // ["name", "age", "gender"]// Object.getOwnPropertyNames()获取所有自有属性
let allProps = Object.getOwnPropertyNames(person);
console.log(allProps);// Object.getOwnPropertySymbols()获取Symbol属性
let symbols = Object.getOwnPropertySymbols(person);
console.log(symbols);
对象方法的定义和调用
1. 定义方法
方法本质上是一个函数类型的属性,有多种定义方式:
let calculator = {// 传统函数表达式add: function(a, b) {return a + b;},// ES6方法简写subtract(a, b) {return a - b;},// 箭头函数(注意this的指向问题)multiply: (a, b) => a * b,// 生成器方法*generateSequence(start, end) {for (let i = start; i <= end; i++) {yield i;}},// 异步方法async fetchData(url) {let response = await fetch(url);return await response.json();}
};
2. 调用方法
使用点表示法或方括号表示法调用对象方法:
let person = {name: "张三",age: 20,// 方法定义sayHello() {console.log(`你好,我是${this.name},今年${this.age}岁`);},// 另一个方法celebrateBirthday() {this.age++;console.log(`庆祝生日!现在${this.age}岁了`);}
};// 直接调用
person.sayHello(); // 输出: 你好,我是张三,今年20岁// 动态调用
let methodName = "sayHello";
person[methodName](); // 同上// 链式调用
person.celebrateBirthday().sayHello(); // 先增加年龄,再打招呼
3. 方法中的this
在对象方法中,this
指向调用该方法的对象,但需要注意一些特殊情况:
let person = {name: "Alice",greet: function() {console.log("Hi, I'm " + this.name);},greetArrow: () => {console.log("Hi, I'm " + this.name); // 箭头函数没有自己的this}
};// 直接调用
person.greet(); // 输出: Hi, I'm Alice
person.greetArrow(); // 输出: Hi, I'm undefined (或全局name)// 方法赋值给变量
let greetFunc = person.greet;
greetFunc(); // 输出: Hi, I'm undefined (this丢失)// 使用bind绑定this
let boundGreet = person.greet.bind(person);
boundGreet(); // 输出: Hi, I'm Alice// 作为回调函数
setTimeout(person.greet, 1000); // this丢失
setTimeout(person.greet.bind(person), 1000); // 正确绑定
4. Getter和Setter
可以使用getter和setter定义访问器属性:
let user = {firstName: "张",lastName: "三",// getterget fullName() {return `${this.firstName} ${this.lastName}`;},// setterset fullName(value) {[this.firstName, this.lastName] = value.split(" ");}
};console.log(user.fullName); // 输出: 张 三
user.fullName = "李 四";
console.log(user.firstName); // 输出: 李
console.log(user.lastName); // 输出: 四
对象的高级特性
1. 属性描述符
可以使用Object.defineProperty
定义或修改属性特性:
let obj = {};Object.defineProperty(obj, "readOnlyProp", {value: 42,writable: false,enumerable: true,configurable: false
});console.log(obj.readOnlyProp); // 42
obj.readOnlyProp = 100; // 静默失败(严格模式下报错)
console.log(obj.readOnlyProp); // 仍然是42// 获取属性描述符
let descriptor = Object.getOwnPropertyDescriptor(obj, "readOnlyProp");
console.log(descriptor);
2. 对象冻结
可以限制对象的修改:
let person = {name: "张三",age: 20
};// 1. Object.preventExtensions: 禁止添加新属性
Object.preventExtensions(person);
person.gender = "男"; // 静默失败(严格模式下报错)// 2. Object.seal: 禁止添加/删除属性
Object.seal(person);
delete person.name; // 静默失败// 3. Object.freeze: 完全冻结对象
Object.freeze(person);
person.age = 21; // 静默失败// 检查对象状态
console.log(Object.isExtensible(person)); // false
console.log(Object.isSealed(person)); // true
console.log(Object.isFrozen(person)); // true
3. 原型和继承
JavaScript使用原型链实现继承:
// 父类
function Animal(name) {this.name = name;
}
Animal.prototype.speak = function() {console.log(`${this.name} makes a noise.`);
};// 子类
function Dog(name) {Animal.call(this, name); // 调用父类构造函数
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {console.log(`${this.name} barks.`);
};let d = new Dog("Rex");
d.speak(); // 输出: Rex barks.
4. ES6类语法
ES6引入了更简洁的类语法:
class Person {constructor(name, age) {this.name = name;this.age = age;}greet() {console.log(`Hello, I'm ${this.name}`);}static createAnonymous() {return new Person("Anonymous", 0);}
}class Student extends Person {constructor(name, age, grade) {super(name, age); // 调用父类构造函数this.grade = grade;}study() {console.log(`${this.name} is studying`);}
}let student = new Student("张三", 20, "A");
student.greet(); // 输出: Hello, I'm 张三
student.study(); // 输出: 张三 is studying