JavaScript

Stone大约 202 分钟

JavaScript

简介

JavaScript 是一种广泛用于客户端 Web 开发的脚本语言,常用来给 HTML 网页添加动态功能,比如响应用户的各种操作。它可以直接嵌入到 HTML 页面中,也可以通过外部文件的形式进行引用。在浏览器中,JavaScript 主要通过浏览器提供的 JavaScript 引擎进行解析和执行。

组成

JavaScript 主要由三个部分组成:

  1. ECMAScript:这是 JavaScript 的核心部分,描述了该语言的基本语法、变量和数据类型、关键字和保留字、操作符、控制语句以及对象等。ECMAScript 是一套国际标准,它定义了 JavaScript 语言的基本结构和行为。
  2. 文档对象模型(DOM):DOM 是 HTML 和 XML 的应用程序接口(API),它将整个页面规划成由节点层级构成的文档。通过 DOM,JavaScrip 可以访问和修改 HTML 文档的内容和结构,实现网页的动态交互效果。
  3. 浏览器对象模型(BOM):BOM 提供了与浏览器窗口进行交互的方法和接口,例如弹出新窗口、移动窗口、改变浏览器窗口大小等。通过 BOM,JavaScript 可以控制浏览器的行为和状态。

书写位置

JavaScript 的书写位置主要有三种:行内式、内嵌式和外链式。

行内式:直接将 JavaScript 代码写在 HTML 标签的事件属性中,如 onclickonload 等。这种方式适用于简单的脚本,但不利于代码的管理和复用。

<button onclick="alert('Hello, World!')">点击我</button>

内嵌式:将 JavaScript 代码写在 HTML 文档的 <script> 标签内,通常放在 <head><body> 标签中。放在 <head> 标签中的 JavaScript 代码会在页面加载时执行,可能会影响页面加载速度;而放在 <body> 标签底部则可以确保在页面完全加载完成后再运行 JavaScript 代码。

<script>  
  function myFunction() {  
    alert("Hello, World!")
  }  
</script>

外链式:将 JavaScript 代码写在单独的 .js 文件中,并通过 HTML 的 <script> 标签的 src 属性引入。这种方式有利于代码的结构化和复用,是实际开发中最为常用的方式。

<script src="myScript.js"></script>

在选择 JavaScript 的书写位置时,需要根据实际需求和项目规模进行权衡。对于简单的脚本,可以使用行内式或内嵌式;对于复杂的项目,建议使用外链式,将代码写在单独的 .js 文件中,以提高代码的可维护性和复用性。同时,还需要注意代码的书写规范和性能优化,以确保 JavaScript 代码能够高效、稳定地运行。

注释

JavaScript 支持两种类型的注释:

  • 单行注释

单行注释以两个正斜杠 // 开始,从 // 到行尾的所有内容都会被 JavaScript 解释器忽略。这种注释通常用于解释代码行或临时禁用某行代码。

  • 多行注释

多行注释以 /* 开始,以 */ 结束。在这两个符号之间的所有内容都会被 JavaScript 解释器忽略,包括换行符。这种注释常用于对代码块或函数进行说明。

输出

常用的输出语句有:

  • document.write():向 body 内输出内容
document.write('Hello World')
document.write('<h1>Hello World</h1>')
  • alert():页面弹出警告对话框
alert('Hello World')
  • console.log():输出到控制台
console.log('Hello World')

变量

在 JavaScript 中,变量是用来存储数据的容器,可以通过变量名来访问和操作这些数据。JavaScript 是一种动态类型的语言,这意味着不需要在声明变量时指定其类型;变量的类型会根据赋给它的值自动确定。

声明变量

语法:let 变量名

let age = 18 // 声明变量 age 并赋值为 18
console.log(age)
age = 19  // 修改变量
console.log(age)

命名规则

  • 变量名可以包含字母、数字、美元符号($)和下划线(_)。
  • 变量名必须以字母、美元符号($)或下划线(_)开始,不能以数字开始。
  • 变量名是区分大小写的(myVarmyvar 是两个不同的变量)。
  • 保留字(如 varletconstfunction 等)不能用作变量名。

常量

使用 const 关键字声明常量并赋值。一旦一个变量被 const 声明,它的值就不能被重新赋值(但是,如果它引用的是一个对象或数组,那么对象的属性和数组的元素是可以被修改的)。

语法:const 常量名 = 常量值

const PI = 3.14159; // 声明一个常量 PI  
// PI = 3.14; // 这行会报错,因为 PI 是常量,不能被重新赋值

在实际开发中,建议优先使用 const

数据类型

JavaScript 是一种动态类型的语言,这意味着不需要在声明变量时指定其数据类型。在运行时,JavaScript 引擎会根据赋值给变量的值来自动确定其数据类型。

JavaScript 数据类型可分为:

  • 基本数据类型:

    • Number:用于表示数字,包括整数和浮点数。

    • String:用于表示文本或字符序列。

    • Boolean:用于表示逻辑值,即 truefalse

    • null:表示一个空值或“无”的值。

    • undefined:表示变量已被声明但未被赋值。

  • 引用数据类型:

    • Object:用于表示复杂的数据结构,如数组、函数等。如果只是数据的内容变化,地址不变,建议使用 const 声明。

数字类型

用于表示数值,包括整数和浮点数。JavaScript 使用 IEEE 754 双精度浮点数格式来表示数字,这意味着它可以表示非常大或非常小的数值,包括正数、负数、零以及特殊的值,如 Infinity(无穷大)、-Infinity(负无穷大)和 NaN(非数字)。

let num = 42;  
let floatNum = 3.14;
console.log(typeof num);

使用 typeof 检测数据类型。

JavaScript 提供了许多内置的运算符和方法来处理数字:

  • 算术运算符:用于执行基本的数学运算,如加法 +、减法 -、乘法 *、除法 /、取模 % 等。
let sum = 5 + 3; // 8  
let diff = 5 - 3; // 2  
let product = 5 * 3; // 15  
let quotient = 5 / 3; // 1.6666666666666667  
let remainder = 5 % 3; // 2
  • 比较运算符:用于比较数字值,如等于 ==、不等于 !=、大于 >、小于 <、大于等于 >=、小于等于 <= 等。
let isGreater = 5 > 3; // true  
let isEqual = 5 == 3; // false
  • 数学函数和常量:通过 Math 对象,可以访问许多数学函数和常量,如 Math.round()Math.floor()Math.ceil()Math.sqrt()Math.PI 等。
let rounded = Math.round(3.14); // 3  
let floored = Math.floor(3.14); // 3  
let ceiling = Math.ceil(3.14); // 4  
let sqrt = Math.sqrt(9); // 3
  • 类型转换:JavaScript 会自动在需要时进行类型转换,也可以使用 Number() 函数或一元加号 + 操作符来显式地将值转换为数字。
let numFromString = Number('123'); // 123  
let numFromBool = Number(true); // 1  
let numFromUnaryPlus = +'123'; // 123

JavaScript 中的数字类型有几个特殊值:

  • Infinity:表示正无穷大,当数值超过 JavaScript 能够表示的最大值时产生。
  • -Infinity:表示负无穷大,当数值小于 JavaScript 能够表示的最小值时产生。
  • NaN:表示非数字(Not a Number),当数学运算的结果不是一个数字时(如 0 / 0)产生。

在 JavaScript 中,NaN(Not a Number)是一个特殊的值,表示一个非数字的结果或值。它经常出现在数学运算的结果不是一个有效的数字时,或者当一个操作或函数期望得到一个数字但得到了非数字类型的值时。NaN 不等于任何值,包括它自身:

NaN === NaN; // false

为了检查一个值是否是 NaN,应该使用 isNaN() 函数或者 Number.isNaN() 方法(ES6 引入,更为严格,仅对数值类型的 NaN 有效)。

isNaN(NaN); // true  
Number.isNaN(NaN); // true  
Number.isNaN("NaN"); // false,因为 "NaN" 是一个字符串,不是数值类型的 NaN

算术运算涉及 NaN 通常返回 NaN

5 + NaN; // NaN  
'abc' - 1; // NaN,因为 'abc' 转换为数字是 NaN

当尝试将非数字字符串转换为数字时,结果通常是 NaN

Number('abc'); // NaN  
parseInt('abc', 10); // NaN  
parseFloat('abc'); // NaN

使用 toFixed() 将数字转换为字符串,并保留指定的小数位数。

const num = 123.456;  
const fixedNumStr = num.toFixed(2); // "123.46"

字符串类型

用于表示文本或字符序列。通过单引号,双引号或反引号包裹。

let str1 = 'Hello';  
let str2 = "World";  
let str3 = `Hello, ${str2}!`; // 使用模板字符串插入变量

ES6 引入了模板字符串,允许嵌入表达式,并且可以跨越多行,从而更易于编写字符串:

let name = 'Alice';  
let age = 30;  
let greeting = `Hello, my name is ${name} and I am ${age} years old.`;  
console.log(greeting); // 输出 'Hello, my name is Alice and I am 30 years old.'

模板字符串使用反引号而不是单引号或双引号,并且可以在 ${} 中嵌入变量或表达式。

JavaScript 提供了许多方法和属性来操作字符串:

使用 + 拼接字符串:

let greeting = 'Hello, ';  
let name = 'Alice';  
let message = greeting + name; // 'Hello, Alice'

使用 length 属性获取字符串长度:

let str = 'Hello';  
console.log(str.length); // 输出 5

获取字符:

let str = 'Hello';  
console.log(str[0]); // 输出 'H'

使用 indexOf 查找子字符串:

let str = 'Hello, world!';  
console.log(str.indexOf('world')); // 输出 7

使用 replace 替换子字符串:

let str = 'Hello, world!';  
let newStr = str.replace('world', 'everyone'); // 'Hello, everyone!'

使用 substring 提取子字符串,索引从 0 开始,不包含结束的索引号:

let str = 'Hello, world!';  
let subStr1 = str.substring(7, 12); // 'world'
let subStr2 = str.substring(7); // 'world!'

使用 split 分割字符串,返回数组:

let str = 'apple,banana,cherry';  
let fruits = str.split(','); // ['apple', 'banana', 'cherry']

转换大小写:

let str = 'Hello';  
console.log(str.toUpperCase()); // 输出 'HELLO'  
console.log(str.toLowerCase()); // 输出 'hello'

使用 trim() 去除字符串两端的空白字符:

let str = "   Hello, World!   ";  
let trimmedStr = str.trim();  
  
console.log(trimmedStr); // 输出 "Hello, World!"  
console.log(str); // 输出 "   Hello, World!   "(原字符串未改变)

使用 startsWith() 方法检测一个字符串是否以指定的子串开始。如果是以指定的子串开始,则返回 true;否则返回 false

const str = "Hello, World!";  
  
// 检测字符串是否以 "Hello" 开始  
const startsWithHello = str.startsWith("Hello");  
console.log(startsWithHello); // 输出 true  
  
// 检测字符串是否以 "World" 开始(返回 false)  
const startsWithWorld = str.startsWith("World");  
console.log(startsWithWorld); // 输出 false  
  
// 从位置 7 开始检测是否以 "World" 开始(返回 true)  
const startsWithWorldFromIndex7 = str.startsWith("World", 7);  
console.log(startsWithWorldFromIndex7); // 输出 true  
  
// 尝试从超出字符串长度的位置开始搜索(仍然返回 false)  
const startsWithWorldFromIndex20 = str.startsWith("World", 20);  
console.log(startsWithWorldFromIndex20); // 输出 false

使用 includes() 方法判断一个字符串是否包含另一个指定的子字符串(区分大小写),返回布尔值 truefalse。如果字符串包含指定的子字符串,则返回 true;否则返回 false

const str = "Hello, World!";  
  
// 检测字符串是否包含 "World"  
const includesWorld = str.includes("World");  
console.log(includesWorld); // 输出 true  
  
// 检测字符串是否包含 "hello"(注意大小写)  
const includesHello = str.includes("hello");  
console.log(includesHello); // 输出 false  
  
// 从位置 7 开始检测是否包含 "World"(实际上从位置 0 开始和从位置 7 开始结果相同,因为 "World" 就在那里)  
const includesWorldFromIndex7 = str.includes("World", 7);  
console.log(includesWorldFromIndex7); // 输出 true  
  
// 尝试从超出字符串长度的位置开始搜索  
const includesWorldFromIndex20 = str.includes("World", 20);  
console.log(includesWorldFromIndex20); // 输出 false

布尔类型

在 JavaScript 中,布尔(Boolean)类型用于表示逻辑值,即真(true)或假(false)。布尔值经常用于条件判断、循环控制以及逻辑运算。

布尔值可以通过字面量方式创建:

let isTrue = true; // 真  
let isFalse = false; // 假

JavaScript 在需要时会将非布尔值自动转换为布尔值。这种转换通常发生在条件语句(如 if 语句)中,或者在使用逻辑操作符时。转换规则如下:

  • 以下值会被转换为 false(称为假值):
    • false
    • 0(包括 +0-0
    • ""(空字符串)
    • null
    • undefined
    • NaN(非数字)
  • 其他所有值(包括所有对象)都会被转换为 true(称为真值)。

JavaScript 提供了 Boolean() 函数来显式地将一个值转换为布尔值:

let boolFromNumber = Boolean(0); // false  
let boolFromString = Boolean(""); // false  
let boolFromNull = Boolean(null); // false  
let boolFromUndefined = Boolean(undefined); // false  
let boolFromNaN = Boolean(NaN); // false  
let boolFromObject = Boolean({}); // true  
let boolFromTrue = Boolean(true); // true

布尔值经常与逻辑运算符一起使用,进行逻辑运算:

  • 逻辑与(AND)
let a = true;  
let b = false;  
console.log(a && b); // 输出 false
  • 逻辑或(OR)
let a = true;  
let b = false;  
console.log(a || b); // 输出 true
  • 逻辑非(NOT)
let a = true;  
console.log(!a); // 输出 false

null

在 JavaScript 中,null 是一个特殊的字面量值,表示一个空的值或没有对象。它通常用于表示一个对象变量不指向任何对象。与 undefined 不同,null 是 JavaScript 的一个关键字,并且是一个表示“无”或“空”的显式值。

null 通常用于以下情况:

  • 初始化对象变量:当声明一个对象变量,但在初始时不想让它指向任何对象时,可以将其设置为 null
let myObject = null;
  • 函数没有返回对象:如果一个函数预期返回一个对象,但在某些情况下不能返回任何对象,它可以返回 null
function getObjectOrNull() {  
  // 假设某些条件导致我们不能返回一个对象  
  return null;  
}
  • 清除对象引用:如果想断开一个变量与某个对象的连接,并释放该对象所占用的内存(虽然现代 JavaScript 引擎的垃圾收集器会自动处理这种情况),可以将该变量设置为 null
myObject = null; // 断开与myObject之前所引用对象的连接

undefined

在 JavaScript 中,undefined 是一个特殊的类型,它表示一个变量被声明了,但是没有赋值。换句话说,当声明了一个变量但没有给它任何值时,这个变量的值就是 undefined

let myVariable;  
console.log(myVariable); // 输出 undefined

如果一个函数没有明确的返回值(即没有 return 语句,或者 return 后面没有跟任何值),那么这个函数将隐式地返回 undefined

function noReturnValue() {  
  // 这个函数没有返回值  
}  
  
let result = noReturnValue();  
console.log(result); // 输出 undefined

在比较 undefinednull 时,使用严格相等运算符 === 和严格不等运算符 !== 是很重要的,因为 null == undefined 会返回 true(这是由于 JavaScript 的类型强制转换规则),但 null === undefined 会返回 false

console.log(null == undefined); // 输出 true  
console.log(null === undefined); // 输出 false
console.log(undefined + 1)  // 输出 NaN
console.log(null + 1)  // 输出 1

类型转换

在 JavaScript 中,类型转换(Type Coercion或Type Casting)是一个自动或显式的过程,它将值从一种类型转换为另一种类型。JavaScript 是一种动态类型的语言,这意味着在运行时,变量的类型可以自动改变。

类型转换可以分为:

  • 隐式转换
  • 显式转换

隐式类型转换是 JavaScript 引擎自动执行的过程,通常发生在运算符需要特定类型的操作数时。

  • 当加法运算符 + 用于一个字符串和一个数字时,数字会被转换为字符串,然后执行字符串连接。
let num = 5;  
let str = "10";  
let result = num + str; // "510",不是15
  • 一元加号运算符 + 可以用来将非数字值转换为数字。
let num = +"5"; // 5  
let notANum = +"hello"; // NaN
  • 其余算数运算符,如 -* 等会把数据转成数字类型,会将空字符串 '' 转换为 0。
let num = 20 - '5';  // 15
let num2 = 20 - '';  // 20
  • 当使用比较运算符(如 ==)比较不同类型的值时,JavaScript 会尝试将值转换为同一类型,然后再进行比较。
let a = "5";  
let b = 5;  
console.log(a == b); // 输出 true,因为字符串 "5" 被转换为数字 5
  • null 经过数字转换之后变为 0。
console.log(null + 3);  // 输出 3
  • undefined 经过数字转换之后变为 NaN
console.log(undefined + 3); // 输出 NaN

显式类型转换是程序员明确指示 JavaScript 引擎将值转换为特定类型的过程。这通常通过特定的函数或方法来完成。

  • 使用 Number() 函数用于将值转换为数字。如果转换失败,Number() 会返回 NaN(不是一个数字)。
let numStr = "123";  
let num = Number(numStr); // 123  
console.log(typeof num); // "number"
  • 使用 parseInt() 函数将值转换为整数。
console.log(parseInt('12px')); // 12
  • 使用 parseFloat() 函数将值转换为浮点数。
console.log(parseFloat('3.14px')); //3.14

运算符

算术运算符:

  • + 加法
  • - 减法
  • * 乘法
  • / 除法
  • % 取模(余数)
  • ++ 递增
  • -- 递减
  • += 加等于
  • -= 减等于
  • *= 乘等于
  • /= 除等于
  • %= 取模等于

比较运算符:

  • == 等于
  • === 严格等于(值和类型都相同)
  • != 不等于
  • !== 严格不等于(值或类型不同)
  • > 大于
  • < 小于
  • >= 大于等于
  • <= 小于等于

逻辑运算符:

  • && 逻辑与(AND)
  • || 逻辑或(OR)
  • ! 逻辑非(NOT)

赋值运算符:

  • = 赋值
  • += 加等于
  • -= 减等于
  • *= 乘等于
  • /= 除等于
  • %= 取模等于

三元运算符:

  • condition ? exprIfTrue : exprIfFalse

这个运算符根据条件(condition)的结果来返回两个表达式之一。如果条件为真,则返回 exprIfTrue,否则返回 exprIfFalse

类型运算符:

  • typeof 返回一个变量的数据类型
  • instanceof 用来测试一个对象是否是其父类对象的实例

字符串运算符:

  • + 连接两个字符串

指数运算符:

  • ** 计算底数的指数幂

可选链运算符:

  • ?.:允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 运算符的功能类似于 . 链式运算符,不同之处在于,在引用为空 (nullish)(null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined
let a = 5;  
let b = 10;  
  
// 算术运算符  
let sum = a + b; // 15  
let difference = a - b; // -5  
let product = a * b; // 50  
let quotient = b / a; // 2  
let remainder = b % a; // 0  
  
// 比较运算符  
let isEqual = (a == b); // false  
let isStrictlyEqual = (a === b); // false  
let isNotEqual = (a != b); // true  
let isStrictlyNotEqual = (a !== b); // true  
  
// 逻辑运算符  
let isBothTrue = (a > 0 && b > 0); // true  
let isEitherTrue = (a < 0 || b > 0); // true  
let isNotTrue = !(a > b); // true  
  
// 赋值运算符  
a += 5; // a 现在是 10  
  
// 条件运算符  
let result = (a > b) ? "a is greater" : "b is greater or equal"; // "b is greater or equal"  
  
// 类型运算符  
let type = typeof a; // "number"  
  
// 字符串运算符  
let str = "Hello, " + "world!"; // "Hello, world!"  
  
// 指数运算符  
let power = 2 ** 3; // 8

// 可选链运算符  
const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};

const dogName = adventurer.dog?.name;
console.log(dogName);
// 输出: undefined

console.log(adventurer.someNonExistentMethod?.());
// 输出: undefined

逻辑短路

在 JavaScript 中,逻辑短路运算(Logical Short-Circuiting)是一种特性,它允许逻辑表达式在某些条件下提前确定其结果,从而避免不必要的计算或函数调用。这主要发生在逻辑与(&&)和逻辑或(||)运算中。

  • 逻辑与(&&)短路

对于逻辑与运算,如果左侧的表达式结果为 false,那么整个表达式的结果就已经确定为 false,因此不会计算右侧的表达式。这种特性被称为逻辑与的短路行为。

let age = 18;
console.log(false && age++);  // 输出 false
console.log(age);  // 输出 18

let x = 11 && 22;
console.log(x);  // 输出 22

console.log(false && 20);  // 输出 false
console.log(5 < 3 && 20);  // 输出 false
console.log(undefined && 20);  // 输出 undefined
console.log(null && 20);  // 输出 null
console.log(0 && 20);  // 输出 0
console.log(10 && 20);  // 输出 20
  • 逻辑或(||)短路

对于逻辑或运算,如果左侧的表达式结果为 true,那么整个表达式的结果就已经确定为 true,因此不会计算右侧的表达式。这种特性被称为逻辑或的短路行为。

let age = 18;
console.log(11 || age++);  // 输出 11
console.log(age);  // 输出 18

let x = 11 || 22;
console.log(x);  // 输出 11

let y = undefined || 0;
console.log(y);  // 输出 0

console.log(false || 20);  // 输出 20
console.log(5 < 3 || 20);  // 输出 20
console.log(undefined || 20);  // 输出 20
console.log(null || 20);  // 输出 20
console.log(0 || 20);  // 输出 20
console.log(10 || 20);  // 输出 10

流程控制

if

在 JavaScript 中,if 语句用于基于某个条件执行代码块。如果条件为 true,则执行 if 语句块中的代码;如果条件为 false,则不执行。

基本语法:

if (condition) {  
  // code to be executed if the condition is true  
}

其中,condition 是一个返回布尔值(truefalse)的表达式。

例如:

let age = 20;  
  
if (age >= 18) {  
  console.log("You are an adult.");  
}

在上面的例子中,如果 age 变量的值大于或等于18,那么将输出 "You are an adult."。

除了基本的 if 语句,还可以使用 else 子句来指定当条件为 false 时要执行的代码:

let age = 16;  
  
if (age >= 18) {  
  console.log("You are an adult.");  
} else {  
  console.log("You are not an adult.");  
}

在这个例子中,如果 age 变量的值小于18,那么将输出 "You are not an adult."。

此外,还可以使用 else if 子句来处理多个条件:

let age = 13;  
  
if (age < 0) {  
  console.log("Invalid age.");  
} else if (age < 18) {  
  console.log("You are a minor.");  
} else {  
  console.log("You are an adult.");  
}

在这个例子中,根据 age 变量的值,将输出不同的消息。

switch

在 JavaScript 中,switch 语句用于基于不同的情况执行不同的代码块。它允许根据一个表达式的值来匹配多个可能的 case,并执行与该 case 相关联的代码。

基本语法:

switch (expression) {  
  case value1:  
    // code to be executed if expression matches value1  
    break;  
  case value2:  
    // code to be executed if expression matches value2  
    break;  
  // ...  
  default:  
    // code to be executed if no match is found  
    break;  
}
  • expression 是需要被求值的表达式。
  • case 后跟的可能匹配值。
  • 如果 expression 的值与某个 case 后面的值匹配(===),那么会执行该 case 下的代码,直到遇到 break 语句或者 switch 语句结束。
  • break 语句用于终止 switch 语句,避免执行多个 case 代码块。如果某个 case 后面没有 break,那么程序会继续执行下一个 case 的代码,直到遇到 break 或者 switch 语句结束,这被称为“贯穿”(fall-through)。
  • default 关键字用于指定当 expression 的值没有与任何 case 匹配时要执行的代码。它是可选的。

例如:

let day = "Monday";  
  
switch (day) {  
  case "Monday":  
    console.log("Today is Monday.");  
    break;  
  case "Tuesday":  
    console.log("Today is Tuesday.");  
    break;  
  case "Wednesday":  
    console.log("Today is Wednesday.");  
    break;  
  default:  
    console.log("Today is some other day.");  
    break;  
}

在这个例子中,根据变量 day 的值,会输出相应的消息。如果 day 的值是 "Monday",那么会输出 "Today is Monday."。如果 day 的值不是任何 case 匹配的值,那么会执行 default 代码块,输出 "Today is some other day."。

while

在 JavaScript 中,while 循环用于重复执行一段代码块,直到指定的条件不再满足为止。只要条件为 true,循环就会继续执行;一旦条件变为 false,循环就会停止。

基本语法:

while (condition) {  
  // code to be executed while the condition is true  
}

其中,condition 是一个表达式,它在每次循环迭代之前都会进行评估。如果 condition 的值为 true,则执行循环体中的代码块;如果为 false,则退出循环。

例如:

let i = 1;  
  
while (i <= 5) {  
  console.log(i);  
  i++; // Increment i to move to the next iteration  
}

在这个例子中,i 被初始化为1。循环的条件是 i <= 5,这意味着只要 i 的值小于或等于 5,循环就会继续执行。在每次循环迭代中,都会打印出 i 的当前值,并将 i 递增 1,以便在下一次迭代中处理下一个数字。当 i 的值增加到 6 时,条件 i <= 5 不再满足,循环停止。

需要注意的是,如果没有正确地更新循环条件中的变量(在这个例子中是 i++),那么循环可能会变成无限循环,这可能会导致浏览器挂起或者程序崩溃。因此,在编写 while 循环时,确保有一个明确的退出条件是非常重要的。

for

在 JavaScript 中,for 语句用于创建一个循环,该循环会重复执行一段代码块,直到指定的条件不再满足。for 循环比 while 循环更加灵活,因为它允许你在循环开始前初始化变量,以及在每次迭代后更新变量。

基本语法:

for (initialization; condition; final expression) {  
  // code to be executed  
}
  • initialization:这是一个表达式(或变量声明),通常用于在循环开始前设置计数器的初始值。
  • condition:这是一个在每次循环迭代前都会评估的条件表达式。如果条件为 true,则执行循环体中的代码块;如果为 false,则退出循环。
  • final expression:在每次循环迭代结束时执行的表达式,通常用于更新或递增计数器。

例如:

for (let i = 1; i <= 5; i++) {  
  console.log(i);  
}

在这个例子中,i 被初始化为1。循环的条件是 i <= 5,这意味着只要 i 的值小于或等于 5,循环就会继续执行。在每次迭代中,都会打印出 i 的当前值,并通过 i++ 递增 i 的值。当 i 增加到 6 时,条件不再满足,循环结束。

注意,在 JavaScript 中,for 循环的初始化部分(initialization)经常用于声明循环控制变量,例如 let i = 1。这确保了 i 只在循环内部可见,这有助于避免在循环外部意外地修改或访问该变量。

除了基本的 for 循环,JavaScript还提供了其他几种循环结构,如 for...in 循环(用于遍历对象的属性)和 for...of 循环(用于遍历可迭代对象,如数组或字符串)。这些循环结构提供了更灵活的方式来处理不同的数据集合。

在 JavaScript 中,for 循环可以嵌套使用,这意味着一个 for 循环可以包含另一个或多个 for 循环。嵌套循环通常用于处理二维数组或需要多次迭代的复杂逻辑。

以下是一个简单的 for 循环嵌套示例,用于打印一个 5x5 的星号(*)方阵:

for (let i = 0; i < 5; i++) { // 外层循环控制行数  
  for (let j = 0; j < 5; j++) { // 内层循环控制列数  
    document.write('*'); // 打印星号  
  }  
  document.write('<br>'); // 每打印完一行后换行  
}

在这个例子中,外层循环(由变量 i 控制)负责迭代 5 次,代表 5 行。对于每一行,内层循环(由变量 j 控制)也会迭代 5 次,代表 5 列。这样,总共会打印出 25 个星号,并且每 5 个星号之后会换行,从而形成一个 5x5 的方阵。

嵌套循环也可以用于更复杂的任务,比如遍历二维数组或执行基于多维数据的计算。以下是一个遍历二维数组的示例:

let matrix = [  
  [1, 2, 3],  
  [4, 5, 6],  
  [7, 8, 9]  
];  
  
for (let i = 0; i < matrix.length; i++) { // 外层循环遍历行  
  for (let j = 0; j < matrix[i].length; j++) { // 内层循环遍历列  
    console.log(matrix[i][j]); // 打印当前元素的值  
  }  
}

在这个例子中,matrix 是一个包含三个数组的二维数组(即一个 3x3 的矩阵)。外层循环遍历每一行,内层循环遍历每一列,并打印出每个元素的值。

continue

在 JavaScript 中,continue 语句用于跳过当前循环迭代中剩余的代码,并立即开始下一次迭代。如果 continue 语句被执行,那么循环体中 continue 语句后面的代码将不会被执行。这通常用于在满足特定条件时跳过某些迭代。

continue 语句只能在循环体内部使用,包括 for 循环和 while 循环。

以下是 continue 语句在 for 循环中的使用示例:

for (let i = 0; i < 10; i++) {  
  if (i === 5) {  
    continue; // 当i等于5时,跳过当前迭代剩余的代码  
  }  
  console.log(i); // 这行代码不会在i等于5时执行  
}

在这个例子中,当 i 的值等于 5 时,continue 语句会被执行,因此不会打印数字 5,而是直接开始下一次迭代。

continue 语句也可以在 while 循环中使用,如下所示:

let i = 0;  
while (i < 10) {  
  i++;  
  if (i === 5) {  
    continue; // 当i等于5时,跳过当前迭代剩余代码  
  }  
  console.log(i); // 这行代码不会在i等于5时执行  
}

这个 while 循环的例子和前面的 for 循环例子效果相同:当 i 等于 5 时,跳过打印操作,继续下一次循环。

break

在 JavaScript 中,除了使用 break 语句终止 switch 语句执行外,还可以用于立即终止当前循环的执行。当 break 语句被执行时,无论循环条件是否满足,循环都将立即停止,并且控制流将转移到循环之后的第一个语句。这常常用于在满足特定条件时提前退出循环。

以下是 break 语句在 for 循环中的使用示例:

for (let i = 0; i < 10; i++) {  
  if (i === 5) {  
    break; // 当i等于5时,退出循环  
  }  
  console.log(i); // 这行代码在i等于5时不会被执行  
}

在这个例子中,当 i 的值等于 5 时,break 语句会被执行,循环将立即终止,不会打印 5 及之后数字。

break 语句也可以在 while 循环中使用,如下所示:

let i = 0;  
while (i < 10) {  
  i++;  
  if (i === 5) {  
    break; // 当i等于5时,退出循环  
  }  
  console.log(i); // 这行代码在i等于5时不会被执行  
}

这个 while 循环的例子和前面的 for 循环例子效果相同:当 i 等于 5 时,循环终止。

数组

JavaScript 中的数组是一种特殊的对象,用于在单个变量中存储多个值。数组可以包含任意类型的元素,包括数字、字符串、对象,甚至是其他数组(即嵌套数组)。数组的长度是动态的,可以随着元素的添加或删除而改变。

语法:let 数组名 = [数据1, 数据2, 数据3, ....]

let array1 = []; // 空数组  
let array2 = [1, 2, 3, 4, 5]; // 包含数字的数组  
let array3 = ['apple', 'banana', 'cherry']; // 包含字符串的数组  
let array4 = [true, false, true]; // 包含布尔值的数组  
let array5 = [array2, array3]; // 包含其他数组的数组(嵌套数组)

可以通过索引来访问数组中的元素。在 JavaScript 中,数组索引是从 0 开始的。

let fruits = ['apple', 'banana', 'cherry'];  
console.log(fruits[0]); // 输出 'apple'  
console.log(fruits[1]); // 输出 'banana'  
console.log(fruits[2]); // 输出 'cherry'

使用 length 属性来获取或设置数组的长度。

let numbers = [1, 2, 3, 4, 5];  
console.log(numbers.length); // 输出 5  
numbers.length = 3; // 设置数组长度为 3,这会删除索引为 3 和 4 的元素  
console.log(numbers); // 输出 [1, 2, 3]

使用 for 循环遍历数组:

let array = [1, 2, 3, 4, 5];  
for (let i = 0; i < array.length; i++) {  
  console.log(array[i]);  
}
let array = [1, 2, 3, 4, 5];   
for (let element of array) {  
  console.log(element);  
}

查找数组最大值:

let arr = [5, 4, 6, 3, 7];  
let max = arr[0]
for (let i = 1; i < arr.length; i++) {  
    if (max < arr[i]) {
      max = arr[i]
    }
}
console.log(max)

查找数组最小值:

let arr = [5, 4, 6, 3, 7];  
let min = arr[0]
for (let i = 1; i < arr.length; i++) {  
    if (min > arr[i]) {
      min = arr[i]
    }
}
console.log(min)

使用 push() 在数组末尾添加一个或多个元素,并返回新的长度:

let arr = [5, 4, 6, 3, 7];  
let length = arr.push(2,8);
console.log(arr);  // 输出 [5, 4, 6, 3, 7, 2, 8]
console.log(length);  // 输出 7

使用 unshift() 在数组开头添加一个或多个元素,并返回新的长度:

let arr = [5, 4, 6, 3, 7];  
let length = arr.unshift(2,8);
console.log(arr);  // 输出 [2, 8, 5, 4, 6, 3, 7]
console.log(length);  // 输出 7

过滤数组:

let arr = [5, 4, 6, 3, 7];  
let newArr = [];
for (let i = 0; i < arr.length; i++) {
  if (arr[i] >= 5) {
    newArr.push(arr[i]);
  }
}
console.log(newArr);  // 输出 [5, 6, 7]

使用 pop() 删除并返回数组的最后一个元素:

let arr = [5, 4, 6, 3, 7];  
let res = arr.pop();
console.log(arr);  // 输出 [5, 4, 6, 3]
console.log(res);  // 输出 7

使用 shift() 删除并返回数组的第一个元素:

let arr = [5, 4, 6, 3, 7];  
let res = arr.shift();
console.log(arr);  // 输出 [4, 6, 3, 7]
console.log(res);  // 输出 5

使用 splice(start, deleteCount, item1, item2, ...) 在指定位置给数组添加/删除指定数量元素,并返回被删除的元素。该方法会直接修改原数组。

参数说明:

  • start(必需):从该位置开始修改数组。如果是负数,则从数组尾部开始计数(例如,-1 指最后一个元素,-2 指倒数第二个元素,依此类推)。
  • deleteCount(可选):整数,表示要移除的数组元素的个数。如果设置为 0 或负数,则不会移除元素。这种情况下,至少要添加一个新元素。
  • item1, item2, ...(可选):要添加到数组中的新元素,从 start 位置开始。如果不指定,则 splice() 只删除数组元素。

splice() 方法返回一个由被删除的元素组成的数组。如果只删除了元素而没有添加新元素,则返回仅包含被删除元素的数组;如果添加了新元素,则返回空数组。

let arr = [5, 4, 6, 3, 7];  

// 删除元素
let removed = arr.splice(1, 2); // 从索引 1 开始,删除 2 个元素 
console.log(arr);  // 输出 [5, 3, 7],原数组已改变 
console.log(removed);  // 输出 [4, 6],被删除的元素

// 添加元素  
arr.splice(1, 0, 'a', 'b'); // 从索引 1 开始,不删除元素,添加 'a' 和 'b'  
console.log(arr); // 输出 [5, 'a', 'b', 3, 7]

使用 map() 对数组中的每个元素执行一个由用户提供的函数,并返回一个新数组,新数组中的元素是原数组元素经过函数处理后的结果。原数组不会被改变。

语法:

array.map(function(currentValue, index, arr), thisArg)

参数说明:

  • function(currentValue, index, arr):必需。数组中每个元素要执行的函数。
    • currentValue:必需。当前正在处理的元素。
    • index:可选。当前正在处理的元素的索引。
    • arr:可选。map() 方法被调用的数组。
  • thisArg:可选。执行回调函数时用作 this 的值。
const numbers = [1, 4, 9];  
  
// 使用 map() 计算每个数字的平方根  
const squareRoots = numbers.map(function(num) {  
  return Math.sqrt(num);  
});  
  
console.log(squareRoots); // 输出: [1, 2, 3]

在这个例子中,map() 方法遍历了 numbers 数组中的每个元素,并对每个元素调用了 Math.sqrt() 函数来计算平方根。然后,它将每个计算得到的平方根收集到一个新的数组 squareRoots 中。

使用 join() 将数组(或数组的一个子集)的所有元素连接到一个字符串中。元素是通过指定的分隔符进行分隔的。如果没有提供分隔符,数组元素会以逗号(,)分隔。原数组不会被改变。

语法:

array.join([separator]);

参数说明:

  • separator:可选。指定一个字符串来分隔数组的每个元素。如果省略该参数,数组元素会用逗号(,)分隔。如果 separator 是空字符串(""),所有元素会连在一起形成一个没有空格或其他字符的字符串。
const fruits = ['apple', 'banana', 'cherry'];  
  
const fruitString = fruits.join();        // 使用默认分隔符 ','  
console.log(fruitString); // 输出: "apple,banana,cherry"  
  
const fruitStringWithSpace = fruits.join(' '); // 使用空格作为分隔符  
console.log(fruitStringWithSpace); // 输出: "apple banana cherry"  
  
const fruitStringWithDash = fruits.join('-'); // 使用 '-' 作为分隔符  
console.log(fruitStringWithDash); // 输出: "apple-banana-cherry"  
  
const emptyString = fruits.join(''); // 使用空字符串作为分隔符  
console.log(emptyString); // 输出: "applebananacherry"

函数

在 JavaScript 中,函数是一组可以重复使用的代码块,它们执行特定的任务。可以定义函数来封装一段代码,并在需要时多次调用它。函数可以接受参数(输入),执行一些操作,并可能返回结果(输出)。

函数声明

语法:

function myFunction() {  
    // 函数体,执行一些操作  
    console.log('function');  
}

函数命名规范:

  • 和变量命名基本一致
  • 使用小驼峰命名法
  • 前缀应该是动词,例如 getset 等。

函数调用

调用函数就是执行函数体中的代码。调用函数时,需要使用函数名,后面跟着一对圆括号。

myFunction(); // 输出 function

函数参数

函数参数是传递给函数的值。在函数定义时,可以指定参数的名称,在调用函数时提供这些参数的值。

function greet(name) {  
  console.log('Hello, ' + name);  
}  
  
greet('Alice'); // 输出 "Hello, Alice"
  • 形参:声明函数时写在函数名右边小括号里的参数。
  • 实参:调用函数时写在函数名右边小括号里的参数。
  • 形参可以理解为在这个函数内声明的局部变量,实参可以理解为给这个变量赋值。
  • 开发者尽量保持形参和实参的个数一致。实参多余形参,多余的实参不参与运算。

默认参数

ES6 引入了默认参数的概念,允许在函数定义时为参数指定默认值。如果调用函数时没有提供某个参数的值,就会使用默认值。

function greet(name = 'Guest') {  
  console.log('Hello, ' + name);  
}  
  
greet(); // 输出 "Hello, Guest"  
greet('Alice'); // 输出 "Hello, Alice"

动态参数

在函数体内,arguments 对象是一个伪数组,表示传递给函数的参数。即使函数在声明时没有指定任何参数,也可以在函数体内通过 arguments 对象来访问。

function dynamicArguments() {  
  for (var i = 0; i < arguments.length; i++) {  
      console.log(arguments[i]);  
  }  
}  

dynamicArguments(1, 2, 3); // 输出:1 2 3

剩余参数

剩余参数(rest parameters)允许将一个不定数量的参数作为数组传入函数。剩余参数使用三个点(...)语法,后面跟着一个参数名,通常是一个数组。在函数体内,这个参数名将引用一个包含所有剩余参数的数组。

function sum(...numbers) {  
  let total = 0;  
  for (let num of numbers) {  
    total += num;  
  }  
  return total;  
}  
  
console.log(sum(1, 2, 3)); // 输出 6  
console.log(sum(1, 2, 3, 4, 5)); // 输出 15

在这个例子中,sum 函数使用剩余参数 numbers 来接收任意数量的参数。在函数体内,可以像操作普通数组一样操作 numbers

剩余参数也可以与其他常规参数一起使用,但剩余参数必须放在参数列表的最后面:

function introduce(name, ...hobbies) {  
  console.log(`My name is ${name}.`);  
  for (const hobby of hobbies) {
    console.log(`One of my hobbies is ${hobby}.`);
  }
}  
  
introduce('Alice', 'reading', 'painting', 'swimming');  
// 输出:  
// My name is Alice.  
// One of my hobbies is reading.  
// One of my hobbies is painting.  
// One of my hobbies is swimming.

在这个例子中,introduce 函数接受一个名为 name 的常规参数和一个名为 hobbies 的剩余参数。当我调用 introduce 函数时,第一个参数被赋给 name,而所有后续的参数都被收集到 hobbies 数组中。

建议使用剩余参数。

函数返回值

在 JavaScript 中,函数可以通过 return 语句返回一个值。当函数执行到 return 语句时,它会立即停止执行并返回指定的值。如果没有指定返回值,或者函数不包含 return 语句,则函数默认返回 undefined

function add(a, b) {  
  return a + b;  
}  

console.log(add(3, 4)); // 输出: 7

在这个例子中,定义了一个名为 add 的函数,它接受两个参数 ab,并返回它们的和。

函数作用域

在 JavaScript 中,函数作用域指的是变量和函数在代码中的可见性和生命周期。了解作用域对于编写可维护和可预测的 JavaScript 代码至关重要。JavaScript 有两种主要的作用域:全局作用域和局部(或函数)作用域。

  • 全局作用域

在代码中的任何位置都能访问到的变量具有全局作用域。这些变量通常是在函数外部声明的,全局作用域中的变量(全局变量)在整个脚本或应用程序中都是可见的。

let globalVariable = "I'm global!";  
  
function demoFunction() {  
    console.log(globalVariable); // 可以访问全局变量
}  
  
demoFunction(); // 输出: I'm global!
  • 局部作用域

在函数内部声明的变量具有局部作用域。这些变量只在声明它们的函数内部可见。当函数执行完毕后,这些局部变量通常会被垃圾回收机制清除(除非它们被闭包引用)。

function demoFunction() {  
  let localVariable = "I'm local!";  
  console.log(localVariable); // 可以访问局部变量  
}  

demoFunction(); // 输出: I'm local!  
console.log(localVariable); // 报错:ReferenceError: localVariable is not defined

匿名函数

除了具名函数外,还可以创建匿名函数。这些函数没有名字,但可以作为值赋给变量,或者作为参数传递给其他函数。包括:

  • 函数表达式:将一个函数赋值给一个变量,或者作为参数传递给其他函数,甚至可以作为另一个函数的返回值。
let functionName = function(parameters) {  
    // 函数体  
    // 执行一些操作  
};

这里的 functionName 是一个变量名,它引用了后面定义的匿名函数。这个匿名函数具有一组参数 parameters 和一个函数体,用于执行特定的操作。

  • 立即执行函数(Immediately Invoked Function Expression,简称 IIFE):在创建后立即被调用。常用于创建独立的作用域,避免变量污染全局命名空间,以及封装一些只需执行一次的初始化代码。
(function() {  
    // 函数体  
    // 执行一些初始化操作  
})();

(function() {  
  // 函数体  
  // 执行一些初始化操作  
}());

!function () { /* ... */ }();
~function () { /* ... */ }();
-function () { /* ... */ }();
+function () { /* ... */ }();
void function () { /* ... */ }();

注意,这里的函数定义被包裹在一对圆括号 () 中,这是为了将函数定义转换为函数表达式,而不是函数声明。紧接着的另一对圆括号 () 用于立即调用这个函数。

(function() {  
  let privateVariable = 'Hello, world!';  
  console.log(privateVariable); // 输出: Hello, world!  
})();  

// 在这里无法访问 privateVariable,因为它在 IIFE 的作用域内  
console.log(privateVariable); // 报错:ReferenceError: privateVariable is not defined

在这个例子中,privateVariable 是在立即执行函数内部定义的,因此它只在该函数的作用域内可见。当函数执行完毕后,privateVariable 就会被垃圾回收机制清除,不会在全局作用域中留下任何痕迹。

也可以向立即执行函数传递参数:

(function(greeting) {  
  console.log(greeting);  
})('Hello, universe!'); // 输出: Hello, universe!

对象

在 JavaScript 中,对象是一组无序的属性集合,其中每个属性都有一个名称(键)和一个值。这些值可以是任何数据类型,包括方法和其他对象。对象提供了一种方式来存储和组织多个值作为一个单一的实体。

创建对象

对象字面量方式,直接使用花括号 {} 创建对象,语法:

let 对象名 = {  
  属性名: 属性值,  
  方法名: 函数
};

其中:

  • 属性都是成对出现的,包括属性名和属性值,使用 : 分隔。
  • 多个属性使用 , 分隔。
  • 方法是由方法名和函数两部分构成,使用 : 分隔。

例如:

let person = {  
  name: 'Stone',  
  age: 30,  
  greet: function() {  
    console.log(`Hello, my name is ${this.name}`);  
  }  
};

还可以使用构造函数open in new windownew Object() 方式创建对象。

操作对象

使用 对象名.属性名 或者 对象名['属性名'] 获取属性值。

console.log(person.name);  // 输出 Stone
console.log(person['age']);  // 输出 30

使用 对象名.属性名 = 新值 为属性设置新值。

person.name = 'stone';

使用 对象名.属性名 = 值 为对象增加属性。

person.gender = 'male';
console.log(person);  // {name: 'stone', age: 30, gender: 'male', greet: ƒ}

使用 delete 对象名.属性名 删除属性。

delete person.gender;
console.log(person); // {name: 'stone', age: 30, greet: ƒ}

使用 对象名.方法名() 调用对象中的方法。

person.greet()  // Hello, my name is stone

使用 for in 遍历对象。

for (const key in person) {
  console.log(key, person[key])
}

内置对象

Math

JavaScript 的 Math 对象是一个内置对象,提供了一系列数学常数和函数,用于执行常见的数学运算。

Math 对象包含一些表示数学常数的属性,例如:

  • Math.E:自然对数的底数(约等于 2.71828)。
  • Math.PI:圆周率(约等于 3.14159)。
  • Math.SQRT2:2 的平方根(约等于 1.41421)。
  • Math.LN2:2 的自然对数(约等于 0.69315)。
  • Math.LN10:10 的自然对数(约等于 2.30259)。
  • Math.LOG2E:以 2 为底的自然对数的值(约等于 1.44269)。
  • Math.LOG10E:以 10 为底的自然对数的值(约等于 0.43429)。

Math 对象包含一系列用于执行数学运算的方法,例如:

  • Math.abs(x):返回 x 的绝对值。
  • Math.ceil(x):返回大于或等于 x 的最小整数(向上取整)。
  • Math.floor(x):返回小于或等于 x 的最大整数(向下取整)。
  • Math.round(x):返回 x 四舍五入后的最接近的整数。
  • Math.max([value1[, value2[, ...]]]):返回参数列表中的最大值。
  • Math.min([value1[, value2[, ...]]]):返回参数列表中的最小值。
  • Math.pow(x, y):返回 x 的 y 次幂。
  • Math.sqrt(x):返回 x 的平方根。
  • Math.random():返回 0(包含)到 1(不包含)之间的伪随机数。
  • Math.sin(x)Math.cos(x)Math.tan(x):分别返回 x(以弧度为单位)的正弦、余弦和正切值。
  • Math.asin(x)Math.acos(x)Math.atan(x)Math.atan2(y, x):分别返回 x 的反正弦、反余弦、反正切和反正切 2 值。
  • Math.exp(x):返回 e 的 x 次幂,其中 e 是自然对数的底数。
  • Math.log(x)Math.log10(x)Math.log2(x):分别返回 x 的自然对数、以 10 为底的对数和以 2 为底的对数。
// 使用数学常数  
let piValue = Math.PI;  
console.log(piValue); // 输出:3.141592653589793  
  
// 使用数学函数  
let absValue = Math.abs(-10);  
console.log(absValue); // 输出:10  
  
let roundedValue = Math.round(10.75);  
console.log(roundedValue); // 输出:11  
  
let maxValue = Math.max(1, 2, 3, 4, 5);  
console.log(maxValue); // 输出:5  
  
let randomValue = Math.random();  
console.log(randomValue); // 输出:一个 0 到 1 之间的随机数  
  
let sinValue = Math.sin(Math.PI / 2);  
console.log(sinValue); // 输出:1,因为 sin(π/2) = 1

生成随机颜色的函数:

function generateRandomColor() {  
  let letters = '0123456789ABCDEF';  
  let color = '#';  
  for (let i = 0; i < 6; i++) {  
      color += letters[Math.floor(Math.random() * 16)];  
  }  
  return color;
}  

console.log(generateRandomColor()); // 输出一个随机颜色,例如:#FF34B8

DOM

DOM(Document Object Model,文档对象模型)是一个编程接口,它允许程序和脚本能够动态地访问和更新文档的内容、结构和样式。DOM 将文档(如 HTML 或 XML)解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合,这个结构集合被称为 DOM 树或 DOM 文档树。

DOM 将文档视为节点树。每个节点(HTML 标签)都是一个对象,并且每个对象都有属性和方法。主要的节点类型包括:

  • Document:整个文档树的根节点。
  • Element:HTML 或 XML 元素,比如 <div><p><a>
  • Attribute:元素的属性,比如 href<a> 元素中。
  • Text:文本节点,包含元素或属性中的文本内容。
  • Comment:注释节点。

使用 JavaScript 来执行各种 DOM 操作,例如:

  • 查询:使用 document.querySelector()document.getElementById() 等方法查询 DOM 中的元素。
  • 创建:使用 document.createElement() 创建新的元素节点。
  • 添加:使用 appendChild()insertBefore() 将新节点添加到 DOM 树中。
  • 删除:使用 removeChild() 从 DOM 树中移除节点。
  • 修改:修改节点的属性、文本内容或样式。
  • 遍历:使用诸如 parentNodechildNodesfirstChildlastChild 等属性遍历 DOM 树。

获取 DOM 元素

通常根据 CSS 选择器来获取 DOM 元素。

获取匹配的第一个元素语法:

document.querySelector('CSS 选择器')

例如:

<body>
  <div class="box"></div>
  <script>
    // const box = document.querySelector('div')
    const box = document.querySelector('.box')
    console.log(box) // 输出 <div class="box"></div>
  </script>
</body>

获取匹配的所有元素语法:

document.querySelectorAll('CSS 选择器')

例如:

<body>
  <div class="box1"></div>
  <div class="box2"></div>
  <script>
    const div = document.querySelectorAll('div')
    console.log(div)  // 输出 NodeList(2) [div.box1, div.box2]
  </script>
</body>

得到的是一个伪数组:

  • 有长度有索引的数组
  • 但是没有 pop()push() 方法
  • 可以使用 for 循环遍历获取里面的每一个对象

例如:

<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
  <script>
    const lis = document.querySelectorAll('ul li')
    for (let i = 0; i < lis.length; i++) {
      console.log(lis[i])
    }
  </script>
</body>

操作元素内容

获取和修改标签元素里面的内容,可以使用 DOM 元素对象的 innerTextinnerHTML 属性。

使用 innerText 修改标签里面的内容,不解析标签。

<body>
  <div class="box">stonecoding</div>
  <script>
    const box = document.querySelector('.box')
    console.log(box.innerText)
    box.innerText = 'stonecoding.net'
  </script>
</body>

使用 innerHTML 修改标签里面的内容,会解析标签,建议使用模板字符串。

<body>
  <div class="box">stonecoding</div>
  <script>
    const box = document.querySelector('.box')
    console.log(box.innerHTML)
    box.innerHTML = '<strong>stonecoding.net</strong>'
  </script>
</body>

操作元素属性

普通属性

可以通过 JavaScript 设置和修改标签元素属性,包括 hreftitlesrc 等。

<body>
  <img src="./images/dog.jpg" alt="">
  <script>
    const img = document.querySelector('img')
    img.src = './images/cat.jpg'
    img.title = 'cat'
  </script>
</body>

样式属性

可以通过 JavaScript 设置和修改标签元素的样式属性,包括:

  • 通过 style 属性操作 CSS,语法:元素.style.样式属性 = '值',如果样式属性中有连字符,需要使用小驼峰书写。
<body>
  <div></div>
  <script>
    const div = document.querySelector('div')
    div.style.width = '200px'
    div.style.height = '200px'
    div.style.backgroundColor = 'skyblue'
  </script>
</body>
  • 通过 className 操作 CSS,语法:元素.className = 'CSS 类名',将 CSS 中的某个类赋予给元素对象,覆盖原 CSS 类名。如果是添加,则需要保留原来的类名。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      width: 200px;
      height: 200px;
      background-color: skyblue;
      border: 1px solid black;
      margin: 0 center;
      padding: 10px;
    }
  </style>
</head>
<body>
  <div></div>
  <script>
    const div = document.querySelector('div')
    div.className = 'box'
  </script>
</body>
</html>
  • 通过 classList 操作 CSS,追加和删除类名,常用。语法:
    • 追加:元素.classList.add('类名')
    • 删除:元素.classList.remove('类名')
    • 切换:元素.classList.toggle('类名')
    • 包含:元素.classList.contains('类名')
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      width: 200px;
      height: 200px;
      background-color: skyblue;
    }

    .active {
      color: pink;
    }
  </style>
</head>
<body>
  <div class="box">stonecoding.net</div>
  <script>
    const box = document.querySelector('.box')
    // box.classList.add('active')
    // box.classList.remove('active')
    box.classList.toggle('active')
  </script>
</body>
</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>
  <title>随机轮播图</title>
  <style>
    * {
      box-sizing: border-box;
    }

    .slider {
      width: 560px;
      height: 400px;
      overflow: hidden;
    }

    .slider-wrapper {
      width: 100%;
      height: 320px;
    }

    .slider-wrapper img {
      width: 100%;
      height: 100%;
      display: block;
    }

    .slider-footer {
      height: 80px;
      background-color: rgb(100, 67, 68);
      padding: 12px 12px 0 12px;
      position: relative;
    }

    .slider-footer .toggle {
      position: absolute;
      right: 0;
      top: 12px;
      display: flex;
    }

    .slider-footer .toggle button {
      margin-right: 12px;
      width: 28px;
      height: 28px;
      appearance: none;
      border: none;
      background: rgba(255, 255, 255, 0.1);
      color: #fff;
      border-radius: 4px;
      cursor: pointer;
    }

    .slider-footer .toggle button:hover {
      background: rgba(255, 255, 255, 0.2);
    }

    .slider-footer p {
      margin: 0;
      color: #fff;
      font-size: 18px;
      margin-bottom: 10px;
    }

    .slider-indicator {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      align-items: center;
    }

    .slider-indicator li {
      width: 8px;
      height: 8px;
      margin: 4px;
      border-radius: 50%;
      background: #fff;
      opacity: 0.4;
      cursor: pointer;
    }

    .slider-indicator li.active {
      width: 12px;
      height: 12px;
      opacity: 1;
    }
  </style>
</head>

<body>
  <div class="slider">
    <div class="slider-wrapper">
      <img src="../images/slider01.jpg" alt="" />
    </div>
    <div class="slider-footer">
      <p>对人类来说会不会太超前了?</p>
      <ul class="slider-indicator">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
      </ul>
      <div class="toggle">
        <button class="prev">&lt;</button>
        <button class="next">&gt;</button>
      </div>
    </div>
  </div>
  <script>
    // 初始数据
    const sliderData = [
      { url: '../images/slider01.jpg', title: '对人类来说会不会太超前了?', color: 'rgb(100, 67, 68)' },
      { url: '../images/slider02.jpg', title: '开启剑与雪的黑暗传说!', color: 'rgb(43, 35, 26)' },
      { url: '../images/slider03.jpg', title: '真正的jo厨出现了!', color: 'rgb(36, 31, 33)' },
      { url: '../images/slider04.jpg', title: '李玉刚:让世界通过B站看到东方大国文化', color: 'rgb(139, 98, 66)' },
      { url: '../images/slider05.jpg', title: '快来分享你的寒假日常吧~', color: 'rgb(67, 90, 92)' },
      { url: '../images/slider06.jpg', title: '哔哩哔哩小年YEAH', color: 'rgb(166, 131, 143)' },
      { url: '../images/slider07.jpg', title: '一站式解决你的电脑配置问题!!!', color: 'rgb(53, 29, 25)' },
      { url: '../images/slider08.jpg', title: '谁不想和小猫咪贴贴呢!', color: 'rgb(99, 72, 114)' },
    ]
    // 生成随机数
    const random = parseInt(Math.random() * sliderData.length)
    // 获取图片元素
    const img = document.querySelector('.slider-wrapper img')
    // 修改图片属性
    img.src = sliderData[random].url
    // 获取 p 元素
    const p = document.querySelector('.slider-footer p')
    // 修改 p 元素内容
    p.innerHTML = sliderData[random].title
    // 修改背景颜色
    const footer = document.querySelector('.slider-footer')
    footer.style.backgroundColor = sliderData[random].color
    // 获取小圆点并添加类
    const li = document.querySelector(`.slider-indicator li:nth-child(${random + 1})`)
    li.classList.add('active')
  </script>
</body>
</html>

表单属性

可以通过 JavaScript 设置和修改表单元素属性。

输入框:

<body>
  <input type="text" value="电脑">
  <script>
    const input = document.querySelector('input')
    console.log(input.value)
    input.value = '手机'
    input.type = 'password'
  </script>
</body>

对于按钮,复选框及下拉框,其 disabledcheckedselected 接收布尔值。

<body>
  <input type="checkbox" name="" id="">
  <script>
    const input = document.querySelector('input')
    input.checked = true
  </script>
</body>

自定义属性

自定义属性以 data- 开头,使用 dataset 获取。

<body>
  <div data-id="1">1</div>
  <div data-id="2">2</div>
  <div data-id="3">3</div>
  <div data-id="4">4</div>
  <div data-id="5">5</div>
  <script>
    const one = document.querySelector('div')
    console.log(one.dataset.id)
  </script>
</body>

间歇定时器

在JavaScript中,可以使用 setInterval() 函数来创建一个间歇定时器。这个函数会按照指定的间隔(以毫秒为单位)来重复执行一个函数或代码段。

语法:

setInterval(函数,间隔时间)

例如每秒钟(1000毫秒)在控制台打印一次消息:

let intervalId = setInterval(function() {  
    console.log("这个消息每秒钟打印一次");  
}, 1000);

在这个例子中,setInterval() 函数的第一个参数是一个匿名函数,这个函数包含了希望间歇执行的代码。第二个参数是间隔的时间,以毫秒为单位。

也可以指定命名函数:

function fn() {
  console.log("这个消息每秒钟打印一次"); 
}
let intervalId = setInterval(fn, 1000);

setInterval() 函数会返回一个唯一的定时器 ID,可以使用这个 ID 来稍后清除定时器。以上把这个 ID 存储在变量 intervalId 中。

如果希望在某个时刻停止这个定时器,可以使用 clearInterval() 函数,并传入定时器的 ID 作为参数:

clearInterval(intervalId);

例子:阅读协议倒计时

<!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>
  <textarea name="" id="" cols="30" rows="10">
      用户注册协议
      欢迎注册成为京东用户!在您注册过程中,您需要完成我们的注册流程并通过点击同意的形式在线签署以下协议,请您务必仔细阅读、充分理解协议中的条款内容后再点击同意(尤其是以粗体或下划线标识的条款,因为这些条款可能会明确您应履行的义务或对您的权利有所限制)。
      【请您注意】如果您不同意以下协议全部或任何条款约定,请您停止注册。您停止注册后将仅可以浏览我们的商品信息但无法享受我们的产品或服务。如您按照注册流程提示填写信息,阅读并点击同意上述协议且完成全部注册流程后,即表示您已充分阅读、理解并接受协议的全部内容,并表明您同意我们可以依据协议内容来处理您的个人信息,并同意我们将您的订单信息共享给为完成此订单所必须的第三方合作方(详情查看
  </textarea>
  <br>
  <button class="btn" disabled>我已经阅读用户协议(5)</button>
  <script>
    const btn = document.querySelector('.btn')
    // 倒计时时长
    let i = 5

    let n = setInterval(function () {
      // 每秒减1
      i--
      // 修改btn的文字
      btn.innerHTML = `我已经阅读用户协议(${i})`
      // 当倒计时到0时停止和修改属性
      if (i === 0) {
        // 关闭定时器
        clearInterval(n)
        btn.disabled = false
        btn.innerHTML = `同意`
      }
    }, 1000)
  </script>
</body>

</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>
  <title>轮播图点击切换</title>
  <style>
    * {
      box-sizing: border-box;
    }

    .slider {
      width: 560px;
      height: 400px;
      overflow: hidden;
    }

    .slider-wrapper {
      width: 100%;
      height: 320px;
    }

    .slider-wrapper img {
      width: 100%;
      height: 100%;
      display: block;
    }

    .slider-footer {
      height: 80px;
      background-color: rgb(100, 67, 68);
      padding: 12px 12px 0 12px;
      position: relative;
    }

    .slider-footer .toggle {
      position: absolute;
      right: 0;
      top: 12px;
      display: flex;
    }

    .slider-footer .toggle button {
      margin-right: 12px;
      width: 28px;
      height: 28px;
      appearance: none;
      border: none;
      background: rgba(255, 255, 255, 0.1);
      color: #fff;
      border-radius: 4px;
      cursor: pointer;
    }

    .slider-footer .toggle button:hover {
      background: rgba(255, 255, 255, 0.2);
    }

    .slider-footer p {
      margin: 0;
      color: #fff;
      font-size: 18px;
      margin-bottom: 10px;
    }

    .slider-indicator {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      align-items: center;
    }

    .slider-indicator li {
      width: 8px;
      height: 8px;
      margin: 4px;
      border-radius: 50%;
      background: #fff;
      opacity: 0.4;
      cursor: pointer;
    }

    .slider-indicator li.active {
      width: 12px;
      height: 12px;
      opacity: 1;
    }
  </style>
</head>

<body>
  <div class="slider">
    <div class="slider-wrapper">
      <img src="../images/slider01.jpg" alt="" />
    </div>
    <div class="slider-footer">
      <p>对人类来说会不会太超前了?</p>
      <ul class="slider-indicator">
        <li class="active"></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
      </ul>
      <div class="toggle">
        <button class="prev">&lt;</button>
        <button class="next">&gt;</button>
      </div>
    </div>
  </div>
  <script>
    // 初始数据
    const sliderData = [
      { url: '../images/slider01.jpg', title: '对人类来说会不会太超前了?', color: 'rgb(100, 67, 68)' },
      { url: '../images/slider02.jpg', title: '开启剑与雪的黑暗传说!', color: 'rgb(43, 35, 26)' },
      { url: '../images/slider03.jpg', title: '真正的jo厨出现了!', color: 'rgb(36, 31, 33)' },
      { url: '../images/slider04.jpg', title: '李玉刚:让世界通过B站看到东方大国文化', color: 'rgb(139, 98, 66)' },
      { url: '../images/slider05.jpg', title: '快来分享你的寒假日常吧~', color: 'rgb(67, 90, 92)' },
      { url: '../images/slider06.jpg', title: '哔哩哔哩小年YEAH', color: 'rgb(166, 131, 143)' },
      { url: '../images/slider07.jpg', title: '一站式解决你的电脑配置问题!!!', color: 'rgb(53, 29, 25)' },
      { url: '../images/slider08.jpg', title: '谁不想和小猫咪贴贴呢!', color: 'rgb(99, 72, 114)' },
    ]
    // 获取元素
    const img = document.querySelector('.slider-wrapper img')
    const p = document.querySelector('.slider-footer p')
    const footer = document.querySelector('.slider-footer')

    let i = 0
    setInterval(function () {
      i++
      // 无缝衔接位置
      if (i >= sliderData.length) {
        i = 0
      }

      img.src = sliderData[i].url
      p.innerHTML = sliderData[i].title
      footer.style.backgroundColor = sliderData[i].color
      // 删除之前的 active
      document.querySelector('.slider-indicator .active').classList.remove('active')
      // 让当前的 li 添加 active
      document.querySelector(`.slider-indicator li:nth-child(${i + 1})`).classList.add('active')
    }, 1000)
  </script>
</body>

</html>

事件

在 JavaScript 中,事件是用户或浏览器自身执行的某种动作,比如点击一个按钮、按下键盘上的键、移动鼠标等。当这些事件发生时,JavaScript 可以侦听这些事件,并执行相应的代码。这是通过事件处理程序(或事件监听器)实现的,它们被附加到 HTML 元素上,以便在特定事件发生时执行。

事件监听

在 JavaScript 中,事件监听通过注册一个或多个函数(称为事件处理器或事件监听器),在指定的 HTML 元素上发生特定事件时被调用。能够对用户的交互(如点击按钮、滚动页面、按键等)或其他事件(如页面加载完成、定时器触发等)做出响应。

步骤:

  1. 选择想要添加事件监听器的 HTML 元素,即事件源。
  2. 使用该元素的 addEventListener() 方法来添加事件监听器。
  3. 指定要监听的事件类型(如 clickloadscroll 等)。
  4. 提供一个事件处理器函数,该函数将在事件发生时被调用。

语法:

元素对象.addEventListener('事件类型',事件处理器函数)

事件类型有:

  • 鼠标事件:

    • click:用户点击某个元素时触发。

    • dblclick:用户双击某个元素时触发。

    • mouseover:鼠标指针移入某个元素上方时触发。

    • mouseout:鼠标指针移出某个元素上方时触发。

    • mousedown:鼠标按键在某个元素上按下时触发。

    • mouseup:鼠标按键在某个元素上释放时触发。

    • mousemove:鼠标指针在元素内部移动时触发。

  • 键盘事件:

    • keydown:用户按下键盘上的某个键时触发。

    • keyup:用户释放键盘上的某个键时触发。

    • keypress:用户按下并释放某个可打印键时触发。

  • 表单事件:

    • submit:用户提交表单时触发。

    • focus:元素获得焦点时触发。

    • blur:元素失去焦点时触发。

    • change:元素的值改变时触发,常用于 <input><textarea><select>元素。

  • 窗口和框架事件:

    • load:页面或某个框架加载完成时触发。

    • resize:窗口或框架的大小改变时触发。

    • scroll:用户滚动窗口或框架的滚动条时触发。

    • unload:窗口、文档或其他资源将要卸载时触发。

  • 触摸事件:

    • touchstart:当用户触摸屏幕时触发。

    • touchmove:当用户在屏幕上移动手指时触发。

    • touchend:当用户停止触摸屏幕时触发。

  • 拖拽事件:

    • dragstart:当拖拽操作开始时触发。

    • dragover:当拖拽元素经过有效放置目标时触发。

    • drop:当拖拽元素在有效放置目标上释放时触发。

    • dragend:当拖拽操作结束时触发。

例如:点击按钮弹出警告框

<body>
  <button>点击</button>
  <script>
    const btn = document.querySelector('button')
    btn.addEventListener('click', function () {
      alert('Hello World')
    })
  </script>
</body>

例子:关闭广告

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      position: relative;
      margin: 30px auto;
      width: 300px;
      height: 100px;
      background-color: skyblue;
      text-align: center;
      line-height: 200px;
    }

    .boxIn {
      position: absolute;
      top: 10px;
      right: 10px;
      height: 20px;
      width: 20px;
      background-color: pink;
      text-align: center;
      line-height: 20px;
      cursor: pointer;
    }
  </style>
</head>

<body>
  <div class="box">
    <div class="boxIn">X</div>
  </div>
  <script>
    // 事件监听实现
    const boxIn = document.querySelector('.boxIn')
    const box = document.querySelector('.box')
    boxIn.addEventListener('click', function () {
      box.style.display = 'none'
    })
  </script>
</body>

</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>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        h2 {
            text-align: center;
        }

        .box {
            width: 600px;
            margin: 50px auto;
            display: flex;
            font-size: 25px;
            line-height: 40px;
        }

        .qs {

            width: 450px;
            height: 40px;
            color: red;

        }

        .btns {
            text-align: center;
        }

        .btns button {
            width: 120px;
            height: 35px;
            margin: 0 50px;
        }
    </style>
</head>

<body>
    <h2>随机点名</h2>
    <div class="box">
        <span>名字是:</span>
        <div class="qs">这里显示姓名</div>
    </div>
    <div class="btns">
        <button class="start">开始</button>
        <button class="end">结束</button>
    </div>

    <script>
        const arr = ['马超', '黄忠', '赵云', '关羽', '张飞']
        
        const start = document.querySelector('.start')
        const end = document.querySelector('.end')
        const qs = document.querySelector('.qs')

        let timerId = 0
        let random = 0

        start.addEventListener('click', function () {
            timerId = setInterval(function () {
                random = parseInt(Math.random() * arr.length)
                qs.innerHTML = arr[random]
            }, 10)

            if (arr.length === 1) {
              start.disabled = end.disabled = true
            }
        })

        end.addEventListener('click', function () {
            clearInterval(timerId)
            arr.splice(random, 1)
        })
    </script>
</body>

</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>
  <title>轮播图点击切换</title>
  <style>
    * {
      box-sizing: border-box;
    }

    .slider {
      width: 560px;
      height: 400px;
      overflow: hidden;
    }

    .slider-wrapper {
      width: 100%;
      height: 320px;
    }

    .slider-wrapper img {
      width: 100%;
      height: 100%;
      display: block;
    }

    .slider-footer {
      height: 80px;
      background-color: rgb(100, 67, 68);
      padding: 12px 12px 0 12px;
      position: relative;
    }

    .slider-footer .toggle {
      position: absolute;
      right: 0;
      top: 12px;
      display: flex;
    }

    .slider-footer .toggle button {
      margin-right: 12px;
      width: 28px;
      height: 28px;
      appearance: none;
      border: none;
      background: rgba(255, 255, 255, 0.1);
      color: #fff;
      border-radius: 4px;
      cursor: pointer;
    }

    .slider-footer .toggle button:hover {
      background: rgba(255, 255, 255, 0.2);
    }

    .slider-footer p {
      margin: 0;
      color: #fff;
      font-size: 18px;
      margin-bottom: 10px;
    }

    .slider-indicator {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      align-items: center;
    }

    .slider-indicator li {
      width: 8px;
      height: 8px;
      margin: 4px;
      border-radius: 50%;
      background: #fff;
      opacity: 0.4;
      cursor: pointer;
    }

    .slider-indicator li.active {
      width: 12px;
      height: 12px;
      opacity: 1;
    }
  </style>
</head>
<body>
  <div class="slider">
    <div class="slider-wrapper">
      <img src="../images/slider01.jpg" alt="" />
    </div>
    <div class="slider-footer">
      <p>对人类来说会不会太超前了?</p>
      <ul class="slider-indicator">
        <li class="active"></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
      </ul>
      <div class="toggle">
        <button class="prev">&lt;</button>
        <button class="next">&gt;</button>
      </div>
    </div>
  </div>
  <script>
    const sliderData = [
      { url: '../images/slider01.jpg', title: '对人类来说会不会太超前了?', color: 'rgb(100, 67, 68)' },
      { url: '../images/slider02.jpg', title: '开启剑与雪的黑暗传说!', color: 'rgb(43, 35, 26)' },
      { url: '../images/slider03.jpg', title: '真正的jo厨出现了!', color: 'rgb(36, 31, 33)' },
      { url: '../images/slider04.jpg', title: '李玉刚:让世界通过B站看到东方大国文化', color: 'rgb(139, 98, 66)' },
      { url: '../images/slider05.jpg', title: '快来分享你的寒假日常吧~', color: 'rgb(67, 90, 92)' },
      { url: '../images/slider06.jpg', title: '哔哩哔哩小年YEAH', color: 'rgb(166, 131, 143)' },
      { url: '../images/slider07.jpg', title: '一站式解决你的电脑配置问题!!!', color: 'rgb(53, 29, 25)' },
      { url: '../images/slider08.jpg', title: '谁不想和小猫咪贴贴呢!', color: 'rgb(99, 72, 114)' },
    ]

    const img = document.querySelector('.slider-wrapper img')
    const p = document.querySelector('.slider-footer p')
    const footerDiv = document.querySelector('.slider-footer')

    let i = 0

    // 右侧按键,点击获取下一张
    const nextBtn = document.querySelector('.next')
    nextBtn.addEventListener('click', function () {
        i++
        i = i >= sliderData.length ? 0 : i
        render()
      }
    )

    // 左侧按键,点击获取上一张
    const prevBtn = document.querySelector('.prev')
    prevBtn.addEventListener('click', function () {
        i--
        i = i < 0 ? sliderData.length - 1 : i
        render()
      }
    )

    // 渲染函数
    function render() {
      img.src = sliderData[i].url
      p.innerText = sliderData[i].title
      footerDiv.style.backgroundColor = sliderData[i].color

      // 更换小圆点 先移除类,再添加类
      document.querySelector('.slider-indicator .active').classList.remove('active')
      document.querySelector(`.slider-indicator li:nth-child(${i + 1})`).classList.add('active')
    }

    // 自动轮播
    let timeId = setInterval(function () {
      nextBtn.click()
    }, 1000)

    // 鼠标移入停止轮播
    const slider = document.querySelector('.slider')
    slider.addEventListener('mouseenter', function () {
        clearInterval(timeId)
      }
    )

    // 鼠标移出继续轮播
    slider.addEventListener('mouseleave', function () {
        // 先清除定时器,再重新设置定时器
        clearInterval(timeId)
        timeId = setInterval(function () {
          nextBtn.click()
        }, 1000)
      }
    )
  </script>
</body>
</html>

例子:小米搜索框

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }

        ul {
          list-style: none;
        }

        .mi {
          position: relative;
          width: 223px;
          margin: 100px auto;
        }

        .mi input {
          width: 223px;
          height: 48px;
          padding: 0 10px;
          font-size: 14px;
          line-height: 48px;
          border: 1px solid #e0e0e0;
          outline: none;
        }

        .mi .search {
          border: 1px solid #ff6700;
        }

        .result-list {
          display: none;
          position: absolute;
          left: 0;
          top: 48px;
          width: 223px;
          border: 1px solid #ff6700;
          border-top: 0;
          background: #fff;
        }

        .result-list a {
          display: block;
          padding: 6px 15px;
          font-size: 12px;
          color: #424242;
          text-decoration: none;
        }

        .result-list a:hover {
          background-color: #eee;
        }
    </style>

</head>

<body>
    <div class="mi">
        <input type="search" placeholder="小米笔记本">
        <ul class="result-list">
          <li><a href="#">全部商品</a></li>
          <li><a href="#">小米11</a></li>
          <li><a href="#">小米10S</a></li>
          <li><a href="#">小米笔记本</a></li>
          <li><a href="#">小米手机</a></li>
          <li><a href="#">黑鲨4</a></li>
          <li><a href="#">空调</a></li>
        </ul>
    </div>
    <script>
        const input = document.querySelector('[type=search]')
        const ul = document.querySelector('.result-list')

        // 获得焦点事件,显示下拉框并添加一个 CSS 类
        input.addEventListener('focus', function () {
          ul.style.display = 'block'
          input.classList.add('search')
        })

        // 失去焦点事件,隐藏下拉框并移除一个 CSS 类
        input.addEventListener('blur', function () {
          ul.style.display = 'none'
          input.classList.remove('search')
        })
    </script>
</body>

</html>

例子:评论字数统计

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>评论字数统计</title>
  <style>
    .wrapper {
      min-width: 400px;
      max-width: 800px;
      display: flex;
      justify-content: flex-end;
    }

    .avatar {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      overflow: hidden;
      background: url(./images/avatar.jpg) no-repeat center / cover;
      margin-right: 20px;
    }

    .wrapper textarea {
      outline: none;
      border-color: transparent;
      resize: none;
      background: #f5f5f5;
      border-radius: 4px;
      flex: 1;
      padding: 10px;
      transition: all 0.5s;
      height: 30px;
    }

    .wrapper textarea:focus {
      border-color: #e4e4e4;
      background: #fff;
      height: 50px;
    }

    .wrapper button {
      background: #00aeec;
      color: #fff;
      border: none;
      border-radius: 4px;
      margin-left: 10px;
      width: 70px;
      cursor: pointer;
    }

    .wrapper .total {
      margin-right: 80px;
      color: #999;
      margin-top: 5px;
      opacity: 0;
      transition: all 0.5s;
    }

    .list {
      min-width: 400px;
      max-width: 800px;
      display: flex;
    }

    .list .item {
      width: 100%;
      display: flex;
    }

    .list .item .info {
      flex: 1;
      border-bottom: 1px dashed #e4e4e4;
      padding-bottom: 10px;
    }

    .list .item p {
      margin: 0;
    }

    .list .item .name {
      color: #FB7299;
      font-size: 14px;
      font-weight: bold;
    }

    .list .item .text {
      color: #333;
      padding: 10px 0;
    }

    .list .item .time {
      color: #999;
      font-size: 12px;
    }
  </style>
</head>

<body>
  <div class="wrapper">
    <i class="avatar"></i>
    <textarea id="tx" placeholder="发一条友善的评论" rows="2" maxlength="200"></textarea>
    <button>发布</button>
  </div>
  <div class="wrapper">
    <span class="total">0/200字</span>
  </div>
  <div class="list">
    <div class="item" style="display: none;">
      <i class="avatar"></i>
      <div class="info">
        <p class="name">清风徐来</p>
        <p class="text">大家都辛苦啦,感谢各位大大的努力,能圆满完成真是太好了[笑哭][支持]</p>
        <p class="time">2022-10-10 20:29:21</p>
      </div>
    </div>
  </div>
  <script>
    const tx = document.querySelector('#tx')
    const total = document.querySelector('.total')

    // 获得焦点,显示字数统计
    tx.addEventListener('focus', function () {
      total.style.opacity = 1
    })

    // 失去焦点,隐藏字数统计
    tx.addEventListener('blur', function () {
      total.style.opacity = 0
    })

    // 输入文字,统计字数
    tx.addEventListener('input', function () {
      total.innerHTML = `${tx.value.length}/200字`
    })
  </script>
</body>

</html>

事件对象

在 JavaScript 中,事件对象是与特定事件相关联的,它包含了关于该事件的所有信息。当某个事件(如点击、键盘输入、鼠标移动等)被触发时,浏览器会创建一个事件对象,并将其作为参数传递给事件处理函数。

事件对象提供了很多属性和方法,以便获取关于事件的详细信息,并进行相应的处理。以下是一些常用的事件对象属性和方法:

属性:

  • type:事件的类型(如 "click", "mousedown", "keyup" 等)。
  • target:触发事件的元素。
  • currentTarget:当前正在处理事件的元素(通常与 target 相同,但在事件冒泡或捕获阶段可能不同)。
  • pageX/pageY:鼠标指针相对于文档左侧和顶部的位置。
  • clientX/clientY:鼠标指针相对于浏览器窗口视口左侧和顶部的位置。
  • screenX/screenY:鼠标指针相对于屏幕左侧和顶部的位置。
  • key:对于键盘事件,表示按下的键。
  • shiftKey, ctrlKey, altKey, metaKey:表示是否同时按下了 Shift、Ctrl、Alt 或 Meta 键。

方法:

  • preventDefault():阻止事件的默认行为。例如,阻止表单的默认提交行为。
  • stopPropagation():阻止事件进一步传播(即阻止事件冒泡或捕获)。
  • stopImmediatePropagation():阻止事件进一步传播,并阻止任何后续的事件处理函数被执行。

当使用 addEventListener 方法添加事件监听器时,事件对象会自动作为回调函数的第一个参数传递,一般命名为 eventev 或者 e

例如:

元素.addEventListener('click', function (e) {})

例子:回车发布评论

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>回车发布评论</title>
  <style>
    .wrapper {
      min-width: 400px;
      max-width: 800px;
      display: flex;
      justify-content: flex-end;
    }

    .avatar {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      overflow: hidden;
      background: url(./images/avatar.jpg) no-repeat center / cover;
      margin-right: 20px;
    }

    .wrapper textarea {
      outline: none;
      border-color: transparent;
      resize: none;
      background: #f5f5f5;
      border-radius: 4px;
      flex: 1;
      padding: 10px;
      transition: all 0.5s;
      height: 30px;
    }

    .wrapper textarea:focus {
      border-color: #e4e4e4;
      background: #fff;
      height: 50px;
    }

    .wrapper button {
      background: #00aeec;
      color: #fff;
      border: none;
      border-radius: 4px;
      margin-left: 10px;
      width: 70px;
      cursor: pointer;
    }

    .wrapper .total {
      margin-right: 80px;
      color: #999;
      margin-top: 5px;
      opacity: 0;
      transition: all 0.5s;
    }

    .list {
      min-width: 400px;
      max-width: 800px;
      display: flex;
    }

    .list .item {
      width: 100%;
      display: flex;
    }

    .list .item .info {
      flex: 1;
      border-bottom: 1px dashed #e4e4e4;
      padding-bottom: 10px;
    }

    .list .item p {
      margin: 0;
    }

    .list .item .name {
      color: #FB7299;
      font-size: 14px;
      font-weight: bold;
    }

    .list .item .text {
      color: #333;
      padding: 10px 0;
    }

    .list .item .time {
      color: #999;
      font-size: 12px;
    }
  </style>
</head>

<body>
  <div class="wrapper">
    <i class="avatar"></i>
    <textarea id="tx" placeholder="发一条友善的评论" rows="2" maxlength="200"></textarea>
    <button>发布</button>
  </div>
  <div class="wrapper">
    <span class="total">0/200字</span>
  </div>
  <div class="list">
    <div class="item" style="display: none;">
      <i class="avatar"></i>
      <div class="info">
        <p class="name">清风徐来</p>
        <p class="text">大家都辛苦啦,感谢各位大大的努力,能圆满完成真是太好了[笑哭][支持]</p>
        <p class="time">2022-10-10 20:29:21</p>
      </div>
    </div>
  </div>
  <script>
    const tx = document.querySelector('#tx')
    const total = document.querySelector('.total')

    // 获得焦点,显示字数统计
    tx.addEventListener('focus', function () {
      total.style.opacity = 1
    })

    // 失去焦点,隐藏字数统计
    tx.addEventListener('blur', function () {
      total.style.opacity = 0
    })

    // 输入文字,统计字数
    tx.addEventListener('input', function () {
      total.innerHTML = `${tx.value.length}/200字`
    })

    // 回车发布评论
    const item = document.querySelector('.item')
    const text = document.querySelector('.text')
    tx.addEventListener('keyup', function (e) {
      if (e.key === 'Enter') {
        if (tx.value.trim()) {
          item.style.display = 'block'
          text.innerHTML = tx.value
        }
        tx.value = ''
        total.innerHTML = '0/200字'
      }
    })
  </script>
</body>

</html>

环境对象

环境对象指的是函数内部特殊的变量 this,表示当前函数运行时所处的环境。

在全局环境中,this 指向全局环境对象(在浏览器中为 window,在 Node.js 中为 global),因为函数默认是使用 window 调用。

function myFunction() {  
  console.log(this);  
}  
  
// window.myFunction();
myFunction(); // 在浏览器中输出 window 对象,在 Node.js 中输出 global 对象

函数的调用方式不同,this 指代的对象也不同。简单来说,谁调用,this 就是谁。

<body>
  <button>点击</button>
  <script>
    const btn = document.querySelector('button')
    btn.addEventListener('click', function () {
      console.log(this) // btn 对象
    })
  </script>
</body>

回调函数

在 JavaScript 中,回调函数是一种特殊的函数,它被传递给另一个函数作为参数,并在需要时被调用。比如前面的间歇定时器中的函数以及事件监听中的事件处理器函数。

例子:Tab 栏切换

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>tab栏切换</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    .tab {
      width: 590px;
      height: 340px;
      margin: 20px;
      border: 1px solid #e4e4e4;
    }

    .tab-nav {
      width: 100%;
      height: 60px;
      line-height: 60px;
      display: flex;
      justify-content: space-between;
    }

    .tab-nav h3 {
      font-size: 24px;
      font-weight: normal;
      margin-left: 20px;
    }

    .tab-nav ul {
      list-style: none;
      display: flex;
      justify-content: flex-end;
    }

    .tab-nav ul li {
      margin: 0 20px;
      font-size: 14px;
    }

    .tab-nav ul li a {
      text-decoration: none;
      border-bottom: 2px solid transparent;
      color: #333;
    }

    .tab-nav ul li a.active {
      border-color: #e1251b;
      color: #e1251b;
    }

    .tab-content {
      padding: 0 16px;
    }

    .tab-content .item {
      display: none;
    }

    .tab-content .item.active {
      display: block;
    }
  </style>
</head>

<body>
  <div class="tab">
    <div class="tab-nav">
      <h3>每日特价</h3>
      <ul>
        <li><a class="active" href="javascript:;">精选</a></li>
        <li><a href="javascript:;">美食</a></li>
        <li><a href="javascript:;">百货</a></li>
        <li><a href="javascript:;">个护</a></li>
        <li><a href="javascript:;">预告</a></li>
      </ul>
    </div>
    <div class="tab-content">
      <div class="item active"><img src="./images/tab00.png" alt="" /></div>
      <div class="item"><img src="./images/tab01.png" alt="" /></div>
      <div class="item"><img src="./images/tab02.png" alt="" /></div>
      <div class="item"><img src="./images/tab03.png" alt="" /></div>
      <div class="item"><img src="./images/tab04.png" alt="" /></div>
    </div>
  </div>

  <script>
    const as = document.querySelectorAll('.tab-nav a')

    for (let i = 0; i < as.length; i++) {
      as[i].addEventListener('mouseenter', function () {
        // 切换 Tab
        document.querySelector('.tab-nav .active').classList.remove('active')
        this.classList.add('active')

        // 切换对应的图片
        document.querySelector('.tab-content .active').classList.remove('active')
        document.querySelector(`.tab-content .item:nth-child(${i + 1})`).classList.add('active')
      })
    }

  </script>
</body>

</html>

例子:全选反选

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>全选反选</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    table {
      border-collapse: collapse;
      border-spacing: 0;
      border: 1px solid #c0c0c0;
      width: 500px;
      margin: 100px auto;
      text-align: center;
    }

    th {
      background-color: #09c;
      font: bold 16px "微软雅黑";
      color: #fff;
      height: 24px;
    }

    td {
      border: 1px solid #d0d0d0;
      color: #404060;
      padding: 10px;
    }

    .allCheck {
      width: 80px;
    }
  </style>
</head>

<body>
  <table>
    <tr>
      <th class="allCheck">
        <input type="checkbox" name="" id="checkAll"> <span class="all">全选</span>
      </th>
      <th>商品</th>
      <th>商家</th>
      <th>价格</th>
    </tr>
    <tr>
      <td>
        <input type="checkbox" name="check" class="ck">
      </td>
      <td>小米手机</td>
      <td>小米</td>
      <td>¥1999</td>
    </tr>
    <tr>
      <td>
        <input type="checkbox" name="check" class="ck">
      </td>
      <td>小米净水器</td>
      <td>小米</td>
      <td>¥4999</td>
    </tr>
    <tr>
      <td>
        <input type="checkbox" name="check" class="ck">
      </td>
      <td>小米电视</td>
      <td>小米</td>
      <td>¥5999</td>
    </tr>
  </table>
  <script>
    const checkAll = document.querySelector('#checkAll')
    const cks = document.querySelectorAll('.ck')

    // 为全选框 checkAll 添加点击事件,点击时遍历各个复选框 cks,将其 checked 属性设置为全选框的 checked 属性
    checkAll.addEventListener('click', function () {
      for (let i = 0; i < cks.length; i++) {
        cks[i].checked = checkAll.checked
      }
    })

    // 为各个复选框添加点击事件,当选中的复选框个数等于所有复选框的个数,则选中全选框
    for (let i = 0; i < cks.length; i++) {
      cks[i].addEventListener('click', function () {
        checkAll.checked = document.querySelectorAll('.ck:checked').length === cks.length
      })
    }
  </script>
</body>

</html>

事件流

在 JavaScript 中,事件流(Event Flow)描述了在发生某个事件(如点击、鼠标移动、键盘输入等)时,事件如何在不同的 DOM 元素之间传播的过程。这涉及到两个主要阶段:

  • 捕获阶段(Capturing Phase):从文档的根节点开始,沿着 DOM 树向下直到到达触发事件的特定元素。在这个阶段,事件会触发所有在元素上注册的、在捕获阶段监听该事件的处理函数。

  • 冒泡阶段(Bubbling Phase):从目标元素开始,然后向上冒泡至 DOM 树的根节点。在这个过程中,事件会触发所有在元素上注册的、在冒泡阶段监听该事件的处理函数。实际工作都是使用事件冒泡为主。

javascript事件流

事件捕获

在 JavaScript 中,事件捕获(Event Capturing)是事件流中的一个阶段,它发生在事件冒泡之前。在事件捕获阶段,事件从最外层祖先元素(通常是 document 对象)开始,一路向下通过 DOM 树,直到它到达触发事件的目标元素。在这个过程中,如果 DOM 元素上注册了捕获阶段的事件监听器,那么这些监听器会按照 DOM 树的层次结构依次被触发。

默认情况下,大多数事件监听器在冒泡阶段被触发,而不是捕获阶段。但是,可以通过 addEventListener 方法的第三个参数来显式地设置监听器在捕获阶段触发。如果第三个参数为 true,则监听器将在捕获阶段被触发;如果为 false 或者省略,则监听器将在冒泡阶段被触发。

例如:

<body>
  <button>点击</button>
  <script>
    document.addEventListener('click', function(event) {  
      console.log('Document click captured.'); 
    }, true); // 第三个参数为true,表示在捕获阶段触发  
      
    const button = document.querySelector('button')
    button.addEventListener('click', function(event) {  
        console.log('Button click captured.');  
    }, true); // 同样在捕获阶段触发
  </script>
</body>

控制台依次输出:

Document click captured.
Button click captured.

在这个例子中,如果点击按钮,首先会触发在 document 对象上注册的捕获阶段监听器,然后触发在 button 元素上注册的捕获阶段监听器。这是因为事件从 document 开始,一路向下通过 DOM 树到达 button 元素。

需要注意的是,虽然事件捕获在某些情况下可能很有用,但在实际开发中,大多数事件处理都是在冒泡阶段进行的。这是因为冒泡阶段更符合用户直观的感受,即事件从内层元素开始向外层元素传播。此外,许多旧的浏览器可能不支持事件捕获,或者默认只在冒泡阶段处理事件,因此在使用事件捕获时需要谨慎考虑兼容性问题。

事件冒泡

在 JavaScript 中,事件冒泡(Event Bubbling)是事件流中的一个重要阶段,发生在事件捕获之后。当某个元素上的事件被触发时,该事件首先会在该元素自身上进行处理(如果有注册的事件监听器的话),然后会沿着 DOM 树向上冒泡,依次触发父级元素上注册的同类型事件监听器,直到到达最顶层的元素(通常是 document 对象)或者事件被取消冒泡。

这种冒泡机制允许在不同层级的 DOM 元素上注册相同类型的事件监听器,以处理不同逻辑。例如,可能在一个按钮(button)上注册一个点击事件监听器,同时在其父级容器(div)上也注册一个点击事件监听器。当用户点击按钮时,按钮上的事件监听器会首先被触发,然后事件会冒泡到父级容器,触发容器上的事件监听器。

默认情况下,事件监听器是在冒泡阶段被触发的。如果使用 addEventListener 方法注册事件监听器,并且没有显式指定第三个参数为 true,那么该监听器将在冒泡阶段被触发。

例如:

<body>
  <button>点击</button>
  <script>
    document.addEventListener('click', function(event) {  
      console.log('Document click captured.'); 
    });
      
    const button = document.querySelector('button')
    button.addEventListener('click', function(event) {  
        console.log('Button click captured.');  
    });
  </script>
</body>

控制台依次输出:

Button click captured.
Document click captured.

阻止冒泡

因为默认为事件冒泡,故容易导致事件影响到父级元素。若想把事件限制在当前元素内,就需要阻止事件冒泡。可以使用 event.stopPropagation() 方法来阻止事件冒泡(也可以阻止事件捕获)。当在事件处理函数中调用这个方法时,它会阻止事件进一步向上冒泡到 DOM 树中的父级元素。这意味着,如果父级元素上也注册了相同类型的事件监听器,那么这些监听器将不会被触发。

语法:

事件对象.stopPropagation()

例如:

<body>
  <button>点击</button>
  <script>
    document.addEventListener('click', function(event) {  
      console.log('Document click captured.'); 
    });
      
    const button = document.querySelector('button')
    button.addEventListener('click', function(event) {  
        console.log('Button click captured.');  
        event.stopPropagation();
    });
  </script>
</body>

控制台只输出:

Button click captured.

解绑事件

在 JavaScript 中,当使用 addEventListener 方法为一个元素绑定事件监听器后,可能在某个时刻想要解绑(移除)这个监听器。这通常发生在不再需要监听某个事件,或者想要避免内存泄漏的时候。为了解绑事件监听器,需要保留对原始监听器函数的引用(即函数必须是具名函数),并使用 removeEventListener 方法。故匿名函数无法被解绑。

<body>
  <button>点击</button>
  <script>      
    const button = document.querySelector('button')

    // 定义一个事件处理函数
    function handleClick() {
      console.log('Button clicked!')
    }
    
    // 绑定事件监听器
    button.addEventListener('click', handleClick);  

    // 解绑事件监听器
    button.removeEventListener('click', handleClick);
  </script>
</body>

在这个例子中,首先定义了一个名为 handleClick 的函数,然后使用 addEventListener 方法将其绑定到按钮的 click 事件上。后面再使用 removeEventListener 方法,并提供相同的事件类型和函数引用来解绑这个监听器。

鼠标经过事件:

  • mouseovermouseout 会有冒泡效果
  • mouseentermouseleave 没有冒泡效果(推荐)

事件委托

事件委托(Event Delegation)可以不必在每个子元素上(例如多个 li)单独设置事件监听器,而是在其父元素(或更高级别的祖先元素)上(例如 ul)设置一个事件监听器来监听子元素的事件。当事件冒泡到父元素时,父元素上的监听器可以通过事件对象(event)的目标(target)的标签名称(tagName),并据此决定是否需要响应事件。这种技术特别适用于动态内容,因为它可以处理在事件监听器被设置之后添加到 DOM 中的新元素。

事件委托基于事件冒泡机制。当子元素上的事件被触发时,这个事件会冒泡到 DOM 树中的父元素,直到到达最顶层的元素(通常是 document 对象)。通过在父元素上监听事件,并检查事件对象的目标元素,可以编写一个通用的事件处理函数来响应不同子元素上的事件。这样就不需要为每个子元素单独设置事件监听器,减少了内存使用,提升了程序性能,并且还可以处理在事件监听器设置之后添加到 DOM 的元素。

<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <p>6</p>
  </ul>
  <script>      
    const ul = document.querySelector('ul')
    ul.addEventListener('click', function (e) {
      if (e.target.tagName === 'LI') {
        e.target.style.color = 'red'
      }
    })
  </script>
</body>

例子:Tab 栏切换

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>tab栏切换</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    .tab {
      width: 590px;
      height: 340px;
      margin: 20px;
      border: 1px solid #e4e4e4;
    }

    .tab-nav {
      width: 100%;
      height: 60px;
      line-height: 60px;
      display: flex;
      justify-content: space-between;
    }

    .tab-nav h3 {
      font-size: 24px;
      font-weight: normal;
      margin-left: 20px;
    }

    .tab-nav ul {
      list-style: none;
      display: flex;
      justify-content: flex-end;
    }

    .tab-nav ul li {
      margin: 0 20px;
      font-size: 14px;
    }

    .tab-nav ul li a {
      text-decoration: none;
      border-bottom: 2px solid transparent;
      color: #333;
    }

    .tab-nav ul li a.active {
      border-color: #e1251b;
      color: #e1251b;
    }

    .tab-content {
      padding: 0 16px;
    }

    .tab-content .item {
      display: none;
    }

    .tab-content .item.active {
      display: block;
    }
  </style>
</head>

<body>
  <div class="tab">
    <div class="tab-nav">
      <h3>每日特价</h3>
      <ul>
        <li><a class="active" href="javascript:;" data-id="0">精选</a></li>
        <li><a href="javascript:;" data-id="1">美食</a></li>
        <li><a href="javascript:;" data-id="2">百货</a></li>
        <li><a href="javascript:;" data-id="3">个护</a></li>
        <li><a href="javascript:;" data-id="4">预告</a></li>
      </ul>
    </div>
    <div class="tab-content">
      <div class="item active"><img src="./images/tab00.png" alt="" /></div>
      <div class="item"><img src="./images/tab01.png" alt="" /></div>
      <div class="item"><img src="./images/tab02.png" alt="" /></div>
      <div class="item"><img src="./images/tab03.png" alt="" /></div>
      <div class="item"><img src="./images/tab04.png" alt="" /></div>
    </div>
  </div>

  <script>
    const ul  = document.querySelector('.tab-nav ul')
    ul.addEventListener('click', function (e) {
      if (e.target.tagName === 'A') {
        document.querySelector('.tab-nav .active').classList.remove('active')
        e.target.classList.add('active')

        const i = +e.target.dataset.id
        document.querySelector('.tab-content .active').classList.remove('active')
        // document.querySelector(`.tab-content .item:nth-child(${i + 1})`).classList.add('active')
        document.querySelectorAll('.tab-content .item')[i].classList.add('active')
      }
    }) 

  </script>
</body>

</html>

阻止默认行为

可以使用 event.preventDefault() 方法来阻止事件的默认行为。例如,阻止表单的默认提交行为或阻止链接的默认跳转行为。

<body>
  <form action="https://stonecoding.net">
    <input type="submit" value="免费注册">
  </form>
  <a href="https://stonecoding.net">程序开发与运维</a>
  <script>      
    const form = document.querySelector('form')
    form.addEventListener('submit', function (e) {
      e.preventDefault()
    })

    const a = document.querySelector('a')
    a.addEventListener('click', function (e) {
      e.preventDefault()
    })
  </script>
</body>

其他事件

页面加载事件

当整个页面及所有依赖资源如样式表和图片都已完成加载时,windowload 事件会被触发。可以使用 load 事件确保所有内容都加载完毕再进行某些操作(如执行脚本或显示界面)。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    // 页面所有资源加载完毕后再执行回调函数
    window.addEventListener('load', function () {
      const btn = document.querySelector('button')
      btn.addEventListener('click', function () {
        console.log('页面及所有资源已加载完毕')
      })
    })
  </script>
</head>
<body>
  <button>点击</button>
</body>
</html>

当 HTML 文档被完全加载和解析完成之后,documentDOMContentLoaded 事件被触发,而不必等待样式表、图像和子框架的完成加载。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    // DOM 加载完毕后再执行回调函数
    document.addEventListener('DOMContentLoaded', function () {
      const btn = document.querySelector('button')
      btn.addEventListener('click', function () {
        console.log('DOM 已加载完毕')
      })
    })
  </script>
</head>
<body>
  <button>点击</button>
</body>
</html>

页面滚动事件

在 JavaScript 中,可以使用 scroll 事件来监听页面的滚动。当页面(或页面内的某个元素)发生滚动时,scroll 事件就会被触发。可以通过为 window 对象或者具体的 DOM 元素添加事件监听器来捕获这个事件。

需要注意的是,scroll 事件可能会在滚动过程中频繁触发,因此如果在事件处理函数中执行了复杂的操作,可能会导致性能问题。如果需要在滚动停止后执行某些操作,可能需要使用防抖(debounce)或节流(throttle)技术来限制事件处理函数的执行频率。

例如:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      height: 3000px;
    }
  </style>
</head>
<body>
  <script>
    window.addEventListener('scroll', function() {  
        // 滚动时执行的代码  
        let scrollTop = document.documentElement.scrollTop;  
        console.log('页面已滚动:' + scrollTop + '像素');  
    });
  </script>
</body>
</html>

其中,document.documentElement 表示 html 元素。

获取滚动位置:

  • scrollTop 属性用于获取或设置元素的顶部滚动位置。它表示元素内容顶部与其视口顶部之间的像素距离。
  • scrollLeft 属性用于获取或设置元素的左侧滚动位置。它表示元素内容左侧与其视口左侧之间的像素距离。

还可以使用 scrollTo() 方法设置元素的滚动位置,将页面或某个可滚动元素滚动到特定位置。当用于window 对象时,它会滚动整个文档。当用于其他元素时,它会滚动该元素的内容。

scrollTo() 方法可以接受两种形式的参数:

  • 两个单独的参数,分别表示 X 轴和 Y 轴的滚动位置。
  • 一个包含 lefttop 属性的对象,还有一个可选的 behavior 属性,设置为 'smooth' 以启用平滑滚动效果。

例子:显示电梯导航和返回顶部

  <script>
    // 当页面滚动大于 300 像素,就显示电梯导航
    const elevator = document.querySelector('.xtx-elevator')
    window.addEventListener('scroll', function () {
      const n = document.documentElement.scrollTop
      elevator.style.opacity = n >= 300 ? 1 : 0
    })

    // 点击返回页面顶部
    const backTop = document.querySelector('#backTop')
    backTop.addEventListener('click', function () {
      // 可读写
      // document.documentElement.scrollTop = 0
      // window.scrollTo(x, y)
      window.scrollTo(0, 0)
    })
  </script>

页面尺寸事件

可以使用 resize 事件来监听浏览器窗口尺寸的变化。当浏览器窗口的大小被调整时,resize 事件就会被触发。

<body>
  <script>
    window.addEventListener('resize', function(event) {   
      let newWidth = window.innerWidth;  
      let newHeight = window.innerHeight;  
      console.log('窗口尺寸已变化: 宽度=' + newWidth + 'px, 高度=' + newHeight + 'px');  
    });
  </script>
</body>

在这个例子中,给 window 对象添加了一个 resize 事件监听器。当浏览器窗口的大小发生变化时,就会执行这个事件处理函数,并打印出新的窗口宽度和高度。

需要注意的是,resize 事件可能会在窗口调整大小的过程中频繁触发,这可能会导致性能问题。如果需要在窗口调整大小完成后执行某些操作,可能需要使用防抖(debounce)或节流(throttle)技术来限制事件处理函数的执行频率。

可以使用 clientWidthclientHeight 属性获取元素的内部宽度和高度(以像素为单位),包括元素的填充(padding),但不包括滚动条(如果存在)、边框(border)和外边距(margin)。这两个属性通常用于获取HTML元素(如 <div><body> 等)的视口大小。

clientWidthclientHeight 的值考虑了元素的 CSS 样式(比如 box-sizing),如果 box-sizing 设置为 border-box,那么 clientWidthclientHeight 将包括元素的边框和内边距。如果 box-sizing 设置为 content-box(默认值),则 clientWidthclientHeight 只包括元素的内容和内边距,不包括边框。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {
      width: 200px;
      height: 200px;
      background-color: pink;
      padding: 10px;
      border: 10px solid skyblue;
    }
  </style>
</head>
<body>
  <div></div>
  <script>
    const div = document.querySelector('div')
    console.log(div.clientWidth) // 控制台输出 220
    console.log(div.clientHeight) // 控制台输出 220
  </script>
</body>
</html>

元素尺寸位置

可以使用 offsetWidthoffsetHeight 属性获取元素的完整宽度和高度(包括元素的填充、边框和滚动条,如果存在的话)。这两个属性返回的是元素的总尺寸,与元素的 box-sizing 属性设置无关。注意获取的是可视宽高,如果盒子是隐藏的,则结果为 0。

  • offsetWidth:元素内容、内边距(padding)、边框(border)以及垂直滚动条(如果存在且可见)的宽度之和。
  • offsetHeight:元素内容、内边距(padding)、边框(border)以及水平滚动条(如果存在且可见)的高度之和。

可以使用 offsetLeftoffsetTop 属性获取当前元素相对于其 offsetParent 元素的左边界和上边界的偏移量。这两个属性返回的是以像素为单位的数值,都是只读属性。

  • offsetLeft:当前元素的左边缘与其 offsetParent 元素的左边缘之间的距离。
  • offsetTop:当前元素的上边缘与其 offsetParent 元素的上边缘之间的距离。

offsetParent 元素通常是当前元素的直接父级元素,但也可能是一个更上级的祖先元素,这取决于元素的 CSS 定位属性(position)。如果元素的 position 属性是 static,那么 offsetParent 就是最近的非 static 定位的祖先元素。如果元素或其任何祖先都没有非 static 定位,那么 offsetParent 将是 <body> 元素。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {
      position: relative;
      width: 200px;
      height: 200px;
      background-color: pink;
      margin: 100px;
    }

    p {
      width: 100px;
      height: 100px;
      background-color: purple;
      margin: 50px;
    }
  </style>
</head>
<body>
  <div>
    <p></p>
  </div>
  <script>
    const div = document.querySelector('div')
    const p = document.querySelector('p')
    console.log(div.offsetLeft)  // 输出 108,表示 div 元素左边缘到 body 元素左边缘的距离,100 + 8
    console.log(p.offsetLeft)  // 输出 50,表示 p 元素左边缘到 div 元素左边缘的记录,因为父元素添加了定位
  </script>
</body>
</html>

image-20240325143734498

例子:修改电梯导航

  <script>
    // 当页面滚动超过 xtx_entry 时,就显示电梯导航
    const entry = document.querySelector('.xtx_entry')
    const elevator = document.querySelector('.xtx-elevator')
    window.addEventListener('scroll', function () {
      const n = document.documentElement.scrollTop
      elevator.style.opacity = n >= entry.offsetTop ? 1 : 0
    })

    // 点击返回页面顶部
    const backTop = document.querySelector('#backTop')
    backTop.addEventListener('click', function () {
      // 可读写
      // document.documentElement.scrollTop = 0
      // window.scrollTo(x, y)
      window.scrollTo(0, 0)
    })
  </script>

例子:仿京东滑动顶部导航栏位置

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .content {
      overflow: hidden;
      width: 1000px;
      height: 3000px;
      background-color: pink;
      margin: 0 auto;
    }

    .backtop {
      display: none;
      width: 50px;
      left: 50%;
      margin: 0 0 0 505px;
      position: fixed;
      bottom: 60px;
      z-index: 100;
    }

    .backtop a {
      height: 50px;
      width: 50px;
      background: url(./images/bg2.png) 0 -600px no-repeat;
      opacity: 0.35;
      overflow: hidden;
      display: block;
      text-indent: -999em;
      cursor: pointer;
    }

    .header {
      position: fixed;
      top: -80px;
      left: 0;
      width: 100%;
      height: 80px;
      background-color: purple;
      text-align: center;
      color: #fff;
      line-height: 80px;
      font-size: 30px;
      transition: all .3s;
    }

    .sk {
      width: 300px;
      height: 300px;
      background-color: skyblue;
      margin-top: 500px;
    }
  </style>
</head>

<body>
  <div class="header">我是顶部导航栏</div>
  <div class="content">
    <div class="sk">秒杀模块</div>
  </div>
  <div class="backtop">
    <img src="./images/close2.png" alt="">
    <a href="javascript:;"></a>
  </div>
  <script>
    const sk = document.querySelector('.sk')
    const header = document.querySelector('.header')

    window.addEventListener('scroll' , function () {
      // 当页面滚动到秒杀模块头部时 改变顶部导航栏位置
      const scrollTop = document.documentElement.scrollTop
      header.style.top = scrollTop >= sk.offsetTop ? 0 : '-80px'
    })
  </script>
</body>

</html>

例子:导航滑块

  <script>
    // 1. 事件委托的方法 获取父元素 tabs-list
    const list = document.querySelector('.tabs-list')
    const line = document.querySelector('.line')

    // 2. 给 a 注册事件
    list.addEventListener('click', function (e) {
      if (e.target.tagName === 'A') {
        line.style.transform = `translateX(${e.target.offsetLeft}px)`
      }
    })
  </script>

可以使用 getBoundingClientRect() 获取元素的大小及其相对于视口(viewport)的位置。这个方法返回一个 DOMRect 对象,该对象包含了 lefttoprightbottomwidthheight 这些属性,这些属性分别表示元素左边界、上边界、右边界、下边界相对于视口的距离,以及元素的宽度和高度。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {
      width: 200px;
      height: 200px;
      background-color: pink;
      margin: 100px;
    }
  </style>
</head>
<body>
  <div></div>
  <script>
    const div = document.querySelector('div')
    console.log(div.getBoundingClientRect().width)
    console.log(div.getBoundingClientRect().height)
    console.log(div.getBoundingClientRect().left)
    console.log(div.getBoundingClientRect().top)
    console.log(div.getBoundingClientRect().right)
    console.log(div.getBoundingClientRect().buttom)
  </script>
</body>
</html>

总结:

属性作用说明
scrollLeft 和 scrollTop被卷去的头部和左侧配合页面滚动来用,可读写
clientWidth 和 clientHeight获得元素宽度和高度不包含滚动条、边框和外边距,用于获取元素大小,只读
offsetWidth 和 offsetHeight获得元素宽度和高度包含滚动条、边框和外边距,只读
offsetLeft 和 offsetTop获取元素距离自己定位父级元素的左、上距离获取元素位置的时候使用,只读

例子:电梯导航

  <script>
    // 模块一,页面滑动可以显示和隐藏电梯导航
    (function () {
      // 当页面滚动超过 xtx_entry 时,就显示电梯导航
      const entry = document.querySelector('.xtx_entry')
      const elevator = document.querySelector('.xtx-elevator')
      window.addEventListener('scroll', function () {
        const n = document.documentElement.scrollTop
        elevator.style.opacity = n >= entry.offsetTop ? 1 : 0
      })

      // 点击返回页面顶部
      const backTop = document.querySelector('#backTop')
      backTop.addEventListener('click', function () {
        // 可读写
        // document.documentElement.scrollTop = 0
        // window.scrollTo(x, y)
        window.scrollTo(0, 0)
      })
    })();
    
    // 模块二,点击电梯导航,跳转到对应位置;滚动页面,高亮显示对应的电梯导航
    (function () {
      // 点击电梯导航,跳转到对应位置
      const list = document.querySelector('.xtx-elevator-list')
      list.addEventListener('click', function (e) {
        if (e.target.tagName === 'A' && e.target.dataset.name) {
          document.querySelector('.xtx-elevator-list .active')?.classList.remove('active')
          e.target.classList.add('active')
          document.documentElement.scrollTop = document.querySelector(`.xtx_goods_${e.target.dataset.name}`).offsetTop
        }
      })

      // 滚动页面,高亮显示对应的电梯导航
      window.addEventListener('scroll', function () {
        document.querySelector('.xtx-elevator-list .active')?.classList.remove('active')

        // 获取 4 个大盒子
        const news = document.querySelector('.xtx_goods_new')
        const popular = document.querySelector('.xtx_goods_popular')
        const brand = document.querySelector('.xtx_goods_brand')
        const topic = document.querySelector('.xtx_goods_topic')
        const n = document.documentElement.scrollTop
        if (n >= news.offsetTop && n < popular.offsetTop) {
          document.querySelector('[data-name=new]').classList.add('active')
        } else if (n >= popular.offsetTop && n < brand.offsetTop) {
          document.querySelector('[data-name=popular]').classList.add('active')
        } else if (n >= brand.offsetTop && n < topic.offsetTop) {
          document.querySelector('[data-name=brand]').classList.add('active')
        } else if (n >= topic.offsetTop) {
          document.querySelector('[data-name=topic]').classList.add('active')
        }
      })
    })();
  </script>

页面内跳转时添加平滑滚动效果:

/* 页面滑动 */
html {
  /* 让滚动条丝滑的滚动 */
  scroll-behavior: smooth;
}

日期对象

使用日期对象来表示时间。

实例化日期对象

在 JavaScript 中,实例化通常指的是创建一个对象的过程,该对象基于某个构造函数或类。构造函数是一个特殊类型的函数,它用于初始化新创建的对象。当使用 new 关键字调用一个构造函数时,就会创建一个新的对象实例。

创建获取当前日期和时间的 Date 对象:

const now = new Date();
console.log(now); // 输出当前日期和时间 Wed Mar 27 2024 11:17:02 GMT+0800 (中国标准时间)

创建获取指定日期和时间的 Date 对象:

const date = new Date("2024-03-01 08:30:00");
console.log(date); // Fri Mar 01 2024 08:30:00 GMT+0800 (中国标准时间)

日期对象方法

Date 对象有许多方法和属性,可以用于获取和设置日期和时间的各个部分。以下是一些常用的:

  • 获取方法:
    • getFullYear():获取年份。
    • getMonth():获取月份(0-11)。
    • getDate():获取日期(1-31)。
    • getHours():获取小时(0-23)。
    • getMinutes():获取分钟(0-59)。
    • getSeconds():获取秒数(0-59)。
    • getMilliseconds():获取毫秒数(0-999)。
    • getTime():获取自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。
  • 设置方法:
    • setFullYear(year, [month], [day]):设置年份。
    • setMonth(month, [day]):设置月份。
    • setDate(date):设置日期。
    • setHours(hours, [minutes], [seconds], [ms]):设置小时。
    • setMinutes(minutes, [seconds], [ms]):设置分钟。
    • setSeconds(seconds, [ms]):设置秒数。
    • setMilliseconds(ms):设置毫秒数。
    • setTime(time):设置自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。
  • 其他方法:
    • toDateString():将日期转换为字符串。
    • toTimeString():将时间转换为字符串。
    • toLocaleString():根据本地时间格式,将日期时间转换为字符串。
    • toLocaleDateString():根据本地时间格式,将日期转换为字符串。
    • toLocaleTimeString():根据本地时间格式,将时间转换为字符串。
    • toISOString():将日期转换为 ISO 格式的字符串。
    • valueOf():返回日期的原始值(与 getTime() 相同)。

例子:获取和设置时间

const date = new Date()
console.log(date.getFullYear())
console.log(date.getMonth() + 1) // 注意:月份是从 0 开始的  
console.log(date.getDate())
console.log(date.getHours())
console.log(date.getMinutes())
console.log(date.getSeconds())

date.setFullYear(2024)  
date.setMonth(5)
date.setDate(1)  
date.setHours(12)  
date.setMinutes(0)  
date.setSeconds(0)  
date.setMilliseconds(0)  
console.log(date)

例子:显示当前时间

const div = document.querySelector('div')
function getMyDate() {
  const date = new Date()
  let h = date.getFullYear()
  let m = date.getMinutes()
  let s = date.getSeconds()
  h = h < 10 ? '0' + h : h
  m = m < 10 ? '0' + m : m
  s = s < 10 ? '0' + s : s
  return `现在是:${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${h}:${m}:${s}`
}

div.innerHTML = getMyDate()
setInterval(() => {
  div.innerHTML = getMyDate()
}, 1000);

例子:显示当前时间

const date = new Date()
console.log(date.toLocaleString())
console.log(date.toLocaleDateString())
console.log(date.toLocaleTimeString())

时间戳

时间戳通常表示自 1970 年 1 月 1 日 00:00:00 UTC(协调世界时)以来的毫秒数。

可以使用 Date.now() 方法或者直接调用 +new Date() 或者 new Date().getTime() 来获取当前的时间戳:

// 使用 Date.now() 方法获取当前时间戳
console.log(Date.now())

// 使用 +new Date() 方法获取当前时间戳
console.log(+new Date())
// 使用 +new Date() 方法获取指定时间戳
console.log(+new Date('2024-3-1 08:30:00'))
  
// 使用 new Date().getTime() 方法获取当前时间戳
console.log(new Date().getTime())
// 使用 new Date().getTime() 方法获取指定时间戳
console.log(new Date('2024-3-1 08:30:00').getTime())

可以使用 new Date(timestamp) 来将其转换为一个 Date 对象:

let timestamp = 1626768000000; // 假设这是某个时间戳  
let date = new Date(timestamp);  
console.log(date); // 输出对应的日期和时间

例子:倒计时

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>倒计时</title>
  <style>
    .countdown {
      width: 240px;
      height: 305px;
      text-align: center;
      line-height: 1;
      color: #fff;
      background-color: brown;
      /* background-size: 240px; */
      /* float: left; */
      overflow: hidden;
    }

    .countdown .next {
      font-size: 16px;
      margin: 25px 0 14px;
    }

    .countdown .title {
      font-size: 33px;
    }

    .countdown .tips {
      margin-top: 80px;
      font-size: 23px;
    }

    .countdown small {
      font-size: 17px;
    }

    .countdown .clock {
      width: 142px;
      margin: 18px auto 0;
      overflow: hidden;
    }

    .countdown .clock span,
    .countdown .clock i {
      display: block;
      text-align: center;
      line-height: 34px;
      font-size: 23px;
      float: left;
    }

    .countdown .clock span {
      width: 34px;
      height: 34px;
      border-radius: 2px;
      background-color: #303430;
    }

    .countdown .clock i {
      width: 20px;
      font-style: normal;
    }
  </style>
</head>

<body>
  <div class="countdown">
    <p class="next">今天是2024年3月27日</p>
    <p class="title">下班倒计时</p>
    <p class="clock">
      <span id="hour"></span>
      <i>:</i>
      <span id="minutes"></span>
      <i>:</i>
      <span id="scond"></span>
    </p>
    <p class="tips">17:30:00 下班</p>
  </div>
  <script>
    function getCountTime() {
      // 得到当前的时间戳
      const now = +new Date()
      // 得到将来的时间戳
      const last = +new Date('2024-03-27 17:30:00')
      // 得到剩余的时间戳,转换为秒
      const count = (last - now) / 1000

      // 转换为时分秒
      let h = parseInt(count / 60 / 60 % 24)
      let m = parseInt(count / 60 % 60)
      let s = parseInt(count % 60)
      h = h < 10 ? '0' + h : h
      m = m < 10 ? '0' + m : m
      s = s < 10 ? '0' + s : s

      // 获取元素
      const hour = document.querySelector('#hour')
      const minutes = document.querySelector('#minutes')
      const scond = document.querySelector('#scond')
      hour.innerHTML = h
      minutes.innerHTML = m
      scond.innerHTML = s
    }
    
    getCountTime()
    setInterval(getCountTime, 1000)
  </script>
</body>

</html>

节点操作

DOM 节点是 DOM 结构的基本单元,包括:

  • 元素节点(Element Nodes)
  • 文本节点(Text Nodes)
  • 属性节点(Attribute Nodes)

查找节点

可以通过元素节点之间的关系查找元素节点。

查找父节点

使用元素的 parentNode 属性获取元素的父节点。如果元素没有父节点(例如,如果元素是 document 对象),则返回 null

例子:获取父节点

<body>
  <div class="dad">
    <div class="son"></div>
  </div>
  <script>
    const son = document.querySelector('.son')
    const dad = son.parentNode

    console.log(son)
    console.log(dad)
  </script>
</body>

例子:关闭广告

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      position: relative;
      margin: 30px auto;
      width: 300px;
      height: 100px;
      background-color: skyblue;
      text-align: center;
      line-height: 200px;
    }

    .boxIn {
      position: absolute;
      top: 10px;
      right: 10px;
      height: 20px;
      width: 20px;
      background-color: pink;
      text-align: center;
      line-height: 20px;
      cursor: pointer;
    }
  </style>
</head>

<body>
  <div class="box">
    <div class="boxIn">X</div>
  </div>
  <script>
    // 事件监听实现
    const boxIn = document.querySelector('.boxIn')
    boxIn.addEventListener('click', function () {
      this.parentNode.style.display = 'none'
    })
  </script>
</body>

</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>
  <style>
    .box {
      position: relative;
      margin: 30px auto;
      width: 300px;
      height: 100px;
      background-color: skyblue;
      text-align: center;
      line-height: 200px;
    }

    .boxIn {
      position: absolute;
      top: 10px;
      right: 10px;
      height: 20px;
      width: 20px;
      background-color: pink;
      text-align: center;
      line-height: 20px;
      cursor: pointer;
    }
  </style>
</head>

<body>
  <div class="box">
    <div class="boxIn">X</div>
  </div>
  <div class="box">
    <div class="boxIn">X</div>
  </div>
  <div class="box">
    <div class="boxIn">X</div>
  </div>
  <script>
    // 事件监听实现
    const boxIns = document.querySelectorAll('.boxIn')
    for (let i = 0; i < boxIns.length; i++) {
      boxIns[i].addEventListener('click', function () {
        this.parentNode.style.display = 'none'
      })   
    }
  </script>
</body>

</html>

查找子节点

有多种方法可以获取一个元素的子节点:

  • childNodes 属性返回一个包含指定元素的子节点的集合,包括元素节点、文本节点和注释节点。如果只想获取元素节点,需要遍历这个集合并检查每个节点的 nodeType 属性。
  • children 属性返回一个只包含指定元素的子元素节点的集合(HTMLCollection),不包括文本节点和注释节点。这是获取子元素节点的更直接和常用的方法。
  • firstChildlastChild 属性分别返回指定元素的第一个和最后一个子节点,包括所有类型的节点(元素节点、文本节点、注释节点)。

例子:获取子节点

<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ul>
  <script>
    const ul = document.querySelector('ul')
    console.log(ul.children)
  </script>
</body>

查找兄弟节点

可以使用 nextElementSiblingpreviousElementSibling 属性获取元素的兄弟元素,只返回元素节点,不包括文本节点或注释节点。

例子:获取兄弟节点

<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ul>
  <script>
    const li2 = document.querySelector('li:nth-child(2)')
    console.log(li2.previousElementSibling)
    console.log(li2.nextElementSibling)
  </script>
</body>

增加节点

增加节点(即向 DOM 中添加新的元素)通常涉及创建新元素,然后将其插入到 DOM 树的适当位置。

步骤:

  • 使用 document.createElement() 方法来创建一个新的元素节点。
  • 使用各种方法来设置新元素的属性(如 idclass 等)或内容(如文本或子元素)。
  • 使用多种方法可以将新元素插入到 DOM 中,包括:
    • 使用 appendChild() 方法将新元素添加到指定父元素的子元素列表的末尾。
    • 使用 insertBefore() 方法将新元素插入到参考元素之前。如果没有提供参考元素,则新元素会被添加到子元素列表的末尾(与 appendChild() 相同)。
    • 使用 replaceChild() 方法用新元素替换父元素中的一个子元素。

例子:增加节点

<body>
  <ul></ul>
  <script>
    const ul = document.querySelector('ul')
    const li1 = document.createElement('li')
    li1.innerHTML = 1
    ul.appendChild(li1)

    const li2 = document.createElement('li')
    li2.innerHTML = 2
    ul.insertBefore(li2, ul.children[0])
  </script>
</body>

例子:重构学成在线

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>学成在线首页</title>
    <link rel="stylesheet" href="./css/style.css">
    <style>

    </style>
</head>

<body>
    <div class="box w">
        <div class="box-hd">
            <h3>精品推荐</h3>
            <a href="#">查看全部</a>
        </div>
        <div class="box-bd">
            <ul class="clearfix">
            </ul>
        </div>
    </div>
    <script> 
        let data = [
            {
                src: 'images/course01.png',
                title: 'Think PHP 5.0 博客系统实战项目演练',
                num: 1125
            },
            {
                src: 'images/course02.png',
                title: 'Android 网络动态图片加载实战',
                num: 357
            },
            {
                src: 'images/course03.png',
                title: 'Angular2 大前端商城实战项目演练',
                num: 22250
            },
            {
                src: 'images/course04.png',
                title: 'Android APP 实战项目演练',
                num: 389
            },
            {
                src: 'images/course05.png',
                title: 'UGUI 源码深度分析案例',
                num: 124
            },
            {
                src: 'images/course06.png',
                title: 'Kami2首页界面切换效果实战演练',
                num: 432
            },
            {
                src: 'images/course07.png',
                title: 'UNITY 从入门到精通实战案例',
                num: 888
            },
            {
                src: 'images/course08.png',
                title: 'Cocos 深度学习你不会错过的实战',
                num: 590
            },
        ]
        
        const ul = document.querySelector('.box-bd ul')
        for (let i = 0; i < data.length; i++) {
            const li = document.createElement('li')
            li.innerHTML = `
                <a href="#">
                    <img src=${data[i].src} alt="">
                    <h4>
                        ${data[i].title}
                    </h4>
                    <div class="info">
                        <span>高级</span> • <span>${data[i].num}</span>人在学习
                    </div>
                </a>
            `
            ul.appendChild(li)
        }
    </script>
</body>

</html>

克隆节点

使用 cloneNode() 方法克隆节点,这个方法接收一个布尔参数,决定了是执行浅拷贝(只克隆节点本身)还是深拷贝(克隆节点及其所有后代)。

  • 如果参数为 false 或省略,将执行浅拷贝(只克隆节点本身,不克隆其后代)。
  • 如果参数为 true,将执行深拷贝(克隆节点及其所有后代)。

注意:

  • 克隆节点时,一些属性如 idname 可能会自动改变,以避免在 DOM 中出现重复的 ID 或名称。
  • 如果执行深拷贝,事件监听器不会被自动复制到克隆的节点上。如果需要复制事件监听器,需要手动为克隆的节点添加事件监听器。
  • 深拷贝可能会消耗更多的资源,特别是当被克隆的节点包含大量后代元素时。因此,在不需要克隆所有后代的情况下,最好使用浅拷贝。

例子:克隆节点并追加节点

<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
  <script>
    const ul = document.querySelector('ul')
    ul.appendChild(ul.children[0].cloneNode(true))
  </script>
</body>

删除节点

可以使用 removeChild() 方法删除节点,该方法属于父元素,用于从其子元素列表中移除指定的子元素。

步骤:

  • 获取删除节点的父节点。
  • 使用父节点的 removeChild() 方法删除该节点。
<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
  <script>
    const ul = document.querySelector('ul')
    ul.removeChild(ul.children[0])
  </script>
</body>

例子:学生信息管理

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>学生信息管理</title>
  <link rel="stylesheet" href="css/index.css" />
</head>

<body>
  <h1>新增学员</h1>
  <form class="info" autocomplete="off">
    姓名:<input type="text" class="uname" name="uname" />
    年龄:<input type="text" class="age" name="age" />
    性别:
    <select name="gender" class="gender">
      <option value=""></option>
      <option value=""></option>
    </select>
    薪资:<input type="text" class="salary" name="salary" />
    就业城市:<select name="city" class="city">
      <option value="北京">北京</option>
      <option value="上海">上海</option>
      <option value="广州">广州</option>
      <option value="深圳">深圳</option>
      <option value="曹县">曹县</option>
    </select>
    <button class="add">录入</button>
  </form>

  <h1>就业榜</h1>
  <table>
    <thead>
      <tr>
        <th>学号</th>
        <th>姓名</th>
        <th>年龄</th>
        <th>性别</th>
        <th>薪资</th>
        <th>就业城市</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <!-- <tr>
        <td>1001</td>
        <td>欧阳霸天</td>
        <td>19</td>
        <td>男</td>
        <td>15000</td>
        <td>上海</td>
        <td>
          <a href="javascript:">删除</a>
        </td>
      </tr> -->
    </tbody>
  </table>
  <script>
    // 获取元素
    const uname = document.querySelector('.uname')
    const age = document.querySelector('.age')
    const gender = document.querySelector('.gender')
    const salary = document.querySelector('.salary')
    const city = document.querySelector('.city')
    const tbody = document.querySelector('tbody')
    // 获取带有name属性的元素
    const items = document.querySelectorAll('[name]')

    // 声明一个空数组
    const arr = []

    // 1. 录入模块
    // 1.1 表单提交事件
    const info = document.querySelector('.info')
    info.addEventListener('submit', function (e) {
      // 阻止默认行为,不跳转
      e.preventDefault()

      // 表单验证
      // 先遍历循环
      for (let i = 0; i < items.length; i++) {
        if (items[i].value === '') {
          return alert('输入内容不能为空')
        }
      }

      // 创建新对象,用于存储表单数据
      const obj = {
        stuId: arr.length + 1,
        uname: uname.value,
        age: age.value,
        gender: gender.value,
        salary: salary.value,
        city: city.value
      }
      // 追加给数组
      arr.push(obj)

      // 清空表单
      this.reset()

      // 调用渲染函数
      render()
    })

    // 2. 渲染数据
    function render() {
      // 先清空tbody
      tbody.innerHTML = ''

      // 遍历数组
      for (let i = 0; i < arr.length; i++) {
        // 生成tr
        const tr = document.createElement('tr')
        tr.innerHTML = `        
          <td>${arr[i].stuId}</td>
          <td>${arr[i].uname}</td>
          <td>${arr[i].age}</td>
          <td>${arr[i].gender}</td>
          <td>${arr[i].salary}</td>
          <td>${arr[i].city}</td>
          <td>
            <a href="javascript:" data-id=${i}>删除</a>
          </td>
        `

        // 追加元素
        tbody.appendChild(tr)
      }
    }

    // 3. 删除数据
    // 3.1 事件委托 tbody
    tbody.addEventListener('click', function (e) {
      if (e.target.tagName === 'A') {
        // 删除数组中对应的数据
        arr.splice(e.target.dataset.id, 1)
        // 重新渲染
        render()
      }
    })
  </script>

</body>

</html>

移动端事件

在 JavaScript 中,处理移动端事件(如触摸事件)与处理桌面端事件(如鼠标事件)有所不同。移动设备(如智能手机和平板电脑)主要依赖触摸界面,因此需要使用特定的触摸事件来处理用户交互。

以下是一些常见的移动端触摸事件:

  • touchstart:当用户的手指触摸屏幕时触发。
  • touchmove:当用户在屏幕上移动手指时触发。
  • touchend:当用户从屏幕上移开手指时触发。
  • touchcancel:当系统停止跟踪触摸时触发,这可能是因为触摸事件被中断(例如,电话来电)。

这些事件对象会提供一个 touches 数组,其中包含屏幕上当前所有触摸点的信息。每个触摸点都是一个 Touch 对象,包含诸如位置(clientXclientY)、标识符(identifier)和目标元素(target)等信息。

Swiper

Swiper 是一款功能强大且高度灵活的移动端触摸滑动插件,专为现代移动设备设计。它可以帮助开发者轻松地在移动网页或应用中实现流畅的滑动效果,提供用户友好的交互体验。

以下是关于 Swiper 的一些主要特点和优势:

  1. 高度可定制性:Swiper 提供了丰富的配置选项和 API 接口,允许开发者根据具体需求调整滑动效果、动画速度、触摸敏感度等参数,实现个性化的滑动体验。
  2. 跨平台兼容性:Swiper 支持多种移动设备和平台,包括 iOS、Android 以及各类浏览器,确保在各种设备上都能提供一致的滑动效果。
  3. 多种滑动模式:Swiper 支持水平滑动、垂直滑动以及自由滑动等多种模式,满足不同场景下的滑动需求。
  4. 丰富的交互效果:除了基本的滑动功能,Swiper 还提供了视差滚动、3D 效果、分页器、导航点等高级功能,为滑动交互增添更多可能性。
  5. 易于集成和使用:Swiper 的代码结构清晰,文档完善,易于集成到现有的项目中。开发者只需引入相关文件,并进行简单的配置,即可快速实现滑动功能。
  6. 性能优化:Swiper 在性能上进行了优化,确保在滑动过程中流畅无卡顿,提供出色的用户体验。
  7. 良好的社区支持:Swiper 拥有庞大的用户群体和活跃的社区,开发者可以在社区中分享经验、解决问题,获取最新的插件更新和资讯。

总之,Swiper 是一款功能强大、易于使用的移动端触摸滑动插件,适合各类移动端项目的开发需求。无论是简单的图片轮播、菜单导航,还是复杂的交互展示,Swiper 都能提供出色的解决方案。

BOM

BOM(Browser Object Model)是浏览器对象模型,主要包含以下对象:

  • Window 对象
    • 代表浏览器窗口或标签页。
    • 提供了很多方法和属性,用于控制浏览器窗口的显示和交互。
    • 是全局对象,使用 var 声明的变量和函数都会自动成为 window 对象的成员。
  • Location 对象
    • 表示 URL,可以用来解析 URL 的各个部分(如协议、主机名、端口、路径、查询字符串等)。
    • 提供了方法,用于重新加载当前页面或导航到新的 URL。
  • Navigator 对象
    • 包含了有关浏览器的信息。
    • 提供了浏览器名称、版本、平台等信息。
  • Screen 对象
    • 提供了有关客户端屏幕的信息。
    • 包含了屏幕宽度、高度和像素深度等属性。
  • History 对象
    • 与浏览器的历史记录相关。
    • 提供了方法,用于向前或向后导航,或检查历史记录中的条目数。
  • Document 对象
    • 虽然 DOM(文档对象模型)和 BOM 是分开的,但 document 对象经常与 BOM 一起讨论,因为它代表了加载在浏览器窗口中的网页文档。
    • 通过 DOM API,可以查询和修改网页的内容。

延迟定时器

延迟定时器是通过 setTimeout() 函数实现的。这个函数允许在指定的毫秒数之后执行一个函数或一段代码。setTimeout() 返回一个表示定时器的 ID,这个 ID 可以用来在需要的时候取消定时器(通过 clearTimeout() 函数)。

语法:

let timerId = setTimeout(func, delay, [param1, param2, ...]);

其中:

  • func:要执行的函数。
  • delay:延迟的毫秒数(1000毫秒等于1秒)。
  • [param1, param2, ...]:可选参数,这些参数将作为 func 函数的参数。

例子:延迟输出

setTimeout(function() {  
  console.log("Hello, World!");  
}, 2000)

例子:延时关闭广告

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    img {
      position: fixed;
      left: 0;
      bottom: 0;
    }
  </style>
</head>
<body>
  <img src="./images/ad.png" alt="">
  <script>
    const img  = document.querySelector('img')
    setTimeout(() => {
      img.style.display = 'none'
    }, 3000);
  </script>
</body>
</html>

执行机制

JavaScript 的执行机制主要涉及到事件循环(Event Loop)和任务队列(Task Queue)。这是 JavaScript 异步编程模型的核心,使得 JavaScript 能够在单线程环境下处理多任务。

JavaScript 是单线程的,这意味着它一次只能处理一个任务。虽然这在一定程度上限制了 JavaScript 的性能,但它也简化了编程模型,避免了多线程编程中常见的复杂问题,如同步和线程安全。

JavaScript 通过异步编程模型解决单线程可能导致的性能问题。因此 JavaScript 中的任务可以是同步的,也可以是异步的。同步任务会按照代码的顺序依次执行,而异步任务则会在稍后的某个时间点执行,不会阻塞代码的执行。

JavaScript 提供了多种方式来实现异步编程,比如回调函数、Promises、async/await 等。异步操作(如 setTimeout、AJAX 请求等)不会立即执行,而是会在稍后某个时间点完成。

事件循环是 JavaScript 运行时环境(如浏览器或 Node.js)中的一个核心机制。它负责监听和调度任务,确保它们能够按照正确的顺序执行。

事件循环的工作流程大致如下:

  • 首先,执行栈(Call Stack)会执行同步任务。
  • 当遇到异步任务(如 setTimeout、Promise、事件监听器等)时,事件循环会将这些任务放到任务队列(Task Queue)中等待。
  • 当执行栈为空时(即没有更多的同步任务需要执行),事件循环会查看任务队列。如果队列中有任务,它会将队列中的第一个任务取出并放入执行栈中执行。这个过程会不断重复,形成一个循环。

除了任务(MacroTask)队列之外,还有一个微任务(MicroTask)队列。Promise 的回调函数(.then())会被放到微任务队列中。在每次事件循环的末尾,JavaScript 引擎会先检查微任务队列,执行完所有的微任务后再检查任务队列。这意味着微任务总是优先于任务队列中的任务执行。

总结来说,JavaScript 的执行机制是通过事件循环和任务队列来实现的。它允许 JavaScript 在单线程环境下处理异步任务,并通过微任务队列来确保某些任务(如 Promise 的回调函数)能够优先执行。这种机制使得 JavaScript 能够高效地处理大量并发任务,而不会导致页面或应用程序的卡顿或崩溃。

例子:事件循环

console.log(1)
setTimeout(() => {
  console.log(2)
}, 0)
console.log(3)

// 输出:1 3 2

例子:事件循环

console.log(1)
setTimeout(() => {
  console.log(2)
}, 0)
const p = new Promise((resolve,reject) => {
  console.log(3)
  resolve(4)
})
p.then(result => {
  console.log(result)
})
console.log(5)

// 输出:1 3 5 4 2

例子:事件循环

// 同步代码,立即执行
console.log(1)
// 异步代码,进入宏任务队列排队
setTimeout(() => {
  console.log(2)
  const p = new Promise(resolve => resolve(3))
  // 以上代码执行后,才会将异步代码 then(),进入微任务队列排队
  p.then(result => console.log(result))
}, 0)
// Promise 为同步代码,立即执行
const p = new Promise(resolve => {
  // 异步代码,进入宏任务队列排队
  setTimeout(() => {
    console.log(4)
  }, 0)
  resolve(5)
})
// then() 为异步代码,进入微任务队列排队
p.then(result => console.log(result))
// Promise 为同步代码,立即执行
const p2 = new Promise(resolve => resolve(6))
// then() 为异步代码,进入微任务队列排队
p2.then(result => console.log(result))
// 同步代码,立即执行
console.log(7)

// 输出:1 7 5 6 2 3 4

Location 对象

JavaScript 的 location 对象提供了与当前窗口中加载的文档的 URL 相关的信息,并且提供了一些方法来导航和重新加载浏览器窗口中的 URL。这个对象通常用于获取 URL 的各个组成部分,或者用于更改浏览器的地址栏和重新加载页面。

属性:

  • location.href:获取或设置整个 URL。当设置这个属性时,浏览器会立即导航到新的 URL。
  • location.protocol:获取 URL 的协议部分(例如 "http:" 或 "https:")。
  • location.host:获取 URL 的主机名和端口号(如果有的话)。
  • location.hostname:获取 URL 的主机名。
  • location.port:获取 URL 的端口号。
  • location.pathname:获取 URL 的路径名。
  • location.search:获取 URL 的查询字符串部分(即 "?" 后面的部分)。
  • location.hash:获取 URL 的哈希(即 "#" 后面的部分)。
  • location.origin:获取 URL 的协议、主机名和端口号。

方法:

  • location.reload():重新加载当前页面。如果传递一个 true 参数,它会从服务器强制重新加载页面,而不是从缓存中加载。
  • location.replace(url):用新的 URL 替换当前页面,历史记录列表中不会生成新的记录。
  • location.assign(url):加载新的 URL,并在历史记录列表中生成新的记录。这与直接设置 location.href 是等效的。

例子:延时跳转到指定页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    span {
      color: red;
    }
  </style>
</head>
<body>
  <a href="https://stonecoding.net">支付成功<span>5</span>秒后跳转到首页</a>
  <script>
    const a = document.querySelector('a')
    let num = 5
    let timerId = setInterval(() => {
      num--
      a.innerHTML = `支付成功<span>${num}</span>秒后跳转到首页`
      if (num === 0) {
        clearInterval(timerId)
        location.href = 'https://stonecoding.net'
      }
    }, 1000);
  </script>
</body>
</html>

JavaScript 的 navigator 对象包含了有关浏览器的信息。通过这个对象,开发人员可以获取到浏览器的名称、版本、操作系统等信息,以便根据不同的浏览器环境执行相应的操作。

navigator 对象的一些常用属性包括:

  • navigator.appName:返回浏览器的名称。尽管在大多数现代浏览器中,这个属性的值都是 "Netscape",但它仍然可以被用来识别浏览器类型。
  • navigator.appVersion:返回浏览器的版本信息。这个字符串通常包含了浏览器的名称、版本、渲染引擎等详细信息。
  • navigator.userAgent:返回用户代理头的字符串表示。这个字符串通常包含了浏览器的名称、版本、操作系统、设备类型等信息,对于进行浏览器兼容性和特性检测非常有用。
  • navigator.language:返回浏览器的主要语言。
  • navigator.platform:返回运行浏览器的操作系统或平台。
  • navigator.plugins:返回一个类似数组的对象,包含了浏览器安装的所有插件的信息。通过这个属性,可以检测浏览器是否支持特定的插件,如 Flash 或 Java。

值得注意的是,navigator 对象的一些属性可能会被用户或浏览器设置修改,因此不能完全依赖它们来进行功能性的决策。此外,不同的浏览器可能会对某些属性的实现有所不同,因此在使用这些属性时需要进行充分的测试和兼容性检查。

例子:根据浏览器类型显示不同页面

<body>
  这里是 PC 端页面
  <script>
    (function () {
      const userAgent = navigator.userAgent
      const android = userAgent.match(/(Android);?[\s\/]+([\d.]+)?/)
      const iphone = userAgent.match(/(iPhone\sOS)\s([\d_]+)/)
      if (android || iphone) {
        location.href = 'https://stonecoding.net'
      }
    })();
  </script>
</body>

History 对象

JavaScript 的 history 对象提供了与浏览器的会话历史记录(即用户访问过的页面)进行交互的接口。通过这个对象,可以编程式地导航到用户历史记录中的不同页面,而不必依赖于浏览器提供的用户界面元素(如前进和后退按钮)。

history 对象的一些常用方法包括:

  • history.back():加载历史记录列表中的前一个 URL。这与浏览器的后退按钮功能相同。
  • history.forward():加载历史记录列表中的下一个 URL。这与浏览器的前进按钮功能相同。
  • history.go(n):加载历史记录列表中的特定页面,其中 n 是相对于当前页面的索引。如果 n 是负数,则后退到历史记录中的页面;如果 n 是正数,则前进到历史记录中的页面。如果 n 是 0 或未指定,则重新加载当前页面。
  • history.pushState(stateObj, title, url):将新的状态添加到历史记录中,但不会触发页面加载。这个方法允许你在不重新加载页面的情况下修改浏览器的 URL。
  • history.replaceState(stateObj, title, url):替换历史记录中的当前条目,而不是添加新条目。与 pushState 类似,它允许你修改当前页面的 URL,但不会重新加载页面。

history 对象还有一个 length 属性,它表示历史记录列表中的 URL 数量(包括当前加载的页面)。

注意,出于安全原因,history 对象的方法不会触发 load 事件,也不会重新运行页面的 JavaScript 代码。它们只是简单地改变了浏览器的地址栏和用户的会话历史记录。

例子:前进和后退按钮

<body>
  <button>后退</button>
  <button>前进</button>
  <script>
    const back = document.querySelector('button:first-child')
    const forward = back.nextElementSibling
    back.addEventListener('click', function () {
      // history.back()
      history.go(-1)
    })

    forward.addEventListener('click', function () {
      // history.forward()
      history.go(1)
    })
  </script>
</body>

本地存储

可以将浏览器数据存储到本地,以便在用户关闭和重新打开浏览器后仍然能够访问这些数据。

提供了两种在浏览器中存储数据的方式:localStoragesessionStorage

  • localStorage:存储的数据没有过期时间,数据会一直保留,直到用户手动删除或通过 JavaScript 代码删除。即使关闭浏览器窗口或重启浏览器,数据也不会丢失。
  • sessionStorage:存储的数据只在单个会话中有效,即当用户关闭浏览器窗口后数据会被清除。

localStorage 的方法有:

  • setItem(key, value)
    • 这个方法用于存储一个键值对。
    • key 是想要存储的键名。
    • value 是与键名相关联的值。
    • 示例:localStorage.setItem('username', 'JohnDoe');
  • getItem(key)
    • 这个方法用于根据键名获取存储的值。
    • 如果键不存在,则返回 null
    • 示例:let username = localStorage.getItem('username');
  • removeItem(key)
    • 这个方法用于删除指定的键值对。
    • key 是想要删除的键名。
    • 示例:localStorage.removeItem('username');
  • key(index)
    • 这个方法用于获取指定索引位置的键名。
    • 索引从 0 开始。
    • 如果没有更多的键,则返回 null
    • 示例:let firstKey = localStorage.key(0);
  • length
    • 这是一个属性,用于获取存储的键值对的数量。
    • 示例:let count = localStorage.length;
  • clear()
    • 这个方法用于删除 localStorage 中的所有数据。
    • 示例:localStorage.clear();

请注意,localStorage 中的数据是以字符串形式存储的。如果存储了一个非字符串值(如数字或对象),会被自动转换为字符串。当从 localStorage 中检索值时,可能需要将其转换回原始数据类型。例如,如果存储了一个数字,当检索它时,将得到一个字符串,需要使用 parseInt()parseFloat() 来将其转换回数字。

此外,由于 localStorage 是同步的,它可能会阻塞页面渲染,特别是在处理大量数据时。因此,在处理大量数据时,请务必注意性能问题。

为了存储复杂数据类型,需要使用 JSON.stringify() 方法将对象或数组转换为 JSON 格式的字符串,然后存储这个字符串。当从 localStorage 中检索数据时,再使用 JSON.parse() 方法将 JSON 字符串转换回原始的对象或数组。

例子:存取对象

<body>
  <script>
    const obj = {
      name: 'stone',
      age: 18,
      gender: 'male'
    }

    localStorage.setItem('obj', JSON.stringify(obj))
    console.log(localStorage.getItem('obj'))
    console.log(JSON.parse(localStorage.getItem('obj')))
  </script>
</body>

例子:学生就业统计表

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>学生就业统计表</title>
  <link rel="stylesheet" href="./iconfont/iconfont.css">
  <link rel="stylesheet" href="css/index.css" />
</head>

<body>
  <h1>学生就业统计表</h1>
  <form class="info" autocomplete="off">
    <input type="text" class="uname" name="uname" placeholder="姓名" />
    <input type="text" class="age" name="age" placeholder="年龄" />
    <input type="text" class="salary" name="salary" placeholder="薪资" />
    <select name="gender" class="gender">
      <option value=""></option>
      <option value=""></option>
    </select>
    <select name="city" class="city">
      <option value="北京">北京</option>
      <option value="上海">上海</option>
      <option value="广州">广州</option>
      <option value="深圳">深圳</option>
      <option value="曹县">曹县</option>
    </select>
    <button class="add">
      <i class="iconfont icon-tianjia"></i>添加
    </button>
  </form>

  <div class="title">共有数据<span>0</span></div>
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>姓名</th>
        <th>年龄</th>
        <th>性别</th>
        <th>薪资</th>
        <th>就业城市</th>
        <th>录入时间</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>
  <script>
    // 渲染
    const arr = JSON.parse(localStorage.getItem('data')) || []
    const tbody = document.querySelector('tbody')
    function render () {
      const trArr = arr.map(function (item, index) {
        return `
          <tr>
            <td>${item.stuId}</td>
            <td>${item.uname}</td>
            <td>${item.age}</td>
            <td>${item.gender}</td>
            <td>${item.salary}</td>
            <td>${item.city}</td>
            <td>${item.time}</td>
            <td>
              <a href="javascript:" data-id="${index}">
                <i class="iconfont icon-shanchu"></i>
                删除
              </a>
            </td>
          </tr>
        `
      })

      tbody.innerHTML = trArr.join()
      document.querySelector('.title span').innerHTML = arr.length
    }
    render()

    // 新增业务
    const info = document.querySelector('.info')
    const uname = document.querySelector('.uname')
    const age = document.querySelector('.age')
    const salary = document.querySelector('.salary')
    const gender = document.querySelector('.gender')
    const city = document.querySelector('.city')
    info.addEventListener('submit', function (e) {
      e.preventDefault()

      if (!uname.value || !age.value || !salary.value) {
        return alert('输入内容不能为空!')
      }

      arr.push({
        stuId: arr.length ? arr[arr.length - 1].stuId + 1 : 1,
        uname: uname.value,
        age: age.value,
        salary: salary.value,
        gender: gender.value,
        city: city.value,
        time: new Date().toLocaleString()
      })

      render()

      // 重置表单
      this.reset()

      // 存储数据到本地
      localStorage.setItem('data', JSON.stringify(arr))
    })

    // 删除业务,采用事件委托形式,给 tbody 注册点击事件
    tbody.addEventListener('click', function (e) {
      if (e.target.tagName === 'A') {
        if (confirm('确认删除')) {
          arr.splice(e.target.dataset.id, 1)
          render()
          localStorage.setItem('data', JSON.stringify(arr))
        }
      }
    })
  </script>
</body>

</html>

正则表达式

JavaScript 中的正则表达式(RegExp)是一种强大的工具,用于在字符串中执行匹配和搜索操作。正则表达式使用特定的模式来描述和匹配文本。

语法:

const 变量名 = /表达式/

在 JavaScript 正则表达式中,元字符(metacharacters)是那些具有特殊含义的字符。它们不表示其字面值,而是表示其他字符或定义字符集、边界或匹配模式。包括:

字符类元字符

  • .:匹配除了换行符(\n\r\u2028\u2029)之外的任何单个字符。
  • [...]:字符集,匹配方括号中的任何字符。例如,[abc] 匹配 'a''b''c'
  • [^...]:否定字符集,匹配任何不在方括号中的字符。
  • \d:匹配任何数字字符,等同于 [0-9]
  • \D:匹配任何非数字字符,等同于 [^0-9]
  • \w:匹配任何单词字符(字母、数字或下划线),等同于 [a-zA-Z0-9_]
  • \W:匹配任何非单词字符,等同于 [^a-zA-Z0-9_]
  • \s:匹配任何空白字符,包括空格、制表符、换页符等。
  • \S:匹配任何非空白字符。

量词元字符

  • *:匹配前面的子表达式零次或多次。
  • +:匹配前面的子表达式一次或多次。
  • ?:匹配前面的子表达式零次或一次。
  • {n}:匹配前面的子表达式恰好 n 次。
  • {n,}:匹配前面的子表达式至少 n 次。
  • {n,m}:匹配前面的子表达式至少 n 次,但不超过 m 次。

边界元字符

  • ^:匹配输入字符串的开始位置。如果在多行模式中使用,则匹配每一行的开始。
  • $:匹配输入字符串的结束位置。如果在多行模式中使用,则匹配每一行的结束。
  • \b:匹配一个单词边界,即字与空格间的位置。
  • \B:匹配非单词边界的位置。

分组和引用元字符

  • (...):捕获括号,匹配括号内的子表达式,并记住匹配的文本,以便后面引用。
  • (?:...):非捕获括号,只进行分组,不记住匹配的文本。
  • \n:在捕获括号内匹配第 n 个捕获到的子表达式,其中 n 是一个正整数。例如,\1 引用第一个捕获的括号。

断言元字符

  • (?=...):正向先行断言,表示后面必须跟着某个模式,但不会消耗任何字符。
  • (?!...):负向先行断言,表示后面不能跟着某个模式。
  • (?<=...):正向后发断言,表示前面必须跟着某个模式,但不会消耗任何字符。
  • (?<!...):负向后发断言,表示前面不能跟着某个模式。

转义字符

  • \:对特殊字符进行转义,使其成为普通字符。例如,\. 匹配点字符(.)。

修饰符

  • g:全局搜索。
  • i:不区分大小写搜索。
  • m:多行搜索。
  • u:启用全Unicode模式。
  • y:执行“粘性”搜索, 只匹配从目标字符串的当前位置开始可以匹配的字符串。

可以使用正则表达式来执行各种操作,如匹配、搜索和替换字符串中的文本。包括:

  • test():测试字符串中是否存在匹配项。
let regex = /foo/;  
let str = 'foobar';  
console.log(regex.test(str)); // 输出:true
  • match():在字符串中搜索匹配项,并返回所有匹配项的数组。
let regex = /o/g;  
let str = 'foobar';  
console.log(str.match(regex)); // 输出:['o', 'o']
  • search():在字符串中搜索匹配项,并返回第一个匹配项的索引。
let regex = /o/;  
let str = 'foobar';  
console.log(str.search(regex)); // 输出:1
  • replace():在字符串中替换匹配项。
let regex = /o/g;  
let str = 'foobar';  
console.log(str.replace(regex, 'x')); // 输出:'fxxbar'
  • exec():在字符串中执行匹配操作,并返回一个包含匹配信息的数组。
let regex = /o/g;  
let str = 'foobar';  
let result = regex.exec(str);  
console.log(result); // 输出:['o', index: 1, input: 'foobar', groups: undefined]

综合案例

例子:注册页面

image-20240401110652093

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>小兔鲜儿 - 新鲜 惠民 快捷!</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="renderer" content="webkit">
  <link rel="shortcut icon" href="./favicon.ico">
  <link rel="stylesheet" href="./css/common.css">
  <link rel="stylesheet" href="./css/register.css">
  <link rel="stylesheet" href="https://at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
</head>

<body>
  <!-- 项部导航 -->
  <div class="xtx_topnav">
    <div class="wrapper">
      <!-- 顶部导航 -->
      <ul class="xtx_navs">
        <li>
          <a href="./login.html">请先登录</a>
        </li>
        <li>
          <a href="./register.html">免费注册</a>
        </li>
        <li>
          <a href="./center-order.html">我的订单</a>
        </li>
        <li>
          <a href="./center.html">会员中心</a>
        </li>
        <li>
          <a href="javascript:;">帮助中心</a>
        </li>
        <li>
          <a href="javascript:;">在线客服</a>
        </li>
        <li>
          <a href="javascript:;">
            <i class="mobile sprites"></i>
            手机版
          </a>
        </li>
      </ul>
    </div>
  </div>
  <!-- 头部 -->
  <div class="xtx_header">
    <div class="wrapper">
      <!-- 网站Logo -->
      <h1 class="xtx_logo"><a href="/">小兔鲜儿</a></h1>
      <!-- 主导航 -->
      <div class="xtx_navs">
        <ul class="clearfix">
          <li>
            <a href="./index.html">首页</a>
          </li>
          <li>
            <a href="./category01.html">生鲜</a>
          </li>
          <li>
            <a href="./category01.html">美食</a>
          </li>
          <li>
            <a href="./category01.html">餐厨</a>
          </li>
          <li>
            <a href="./category01.html">电器</a>
          </li>
          <li>
            <a href="./category01.html">居家</a>
          </li>
          <li>
            <a href="./category01.html">洗护</a>
          </li>
          <li>
            <a href="./category01.html">孕婴</a>
          </li>
          <li>
            <a href="./category01.html">服装</a>
          </li>
        </ul>
      </div>
      <!-- 站内搜索 -->
      <div class="xtx_search clearfix">
        <!-- 购物车 -->
        <a href="./cart-none.html" class="xtx_search_cart sprites">
          <i>2</i>
        </a>
        <!-- 搜索框 -->
        <div class="xtx_search_wrapper">
          <input type="text" placeholder="搜一搜" onclick="location.href='./search.html'">
        </div>
      </div>
    </div>
  </div>
  <div class="xtx-wrapper">
    <div class="container">
      <!-- 卡片 -->
      <div class="xtx-card">
        <h3>新用户注册</h3>
        <form class="xtx-form">
          <div data-prop="username" class="xtx-form-item">
            <span class="iconfont icon-zhanghao"></span>
            <input name="username" type="text" placeholder="设置用户名称">
            <span class="msg"></span>
          </div>
          <div data-prop="phone" class="xtx-form-item">
            <span class="iconfont icon-shouji"></span>
            <input name="phone" type="text" placeholder="输入手机号码  ">
            <span class="msg"></span>
          </div>
          <div data-prop="code" class="xtx-form-item">
            <span class="iconfont icon-zhibiaozhushibiaozhu"></span>
            <input name="code" type="text" placeholder="短信验证码">
            <span class="msg"></span>
            <a class="code" href="javascript:;">发送验证码</a>
          </div>
          <div data-prop="password" class="xtx-form-item">
            <span class="iconfont icon-suo"></span>
            <input name="password" type="password" placeholder="设置6至20位字母、数字和符号组合">
            <span class="msg"></span>
          </div>
          <div data-prop="confirm" class="xtx-form-item">
            <span class="iconfont icon-suo"></span>
            <input name="confirm" type="password" placeholder="请再次输入上面密码">
            <span class="msg"></span>
          </div>
          <div class="xtx-form-item pl50">
            <i class="iconfont icon-queren"></i>
            已阅读并同意<i>《用户服务协议》</i>
          </div>
          <div class="xtx-form-item">
            <button class="submit">下一步</button>
            <!-- <a class="submit" href="javascript:;">下一步</a> -->
          </div>
        </form>
      </div>
    </div>
  </div>
  <!-- 公共底部 -->
  <div class="xtx_footer clearfix">
    <div class="wrapper">
      <!-- 联系我们 -->
      <div class="contact clearfix">
        <dl>
          <dt>客户服务</dt>
          <dd class="chat">在线客服</dd>
          <dd class="feedback">问题反馈</dd>
        </dl>
        <dl>
          <dt>关注我们</dt>
          <dd class="weixin">公众号</dd>
          <dd class="weibo">微博</dd>
        </dl>
        <dl>
          <dt>下载APP</dt>
          <dd class="qrcode">
            <img src="./uploads/qrcode.jpg">
          </dd>
          <dd class="download">
            <span>扫描二维码</span>
            <span>立马下载APP</span>
            <a href="javascript:;">下载页面</a>
          </dd>
        </dl>
        <dl>
          <dt>服务热线</dt>
          <dd class="hotline">
            400-0000-000
            <small>周一至周日 8:00-18:00</small>
          </dd>
        </dl>
      </div>
    </div>
    <!-- 其它 -->
    <div class="extra">
      <div class="wrapper">
        <!-- 口号 -->
        <div class="slogan">
          <a href="javascript:;" class="price">价格亲民</a>
          <a href="javascript:;" class="express">物流快捷</a>
          <a href="javascript:;" class="quality">品质新鲜</a>
        </div>
        <!-- 版权信息 -->
        <div class="copyright">
          <p>
            <a href="javascript:;">关于我们</a>
            <a href="javascript:;">帮助中心</a>
            <a href="javascript:;">售后服务</a>
            <a href="javascript:;">配送与验收</a>
            <a href="javascript:;">商务合作</a>
            <a href="javascript:;">搜索推荐</a>
            <a href="javascript:;">友情链接</a>
          </p>
          <p>CopyRight &copy; 小兔鲜儿</p>
        </div>
      </div>
    </div>
  </div>
  <script>
    // 短信验证码模块
    const code = document.querySelector('.code')
    let flag = true
    code.addEventListener('click', function () {
      if (flag) {
        flag = false
        let i = 5
        code.innerHTML = `0${i}秒后自动获取`
        let timeId = setInterval(() => {
          i--
          code.innerHTML = `0${i}秒后自动获取`
          if (i === 0) {
            clearInterval(timeId)
            code.innerHTML = '重新获取'
            flag = true
          }
        }, 1000);
      }
    })

    // 验证用户名
    const username = document.querySelector('[name=username]')
    username.addEventListener('change', verifyName)
    function verifyName() {
      const span = username.nextElementSibling
      const reg = /^[a-zA-Z0-9_]{6,10}$/
      if (!reg.test(username.value)) {
        span.innerHTML = '请输入 6-10 位字符'
        return false
      }
      span.innerHTML = ''
      return true
    }

    // 验证手机号
    const phone = document.querySelector('[name=phone]')
    phone.addEventListener('change', verifyPhone)
    function verifyPhone() {
      const span = phone.nextElementSibling
      const reg = /^1[3-9]\d{9}$/
      if (!reg.test(phone.value)) {
        span.innerHTML = '请输入正确的手机号'
        return false
      }
      span.innerHTML = ''
      return true
    }

    // 验证验证码
    const codeInput = document.querySelector('[name=code]')
    codeInput.addEventListener('change', verifyCode)
    function verifyCode() {
      const span = codeInput.nextElementSibling
      const reg = /^\d{6}$/
      if (!reg.test(codeInput.value)) {
        span.innerHTML = '请输入 6 位数字'
        return false
      }
      span.innerHTML = ''
      return true
    }

    // 验证密码
    const password = document.querySelector('[name=password]')
    password.addEventListener('change', verifyPassword)
    function verifyPassword() {
      const span = password.nextElementSibling
      const reg = /^[a-zA-Z0-9_]{6,20}$/
      if (!reg.test(password.value)) {
        span.innerHTML = '请输入 6-20 位字母、数字和符号组合'
        return false
      }
      span.innerHTML = ''
      return true
    }

    // 验证再次输入的密码
    const confirm = document.querySelector('[name=confirm]')
    confirm.addEventListener('change', verifyConfirm)
    function verifyConfirm() {
      const span = confirm.nextElementSibling
      if (confirm.value !== password.value) {
        span.innerHTML = '两次输入密码不一致'
        return false
      }
      span.innerHTML = ''
      return true
    }

    // 同意协议模块
    const queren = document.querySelector('.icon-queren')
    queren.addEventListener('click', function () {
      this.classList.toggle('icon-queren2')
    })

    // 提交模块
    const form = document.querySelector('form')
    form.addEventListener('submit', function (e) {
      if (!queren.classList.contains('icon-queren2')) {
        alert('请勾选同意协议')
        e.preventDefault()
      }

      if (!verifyName()) e.preventDefault()
      if (!verifyPhone()) e.preventDefault()
      if (!verifyCode()) e.preventDefault()
      if (!verifyPassword()) e.preventDefault()
      if (!verifyConfirm()) e.preventDefault()
    })
  </script>
</body>

</html>

例子:登录页面

image-20240401110748586

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>小兔鲜儿 - 新鲜 惠民 快捷!</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="renderer" content="webkit">
  <link rel="shortcut icon" href="../favicon.ico">
  <link rel="stylesheet" href="./css/common.css">
  <link rel="stylesheet" href="./css/login.css">
  <link rel="stylesheet" href="https://at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
</head>

<body>
  <!-- 登录头部 -->
  <div class="xtx-login-header">
    <h1 class="logo"></h1>
    <a class="home" href="./index.html">进入网站首页</a>
  </div>
  <!-- 登录内容 -->
  <div class="xtx-login-main">
    <div class="wrapper">
      <form action="">
        <div class="box">
          <div class="tab-nav">
            <a href="javascript:;" class="active" data-id="0">账户登录</a>
            <a href="javascript:;" data-id="1">二维码登录</a>
          </div>
          <div class="tab-pane">
            <div class="link">
              <a href="javascript:;">手机验证码登录</a>
            </div>
            <div class="input">
              <span class="iconfont icon-zhanghao"></span>
              <input required type="text" placeholder="请输入用户名称/手机号码" name="username">
            </div>
            <div class="input">
              <span class="iconfont icon-suo"></span>
              <input required type="password" placeholder="请输入密码" name="password">
            </div>
            <div class="agree">
              <label for="my-checkbox">
                <input type="checkbox" value="1" id="my-checkbox" class="remember" name="agree">
                <span class="iconfont icon-xuanze"></span>
              </label>
              我已同意 <a href="javascript:;">《服务条款》</a href="javascript:;"><a>《服务条款》</a>
            </div>
            <div class="button clearfix">
              <button type="submit" class="dl">登 录</button>
              <!-- <a class="dl" href="./center.html">登 录</a> -->
              <a class="fl" href="./forget.html">忘记密码?</a>
              <a class="fr" href="./register.html">免费注册</a>
            </div>
          </div>
          <div class="tab-pane" style="display: none;">
            <img class="code" src="./images/code.png" alt="">
          </div>
        </div>
      </form>
    </div>
  </div>
  <!-- 登录底部 -->
  <div class="xtx-login-footer">
    <!-- 版权信息 -->
    <div class="copyright">
      <p>
        <a href="javascript:;">关于我们</a>
        <a href="javascript:;">帮助中心</a>
        <a href="javascript:;">售后服务</a>
        <a href="javascript:;">配送与验收</a>
        <a href="javascript:;">商务合作</a>
        <a href="javascript:;">搜索推荐</a>
        <a href="javascript:;">友情链接</a>
      </p>
      <p>CopyRight &copy; 小兔鲜儿</p>
    </div>
  </div>
  <script>
    const tab_nav = document.querySelector('.tab-nav')
    const pane = document.querySelectorAll('.tab-pane')

    // 事件委托方式为被点击的标签添加 active 类,然后隐藏所有标签内容,再显示点击的标签内容
    tab_nav.addEventListener('click', function (e) {
      if (e.target.tagName === 'A') {
        tab_nav.querySelector('.active').classList.remove('active')
        e.target.classList.add('active')

        for (let i = 0; i < pane.length; i++) {
          pane[i].style.display = 'none'
        }

        pane[e.target.dataset.id].style.display = 'block'
      }
    })
    
    // 点击提交模块
    const form = document.querySelector('form')
    const agree = document.querySelector('[name=agree]')
    const username = document.querySelector('[name=username]')

    form.addEventListener('submit', function (e) {
      e.preventDefault()

      if (!agree.checked) {
        return alert("请勾选同意协议")
      }

      localStorage.setItem('xtx-uname', username.value)
      location.href = './index.html'
    })

  </script>
</body>

</html>

例子:首页

image-20240401110902658

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>小兔鲜儿 - 新鲜 惠民 快捷!</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="renderer" content="webkit">
  <link rel="shortcut icon" href="./favicon.ico">
  <link rel="stylesheet" href="./css/common.css">
  <link rel="stylesheet" href="./css/index.css">
  <link rel="stylesheet" href="https://at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
</head>

<body>
  <!-- 项部导航 -->
  <div class="xtx_topnav">
    <div class="wrapper">
      <!-- 顶部导航 -->
      <ul class="xtx_navs">
        <li>
          <a href="./login.html">请先登录</a>
        </li>
        <li>
          <a href="./register.html">免费注册</a>
        </li>
        <li>
          <a href="./center-order.html">我的订单</a>
        </li>
        <li>
          <a href="./center.html">会员中心</a>
        </li>
        <li>
          <a href="javascript:;">帮助中心</a>
        </li>
        <li>
          <a href="javascript:;">在线客服</a>
        </li>
        <li>
          <a href="javascript:;">
            <i class="mobile sprites"></i>
            手机版
          </a>
        </li>
      </ul>
    </div>
  </div>

  <script>
    const li1 = document.querySelector('.xtx_navs li:first-child')
    const li2 = li1.nextElementSibling
    
    // 渲染函数
    function render() {
      const uname = localStorage.getItem('xtx-uname')
      if (uname) {
        li1.innerHTML = `<a href="javascript:;"><i class="iconfont icon-user">${uname}</i></a>`
        li2.innerHTML = '<a href="javascript:;">退出登录</a>'
      } else {
        li1.innerHTML = '<a href="./login.html">请先登录</a>'
        li2.innerHTML = '<a href="./register.html">免费注册</a>'
      }
    }

    render()

    // 退出登录
    li2.addEventListener('click', function () {
      localStorage.removeItem('xtx-uname')
      render()
    })
  </script>
</body>

</html>

例子:放大镜效果

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>小兔鲜儿 - 新鲜 惠民 快捷!</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="renderer" content="webkit">
  <!-- <link rel="stylesheet" href="//at.alicdn.com/t/font_1939705_bgtmkonu28.css"> -->
  <link rel="stylesheet" href="./css/common.css">
  <link rel="stylesheet" href="./css/product.css">
</head>

<body>
  <!-- 项部导航 -->
  <div class="xtx_topnav">
    <div class="wrapper">
      <!-- 顶部导航 -->
      <ul class="xtx_navs">
        <li>
          <a href="javascript:;">请先登录</a>
        </li>
        <li>
          <a href="javascript:;">免费注册</a>
        </li>
        <li>
          <a href=".javascript:;">我的订单</a>
        </li>
        <li>
          <a href="javascript:;">会员中心</a>
        </li>
        <li>
          <a href="javascript:;">帮助中心</a>
        </li>
        <li>
          <a href="javascript:;">在线客服</a>
        </li>
        <li>
          <a href="javascript:;">
            <i class="mobile sprites"></i>
            手机版
          </a>
        </li>
      </ul>
    </div>
  </div>
  <!-- 头部 -->
  <div class="xtx_header">
    <div class="wrapper">
      <!-- 网站Logo -->
      <h1 class="xtx_logo"><a href="/">小兔鲜儿</a></h1>
      <!-- 主导航 -->
      <div class="xtx_navs">
        <ul class="clearfix">
          <li>
            <a href="javascript:;">首页</a>
          </li>
          <li>
            <a href="javascript:;">生鲜</a>
          </li>
          <li>
            <a href="javascript:;">美食</a>
          </li>
          <li>
            <a href="javascript:;">餐厨</a>
          </li>
          <li>
            <a href="javascript:;">电器</a>
          </li>
          <li>
            <a href="javascript:;">居家</a>
          </li>
          <li>
            <a href="javascript:;">洗护</a>
          </li>
          <li>
            <a href="javascript:;">孕婴</a>
          </li>
          <li>
            <a href="javascript:;">服装</a>
          </li>
        </ul>
      </div>
      <!-- 站内搜索 -->
      <div class="xtx_search clearfix">
        <!-- 购物车 -->
        <a href="javascript:;" class="xtx_search_cart sprites">
          <i>2</i>
        </a>
        <!-- 搜索框 -->
        <div class="xtx_search_wrapper">
          <input type="text" placeholder="搜一搜" onclick="location.href='./search.html'">
        </div>
      </div>
    </div>
  </div>
  <div class="xtx-wrapper">
    <div class="container">
      <!-- 面包屑 -->
      <div class="xtx-bread">
        <a href="javascript:;"> 首页 > </a>
        <a href="javascript:;"> 电子产品 > </a>
        <a href="javascript:;"> 电视 > </a>
        <span>小米电视4A 32英寸</span>
      </div>
      <!-- 商品信息 -->
      <div class="xtx-product-info">
        <div class="left">
          <div class="pictrue">
            <div class="middle">
              <img src="./images/1.jpg" alt="">
              <div class="layer"></div>
            </div>
            <div class="small">
              <ul>
                <li class="active"><img src="./images/1.jpg" alt=""></li>
                <li><img src="./images/2.jpg" alt=""></li>
                <li><img src="./images/3.jpg" alt=""></li>
                <li><img src="./images/4.jpg" alt=""></li>
                <li><img src="./images/5.jpg" alt=""></li>
              </ul>
            </div>
            <div class="large"></div>
          </div>
          <div class="other">
            <ul>
              <li>
                <p>销量人气</p>
                <p>1999+</p>
                <p>销量人气</p>
              </li>
              <li>
                <p>商品评价</p>
                <p>999+</p>
                <p>查看评价</p>
              </li>
              <li>
                <p>收藏人气</p>
                <p>299+</p>
                <p><a href="javascript:;">收藏商品</a></p>
              </li>
              <li>
                <p>品牌信息</p>
                <p>小米</p>
                <p><a href="javascript:;">品牌主页</a></p>
              </li>
            </ul>
          </div>
        </div>
        <div class="right">
          <h3 class="name">小米电视4A 32英寸</h3>
          <p class="desc">全面屏设计 / 高清分辨率 / 海量内容 / 1G+4G大内存 / 多核处理器</p>
          <p class="price"><span class="now">¥1899</span><span class="old">¥2999</span></p>
          <div class="address">
            <div class="item">
              <div class="dt">促销</div>
              <div class="dd">12月好物放送,App领券购买直降120元</div>
            </div>
            <div class="item">
              <div class="dt">配送</div>
              <div class="dd"><div class="box">
                  <span>陕西 西安 <i></i></span>
                </div>
              </div>
            </div>
            <div class="item">
              <div class="dt">服务</div>
              <div class="dd">
                <span class="fw">无忧退货</span>
                <span class="fw">快速退款</span>
                <span class="fw">免费包邮</span>
                <a href="#" class="lj">了解详情</a>
              </div>
            </div>
          </div>
          <div class="attrs">
            <div class="item">
              <div class="dt">颜色</div>
              <div class="dd">
                <img src="./uploads/img/cate-06.png" alt="">
                <img src="./uploads/img/cate-07.png" alt="">
              </div>
            </div>
            <div class="item">
              <div class="dt">颜色</div>
              <div class="dd">
                <span class="size">22英寸</span>
                <span class="size">42英寸</span>
                <span class="size">52英寸</span>
                <span class="size">62英寸</span>
              </div>
            </div>
            <div class="item">
              <div class="dt">数量</div>
              <div class="dd">
                <div class="num">
                  <a href="javascript:;">-</a>
                  <input type="text" value="1">
                  <a href="javascript:;">+</a>
                </div>
              </div>
            </div>
            <div class="item">
              <a class="buy" href="javascript:;">立即购买</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
    // 获取元素,包括小盒子,中盒子,大盒子
    const small = document.querySelector('.small')
    const middle = document.querySelector('.middle')
    const large = document.querySelector('.large')

    // 鼠标经过小盒子,左侧中盒子显示对应的图片
    // 采取事件委托方式,为小盒子添加 mouseover 事件,用于切换 li 元素上的 active 属性,并修改中盒子和大盒子中的图片为移动到的图片
    small.addEventListener('mouseover', function (e) {
      if (e.target.tagName === 'IMG') {
        this.querySelector('.active').classList.remove('active')
        e.target.parentNode.classList.add('active')
        middle.querySelector('img').src = e.target.src
        large.style.backgroundImage = `url(${e.target.src})`
      }
    })

    // 鼠标经过中盒子,右侧大盒子显示对应的图片
    // 大盒子使用 display 属性来显示和隐藏,并使用 setTimeout 进行延时隐藏
    middle.addEventListener('mouseenter', show)
    middle.addEventListener('mouseleave', hide)
    let timeId = 0

    function show() {
      clearTimeout(timeId)
      large.style.display = 'block'
    }

    function hide() {
      timeId = setTimeout(() => {
        large.style.display = 'none'
      }, 200);
    }

    // 鼠标经过大盒子,大盒子保持显示;鼠标离开大盒子,大盒子隐藏
    large.addEventListener('mouseenter', show)
    large.addEventListener('mouseleave', hide)

    // 鼠标经过中盒子,显示和隐藏黑色遮罩层盒子
    const layer = document.querySelector('.layer')
    middle.addEventListener('mouseenter', function () {
      layer.style.display = 'block'
    })

    middle.addEventListener('mouseleave', function () {
      layer.style.display = 'none'
    })

    // 黑色遮罩层盒子跟随鼠标移动,鼠标在中盒子里面的坐标 = 鼠标在页面的坐标 - 中盒子的坐标
    middle.addEventListener('mousemove', function (e) {
      let x = e.pageX - middle.getBoundingClientRect().left
      let y = e.pageY - middle.getBoundingClientRect().top - document.documentElement.scrollTop
      if (x >= 0 && x <= 400 && y >= 0 && y <= 400) {
        let mx = 0, my = 0

        if (x < 100) mx = 0
        if (x >= 100 && x <= 300) mx = x - 100
        if (x > 300) mx = 200

        if (y < 100) my = 0
        if (y >= 100 && y <= 300) my = y - 100
        if (y > 300) my = 200

        layer.style.left = mx + 'px'
        layer.style.top = my + 'px'

        // 大盒子的背景图片跟随中盒子移动,移动距离是中盒子 2 倍
        large.style.backgroundPositionX = -2 * mx + 'px'
        large.style.backgroundPositionY = -2 * my + 'px'
      }
    })
  </script>
</body>

</html><!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>小兔鲜儿 - 新鲜 惠民 快捷!</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="renderer" content="webkit">
  <!-- <link rel="stylesheet" href="//at.alicdn.com/t/font_1939705_bgtmkonu28.css"> -->
  <link rel="stylesheet" href="./css/common.css">
  <link rel="stylesheet" href="./css/product.css">
</head>

<body>
  <!-- 项部导航 -->
  <div class="xtx_topnav">
    <div class="wrapper">
      <!-- 顶部导航 -->
      <ul class="xtx_navs">
        <li>
          <a href="javascript:;">请先登录</a>
        </li>
        <li>
          <a href="javascript:;">免费注册</a>
        </li>
        <li>
          <a href=".javascript:;">我的订单</a>
        </li>
        <li>
          <a href="javascript:;">会员中心</a>
        </li>
        <li>
          <a href="javascript:;">帮助中心</a>
        </li>
        <li>
          <a href="javascript:;">在线客服</a>
        </li>
        <li>
          <a href="javascript:;">
            <i class="mobile sprites"></i>
            手机版
          </a>
        </li>
      </ul>
    </div>
  </div>
  <!-- 头部 -->
  <div class="xtx_header">
    <div class="wrapper">
      <!-- 网站Logo -->
      <h1 class="xtx_logo"><a href="/">小兔鲜儿</a></h1>
      <!-- 主导航 -->
      <div class="xtx_navs">
        <ul class="clearfix">
          <li>
            <a href="javascript:;">首页</a>
          </li>
          <li>
            <a href="javascript:;">生鲜</a>
          </li>
          <li>
            <a href="javascript:;">美食</a>
          </li>
          <li>
            <a href="javascript:;">餐厨</a>
          </li>
          <li>
            <a href="javascript:;">电器</a>
          </li>
          <li>
            <a href="javascript:;">居家</a>
          </li>
          <li>
            <a href="javascript:;">洗护</a>
          </li>
          <li>
            <a href="javascript:;">孕婴</a>
          </li>
          <li>
            <a href="javascript:;">服装</a>
          </li>
        </ul>
      </div>
      <!-- 站内搜索 -->
      <div class="xtx_search clearfix">
        <!-- 购物车 -->
        <a href="javascript:;" class="xtx_search_cart sprites">
          <i>2</i>
        </a>
        <!-- 搜索框 -->
        <div class="xtx_search_wrapper">
          <input type="text" placeholder="搜一搜" onclick="location.href='./search.html'">
        </div>
      </div>
    </div>
  </div>
  <div class="xtx-wrapper">
    <div class="container">
      <!-- 面包屑 -->
      <div class="xtx-bread">
        <a href="javascript:;"> 首页 > </a>
        <a href="javascript:;"> 电子产品 > </a>
        <a href="javascript:;"> 电视 > </a>
        <span>小米电视4A 32英寸</span>
      </div>
      <!-- 商品信息 -->
      <div class="xtx-product-info">
        <div class="left">
          <div class="pictrue">
            <div class="middle">
              <img src="./images/1.jpg" alt="">
              <div class="layer"></div>
            </div>
            <div class="small">
              <ul>
                <li class="active"><img src="./images/1.jpg" alt=""></li>
                <li><img src="./images/2.jpg" alt=""></li>
                <li><img src="./images/3.jpg" alt=""></li>
                <li><img src="./images/4.jpg" alt=""></li>
                <li><img src="./images/5.jpg" alt=""></li>
              </ul>
            </div>
            <div class="large"></div>
          </div>
          <div class="other">
            <ul>
              <li>
                <p>销量人气</p>
                <p>1999+</p>
                <p>销量人气</p>
              </li>
              <li>
                <p>商品评价</p>
                <p>999+</p>
                <p>查看评价</p>
              </li>
              <li>
                <p>收藏人气</p>
                <p>299+</p>
                <p><a href="javascript:;">收藏商品</a></p>
              </li>
              <li>
                <p>品牌信息</p>
                <p>小米</p>
                <p><a href="javascript:;">品牌主页</a></p>
              </li>
            </ul>
          </div>
        </div>
        <div class="right">
          <h3 class="name">小米电视4A 32英寸</h3>
          <p class="desc">全面屏设计 / 高清分辨率 / 海量内容 / 1G+4G大内存 / 多核处理器</p>
          <p class="price"><span class="now">¥1899</span><span class="old">¥2999</span></p>
          <div class="address">
            <div class="item">
              <div class="dt">促销</div>
              <div class="dd">12月好物放送,App领券购买直降120元</div>
            </div>
            <div class="item">
              <div class="dt">配送</div>
              <div class="dd"><div class="box">
                  <span>陕西 西安 <i></i></span>
                </div>
              </div>
            </div>
            <div class="item">
              <div class="dt">服务</div>
              <div class="dd">
                <span class="fw">无忧退货</span>
                <span class="fw">快速退款</span>
                <span class="fw">免费包邮</span>
                <a href="#" class="lj">了解详情</a>
              </div>
            </div>
          </div>
          <div class="attrs">
            <div class="item">
              <div class="dt">颜色</div>
              <div class="dd">
                <img src="./uploads/img/cate-06.png" alt="">
                <img src="./uploads/img/cate-07.png" alt="">
              </div>
            </div>
            <div class="item">
              <div class="dt">颜色</div>
              <div class="dd">
                <span class="size">22英寸</span>
                <span class="size">42英寸</span>
                <span class="size">52英寸</span>
                <span class="size">62英寸</span>
              </div>
            </div>
            <div class="item">
              <div class="dt">数量</div>
              <div class="dd">
                <div class="num">
                  <a href="javascript:;">-</a>
                  <input type="text" value="1">
                  <a href="javascript:;">+</a>
                </div>
              </div>
            </div>
            <div class="item">
              <a class="buy" href="javascript:;">立即购买</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
    // 获取元素,包括小盒子,中盒子,大盒子
    const small = document.querySelector('.small')
    const middle = document.querySelector('.middle')
    const large = document.querySelector('.large')

    // 鼠标经过小盒子,左侧中盒子显示对应的图片
    // 采取事件委托方式,为小盒子添加 mouseover 事件,用于切换 li 元素上的 active 属性,并修改中盒子和大盒子中的图片为移动到的图片
    small.addEventListener('mouseover', function (e) {
      if (e.target.tagName === 'IMG') {
        this.querySelector('.active').classList.remove('active')
        e.target.parentNode.classList.add('active')
        middle.querySelector('img').src = e.target.src
        large.style.backgroundImage = `url(${e.target.src})`
      }
    })

    // 鼠标经过中盒子,右侧大盒子显示对应的图片
    // 大盒子使用 display 属性来显示和隐藏,并使用 setTimeout 进行延时隐藏
    middle.addEventListener('mouseenter', show)
    middle.addEventListener('mouseleave', hide)
    let timeId = 0

    function show() {
      clearTimeout(timeId)
      large.style.display = 'block'
    }

    function hide() {
      timeId = setTimeout(() => {
        large.style.display = 'none'
      }, 200);
    }

    // 鼠标经过大盒子,大盒子保持显示;鼠标离开大盒子,大盒子隐藏
    large.addEventListener('mouseenter', show)
    large.addEventListener('mouseleave', hide)

    // 鼠标经过中盒子,显示和隐藏黑色遮罩层盒子
    const layer = document.querySelector('.layer')
    middle.addEventListener('mouseenter', function () {
      layer.style.display = 'block'
    })

    middle.addEventListener('mouseleave', function () {
      layer.style.display = 'none'
    })

    // 黑色遮罩层盒子跟随鼠标移动,鼠标在中盒子里面的坐标 = 鼠标在页面的坐标 - 中盒子的坐标
    middle.addEventListener('mousemove', function (e) {
      let x = e.pageX - middle.getBoundingClientRect().left
      let y = e.pageY - middle.getBoundingClientRect().top - document.documentElement.scrollTop
      if (x >= 0 && x <= 400 && y >= 0 && y <= 400) {
        let mx = 0, my = 0

        if (x < 100) mx = 0
        if (x >= 100 && x <= 300) mx = x - 100
        if (x > 300) mx = 200

        if (y < 100) my = 0
        if (y >= 100 && y <= 300) my = y - 100
        if (y > 300) my = 200

        layer.style.left = mx + 'px'
        layer.style.top = my + 'px'

        // 大盒子的背景图片跟随中盒子移动,移动距离是中盒子 2 倍
        large.style.backgroundPositionX = -2 * mx + 'px'
        large.style.backgroundPositionY = -2 * my + 'px'
      }
    })
  </script>
</body>

</html>

作用域

作用域规定了变量能够被访问的 “范围”,离开了这个 “范围” 变量便不能被访问。

如前所述,作用域分为:

  • 局部作用域
  • 全局作用域

局部作用域

局部作用域分为:

  • 函数作用域:在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。

    • 函数的参数也是函数内部的局部变量
    • 不同函数内部声明的变量无法互相访问
    • 函数执行完毕后,函数内部的变量实际被清空了
  • 块作用域:在 JavaScript 中使用 {} 包裹的代码称为代码块,代码块内部声明的变量无法被外部访问。

    • let 声明的变量会产生块作用域,var 不会产生块作用域

    • const 声明的常量也会产生块作用域

    • 不同代码块之间的变量无法互相访问

    • 推荐使用 letconst

在 JavaScript 中,块级作用域(Block Scope)是 ES6(ECMAScript 2015)引入的一个新特性,主要通过 letconst 关键字来实现。在 ES5 及更早的版本中,只有函数作用域和全局作用域,而 var 关键字声明的变量在任何包含它的执行上下文中都是可见的,这可能会导致一些意料之外的行为。

块级作用域指的是在代码块 {} 内声明的变量只在该代码块内有效。当代码块执行完毕后,该变量就会被销毁,不可再访问。这有助于避免变量污染全局作用域,提高代码的可读性和可维护性。

例如:

if (true) {  
  let x = 10; // x 只在 if 代码块内有效  
  console.log(x); // 输出 10  
}  
  
// 尝试在 if 代码块外部访问 x 会导致 ReferenceError  
console.log(x); // ReferenceError: x is not defined

在这个例子中,let x = 10; 声明了一个只在 if 代码块内有效的变量 x。当 if 代码块执行完毕后,x 变量就被销毁了,因此在 if 代码块外部尝试访问 x 会导致 ReferenceError

全局作用域

在 JavaScript 中,全局作用域是指不在任何函数内部定义的变量和函数所处的范围。在全局作用域中声明的变量和函数可以被整个脚本中的任何代码访问。在浏览器环境中,全局作用域通常指的是 window 对象,而在 Node.js 环境中,全局作用域则是 global 对象。

使用 varletconst 在全局作用域中声明的变量都会成为全局对象的属性。不过,由于 letconst 具有块级作用域的特性,在全局作用域中使用它们声明的变量不会像 var 那样被提升到整个脚本的顶部。

<script> 标签和 .js 文件的最外层就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。

  • window 对象动态添加的属性默认也是全局的,不推荐。
  • 函数中未使用任何关键字声明的变量为全局变量,不推荐。
  • 尽可能少的声明全局变量,防止全局变量被污染。

例如:

// 使用 var 在全局作用域中声明变量  
var globalVar = "I'm global!";  
  
function checkGlobalVar() {  
  console.log(globalVar); // 可以访问全局变量  
}  
  
checkGlobalVar(); // 输出 "I'm global!"  
  
// 在浏览器环境中,全局变量也是 window 对象的属性  
console.log(window.globalVar); // 输出 "I'm global!"  
  
// 使用 let 在全局作用域中声明变量(具有块级作用域,但在此例中是全局块的顶部)  
let globalLetVar = "I'm also global, but with block scope!";  
  
// 使用 const 在全局作用域中声明常量  
const GLOBAL_CONST = "I'm a global constant!";  
  
// 在 Node.js 环境中,全局变量是 global 对象的属性  
// 注意:这个示例仅在 Node.js 环境中有效  
console.log(global.GLOBAL_CONST); // 输出 "I'm a global constant!"

需要注意的是,过度使用全局变量可能会导致代码难以维护和理解,因为它们可以在任何地方被修改。因此,最佳实践是尽可能避免使用全局变量,而是使用局部变量、参数传递或模块化的方式来组织代码。

在模块化 JavaScript(如使用 ES6 模块或 CommonJS)中,每个模块都有自己的作用域,这样可以更好地封装和管理变量和函数,减少全局作用域的污染。

作用域链

作用域链本质上是底层的变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域。

  • 嵌套关系的作用域串联起来形成了作用域链
  • 相同作用域链中按照从小到大的规则查找变量
  • 子级作用域能够访问父级作用域,父级作用域无法访问子级作用域

例如:

function outerFunction() {  
  let outerVariable = 'I am from outer function!';  
  
  function innerFunction() {  
    let innerVariable = 'I am from inner function!';  
    console.log(outerVariable); // 可以访问外部函数的变量  
  }  
  
  innerFunction(); // 调用内部函数  
}  
  
outerFunction(); // 输出 "I am from outer function!"

在这个例子中,innerFunction 可以访问 outerVariable,因为当 innerFunction 被调用时,它的作用域链包含了 outerFunction 的活动对象。

垃圾回收

JavaScript 环境中分配的内存,一般有如下生命周期:

  • 内存分配:当声明变量、函数及对象时,系统会自动为其分配内存
  • 内存使用:即读写内存,也就是使用变量、函数等
  • 内存回收:使用完毕后,由垃圾回收器自动回收不再使用的内存

说明:

  • 全局变量一般不会回收(关闭页面回收)
  • 一般情况下,局部变量如果不再使用就会被自动回收。

JavaScript 的垃圾回收机制是自动内存管理的一部分,它负责找出不再使用的变量,并释放其占用的内存空间。这样,开发人员就无需手动分配和释放内存,从而减少了出错的可能性,并提高了代码的可维护性。

JavaScript 使用了标记-清除(Mark-and-Sweep)算法来进行垃圾回收。这个算法的基本思想分为两个阶段:标记和清除。

标记阶段

  • 根(Roots):在 JavaScript 中,根通常包括全局对象(在浏览器中是 window 对象)和任何当前正在执行的函数的局部变量和内部函数。
  • 标记活动对象:垃圾回收器会从根开始,递归地访问这些对象的所有属性。任何被访问到的对象都会被标记为“活动的”(即还在使用中的)。
  • 标记闭包中的变量:如果某个对象被一个闭包引用,那么它也会被标记为活动的,即使这个闭包当前并不在执行上下文中。

清除阶段

  • 回收非活动对象:在标记阶段结束后,垃圾回收器会遍历堆中的所有对象。任何未被标记为活动的对象(即不再被引用或使用的对象)都会被视为垃圾,并释放其占用的内存。
  • 内存整理:在某些实现中,垃圾回收器可能还会进行内存整理,将存活的对象移动到内存中的连续区域,以便更有效地使用内存。

闭包

闭包(Closure)可以简单理解为内层函数加上外层函数的变量,即内层函数访问了外层函数的变量。

闭包有三个主要特性:

  • 函数嵌套函数:闭包存在于嵌套函数中,即一个函数内部定义的另一个函数。
  • 词法作用域:闭包可以访问其外部函数的变量和参数,即使外部函数已经执行完毕。
  • 数据封装和私有变量:通过闭包,可以创建私有变量,只能通过特定的公开方法进行访问和修改。

例如:

function outerFunction() {  
  let outerVariable = 'I am from outer function!';  
  
  function innerFunction() {  
    console.log(outerVariable);  
  }  
  
  return innerFunction;  
}  
  
let myInnerFunction = outerFunction();  
myInnerFunction(); // 输出: I am from outer function!

在这个例子中,innerFunction 是一个闭包,因为它可以访问 outerFunction 的局部变量 outerVariable。此时可以通过外部函数 myInnerFunction 访问函数 outerFunction 的内部变量 outerVariable

闭包在 JavaScript 中有很多用途,包括但不限于:

  • 数据封装和私有变量:通过闭包,可以创建私有变量和方法,只能通过公开的方法进行访问。
  • 回调函数和高阶函数:闭包常用作回调函数,因为它们可以记住自己的词法环境。
  • 实现装饰器/函数修饰器:闭包可以用来修改或增强函数的行为。
  • 实现模块和库:利用闭包,可以创建具有私有状态和方法的模块或库。

例子:使用闭包统计函数调用次数

function fn() {
  let count = 1
  function fun() {
    count++
    console.log(`函数被调用了${count}`)
  }
  return fun
}

const result = fn()
result()

需要注意的是,过度使用闭包可能会导致内存泄漏,因为闭包可以保留其外部函数的变量,即使外部函数不再需要这些变量。只要闭包还存在,那么闭包中的变量就不会被垃圾回收机制回收。因此,在使用闭包时,应谨慎管理内存使用。

变量提升

变量的声明(使用 var 关键字)会被提升到作用域的最顶部,但是初始化(赋值)不会。

// 使用 var 的示例  
console.log(x); // 输出: undefined,因为 var x 的声明被提升,但初始化没有  
var x = 10;  
  
// 使用 let 的示例  
console.log(y); // 抛出 ReferenceError,因为 let y 的声明不会被提升
let y = 20;  

建议在 JavaScript 开发中,使用 letconst 而不是 var,因为它们提供了更严格的作用域规则和更好的错误检查。

函数提升

在 JavaScript 中,函数提升(Function Hoisting)是一种特定的变量提升行为,它只适用于函数声明(function declarations),而不适用于函数表达式(function expressions)。函数提升意味着函数声明会被提升到其所在作用域的顶部,这包括全局作用域和函数作用域。

这里有一些关于函数提升的重要点:

  • 函数声明提升:使用 function 关键字进行的函数声明会被提升到其所在作用域的最顶部。这意味着你可以在函数声明之前调用该函数,而不会遇到错误。
  • 函数表达式不提升:使用函数表达式(即赋值给一个变量的函数)并不会发生提升。函数表达式只是普通的变量赋值,而变量赋值并不会被提升。
  • 函数声明优先于变量声明:如果同时存在一个函数声明和一个同名的变量声明,函数声明会覆盖变量声明。这是因为函数声明被提升到作用域的最顶部,而变量声明则位于函数声明之后。

例如:

// 函数声明提升示例  
foo(); // 输出: "Function Declaration"  
  
function foo() {  
  console.log("Function Declaration");  
}  
  
// 函数表达式不提升示例  
bar(); // 抛出 TypeError,因为 bar 是 undefined  
  
var bar = function() {  
  console.log("Function Expression");  
};  

推荐按照代码的逻辑顺序声明和定义函数和变量,使用 letconst 来定义变量(它们不会提升),并且尽量使用函数表达式或箭头函数(它们也不会提升),除非有明确的需求要使用函数声明。

高级

展开运算符

在 JavaScript 中,展开运算符(Spread Operator)是一种语法,将一个可迭代对象(如数组或对象)展开到不同的位置。展开运算符使用三个点(...)来表示。这个特性在 ES6(ECMAScript 2015)中引入,极大地增强了 JavaScript 的灵活性和表达能力。

在数组中,展开运算符可以用来将数组的元素展开到另一个数组中,或者用于函数调用时传递数组元素作为独立的参数。

例子:展开数组

const array1 = [1, 2, 3];  
const array2 = [...array1, 4, 5, 6];  
console.log(array2); // 输出:[1, 2, 3, 4, 5, 6]
console.log(Math.min(...array2)); // 输出:1
console.log(Math.max(...array2)); // 输出:6

例子:函数调用时传递数组元素

function sum(a, b, c) {  
  return a + b + c;  
}  
  
const numbers = [1, 2, 3];  
const total = sum(...numbers);  
console.log(total); // 输出:6

在对象中,展开运算符可以用来复制一个对象的所有可枚举属性到另一个对象中。

例子:展开对象

const obj1 = { a: 1, b: 2 };  
const obj2 = { ...obj1, c: 3 };  
console.log(obj2); // 输出:{ a: 1, b: 2, c: 3 }

注意:

  • 展开运算符不会展开对象原型链上的属性,只会复制对象自身的可枚举属性。
  • 如果对象的属性名有冲突,后展开的属性会覆盖先展开的属性。
  • 展开运算符可以嵌套使用,即可以在一个已经使用展开运算符的表达式中再次使用它。

箭头函数

JavaScript 的箭头函数(Arrow Functions)是 ES6(ECMAScript 2015)引入的一个新特性,它提供了一种更简洁、更直观的方式来编写函数表达式。箭头函数在语法上更紧凑,并且不绑定自己的 this 值,而是捕获其所在上下文的 this 值。这使得箭头函数在处理回调函数和避免 this 指向问题方面特别有用。

语法:

(parameters) => { functionBody }

如果函数体只有一条语句,可以省略花括号:

(parameters) => expression

当参数只有一个时,圆括号也是可选的:

parameter => expression

例子:基本箭头函数

const add = (a, b) => a + b;  
console.log(add(1, 2)); // 输出:3

例子:不带参数的箭头函数

const getRandom = () => Math.random();  
console.log(getRandom()); // 输出一个随机数

例子:只有一个参数的箭头函数(省略圆括号)

const double = num => num * 2;  
console.log(double(5)); // 输出:10

例子:返回对象

const fn = (username,age) => ({username:username,age:age})
console.log(fn('tom',18))

箭头函数没有 arguments 对象。如果需要在箭头函数内部访问类似 arguments 的功能,可以使用剩余参数(rest parameters):

const sum = (...numbers) => numbers.reduce((acc, val) => acc + val, 0);  
console.log(sum(1, 2, 3, 4)); // 输出:10

箭头函数不会创建自己的 this 上下文,所以 this 的值由外部代码块决定,指向上一层函数作用域的 this。这在回调函数中特别有用,因为回调通常会改变 this 的上下文。

例子:箭头函数中的 this

const fn = () => console.log(this)
fn() // 输出:Window

const person = {
  name: 'tom',
  sayHi: () => {
    console.log(this)
  }
}
person.sayHi() // 输出:Window

const animal = {
  name: 'jerry',
  sayHi: function () {
    // console.log(this) // 输出:animal
    const count = () => {
      console.log(this) 
    }
    count()
  }
}
animal.sayHi() // 输出:animal

在开发者,事件回调函数使用箭头函数时,this 为全局的 Window,因此 DOM 事件回调函数为了方便,还是不太推荐使用箭头函数。

<body>
  <button>点我</button>
  <script>
    const btn = document.querySelector('button')

    // 箭头函数,此时 this 指向 Window 
    btn.addEventListener('click', () => {
      console.log(this)
    })

    // 普通函数,此时 this 指向 DOM 对象
    btn.addEventListener('click', function () {
      console.log(this)
    })
  </script>
</body>

解构赋值

JavaScript 的解构赋值(Destructuring assignment)是一种表达式,它使得从数组或对象中提取数据并将其赋值给不同的变量成为可能。这是一种简洁且高效的赋值方法,可以避免手动提取和赋值的过程。

数组解构

在数组解构中,可以将数组的元素快速批量分配给不同的变量,变量的顺序对应数组元素的位置,依次进行赋值操作。

例如:

const [first, second, third] = [1, 2, 3];  
console.log(first); // 输出: 1  
console.log(second); // 输出: 2  
console.log(third); // 输出: 3

// 默认值
const [a = 1, b = 1] = [2]
console.log(a, b) // 输出:2 1

// 按需解构
const [x, , z] = [1, 2, 3]
console.log(x, z) // 输出:1 3

还可以使用剩余参数(rest parameter)来捕获数组中的剩余元素:

const [first, ...rest] = [1, 2, 3, 4, 5];  
console.log(first); // 输出: 1  
console.log(rest); // 输出: [2, 3, 4, 5]

例子:交换变量值

let a = 1
let b = 2;
[b, a] = [a, b]
console.log(a, b) // 输出:2 1

例子:多维数组解构

const nestedArray = [1, [2, 3], 4];  
const [first, [second, third], fourth] = nestedArray;  
console.log(second); // 输出: 2 

对象解构

可以从对象中提取属性值并赋值给变量。

例如:对象解构,变量名和属性名相同

const person = {  
  name: 'Alice',  
  age: 30,  
  city: 'New York'  
};  
  
const { name, age, city } = person;
console.log(name); // 输出: Alice  
console.log(age); // 输出: 30  
console.log(city); // 输出: New York

给变量重命名:

const person = {  
  name: 'Alice',  
  age: 30,  
  city: 'New York'  
};  
  
const { name: userName, age: userAge } = person;  
console.log(userName); // 输出: Alice  
console.log(userAge); // 输出: 30

使用剩余参数来捕获对象中的剩余属性:

const person = {  
  name: 'Alice',  
  age: 30,  
  city: 'New York'  
};  
  
const { name, ...otherInfo } = person;  
console.log(name); // 输出: Alice  
console.log(otherInfo); // 输出: { age: 30, city: 'New York' }

解构嵌套对象:

const nestedObject = {  
  a: 1,  
  b: {  
    c: 2,  
    d: 3  
  }  
};  
  
const { a, b: { c, d } } = nestedObject;  
console.log(c); // 输出: 2  
console.log(d); // 输出: 3

对象数组解构:

const students = [
  {uname: 'tom', age: 18}
]

const [{uname, age}] = students
console.log(uname, age)

解构赋值在函数参数中也非常有用,可以让函数参数更简洁和清晰。

function greet({ name, age }) {  
  console.log(`Hello, ${name}! You are ${age} years old.`);  
}  
  
greet({ name: 'Bob', age: 25 }); // 输出: Hello, Bob! You are 25 years old.

数组方法

JavaScript 中的数组(Array)对象提供了许多内置方法,用于对数组执行各种操作。以下是一些常用的数组方法:

  • 迭代方法

    • forEach(callback):对数组的每个元素执行一次提供的函数。

    • map(callback):创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

    • filter(callback):创建一个新数组,其包含通过所提供函数实现的测试的所有元素。

    • some(callback):测试数组中是否至少有一个元素通过由提供的函数实现的测试。

    • every(callback):测试数组的所有元素是否都通过了由提供的函数实现的测试。

    • find(callback):返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined

    • findIndex(callback):返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1

  • 数组操作

    • push(...items):将一个或多个元素添加到数组的末尾,并返回新的长度。

    • pop():删除并返回数组的最后一个元素。

    • shift():删除并返回数组的第一个元素。

    • unshift(...items):将一个或多个元素添加到数组的开头,并返回新的长度。

    • splice(start, deleteCount, ...items):通过删除或替换现有元素或者添加新元素来修改数组,并以数组形式返回被修改的内容。

    • slice(begin, end):返回一个新的数组对象,这一对象是一个由开始到结束(不包括结束)选择的、由原数组的浅拷贝构成。原始数组不会被改变。

    • concat(...values)concat(array1, array2, ..., arrayX):用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

  • 排序和搜索

    • sort(compareFunction):对数组的元素进行排序,并返回数组。排序可以是字母或数字,并按升序或降序。默认排序顺序是根据字符串Unicode码点。

    • reverse():颠倒数组中元素的顺序,并返回该数组。数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。

    • indexOf(searchElement, fromIndex):返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1

    • lastIndexOf(searchElement, fromIndex):返回指定元素在数组中的最后一个索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

  • 转换为字符串

    • join([separator]):把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分隔。

    • toString():返回一个字符串,表示指定的数组及其元素。

  • 其他方法

    • reduce(callback[, initialValue]):对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个输出值。

    • fill(value[, start[, end]]):用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。

    • copyWithin(target, start, end):在数组内部,将一段连续元素复制到另一个位置,并返回数组。

    • includes(searchElement[, fromIndex]):判断一个数组是否包含一个指定的值,根据情况,如果需要,搜索从索引 fromIndex 开始。

    • flat([depth]):按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

    • flatMap(callback):首先使用映射函数映射数组,然后使用 flat 将结果压缩成一个新数组。它与 mapflat 方法的结合具有相同的效果,但 flatMap 比先调用 map 再调用 flat 链式调用更简洁。

forEach

forEach 是 JavaScript 数组的一个内置方法,用于遍历数组的每个元素并执行一个回调函数。这个方法对数组的每个元素执行一次回调函数,并且没有返回值(返回 undefined)。它通常用于执行一些不需要生成新数组的操作,比如打印数组元素、更新数组中的某些值或遍历对象数组。

forEach 方法接收一个回调函数作为参数,这个回调函数会被应用到数组中的每个元素上。这个回调函数可以接收三个参数:当前元素的值、当前元素的索引以及正在操作的数组本身。

例如:

const array = [1, 2, 3, 4, 5];  
  
array.forEach(function(value, index, array) {  
  console.log(value); // 输出数组的每个元素  
  console.log(index); // 输出当前元素的索引  
  console.log(array); // 输出正在操作的数组本身  
});

可以使用箭头函数来简化回调函数的书写:

const array = [1, 2, 3, 4, 5];  
  
array.forEach((value, index, array) => {  
  console.log(value);  
});

可以只提供一个参数:

const array = [1, 2, 3, 4, 5];  
  
array.forEach(value => {  
  console.log(value);  
});

注意,forEach 不会像 mapfilterreduce 那样返回一个新数组。它仅用于遍历数组元素并执行一些操作。如果你需要在遍历过程中收集结果,那么你可能需要使用 map 或其他方法,或者手动创建一个新的数组来存储结果。

例子:商品渲染

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>商品渲染</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .list {
      width: 990px;
      margin: 0 auto;
      display: flex;
      flex-wrap: wrap;
      padding-top: 100px;
    }

    .item {
      width: 240px;
      margin-left: 10px;
      padding: 20px 30px;
      transition: all .5s;
      margin-bottom: 20px;
    }

    .item:nth-child(4n) {
      margin-left: 0;
    }

    .item:hover {
      box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
      transform: translate3d(0, -4px, 0);
      cursor: pointer;
    }

    .item img {
      width: 100%;
    }

    .item .name {
      font-size: 18px;
      margin-bottom: 10px;
      color: #666;
    }

    .item .price {
      font-size: 22px;
      color: firebrick;
    }

    .item .price::before {
      content: "¥";
      font-size: 14px;
    }
  </style>
</head>

<body>
  <div class="list">
    <!-- <div class="item">
      <img src="" alt="">
      <p class="name"></p>
      <p class="price"></p>
    </div> -->
  </div>
  <script>
    const goodsList = [
      {
        id: '4001172',
        name: '称心如意手摇咖啡磨豆机咖啡豆研磨机',
        price: '289.00',
        picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg',
      },
      {
        id: '4001594',
        name: '日式黑陶功夫茶组双侧把茶具礼盒装',
        price: '288.00',
        picture: 'https://yanxuan-item.nosdn.127.net/3346b7b92f9563c7a7e24c7ead883f18.jpg',
      },
      {
        id: '4001009',
        name: '竹制干泡茶盘正方形沥水茶台品茶盘',
        price: '109.00',
        picture: 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png',
      },
      {
        id: '4001874',
        name: '古法温酒汝瓷酒具套装白酒杯莲花温酒器',
        price: '488.00',
        picture: 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png',
      },
      {
        id: '4001649',
        name: '大师监制龙泉青瓷茶叶罐',
        price: '139.00',
        picture: 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png',
      },
      {
        id: '3997185',
        name: '与众不同的口感汝瓷白酒杯套组1壶4杯',
        price: '108.00',
        picture: 'https://yanxuan-item.nosdn.127.net/8e21c794dfd3a4e8573273ddae50bce2.jpg',
      },
      {
        id: '3997403',
        name: '手工吹制更厚实白酒杯壶套装6壶6杯',
        price: '99.00',
        picture: 'https://yanxuan-item.nosdn.127.net/af2371a65f60bce152a61fc22745ff3f.jpg',
      },
      {
        id: '3998274',
        name: '德国百年工艺高端水晶玻璃红酒杯2支装',
        price: '139.00',
        picture: 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg',
      },
    ]

    // 1. 声明一个字符串
    let str = ''

    // 2. 遍历数组数据
    goodsList.forEach(item => {
      const { name, price, picture } = item
      str += `
      <div class="item">
        <img src=${picture} alt="">
        <p class="name">${name}</p>
        <p class="price">${price}</p>
      </div>
      `
    })
    // 3. 把拼接好的字符串添加到页面中
    document.querySelector('.list').innerHTML = str
  </script>
</body>

</html>

filter

filter 是 JavaScript 数组的一个内置方法,用于创建一个新数组,该数组中的元素是通过检查指定函数而得出的所有使该函数返回 true 的原数组元素。换句话说,filter 方法会对数组的每个元素执行一个提供的测试函数,并返回一个新数组,该数组只包含那些使测试函数返回 true 的元素。

filter 方法接收一个回调函数作为参数,该回调函数用于确定哪些元素应该包含在新数组中。这个回调函数可以接收三个参数:当前元素的值、当前元素的索引以及正在操作的数组本身。

例如:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];  
  
const evenNumbers = numbers.filter(function(value) {  
  return value % 2 === 0; // 返回所有偶数  
});  
  
console.log(evenNumbers); // 输出: [2, 4, 6, 8, 10]

可以使用箭头函数来简化回调函数的书写:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];  
  
const evenNumbers = numbers.filter(value => value % 2 === 0);  
  
console.log(evenNumbers); // 输出: [2, 4, 6, 8, 10]

filter 方法不会改变原数组,而是返回一个新数组,其中包含了通过测试的元素。

如果想要基于多个条件来过滤数组,可以在 filter 的回调函数中组合这些条件:

const users = [  
  { name: 'Alice', age: 25, isActive: true },  
  { name: 'Bob', age: 30, isActive: false },  
  { name: 'Charlie', age: 35, isActive: true },  
  { name: 'David', age: 20, isActive: true }  
];  
  
const activeUsers = users.filter(user => user.isActive && user.age >= 25);  
  
console.log(activeUsers);  
/* 输出:  
[  
  { name: 'Alice', age: 25, isActive: true },  
  { name: 'Charlie', age: 35, isActive: true }  
]  
*/

在上面的例子中,filter 方法返回了一个新数组,其中只包含 isActivetrue 且年龄大于或等于 25 的用户。

例子:根据价格筛选商品

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>价格筛选</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .list {
      width: 990px;
      margin: 0 auto;
      display: flex;
      flex-wrap: wrap;
    }

    .item {
      width: 240px;
      margin-left: 10px;
      padding: 20px 30px;
      transition: all .5s;
      margin-bottom: 20px;
    }

    .item:nth-child(4n) {
      margin-left: 0;
    }

    .item:hover {
      box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
      transform: translate3d(0, -4px, 0);
      cursor: pointer;
    }

    .item img {
      width: 100%;
    }

    .item .name {
      font-size: 18px;
      margin-bottom: 10px;
      color: #666;
    }

    .item .price {
      font-size: 22px;
      color: firebrick;
    }

    .item .price::before {
      content: "¥";
      font-size: 14px;
    }

    .filter {
      display: flex;
      width: 990px;
      margin: 0 auto;
      padding: 50px 30px;
    }

    .filter a {
      padding: 10px 20px;
      background: #f5f5f5;
      color: #666;
      text-decoration: none;
      margin-right: 20px;
    }

    .filter a:active,
    .filter a:focus {
      background: #05943c;
      color: #fff;
    }
  </style>
</head>

<body>
  <div class="filter">
    <a data-index="1" href="javascript:;">0-100元</a>
    <a data-index="2" href="javascript:;">100-300元</a>
    <a data-index="3" href="javascript:;">300元以上</a>
    <a href="javascript:;">全部区间</a>
  </div>
  <div class="list">
    <!-- <div class="item">
      <img src="" alt="">
      <p class="name"></p>
      <p class="price"></p>
    </div> -->
  </div>
  <script>
    const goodsList = [
      {
        id: '4001172',
        name: '称心如意手摇咖啡磨豆机咖啡豆研磨机',
        price: '289.00',
        picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg',
      },
      {
        id: '4001594',
        name: '日式黑陶功夫茶组双侧把茶具礼盒装',
        price: '288.00',
        picture: 'https://yanxuan-item.nosdn.127.net/3346b7b92f9563c7a7e24c7ead883f18.jpg',
      },
      {
        id: '4001009',
        name: '竹制干泡茶盘正方形沥水茶台品茶盘',
        price: '109.00',
        picture: 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png',
      },
      {
        id: '4001874',
        name: '古法温酒汝瓷酒具套装白酒杯莲花温酒器',
        price: '488.00',
        picture: 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png',
      },
      {
        id: '4001649',
        name: '大师监制龙泉青瓷茶叶罐',
        price: '139.00',
        picture: 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png',
      },
      {
        id: '3997185',
        name: '与众不同的口感汝瓷白酒杯套组1壶4杯',
        price: '108.00',
        picture: 'https://yanxuan-item.nosdn.127.net/8e21c794dfd3a4e8573273ddae50bce2.jpg',
      },
      {
        id: '3997403',
        name: '手工吹制更厚实白酒杯壶套装6壶6杯',
        price: '99.00',
        picture: 'https://yanxuan-item.nosdn.127.net/af2371a65f60bce152a61fc22745ff3f.jpg',
      },
      {
        id: '3998274',
        name: '德国百年工艺高端水晶玻璃红酒杯2支装',
        price: '139.00',
        picture: 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg',
      },
    ]

    // 1. 渲染函数
    function render(arr) {
      let str = ''
      arr.forEach(item => {
        const { name, price, picture } = item
        str += `
        <div class="item">
          <img src=${picture} alt="">
          <p class="name">${name}</p>
          <p class="price">${price}</p>
        </div>
        `
      })
      document.querySelector('.list').innerHTML = str
    }

    render(goodsList)

    // 2. 使用事件委托实现筛选
    const filter = document.querySelector('.filter')
    filter.addEventListener('click', function (e) {
      // e.target.dataset.index e.target.tarName
      const { tagName, dataset } = e.target
      if (tagName === 'A') {
        let arr = goodsList

        if (dataset.index === '1') {
          arr = goodsList.filter(item => item.price > 0 && item.price <= 100)
        } else if (dataset.index === '2') {
          arr = goodsList.filter(item => item.price >= 100 && item.price <= 300)
        } else if (dataset.index === '3') {
          arr = goodsList.filter(item => item.price >= 300)
        }

        // 渲染函数
        render(arr)
      }
    })
  </script>
</body>

</html>

reduce

reduce 是 JavaScript 数组的一个内置方法,用于数组元素的累加,返回累加结果。

语法:

arr.reduce(callback[, initialValue])
  • callback(回调函数):执行数组中每个元素的累加操作,包含四个参数:
    • accumulator(累加器累计回调的返回值;它是上一次调用回调时返回的累积值)
    • currentValue(数组中正在处理的当前元素)
    • currentIndex(数组中正在处理的当前元素的索引)
    • arrayreduce() 方法正在操作的数组)
  • initialValue(可选)作为第一次调用 callback 函数时的第一个参数的值 accumulator。如果没有提供初始值,则将使用数组中的第一个元素。在没有初始值的空数组上调用 reduce 将报错。

例子:计算数组元素的和

const numbers = [1, 2, 3];  
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 10);  
console.log(sum); // 输出: 16

// 上一次值     当前值      返回值(第一次循环)
//  10           1           11
// 上一次值     当前值      返回值(第二次循环)
//  11           2           13
// 上一次值     当前值      返回值(第三次循环)
//  13           3           16

例子:计算工资总计

const arr = [{
  name: '张三',
  salary: 10000
}, {
  name: '李四',
  salary: 10000
}, {
  name: '王五',
  salary: 20000
},
]

// 计算所有人的薪资
// 初始值必须写,因为这个初始值是一个对象
const total = arr.reduce(function (accu, curr) {
  return accu + curr.salary
}, 0)
console.log(total)

find

find 是 JavaScript 数组的一个内置方法,它返回数组中满足所提供测试函数的第一个元素的值。否则返回 undefined

语法:

arr.find(callback(element[, index[, array]])[, thisArg])
  • callback:用于测试数组的每个元素的函数。
    • element:当前正在处理的元素。
    • index(可选):当前正在处理的元素的索引。
    • array(可选):find() 方法被调用的数组。
  • thisArg(可选):执行 callback 函数时用作 this 的值。

这个函数会遍历数组中的每个元素,并对每个元素执行提供的函数。如果函数对某个元素返回 true,那么 find() 会立即返回该元素的值,并停止遍历数组。如果没有元素使函数返回 true,那么 find() 会返回 undefined

例如:

let array = [1, 2, 3, 4, 5];  
  
let found = array.find(function(element) {  
  return element > 3;  
});  
  
console.log(found);  // 输出:4

在这个例子中,find() 方法遍历数组 array,并使用提供的函数检查每个元素是否大于 3。当找到第一个大于 3 的元素(即 4)时,find() 方法返回该元素的值,并停止遍历数组。如果没有找到任何大于 3 的元素,find() 方法将返回 undefined

可以使用箭头函数来简化代码:

let array = [1, 2, 3, 4, 5, 6];  
  
let found = array.find(element => element > 3);  
  
console.log(found); // 输出:4

注意,find() 方法不会改变原数组。

例子:查找对象数组中的某个对象

let arr = [
  {name: 'tom', age: 18},
  {name: 'jerry', age: 20}
]

const tom = arr.find(item => item.name === 'tom')
console.log(tom) // 输出:{name: 'tom', age: 18}

构造函数

可以使用 new 关键字与自定义的构造函数来快速创建多个类似的对象。构造函数是一种特殊的函数,用于初始化新创建的对象。当使用 new 关键字创建一个对象时,就会调用这个构造函数。构造函数约定以大写字母开头,以区别于普通函数,并使用 new 调用。

例如:

function Person(name, age) {  
  this.name = name;  
  this.age = age;  
  
  this.greet = function() {  
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);  
  };  
}  
  
// 使用 new 关键字和构造函数创建一个新的 Person 对象  
let john = new Person('John', 30);  
  
console.log(john.name); // 输出: John  
console.log(john.age);  // 输出: 30  
john.greet();           // 输出: Hello, my name is John and I am 30 years old.

在上面的例子中,Person 是一个构造函数,它接受两个参数 nameage,然后使用 this 关键字给新创建的对象添加属性和方法。

构造函数的特点:

  • 使用 new 关键字:当使用 new 关键字调用构造函数进行实例化时,JavaScript 会创建一个新的空对象,然后调用构造函数并将这个新对象作为上下文(this 指向该空对象)传入,再执行构造函数中的代码。构造函数内部无需写 return,返回值即为新创建的对象。
  • this 关键字的使用:在构造函数中,this 引用的是通过 new 创建的新对象实例。这允许为实例添加属性和方法。
  • 原型(Prototype):JavaScript 中的每个对象都有一个指向其原型对象的内部链接。构造函数也有一个 prototype 属性,它用于定义所有实例共享的属性和方法。这通常用于减少内存使用,因为方法不需要在每个实例上单独创建。

例子:创建对象

function Goods(name, price, count) {
  this.name = name
  this.price = price
  this.count = count
}

console.log(new Goods('小米', 1999, 20))
console.log(new Goods('华为', 3999, 50))

实例成员

通过构造函数创建的对象成为实例对象,实例对象中的属性和方法称为实例成员,包括实例属性和实例方法。

例如:

function Person(name, age) {  
  this.name = name; // 实例属性  
  this.age = age;   // 实例属性  
  
  this.greet = function() { // 实例方法  
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);  
  };  
}  
  
const john = new Person('John', 30);  
console.log(john.name); // 输出 "John"  
console.log(john.age);  // 输出 30  
john.greet();           // 输出 "Hello, my name is John and I'm 30 years old."

在上面的例子中,nameage 是实例属性,而 greet 是一个实例方法。它们都只存在于 john 这个实例对象上。

注意:

  • 实例属性和方法是通过 this 关键字在构造函数内部或对象字面量中定义的。
  • 每个通过构造函数创建的实例对象都会有自己独立的实例成员副本。如果在一个实例对象上修改了实例成员,不会影响其他实例。
  • 与原型成员不同,实例成员不会在对象之间共享。这意味着如果有很多具有相同方法的对象实例,使用原型成员会更加高效,因为所有实例都会共享同一个方法,而不是每个实例都有自己的方法副本。

静态成员

构造函数的属性和方法被称为静态成员,包括静态属性和静态方法。

// 构造函数
function Person() {
  // 省略实例成员
}

// 静态属性
Person.eyes = 2
Person.arms = 2

// 静态方法
Person.walk = function () {
  console.log('walking...')
  console.log(this.eyes)
}

Person.walk()

注意:

  • 静态成员只能通过构造函数来访问。
  • 静态方法中的 this 执行构造函数。

在 ES6 及更高版本的 JavaScript 中,可以使用 class 语法来定义类,并使用 static 关键字来定义静态成员。

包装类型

在 JavaScript 中,原始数据类型(如numberstringbooleannullundefined)是不可变的,并且没有方法。然而,JavaScript 提供了特殊的对象类型,称为包装类型(Wrapper Types),它们可以将原始数据类型包装成对象,从而使其可以调用方法。这些包装类型包括NumberStringBooleanObjectSymbol(注意:nullundefined没有对应的包装类型)。

当尝试对原始值调用方法时,JavaScript 会临时将其转换为对应的包装对象,然后调用该对象的方法。一旦方法调用完成,这个临时对象就会被销毁,原始值保持不变。

例如:

// 字符串包装类型  
const str = "Hello";  
const upperCaseStr = str.toUpperCase(); // 调用 String 包装类型的方法  
console.log(upperCaseStr); // 输出 "HELLO"  
  
// 数字包装类型  
const num = 42;  
const numStr = num.toString(); // 调用 Number 包装类型的方法  
console.log(numStr); // 输出 "42"  
  
// 布尔包装类型  
const bool = true;  
const boolStr = bool.toString(); // 调用 Boolean 包装类型的方法  
console.log(boolStr); // 输出 "true"

内置构造函数

Object

Object 是内置的构造函数,用于创建普通对象。但推荐使用字面量方式声明对象,而不是使用 new Object() 方式。经常使用的是 Object 的静态方法。

  • Object.keys():获取对象中所有属性名,返回一个数组。
const obj = {  
  key1: 'value1',  
  key2: 'value2',  
  key3: 'value3'  
};  
  
const keys = Object.keys(obj);  
  
console.log(keys); // 输出: ['key1', 'key2', 'key3']

在这个例子中,Object.keys(obj) 返回一个包含 obj 对象所有自身属性名的数组。

  • Object.values():获取对象中所有属性值,返回一个数组。
const obj = {  
  key1: 'value1',  
  key2: 'value2',  
  key3: 'value3'  
};  
  
const values = Object.values(obj);  
  
console.log(values); // 输出: ['value1', 'value2', 'value3']
  • Object.assign():对象拷贝,经常用于给对象添加对象,返回目标对象。如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target = { a: 1 };  
const source1 = { b: 2 };  
const source2 = { c: 3 };  
  
const result = Object.assign(target, source1, source2);  
  
console.log(result); // 输出: { a: 1, b: 2, c: 3 }  
console.log(target); // 输出: { a: 1, b: 2, c: 3 },注意 target 也被修改了

例子:从对象获取数据并转换为字符串

const spec = {size: '40cm*40cm', color: '黑色'}
const desc = Object.values(spec).join('/')
console.log(desc) // 输出:40cm*40cm/黑色

Array

Array 是内置的构造函数,用于创建数组。但推荐使用字面量创建数组,而不是使用 new Array() 创建。经常使用的是 Array 的静态方法。

  • Array.isArray():是 JavaScript 中的一个静态方法,用于确定一个对象是否是一个数组。这个方法直接属于 Array 构造函数,而不是数组的实例方法,因此它应该在 Array 上调用,而不是在数组实例上。
const array = [1, 2, 3];  
const notArray = "This is not an array";  
  
console.log(Array.isArray(array)); // 输出: true  
console.log(Array.isArray(notArray)); // 输出: false
  • Array.from():是 JavaScript 中的一个静态方法,用于从一个类似数组或可迭代的对象创建一个新的数组实例。这个方法在 ES6 (ECMAScript 2015) 中被引入,它提供了一种将非数组对象转换为数组的简单方式。
const arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 };  
const array1 = Array.from(arrayLike);  
console.log(array1); // 输出: ['a', 'b', 'c']

const str = 'hello';  
const array2 = Array.from(str);  
console.log(array2); // 输出: ['h', 'e', 'l', 'l', 'o']
<body>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
  <script>
    const lis = document.querySelectorAll('ul li')
    console.log(lis)  // 输出为伪数组 NodeList(3) [li, li, li],不能调用 pop() 方法
    const lisArr = Array.from(lis)
    console.log(lisArr)  // 输出为数组 [li, li, li],可以调用 pop() 方法
    lisArr.pop()
    console.log(lisArr)  // 输出为数组 [li, li]
  </script>
</body>
  • Array.of():是 JavaScript 中的一个静态方法,用于创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。这个方法在 ES6(ECMAScript 2015)中被引入,提供了一种更直观的方式来创建数组,特别是想从一组值直接创建一个数组时,而不是从一个类数组对象或可迭代对象。
const array = Array.of(1, 2, 3);  
console.log(array); // 输出: [1, 2, 3]

购物车案例

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .list {
      width: 990px;
      margin: 100px auto 0;
    }

    .item {
      padding: 15px;
      transition: all .5s;
      display: flex;
      border-top: 1px solid #e4e4e4;
    }

    .item:nth-child(4n) {
      margin-left: 0;
    }

    .item:hover {
      cursor: pointer;
      background-color: #f5f5f5;
    }

    .item img {
      width: 80px;
      height: 80px;
      margin-right: 10px;
    }

    .item .name {
      font-size: 18px;
      margin-right: 10px;
      color: #333;
      flex: 2;
    }

    .item .name .tag {
      display: block;
      padding: 2px;
      font-size: 12px;
      color: #999;
    }

    .item .price,
    .item .sub-total {
      font-size: 18px;
      color: firebrick;
      flex: 1;
    }

    .item .price::before,
    .item .sub-total::before,
    .amount::before {
      content: "¥";
      font-size: 12px;
    }

    .item .spec {
      flex: 2;
      color: #888;
      font-size: 14px;
    }

    .item .count {
      flex: 1;
      color: #aaa;
    }

    .total {
      width: 990px;
      margin: 0 auto;
      display: flex;
      justify-content: flex-end;
      border-top: 1px solid #e4e4e4;
      padding: 20px;
    }

    .total .amount {
      font-size: 18px;
      color: firebrick;
      font-weight: bold;
      margin-right: 50px;
    }
  </style>
</head>

<body>
  <div class="list">
    <!-- <div class="item">
      <img src="https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg" alt="">
      <p class="name">称心如意手摇咖啡磨豆机咖啡豆研磨机 <span class="tag">【赠品】10优惠券</span></p>
      <p class="spec">白色/10寸</p>
      <p class="price">289.90</p>
      <p class="count">x2</p>
      <p class="sub-total">579.80</p>
    </div> -->
  </div>
  <div class="total">
    <div>合计:<span class="amount">1000.00</span></div>
  </div>
  <script>
    const goodsList = [
      {
        id: '4001172',
        name: '称心如意手摇咖啡磨豆机咖啡豆研磨机',
        price: 289.9,
        picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg',
        count: 2,
        spec: { color: '白色' }
      },
      {
        id: '4001009',
        name: '竹制干泡茶盘正方形沥水茶台品茶盘',
        price: 109.8,
        picture: 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png',
        count: 3,
        spec: { size: '40cm*40cm', color: '黑色' }
      },
      {
        id: '4001874',
        name: '古法温酒汝瓷酒具套装白酒杯莲花温酒器',
        price: 488,
        picture: 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png',
        count: 1,
        spec: { color: '青色', sum: '一大四小' }
      },
      {
        id: '4001649',
        name: '大师监制龙泉青瓷茶叶罐',
        price: 139,
        picture: 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png',
        count: 1,
        spec: { size: '小号', color: '紫色' },
        gift: '50g茶叶,清洗球'
      }
    ]

    // 渲染页面
    document.querySelector('.list').innerHTML = goodsList.map(item => {
      const { id, name, price, picture, count, spec, gift } = item
      const specStr = Object.values(spec).join('/')
      const giftStr = gift ? gift.split(',').map(item => `<span class="tag">【赠品】${item}</span>`).join('') : ''
      const subTotal = ((price * 100 * count) / 100).toFixed(2)

      return `
        <div class="item">
          <img src=${picture} alt="">
          <p class="name">${name} ${giftStr}</p>
          <p class="spec">${specStr}</p>
          <p class="price">${price.toFixed(2)}</p>
          <p class="count">x${count}</p>
          <p class="sub-total">${subTotal}</p>
        </div>
        `
    }).join('')

    // 合计模块
    document.querySelector('.amount').innerHTML = goodsList.reduce((accu, curr) => accu + (curr.price * 100 * curr.count) / 100, 0).toFixed(2)
  </script>
</body>

</html>

面向对象

JavaScript 支持面向过程(Procedural Programming)和面向对象(Object-Oriented Programming,OOP)两种编程范式。这两种范式提供了不同的方法来组织和管理代码,每种都有其适用场景。

面向过程编程(Procedural Programming)

面向过程编程是一种基于一系列步骤或函数(也称为过程)来构建程序的编程范式。在面向过程编程中,主要的关注点在于将问题分解为一系列线性执行的步骤或函数。每个函数通常执行一个特定的任务,并且程序流程通过调用这些函数来控制。

在 JavaScript 中,可以通过编写一系列的函数并使用它们来执行特定的任务来实现面向过程编程。这种风格通常用于执行一系列简单的、顺序性的任务,或者当对象的概念不是主要的关注点时。

function add(a, b) {  
  return a + b;  
}  
  
function multiply(a, b) {  
  return a * b;  
}  
  
const result = add(5, 3);  
const product = multiply(result, 2);  
console.log(product); // 输出: 16

面向对象编程(Object-Oriented Programming, OOP)

面向对象编程是一种基于“对象”的编程范式。在面向对象编程中,程序由对象组成,这些对象包含数据(属性)和可以操作这些数据的方法。对象之间通过消息传递来交互,这通常是通过调用对象的方法来实现的。面向对象编程还强调封装、继承和多态等概念。

在 JavaScript 中,可以使用构造函数、原型或 ES6 的类语法来创建对象。对象可以包含属性和方法,并且可以通过继承来共享属性和方法。

使用构造函数和原型实现面向对象编程:

function Person(name, age) {  
  this.name = name;  
  this.age = age;  
}  
  
Person.prototype.greet = function() {  
  console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);  
};  
  
const john = new Person('John', 30);  
john.greet(); // 输出: Hello, my name is John and I'm 30 years old.

使用 ES6 类语法实现面向对象编程:

class Person {  
  constructor(name, age) {  
    this.name = name;  
    this.age = age;  
  }  
  
  greet() {  
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);  
  }  
}  
  
const john = new Person('John', 30);  
john.greet(); // 输出: Hello, my name is John and I'm 30 years old.

选择面向过程还是面向对象主要取决于具体需求、项目的复杂性和团队的编程风格。对于简单的脚本或小型程序,面向过程编程可能更为简单和直接。然而,对于大型、复杂的项目,特别是那些需要代码重用、封装和扩展性的项目,面向对象编程通常更为合适。

在实际开发中,可能会发现自己在同一个项目中同时使用面向过程和面向对象的编程风格,这取决于具体任务的需要。JavaScript 的灵活性允许你根据需要选择最适合的编程范式。

原型对象

在JavaScript中,原型(Prototype)是一个非常重要的概念,它与面向对象编程(OOP)中的继承和对象创建密切相关。

每个构造函数都有一个 prototype 属性,这个属性是一个指向原型对象的指针。原型对象包含了可以由特定类型的所有实例共享的属性和方法。换句话说,当创建一个由某个构造函数生成的新对象时,这个新对象内部将包含一个指向构造函数的 prototype 对象的指针。

可以看到对象实例化不会多次创建原型上的函数,即可以将不变的方法,直接定义在 prototype 上,这样所有对象的实例就可以共享这些方法。

例如:

function Person(name, age) {  
  this.name = name;  
  this.age = age;  
}  
  
Person.prototype.greet = function() {  
  console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);  // this 指向调用 greet 方法的 Person 实例
};  
  
const john = new Person('John', 30);  
john.greet(); // 输出: Hello, my name is John and I'm 30 years old.

在这个示例中,Person 是一个构造函数,它接受 nameage 作为参数来初始化新创建的 Person 对象。Person.prototype.greet 是一个定义在 Person 原型上的方法,因此所有通过 new Person() 创建的对象都会继承这个方法。

例子:创建自定义函数,挂载到原型对象上

Array.prototype.max = function () {
  return Math.max(...this)
}

console.log([5, 4, 6].max())

constructor 属性

在 JavaScript 中,每个对象都有一个 constructor 属性,它指向创建该对象实例的构造函数。这个属性是在对象创建时自动设置的,并且通常用于确定对象的类型或检查对象是否由特定的构造函数创建。

例如:

// 定义一个构造函数  
function Person(name, age) {  
  this.name = name;  
  this.age = age;  
}  
  
// 创建一个 Person 实例  
const john = new Person('John Doe', 30);  
  
// 访问 john 的 constructor 属性  
console.log(john.constructor); // 输出: [Function: Person]  
console.log(john.constructor === Person); // 输出: true
console.log(Person.prototype.constructor); // 输出: [Function: Person]  
console.log(Person.prototype.constructor === Person); // 输出: true

在这个例子中,john 对象是由 Person 构造函数创建的,因此 john.constructor 指向 Person 函数。

然而,当修改原型时,必须小心处理 constructor 属性。例如,如果直接在一个原型对象上设置一个新对象,constructor 属性可能不会正确地指向原构造函数。

function Person(name, age) {  
  this.name = name;  
  this.age = age;  
}  
  
Person.prototype = {  
  greet: function() {  
    console.log(`Hello, my name is ${this.name}`);  
  }  
};  
  
const john = new Person('John Doe', 30);  
  
// 注意:这里 john.constructor 不再指向 Person,而是指向 Object  
console.log(john.constructor); // 输出: [Function: Object]  
console.log(john.constructor === Person); // 输出: false

在这个错误的示例中,直接替换了 Person.prototype 对象,而没有保持对原始 constructor 属性的引用。因此,现在 john.constructor 指向了 Object 构造函数,而不是 Person

为了避免这种情况,可以在替换原型对象时显式地设置 constructor 属性:

function Person(name, age) {  
  this.name = name;  
  this.age = age;  
}  
  
Person.prototype = {  
  constructor: Person, // 显式设置 constructor 属性  
  greet: function() {  
    console.log(`Hello, my name is ${this.name}`);  
  }  
};  
  
const john = new Person('John Doe', 30);  
  
console.log(john.constructor); // 输出: [Function: Person]  
console.log(john.constructor === Person); // 输出: true

在这个修复后的示例中,显式地将 constructor 属性设置回 Person 构造函数,以确保它正确地指向创建 john 实例的构造函数。

对象原型

实例对象都有一个属性 __proto__ 指向构造函数的 prototype 原型对象。对象原型 __proto__[[prototype]])的 constructor 属性指向创建该实例对象的构造函数。

// 定义一个构造函数  
function Person(name, age) {  
  this.name = name;  
  this.age = age;  
}  
  
// 创建一个 Person 实例  
const john = new Person('John Doe', 30);  
  
// 访问 john 的 constructor 属性  
console.log(john.constructor); // 输出: [Function: Person]  
console.log(john.constructor === Person); // 输出: true
console.log(Person.prototype.constructor); // 输出: [Function: Person]  
console.log(Person.prototype.constructor === Person); // 输出: true
console.log(john.__proto__ === Person.prototype); // 输出: true
console.log(john.__proto__.constructor === Person); // 输出: true

image-20240407153623181

原型继承

在 JavaScript 中,原型继承允许创建一个新对象,该对象可以继承另一个对象的属性和方法。

例如:

function Parent() {  
  this.name = 'Parent';  
  this.play = [1, 2, 3];  
}  
  
function Child() {  
  this.type = 'Child';  
}  
  
// 继承 Parent  
Child.prototype = new Parent();  
  
const child1 = new Child();  
console.log(child1.name); // 输出 "Parent"  
console.log(child1.play); // 输出 [1, 2, 3]  
  
const child2 = new Child();  
child2.play.push(4);  
  
console.log(child1.play); // 输出 [1, 2, 3, 4],因为 play 数组是共享的

原型链

在 JavaScript 中,每个对象都有一个内部链接(__proto__)指向它的原型对象。这个原型对象自身也有原型,这样就形成了一个链条,直到某个对象的原型为 null。这个链条就被称为原型链。当试图访问一个对象的某个属性时,如果该对象本身没有这个属性,JavaScript 就会沿着原型链查找这个属性。

原型链的工作原理

  1. 属性查找:当尝试访问一个对象的属性时,JavaScript 首先会在对象本身上查找该属性。
  2. 原型链查找:如果在对象本身上找不到该属性,JavaScript 就会查找对象的原型(__proto__),然后在这个原型上查找属性。
  3. 继续向上查找:如果原型上也没有找到该属性,那么 JavaScript 就会继续查找原型的原型,直到找到该属性或到达原型链的顶端(null)。
  4. 查找结束:如果到达原型链的顶端仍未找到属性,那么返回 undefined

例如:

// 定义一个构造函数  
function Person(name, age) {  
  this.name = name;  
  this.age = age;  
}  
  
// 创建一个 Person 实例  
const john = new Person('John Doe', 30);  

// 实例对象的原型对象
console.log(john.__proto__ === Person.prototype); // 输出: true
  
// 原型对象的原型对象
console.log(Person.prototype.__proto__ === Object.prototype); // 输出: true
console.log(Object.prototype.__proto__); // 输出: null

可以使用 instanceof 检测构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。换句话说,它用来确定一个对象是否是一个构造函数的实例。

语法:

object instanceof constructor

其中 object 是要检测的对象,而 constructor 是一个构造函数。

如果 object 的原型链中包含 constructor.prototype,那么 instanceof 运算符将返回 true,否则返回 false

例如:

function Car(make, model, year) {  
  this.make = make;  
  this.model = model;  
  this.year = year;  
}  
  
const myCar = new Car('xiaomi', 'su7', 2024);  
  
console.log(myCar instanceof Car); // 输出: true  
console.log(myCar instanceof Object); // 输出: true,因为所有对象都继承自 Object  
console.log(myCar instanceof Array); // 输出: false,因为 myCar 不是一个数组

在上面的例子中,myCar 是通过 Car 构造函数创建的,因此 myCar instanceof Car 返回 true。同时,因为所有的 JavaScript 对象最终都继承自 Object,所以 myCar instanceof Object 也返回 true。但是,myCar 显然不是一个数组,因此 myCar instanceof Array 返回 false

拷贝

在JavaScript中,拷贝操作通常分为浅拷贝(Shallow Copy)和深拷贝(Deep Copy)两种。这两种拷贝方式的主要区别在于如何处理对象中的引用类型(如数组、对象等)。

浅拷贝

浅拷贝只会复制对象的顶层属性和值。如果对象的属性值是一个对象或数组,那么它只会复制内存中的地址引用,而不是真正的对象本身。因此,原始对象和拷贝后的对象仍然共享同一个内部对象。对拷贝后的对象进行修改,可能会影响到原始对象。

JavaScript中常见的浅拷贝方式有:

  • 使用 Object.assign() 方法
  • 使用展开运算符(...
  • 使用数组的 slice() 方法(仅对一维数组有效)
  • 使用数组的 concat() 方法(仅对一维数组有效)

例如:

const obj1 = { a: 1, b: { c: 2 } };  
const obj2 = {...obj1};  
obj2.b.c = 3;  
console.log(obj1.b.c); // 输出 3,说明obj1也被修改了

深拷贝

深拷贝会递归地复制对象的所有层级。它会创建一个新的对象,并复制原始对象的所有属性和值。如果属性值是一个对象或数组,那么它会递归地复制这个对象或数组。因此,原始对象和拷贝后的对象是完全独立的,对其中一个对象的修改不会影响到另一个。

JavaScript中实现深拷贝的方式有:

  • 使用 JSON.parse(JSON.stringify(obj)) 方法(注意:这种方法无法处理函数和循环引用的情况)
  • 使用第三方库,如 lodash 的 _.cloneDeep() 方法
  • 自定义递归函数实现深拷贝

例子:使用 JSON.parse(JSON.stringify(obj)) 方法实现深拷贝

let obj1 = { a: 1, b: { c: 2 } };  
let obj2 = JSON.parse(JSON.stringify(obj1));  
obj2.b.c = 3;  
console.log(obj1.b.c); // 输出 2,说明obj1没有被修改

需要注意的是,JSON.parse(JSON.stringify(obj)) 这种深拷贝方式虽然简单,但有一些限制和缺点。例如,它无法处理函数、undefinedSymbol 值和循环引用的情况。因此,在实际开发中,如果需要处理这些复杂情况,可能需要使用其他方式来实现深拷贝。

例子:使用 lodash 的 _.cloneDeep() 方法实现深拷贝

<body>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <script>
    // 原始对象  
    let obj1 = {  
      a: 1,  
      b: {  
        c: 2,  
        d: [3, 4]  
      },  
      e: new Date()  
    };  
      
    // 使用lodash的cloneDeep方法实现深拷贝  
    let obj2 = _.cloneDeep(obj1);  
      
    // 修改obj2的属性,不会影响到obj1  
    obj2.b.c = 3;  
    obj2.b.d.push(5);  
    obj2.e.setDate(obj2.e.getDate() + 1);  
      
    console.log(obj1); // 原始对象未改变  
    console.log(obj2); // 拷贝后的对象已改变
  </script>
</body>

异常处理

throw

在 JavaScript 中,可以使用 throw 语句显式地抛出一个用户自定义的异常。throw 语句后面通常跟着一个表达式,这个表达式的结果将被作为异常对象抛出。最常见的是创建一个新的 Error 对象(或其子类的实例),然后抛出它。

例子:

function divide(a, b) {  
  if (b === 0) {  
      // 抛出一个自定义异常  
      throw new Error('除数不能为0');  
  }  
  return a / b;  
}  

console.log(divide(10, 0));

在上面的例子中,divide 函数检查其第二个参数 b 是否为 0。如果是 0,它将使用 throw 语句抛出一个新的 Error 对象,其中包含一条描述错误的消息,并终止程序运行。

try

在 JavaScript 中,try...catch...finally 语句用于处理可能引发异常的代码块,并提供了一种机制来清理资源或执行不论是否发生异常都需要执行的代码。

  • try 代码块包含可能引发异常的代码。
  • catch 代码块包含当 try 代码块中抛出异常时执行的代码。
  • finally 代码块包含无论是否发生异常都会执行的代码。

语法:

try {  
    // 尝试执行的代码块  
    // 如果这里抛出异常,控制权会转移到 catch 代码块  
} catch (error) {  
    // 当 try 代码块中的代码抛出异常时,执行这里的代码  
    // error 对象包含了异常的信息  
    console.error("捕获到异常:", error);  
} finally {  
    // 无论是否发生异常,finally 代码块中的代码都会执行  
    // 通常用于清理资源或执行一些必须完成的操作  
}

catch 语句中的 error 参数是一个包含异常信息的对象。可以通过 error 对象的属性(如 messagestack)来获取有关异常的详细信息。

finally 代码块是可选的,但它在某些情况下非常有用,例如当需要在异常处理完成后释放资源或执行一些必要的清理操作时。

例子:

let fileHandle;  
  
try {  
    // 假设 openFile 是一个可能抛出异常的函数  
    fileHandle = openFile("example.txt");  
    // 其他可能引发异常的代码...  
} catch (error) {  
    console.error("在尝试打开文件或执行其他操作时发生错误:", error);  
    // 在这里处理错误,比如记录日志或通知用户  
} finally {  
    // 无论是否发生异常,都会关闭文件句柄  
    if (fileHandle) {  
        closeFile(fileHandle);  
    }  
    // 执行其他必要的清理工作...  
}

在这个例子中,try 代码块尝试打开一个文件,并可能执行其他操作。如果发生任何异常,控制权将转移到 catch 代码块,其中将打印错误信息。无论是否发生异常,finally 代码块都会执行,确保文件句柄被正确关闭。

注意,如果 trycatch 代码块中有 return 语句,并且执行了 return,那么 finally 代码块仍然会执行,但是 finally 中的 return 语句会覆盖 trycatch 中的 return 语句。因此,需要小心处理 finally 代码块中的 return 语句,以避免意外的行为。

debugger

在 JavaScript 中,debugger 是一个语句,当执行到该语句时,如果浏览器的开发者工具(Developer Tools)是打开的,并且调试器(Debugger)功能是启用的,那么执行会暂停,允许开发者检查当前的执行上下文(例如变量值、函数调用栈等)。这对于调试代码和排查问题非常有用。

例子:

function myFunction() {  
  let a = 5;  
  let b = 10;  

  debugger; // 当执行到这里时,如果调试器开启,将会暂停执行  

  let sum = a + b;  
  console.log(sum);  
}  

myFunction(); // 调用函数,如果调试器开启且遇到 debugger 语句,将会暂停

在这个例子中,当 myFunction 被调用时,执行会在 debugger 语句处暂停(前提是浏览器的开发者工具是打开的)。这时可以查看 ab 的值,单步执行代码,或者执行其他调试操作。

注意:在生产环境的代码中,通常不会保留 debugger 语句,因为它们会干扰正常的代码执行。在将代码部署到生产环境之前,确保移除或注释掉所有的 debugger 语句。

this

this 指向

在JavaScript中,this 是一个特殊的关键字,它引用的是函数执行时的上下文对象。this 的值取决于函数是如何被调用的,而不是如何定义的。下面是一些常见情况下 this 的指向:

  • 全局上下文:在全局作用域中,this 通常指向全局对象(在浏览器中是 window)。
console.log(this === window); // 输出 true
  • 对象方法:当一个函数作为对象的方法被调用时,this 指向调用该函数的对象。
const obj = {  
    prop: 'Hello',  
    method: function() {  
        console.log(this.prop); // 输出 'Hello'  
    }  
};  
obj.method();
  • 构造函数:当使用 new 关键字调用一个函数时,该函数作为构造函数,this 指向新创建的对象实例。
function MyConstructor() {  
  this.prop = 'Hello';  
}  
const instance = new MyConstructor();  
console.log(instance.prop); // 输出 'Hello'
  • 函数调用:如果一个函数不是作为对象的方法被调用,也不是用 new 关键字调用的,那么 this 通常指向全局对象(在严格模式下是 undefined)。
function myFunction() {  
  console.log(this); // 在非严格模式下输出 window  
}  
myFunction();
  • 箭头函数:箭头函数不绑定自己的 this,而是捕获其所在上下文的 this 值,即最近作用域中的 this,不建议在构造函数,原型函数和 DOM 事件函数中使用箭头函数。
const obj = {  
    prop: 'Hello',  
    method: function() {  
        const arrowFunction = () => {  
            console.log(this.prop); // 输出 'Hello'  
        };  
        arrowFunction();  
    }  
};  
obj.method();

改变指向

可以使用 call(), apply(), 或 bind() 方法来改变this的指向。

call

call 方法调用一个函数,并指定该函数的 this 值和参数列表。使用 call 方法可以显式地设置函数执行时的上下文(即 this 的指向)。

语法:

functionName.call(thisArg, arg1, arg2, ...);
  • functionName:需要调用的函数名或函数引用。
  • thisArg:在 functionName 函数运行时使用的 this 值。
  • arg1, arg2, ...:传递给 functionName 函数的参数列表。

例子:

function greet(greeting, punctuation) {  
  console.log(greeting + ', ' + this.name + punctuation);  
}  
  
const person = { name: 'Alice' };  
  
// 使用 call 方法调用 greet 函数,并设置 this 为 person 对象  
greet.call(person, 'Hello', '!'); // 输出 "Hello, Alice!"

在上面的例子中,greet 函数原本没有定义 this.name,但是通过 call 方法,可以将 this 指向 person 对象,并传入了 'Hello''!' 作为参数。因此,在 greet 函数内部,this.name 就可以访问到 person 对象的 name 属性了。

apply

apply 方法调用一个函数,并指定该函数的 this 值,同时以一个数组(或类似数组对象)的形式提供参数。apply 方法允许将数组或类数组对象作为函数的参数列表来调用函数。

语法:

functionName.apply(thisArg, [argsArray])
  • functionName:需要调用的函数名或函数引用。
  • thisArg:在 functionName 函数运行时使用的 this 值。
  • argsArray:一个数组或者类似数组的对象,其中的数组元素将作为单独的参数传给 functionName 函数。

例子:

function sum(a, b) {  
  return this.multiplier * (a + b);  
}  
const obj = { multiplier: 2 };  
const numbers = [1, 2];  
const result = sum.apply(obj, numbers);
console.log(result); // 输出 6

apply 方法将数组 numbers 数组中的每个元素作为独立的参数传递给 sum 函数。

bind

bind 方法不会调用函数,但是可以改变函数内部的 this 指向。

语法:

function.bind(thisArg, [arg1], [arg2], ...)
  • function:需要绑定的函数。
  • thisArg:当这个新函数被调用时,bind 的第一个参数将作为它运行时的 this 值。如果这个参数是 nullundefined,则 this 会指向全局对象(在浏览器中通常是 window)。
  • arg1, arg2, ...:当新函数被调用时,这些参数将前置到绑定函数的参数列表。

bind 返回一个新的函数,当这个新函数被调用时,bind 的第一个参数将作为它运行时的 this,之后的参数将会在传递的实参前传入作为它的参数。

例如:

function greet(name, punctuation) {  
  console.log(this.greeting + ', ' + name + punctuation);  
}  
  
const obj = { greeting: 'Hello' };  
  
// 创建一个新的函数,它的 this 指向 obj 对象,并预先设置了一个参数 'Alice'  
const greetAlice = greet.bind(obj, 'Alice');  
  
// 调用 greetAlice 函数,只需提供剩余的参数  
greetAlice('!'); // 输出 "Hello, Alice!"

在这个例子中,定义了一个 greet 函数,它期望有两个参数 namepunctuation。使用 bind 方法来创建一个新的函数 greetAlice,它的 this 值被绑定到 obj 对象,并且第一个参数 name 被预先设置为 'Alice'。这样,当调用 greetAlice 函数时,只需要提供剩余的参数 punctuation 即可。

bind 方法的一个主要用途是确保函数在特定的上下文中被调用,特别是在回调函数、事件处理器或需要部分应用函数参数的场景中。

需要注意的是,bind 方法只是创建一个新的函数,并不会立即执行它。它只是设置了函数被调用时的上下文和前置参数。如果想立即执行并返回结果,需要显式地调用返回的函数。

性能优化

防抖

在JavaScript中,防抖(debounce)是一种处理高频触发事件的策略,其主要思想是在事件被触发后 n 秒内函数只能执行一次,如果在这 n 秒内又被重新触发,则重新计算执行时间,即以最后一次执行为准。防抖常用于限制某个函数的执行频率,比如限制滚动事件的触发频率、限制窗口大小调整事件的触发频率等。

可以使用 lodash_.debounce 方法来方便地实现防抖功能。

语法:

_.debounce(func, [wait=0], [options=])
  • func (Function): 要防抖动的函数。
  • [wait=0] (number): 需要延迟的毫秒数。
  • [options=] (Object): 选项对象。
  • [options.leading=false] (boolean): 指定在延迟开始前调用。
  • [options.maxWait] (number): 设置 func 允许被延迟的最大值。
  • [options.trailing=true] (boolean): 指定在延迟结束后调用。

例子:利于防抖处理鼠标滑过盒子数字变化,鼠标停止 500 毫秒后,才发生变化

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      margin: 100px auto;
      width: 200px;
      height: 200px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>
<body>
  <div class="box"></div>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <script>
    const box = document.querySelector('.box')
    
    let i = 1
    function mouseMove() {
      box.innerHTML = i++
    }

    box.addEventListener('mousemove',  _.debounce(mouseMove, 500))
  </script>
</body>
</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>
  <style>
    .box {
      margin: 100px auto;
      width: 200px;
      height: 200px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>
<body>
  <!-- <script src="./1.js"></script> -->
  <div class="box"></div>
  <script>
    function debounce(fn, t) {
      let timer
      return function () {
        if (timer) clearTimeout(timer)
        timer = setTimeout(function () {
          fn()
        }, t)
        }
    }

    let i = 1
    function mouseMove() {
      box.innerHTML = i++
    }

    const box = document.querySelector('.box')
    box.addEventListener('mousemove', debounce(mouseMove, 500))
  </script>
</body>
</html>

节流

在JavaScript中,节流(throttle)是另一种处理高频触发事件的策略。与防抖(debounce)不同的是,节流是在一段时间内只允许执行一次函数。也就是说,无论在这段时间内触发了多少次事件,函数都只会在第一次触发时执行,直到过了指定的时间间隔后,才会再次执行。

节流常用于限制某个函数的执行频率,确保它不会过于频繁地执行,从而优化性能。比如,在滚动事件中,可能希望每隔一段时间才执行一次函数,而不是每次滚动都执行。

可以使用 lodash_.throttle 方法来方便地实现节流功能。

语法:

_.throttle(func, [wait=0], [options=])
  • func (Function): 要节流的函数。
  • [wait=0] (number): 需要节流的毫秒。
  • [options=] (Object): 选项对象。
  • [options.leading=true] (boolean): 指定调用在节流开始前。
  • [options.trailing=true] (boolean): 指定调用在节流结束后。

例子:利于节流处理鼠标滑过盒子数字变化,发生变化 5000 毫秒后,才会再次发生变化

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      margin: 100px auto;
      width: 200px;
      height: 200px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>
<body>
  <div class="box"></div>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <script>
    const box = document.querySelector('.box')
    
    let i = 1
    function mouseMove() {
      box.innerHTML = i++
    }

    box.addEventListener('mousemove',  _.throttle(mouseMove, 5000))
  </script>
</body>
</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>
  <style>
    .box {
      margin: 100px auto;
      width: 200px;
      height: 200px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>
<body>
  <!-- <script src="./1.js"></script> -->
  <div class="box"></div>
  <script>
    function throttle(fn, t) {
      let timer = null
      return function () {
        if (!timer) {
          timer = setTimeout(function () {
            fn()
            // 在 setTimeout 中是无法使用 clearTimeout 清除定时器的,因为定时器还在运行
            timer = null
          }, t)
        }
      }
    }

    let i = 1
    function mouseMove() {
      box.innerHTML = i++
    }

    const box = document.querySelector('.box')
    box.addEventListener('mousemove', throttle(mouseMove, 3000))
  </script>
</body>
</html>

总结:

性能优化说明使用场景
防抖单位时间内,频繁触发事件,只执行最后一次搜索输入框,输入框验证
节流单位时间内,频繁触发事件,只执行第一次高频事件,如鼠标移动,页面尺寸缩放,滚动条滚动等

例子:节流综合案例

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta name="referrer" content="never" />
  <title>综合案例</title>
  <style>
    * {
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }

    .container {
      width: 1200px;
      margin: 0 auto;
    }

    .video video {
      width: 100%;
      padding: 20px 0;
    }

    .elevator {
      position: fixed;
      top: 280px;
      right: 20px;
      z-index: 999;
      background: #fff;
      border: 1px solid #e4e4e4;
      width: 60px;
    }

    .elevator a {
      display: block;
      padding: 10px;
      text-decoration: none;
      text-align: center;
      color: #999;
    }

    .elevator a.active {
      color: #1286ff;
    }

    .outline {
      padding-bottom: 300px;
    }
  </style>
</head>

<body>
  <div class="container">
    <div class="header">
      <a href="http://pip.itcast.cn">
        <img src="https://pip.itcast.cn/img/logo_v3.29b9ba72.png" alt="" />
      </a>
    </div>
    <div class="video">
      <video src="https://v.itheima.net/LapADhV6.mp4" controls></video>
    </div>
    <div class="elevator">
      <a href="javascript:;" data-ref="video">视频介绍</a>
      <a href="javascript:;" data-ref="intro">课程简介</a>
      <a href="javascript:;" data-ref="outline">评论列表</a>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <script>
    const video = document.querySelector('video')
    video.ontimeupdate = _.throttle(() => {
      localStorage.setItem('currentTime', video.currentTime)
    }, 1000)
    video.onloadeddata = () => {
      video.currentTime = localStorage.getItem('currentTime') || 0
    }
  </script>
</body>

</html>

HTTP

HTTP 协议,全称为 Hypertext Transfer Protocol,即超文本传输协议,是互联网上应用最为广泛的一种网络传输协议。它定义了客户端(如 Web 浏览器)与服务器之间的通信规范,用于实现各种资源(如 HTML 页面、图像、音频、视频等)的传输和访问。

HTTP 协议的主要特点包括:

  • 无连接:HTTP 协议不需要在客户端和服务器之间建立持久的连接,每个请求都是独立的,请求处理完毕后就断开连接(HTTP/1.0 后通过 Connection 字段引入了 keep-alive,可以在一个 TCP 连接上发送多个 HTTP 请求)。这种特性减少了网络开销,但也可能导致每次请求都需要重新建立连接,增加了延迟。
  • 无状态:HTTP 协议对事务的处理没有记忆能力,即服务器不会保存客户端的状态信息,每次请求都需要提供完整的请求信息。这种无状态性使得 HTTP 协议更加简单和灵活,但也要求应用层来实现会话管理等功能。
  • 基于请求响应模式:HTTP 协议采用客户端-服务器架构模式,客户端向服务器发送请求,服务器返回相应的响应。这种模式有效分离了应用逻辑,提高了系统的可维护性和扩展性。

HTTP协议的工作原理大致如下:客户端向服务器发送请求消息,包含请求方法(如 GET、POST 等)、URL、协议版本以及请求头等信息。服务器接收到请求后,根据请求信息生成响应消息,并将其发送给客户端。响应消息包含协议版本、状态码、响应头和响应体等信息。客户端接收到响应后,根据需要进行处理,如解析 HTML 代码并渲染网页。如果需要继续与服务器通信,客户端可以发起新的请求并重复上述步骤。

HTTP 协议的应用场景非常广泛,包括但不限于 Web 浏览器访问Web服务器、Web API 调用、文件传输、Web 服务以及电子邮件的发送和接收等。随着技术的发展,HTTP 协议也在不断演进,从最初的 HTTP/0.9 到现在的 HTTP/3,每个版本都带来了新的特性和优化,以适应不断变化的网络环境和应用需求。

请求方法

请求方法通常指的是在 HTTP 协议中使用的不同类型的请求。HTTP 请求方法(也被称为 HTTP 动词)定义了要执行的操作的类型。

常见的 HTTP 请求方法:

  • GET:请求获取由指定 URL 标识的资源。这是最常见的请求方法,通常用于从服务器检索数据。
  • POST:请求服务器接受包含在请求体中的数据,常用于提交表单数据或上传文件。
  • PUT:请求服务器用请求体中的数据替换目标 URL 指定的资源。
  • DELETE:请求服务器删除目标 URL 指定的资源。

URL

URL(Uniform Resource Locator,统一资源定位符)是互联网上用于标识某一处资源的字符串,通过这个字符串,可以在网络上找到并访问相应的资源。URL 是互联网的基础组件之一,它提供了一种标准化的方式来定位和访问网络资源,如网页、图片、视频、文件等。

URL通常包含以下几个部分:

  • 协议(Scheme):指定了访问资源所使用的通信协议,如httphttpsftp等。
  • 域名(Hostname):资源所在的服务器的地址,可以是 IP 地址或域名。
  • 端口(Port):特定于服务器的端口号,如果未指定,则使用协议的默认端口(如 HTTP 的默认端口是 80,HTTPS 的默认端口是 443)。
  • 路径(Path):服务器上资源的具体位置,如目录和文件名。
  • 查询参数(Query String):用于传递给资源的参数,以键值对的形式出现,并以问号(?)开始。
  • 片段标识符(Fragment Identifier):用于标识资源的特定部分,通常用于网页内导航,以井号(#)开始。

例如:

https://stonecoding.net:80/path/to/resource?param1=value1&param2=value2#section1
  • https 是协议。
  • stonecoding.net 是域名。
  • :80 是端口号。
  • /path/to/resource 是路径。
  • ?param1=value1&param2=value2 是查询参数。
  • #section1 是片段标识符。

URL 的主要作用是为网络资源提供一个全球唯一的标识符,这样用户、应用程序和搜索引擎等就可以通过这个标识符来找到和访问相应的资源。同时,URL 也是网络爬虫和搜索引擎进行网页抓取和索引的基础。

需要注意的是,URL 并不直接包含资源的内容,它只是指向资源的地址。要获取资源的内容,通常需要向 URL 所指定的服务器发送请求。

请求报文

HTTP 请求报文是客户端(如 Web 浏览器)发送给服务器的信息,用于请求获取或操作指定的资源。一个完整的HTTP 请求报文由请求行、请求头部和请求体(可选)三部分组成。

  • 请求行: 请求行包含 HTTP 请求方法、请求的 URL 和 HTTP 协议版本三个字段,用空格分隔,并以回车换行符(CRLF)结束。
    • HTTP 方法:表示请求资源所使用的具体动作,如 GET、POST、PUT、DELETE 等。
    • 请求的 URL:指定了请求资源的具体地址,可以是绝对 URL 或相对 URL。
    • HTTP 协议版本:指定了客户端使用的HTTP协议版本,如 HTTP/1.1。
  • 请求头部: 请求头部包含了一些额外的信息,用于描述请求或客户端本身的一些属性。每个头部字段都由字段名和字段值组成,字段名和字段值之间用冒号(:)分隔,多个头部字段之间用回车换行符(CRLF)分隔。
    • 常见的请求头部字段包括:
      • Host:指定请求的主机名和端口号。
      • User-Agent:表示发出请求的客户端的类型、操作系统、软件供应商等信息。
      • Accept:告诉服务器客户端可以处理的内容类型。
      • Accept-Language:告诉服务器客户端支持的自然语言。
      • Connection:指定客户端是否希望保持连接(keep-alive)。
      • Cookie:用于保存用户的登录状态等会话信息。
      • Content-Type:当请求包含请求体时,该头部字段用于指定请求体的媒体类型。例如,在提交表单时,Content-Type 可能被设置为 application/x-www-form-urlencodedmultipart/form-data 或者 application/json
      • Content-Length:当请求包含请求体时,该头部字段用于指定请求体的长度(以字节为单位)。
  • 请求体: 请求体(Request Body)是可选的,主要用于 POST、PUT 等请求方法中,用于向服务器发送数据。请求体的格式和内容取决于具体的请求方法和内容类型。

响应报文

HTTP 响应报文是服务器在接收到客户端的 HTTP 请求后,返回给客户端的报文。它主要由三部分组成:响应行、响应头和响应体。

  • 响应行:由协议版本、状态码和状态描述组成。协议版本表明使用的 HTTP 协议版本,如 HTTP/1.1。状态码是一个三位数的数字,用于表示请求的处理结果,如 200 表示请求成功,404 表示请求的资源未找到等。状态描述是对状态码的文本描述,用于提供更详细的信息。
  • 响应头:包含了一系列字段,用于描述服务器的基本信息、数据的描述以及通知客户端如何处理返回的数据。常见的响应头字段包括 Content-Type(表示返回数据的类型)、Content-Length(表示返回数据的长度)、Date(表示响应的日期和时间)等。
  • 响应体:是响应报文的主要部分,包含了服务器返回给客户端的具体数据。响应体的格式和内容取决于请求的资源类型和状态码。例如,对于 HTML 页面请求,响应体可能包含 HTML 代码;对于 JSON 数据请求,响应体可能包含 JSON 格式的数据。

其中响应行中的响应状态码有:

  • 1xx 信息性状态码:表示请求已被接收,继续处理。
    • 100 Continue:客户端应继续发送请求。
    • 101 Switching Protocols:服务器根据客户端的请求切换协议。
  • 2xx 成功状态码:表示请求已成功被服务器接收、理解、并接受。
    • 200 OK:请求成功。
    • 201 Created:请求成功并且服务器创建了新的资源。
    • 202 Accepted:请求已接受,但处理尚未完成。
    • 204 No Content:服务器成功处理了请求,但没有返回任何内容。
  • 3xx 重定向状态码:表示需要采取进一步的操作以完成请求。
    • 301 Moved Permanently:请求的网页已永久移动到新的位置。
    • 302 Found:请求的网页临时移动到新的位置。
    • 304 Not Modified:客户端缓存的资源是最新的,无需再次发送请求。
  • 4xx 客户端错误状态码:表示请求包含错误或无法被服务器理解。
    • 400 Bad Request:请求有误,服务器无法理解。
    • 401 Unauthorized:请求需要用户验证。
    • 403 Forbidden:服务器理解请求客户端的请求,但是拒绝执行此请求。
    • 404 Not Found:服务器无法根据客户端的请求找到资源。
    • 405 Method Not Allowed:请求中指定的方法不被允许。
  • 5xx 服务器错误状态码:表示服务器在处理请求的过程中发生了错误。
    • 500 Internal Server Error:服务器内部错误,无法完成请求。
    • 501 Not Implemented:服务器不支持当前请求所需要的某个功能。
    • 503 Service Unavailable:由于临时的服务器维护或者过载,服务器当前无法处理请求。

AJAX

AJAX(Asynchronous JavaScript and XML)是一种创建交互式、快速动态网页应用的网页开发技术,无需重新加载整个页面就能够更新部分网页内容。通过 AJAX,开发者可以发送异步请求到服务器,并在不干扰当前页面显示或用户交互的情况下获取数据。

XMLHttpRequest

XMLHttpRequest(XHR)是一个用于发送 HTTP 请求到服务器并接收响应的 JavaScript 对象。它是 AJAX 技术的核心,允许网页在不重新加载整个页面的情况下,与服务器交换数据并更新部分页面内容。

使用 XMLHttpRequest 与服务器通信的步骤:

  • 创建 XMLHttpRequest 对象
  • 使用 open 方法配置请求方法与请求地址(URL)
  • 监听 loadend 事件,接收响应结果
  • 使用 send 方法发起请求,如果有请求体,使用 setRequestHeader 方法设置请求头类型

例子:使用 XHR 发起请求

<!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>
  <p></p>
  <script>
    const xhr = new XMLHttpRequest()
    xhr.open('GET','http://hmajax.itheima.net/api/province')
    xhr.addEventListener('loadend', () => {
      console.log(xhr.response)
      const data = JSON.parse(xhr.response)
      console.log(data)
      document.querySelector('p').innerHTML = data.list.join('<br>')
    })
    xhr.send()
  </script>
</body>
</html>

例子:使用 XHR 携带查询参数发起请求

<!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>
  <p></p>
  <script>
    const xhr = new XMLHttpRequest()
    xhr.open('GET','http://hmajax.itheima.net/api/city?pname=辽宁省')
    xhr.addEventListener('loadend', () => {
      console.log(xhr.response)
      const data = JSON.parse(xhr.response)
      console.log(data)
      document.querySelector('p').innerHTML = data.list.join('<br>')
    })
    xhr.send()
  </script>
</body>
</html>

例子:使用 XHR 携带请求参数发起请求

<!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>
  <button class="reg-btn">注册用户</button>
  <script>
    document.querySelector('.reg-btn').addEventListener('click', () => {
      const xhr = new XMLHttpRequest()
      xhr.open('POST', 'http://hmajax.itheima.net/api/register')
      xhr.addEventListener('loadend', () => {
        console.log(xhr.response)
      })
      xhr.setRequestHeader('Content-Type','application/json')
      const userObj = {
        username: 'stonecoding',
        password: '7654321'
      }
      const userStr = JSON.stringify(userObj)
      xhr.send(userStr)
    })
  </script>
</body>
</html>

Promise

JavaScript 中的 Promise 是一种用于处理异步操作的对象。Promise 代表了异步操作的最终完成(或失败)及其结果值。Promise 的出现极大地简化了异步编程的复杂性,特别是当涉及到多个异步操作需要按照特定顺序执行时。

Promise 有三种状态:

  • Pending(待定):初始状态,既不是成功,也不是失败状态。
  • Fulfilled(已实现):意味着操作成功完成。
  • Rejected(已拒绝):意味着操作失败。

Promise 对象一旦从 Pending 状态变为 Fulfilled 或 Rejected 状态后,就不会再改变。

Promise 的构造函数接受一个函数作为参数,这个函数被称为 Executor 函数。Executor 函数接受两个参数:resolvereject,它们是两个函数,用于改变 Promise 的状态。

Promise 对象提供了 .then().catch() 方法,用于指定异步操作成功或失败时要执行的回调函数。.then() 方法用于指定异步操作成功时要执行的回调函数,而 .catch() 方法用于指定异步操作失败时要执行的回调函数。

例子:使用 Promise 管理异步任务

<!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>
  <script>
    // 1. 创建 Promise 对象,创建后为 pending 状态
    const promise = new Promise((resolve, reject) => {
      // 2. 执行异步代码
      setTimeout(() => {
        const flag = false
        if (flag) {
          // resolve() => fulfilled 状态 => 触发 then()
          resolve('模拟 AJAX 请求 - 成功结果')
        } else {
          // reject() => rejected 状态 => 触发 catch()
          reject(new Error('模拟 AJAX 请求 - 失败结果'))
        }
      }, 2000);
    })

    // 3. 获取结果
    promise.then(result => {
      console.log(result)
    }).catch(error => {
      console.log(error)
    })
  </script>
</body>
</html>

例子:使用 Promise 管理 XHR 获取数据

<!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>
  <p></p>
  <script>
    // 1. 创建 Promise 对象,创建后为 pending 状态
    const promise = new Promise((resolve, reject) => {
      // 2. 执行异步代码
      const xhr = new XMLHttpRequest()
      xhr.open('GET', 'http://hmajax.itheima.net/api/province')
      xhr.addEventListener('loadend', () => {
        console.log(xhr.response)
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.response))
        } else {
          reject(new Error(xhr.response))
        }
      })
      xhr.send()
    })

    // 3. 获取结果
    promise.then(result => {
      console.log(result)
      document.querySelector('p').innerHTML = result.list.join('<br>')
    }).catch(error => {
      // 错误对象要用 console.dir 详细打印
      console.dir(error)
      document.querySelector('p').innerHTML = error.message
    })
  </script>
</body>
</html>

例子:基于 Promise 和 XHR 封装 myAxios 函数

<!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>
  <p></p>
  <script>
    function myAxios(config) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        xhr.open(config.method || 'GET', config.url)
        xhr.addEventListener('loadend', () => {
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve(JSON.parse(xhr.response))
          } else {
            reject(new Error(xhr.response))
          }
        })
        xhr.send()
      })
    }

    myAxios({
      url: 'http://hmajax.itheima.net/api/province'
    }).then(result => {
      console.log(result)
      document.querySelector('p').innerHTML = result.list.join('<br>')
    }).catch(error => {
      console.log(error)
      document.querySelector('p').innerHTML = error.message
    })
  </script>
</body>
</html>

例子:修改 myAxios 函数支持传递查询参数

<!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>
  <p></p>
  <script>
    function myAxios(config) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        if (config.params) {
          const paramsObj = new URLSearchParams(config.params)
          const queryStr = paramsObj.toString()
          config.url += `?${queryStr}`
        }
        xhr.open(config.method || 'GET', config.url)
        xhr.addEventListener('loadend', () => {
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve(JSON.parse(xhr.response))
          } else {
            reject(new Error(xhr.response))
          }
        })
        xhr.send()
      })
    }

    myAxios({
      url: 'http://hmajax.itheima.net/api/area',
      params: {
        pname: '辽宁省',
        cname: '大连市'
      }
    }).then(result => {
      console.log(result)
      document.querySelector('p').innerHTML = result.list.join('<br>')
    }).catch(error => {
      console.log(error)
      document.querySelector('p').innerHTML = error.message
    })
  </script>
</body>
</html>

例子:修改 myAxios 函数支持传递请求体

<!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>
  <button class="reg-btn">注册用户</button>
  <script>
    function myAxios(config) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()

        if (config.params) {
          const paramsObj = new URLSearchParams(config.params)
          const queryStr = paramsObj.toString()
          config.url += `?${queryStr}`
        }

        xhr.open(config.method || 'GET', config.url)

        xhr.addEventListener('loadend', () => {
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve(JSON.parse(xhr.response))
          } else {
            reject(new Error(xhr.response))
          }
        })

        if (config.data) {
          const jsonStr = JSON.stringify(config.data)
          xhr.setRequestHeader('Content-Type', 'application/json')
          xhr.send(jsonStr)
        } else {
          xhr.send()
        }
      })
    }

    document.querySelector('.reg-btn').addEventListener('click', () => {
      myAxios({
        url: 'http://hmajax.itheima.net/api/register',
        method: 'POST',
        data: {
          username: 'stonecoding.net',
          password: '7654321'
        }
      }).then(result => {
        console.log(result)
      }).catch(error => {
        console.dir(error)
      })
    })
  </script>
</body>
</html>

axios

axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 node.js。它使得发送 HTTP 请求变得简单,且支持 Promise API、拦截请求和响应、转换请求和响应数据、取消请求、自动转换 JSON 数据,以及客户端支持防止 CSRF/XSRF。

使用 axios 发送 AJAX 请求通常比使用原生的 XMLHttpRequest 更加直观和方便。

axios 常用请求配置选项有:

  • url:请求的 URL
  • method:请求方法,默认为 get
  • params:请求参数
  • data:请求体

axios 响应包括:

  • data:服务器响应的数据
  • status:HTTP 状态码
  • statusText:HTTP 状态信息
  • headers:服务器响应的头信息
  • config:为请求提供的配置信息
  • request:请求对象

例子:使用 axios 发起请求,使用 url 配置选项指定 URL

<!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>
  <p></p>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    axios({
      url: 'http://hmajax.itheima.net/api/province'
    }).then(result => {
      console.log(result)
      console.log(result.data.list)
      console.log(result.data.list.join('<br>'))
      document.querySelector('p').innerHTML = result.data.list.join('<br>')
    })
  </script>
</body>
</html>

例子:使用 axiosparams 配置选项携带查询参数发起请求

<!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>
  <p></p>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    axios({
      url: 'http://hmajax.itheima.net/api/city',
      params: {
        pname: '河北省'
      }
    }).then(result => {
      document.querySelector('p').innerHTML = result.data.list.join('<br>')
    })
  </script>
</body>
</html>

例子:使用 axiosmethoddata 配置选项携带请求参数发起请求

<!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>
  <button class="btn">注册用户</button>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    document.querySelector('.btn').addEventListener('click', () => {
      axios({
        url: 'http://hmajax.itheima.net/api/register',
        method: 'post',
        data: {
          username: 'stonebox',
          password: '7654321'
        }
      }).then(result => {
        console.log(result)
      })
    })
  </script>
</body>
</html>

例子:axios 的错误处理

<!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>
  <button class="btn">注册用户</button>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    document.querySelector('.btn').addEventListener('click', () => {
      axios({
        url: 'http://hmajax.itheima.net/api/register',
        method: 'post',
        data: {
          username: 'stonebox',
          password: '7654321'
        }
      }).then(result => {
        console.log(result)
      }).catch(error => {
        console.log(error.response.data.message)
      })
    })
  </script>
</body>
</html>

可以为每个请求指定默认配置:

axios.defaults.baseURL = 'https://stonecoding.net';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

可以配置请求和响应拦截器:

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  });

例如:

// axios 公共配置
// 基地址
axios.defaults.baseURL = 'https://geek.itheima.net'

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    // 统一携带 token 令牌字符串在请求头上
    const token = localStorage.getItem('token')
    token && (config.headers.Authorization = `Bearer ${token}`)
    return config
}, function(error) {
    // 对请求错误做些什么
    return Promise.reject(error)
})

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数
    // 对响应数据做点什么 例如:直接返回服务器响应结果对象
    const result = response.data
    return result
}, function(error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理
    console.dir(error)
    if (error?.response?.status === 401) {
        alert('身份验证失败,请重新登录')
        localStorage.clear()
        location.href = '../login/index.html'
    }
    return Promise.reject(error)
})

回调地狱

回调地狱(Callback Hell)是 JavaScript 编程中遇到的一个常见问题,特别是在处理异步操作时,回调函数一直向下嵌套回调函数,形成回调函数地狱。

例如:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>回调地狱</title>
</head>

<body>
  <form>
    <span>省份:</span>
    <select>
      <option class="province"></option>
    </select>
    <span>城市:</span>
    <select>
      <option class="city"></option>
    </select>
    <span>地区:</span>
    <select>
      <option class="area"></option>
    </select>
  </form>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    /**
     * 目标:演示回调函数地狱
     * 需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中
     * 概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
     * 缺点:可读性差,异常无法获取,耦合性严重,牵一发动全身
    */
    axios({
      url: 'http://hmajax.itheima.net/api/province'
    }).then(result => {
      const pname = result.data.list[0]
      document.querySelector('.province').innerHTML = pname

      axios({
        url: 'http://hmajax.itheima.net/api/city',
        params: { pname }
      }).then(result => {
        const cname = result.data.list[0]
        document.querySelector('.city').innerHTML = cname

        axios({
          url: 'http://hmajax.itheima.net/api/area',
          params: { pname, cname }
        }).then(result => {
          console.log(result)
          const areaName = result.data.list[0]
          document.querySelector('.area').innerHTML = areaName
        })

      })
      
    }).catch(error => {
      console.dir(error)
    })
  </script>
</body>

</html>

可以使用 Promises 的 .then() 方法将多个异步操作连接在一起,形成一个处理流程。每个 .then() 方法都会接收前一个 Promise 的结果作为参数,并返回一个新的 Promise,从而可以进一步链式调用,解决回调函数地狱问题。

如果在链中的任何地方发生错误,可以使用 .catch() 方法来捕获并处理它。这允许以一种集中的方式处理所有可能发生的错误。

请注意,为了保持代码的清晰和可读性,通常建议在每个 .then() 方法中只处理一个异步操作,并将结果传递给下一个 .then()。如果需要在链中的某个点停止处理并返回最终结果,可以在该点调用 resolve()并传递所需的值。如果需要提前终止 Promise 链并处理错误,可以在任何点调用 reject() 并传递错误信息。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Promise_链式调用解决回调地狱</title>
</head>

<body>
  <form>
    <span>省份:</span>
    <select>
      <option class="province"></option>
    </select>
    <span>城市:</span>
    <select>
      <option class="city"></option>
    </select>
    <span>地区:</span>
    <select>
      <option class="area"></option>
    </select>
  </form>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    let pname = ''
    axios({
      url: 'http://hmajax.itheima.net/api/province'
    }).then(result => {
      pname = result.data.list[0]
      document.querySelector('.province').innerHTML = pname
      return axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }})
    }).then(result => {
      const cname = result.data.list[0]
      document.querySelector('.city').innerHTML = cname
      return axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }})
    }).then(result => {
      const areaName = result.data.list[0]
      document.querySelector('.area').innerHTML = areaName
    })
  </script>
</body>

</html>

async

asyncawait 是 ES2017(ES8)中引入的 JavaScript 关键字,它们用于简化基于 Promise 的异步代码。async 关键字用于声明一个函数是异步的,而 await 关键字只能在 async 函数内部使用,用于等待一个 Promise 的解决(resolve)并返回其结果。

使用 async/await 可以让异步代码看起来和同步代码非常相似,这大大提高了代码的可读性和可维护性,是 JavaScript 异步编程的终极解决方案。

例如:

async function foo() {
  await 1;
}

等价于:

function foo() {
  return Promise.resolve(1).then(() => undefined);
}

每个 await 表达式之后的代码可以被认为存在于 .then 回调中。通过这种方式,可以通过函数的每个可重入步骤来逐步构建 Promise 链。而返回值构成了链中的最后一个环。

整个 foo 函数的执行将会被分为三个阶段,例如:

async function foo() {
  const result1 = await new Promise((resolve) =>
    setTimeout(() => resolve("1")),
  );
  const result2 = await new Promise((resolve) =>
    setTimeout(() => resolve("2")),
  );
}
foo();
  1. foo 函数的第一行将会同步执行,其中 await 配置了待定的 Promise。然后 foo 的进程将被暂停,并将控制权交还给调用 foo 的函数。
  2. 一段时间后,当第一个 Promise 被兑现或拒绝的时候,控制权将重新回到 foo 内。第一个 Promise 的兑现结果(如果没有被拒绝的话)将作为 await 表达式的返回值。在这里 1 被赋值给 result1。程序继续执行,并计算第二个 await 表达式。同样的,foo 的进程将被暂停,并交出控制权。
  3. 一段时间后,当第二个 Promise 被兑现或拒绝的时候,控制权将重新回到 foo。第二个 Promise 的兑现结果将作为第二个 await 表达式的返回值。在这里 2 被赋值给 result2。程序继续执行到返回表达式(如果有的话)。默认的返回值 undefined 将作为当前 Promise 的兑现值被返回。

注意:Promise 链不是一次就构建好的,相反,Promise 链是随着控制权依次在异步函数中交出并返回而分阶段构建的。因此在处理并发异步操作时,我们必须小心错误处理。

例子:使用 asyncawait 解决回调函数地狱

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>async函数和await_解决回调函数地狱</title>
</head>

<body>
  <form>
    <span>省份:</span>
    <select>
      <option class="province"></option>
    </select>
    <span>城市:</span>
    <select>
      <option class="city"></option>
    </select>
    <span>地区:</span>
    <select>
      <option class="area"></option>
    </select>
  </form>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    /**
     * 目标:掌握 async 和 await 语法,解决回调函数地狱
     * 概念:在 async 函数内,使用 await 关键字,获取 Promise 对象"成功状态"结果值
     * 注意:await 必须用在 async 修饰的函数内(await 会阻止"异步函数内"代码继续执行,原地等待结果)
    */
    // 1. 定义 async 修饰函数
    async function getData() {
      // 2. await 等待 Promise 对象成功的结果
      const pObj = await axios({ url: 'http://hmajax.itheima.net/api/province' })
      const pname = pObj.data.list[0]
      const cObj = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })
      const cname = cObj.data.list[0]
      const aObj = await axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })
      const areaName = aObj.data.list[0]

      document.querySelector('.province').innerHTML = pname
      document.querySelector('.city').innerHTML = cname
      document.querySelector('.area').innerHTML = areaName
    }

    getData()
  </script>
</body>

</html>

在这个例子中,getData 函数被标记为 async,这意味着它总是返回一个 Promise。在函数体内,使用 await 来等待每个异步函数的结果。await 会暂停 async 函数的执行,直到 Promise 解决并返回其结果。如果 Promise 被拒绝(rejected),则 await 表达式会抛出一个异常,这可以被 try/catch 块捕获。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>async函数和await_错误捕获</title>
</head>

<body>
  <form>
    <span>省份:</span>
    <select>
      <option class="province"></option>
    </select>
    <span>城市:</span>
    <select>
      <option class="city"></option>
    </select>
    <span>地区:</span>
    <select>
      <option class="area"></option>
    </select>
  </form>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    // 使用 try...catch 捕获错误
    async function getData() {
      try {
        const pObj = await axios({ url: 'http://hmajax.itheima.net/api/province' })
        const pname = pObj.data.list[0]
        const cObj = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })
        const cname = cObj.data.list[0]
        const aObj = await axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })
        const areaName = aObj.data.list[0]

        document.querySelector('.province').innerHTML = pname
        document.querySelector('.city').innerHTML = cname
        document.querySelector('.area').innerHTML = areaName
      } catch (error) {
        console.dir(error)
      }
    }

    getData()
  </script>
</body>

</html>

all

Promise.all() 是 JavaScript 中 Promise 对象的一个静态方法,它接收一个 Promise 对象的数组(或类似数组对象)作为参数,并返回一个新的 Promise 实例,这个新的 Promise 会在所有给定的 Promise 都成功解析(fulfilled)后解析,或者任何一个 Promise 失败(rejected)后立即失败。

使用 Promise.all() 的主要优势在于它可以并行处理多个异步操作,并且只在所有操作都完成时触发一个单一的回调。这在处理多个异步请求、等待多个数据库查询或其他需要并行处理的情况时非常有用。

例子:

// 创建两个 Promise 对象  
const promise1 = new Promise((resolve) => {  
  setTimeout(resolve, 500, 'Promise 1 resolved');  
});  
  
const promise2 = new Promise((resolve) => {  
  setTimeout(resolve, 100, 'Promise 2 resolved');  
});  
  
// 使用 Promise.all() 等待两个 Promise 都解析  
Promise.all([promise1, promise2]).then((results) => {  
  console.log(results);  
  // 输出: ['Promise 1 resolved', 'Promise 2 resolved']  
}).catch((error) => {  
  console.error('An error occurred:', error);  
});

在上面的例子中,promise1promise2 分别在 500 毫秒和 100 毫秒后解析。尽管 promise2 先解析,但 Promise.all() 会在所有 Promise 都解析完成后才调用 then 回调,并且回调中的 results 数组将按照原始 Promise 数组的顺序包含每个 Promise 的解析值。

如果任何一个 Promise 被拒绝(rejected),Promise.all() 会立即以第一个被拒绝的 Promise 的错误作为原因被拒绝,并且调用 catch 回调。

const promise1 = new Promise((resolve) => {  
  setTimeout(resolve, 500, 'Promise 1 resolved');  
});  
  
const promise2 = new Promise((_, reject) => {  
  setTimeout(reject, 100, 'Promise 2 rejected');  
});  
  
Promise.all([promise1, promise2]).then((results) => {  
  console.log(results);  
}).catch((error) => {  
  console.error('An error occurred:', error); // 输出: 'An error occurred: Promise 2 rejected'  
});

在这个例子中,promise2 在 100 毫秒后被拒绝,因此 Promise.all() 会立即被拒绝,并调用 catch 回调,输出 'Promise 2 rejected'

上次编辑于:
贡献者: stonebox