JavaScript 核心进阶:数组、函数、作用域与对象
本文聚焦 JavaScript 中支撑复杂开发的核心概念与工具,包括数组(数据集合管理)、函数(代码封装复用)、作用域(变量访问规则)、预解析(代码执行机制)、对象(结构化数据存储)及内置对象(高效开发工具),通过清晰的逻辑拆解与实用案例,帮助读者掌握进阶开发能力。
1. 数组:有序数据集合
数组是用于存储多个任意类型值的有序数据结构,通过索引快速访问元素,是管理批量数据的核心工具。
1.1 数组的创建与初始化
推荐优先使用字面量语法(简洁高效),其他方式根据场景选择:
| 创建方式 | 语法示例 | 说明 |
|---|---|---|
| 字面量语法(推荐) | const fruits = ['苹果', '香蕉']; const emptyArr = []; |
直接用 [] 包裹元素,创建空数组或带初始值的数组 |
| 构造函数 | const nums = new Array(1, 2, 3); const len5Arr = new Array(5); |
传多个值:创建含指定元素的数组;传单个数字:创建指定长度的空数组(元素为 undefined) |
| 静态方法 | const digits = Array.of(0, 1, 2); const strArr = Array.from('hello'); |
Array.of():统一处理单个/多个参数(避免构造函数歧义);Array.from():从可迭代对象(如字符串、类数组)创建数组 |
// 1. 字面量语法(推荐)
const fruits = ['苹果', '香蕉', '橙子']; // 创建包含3个元素的数组
const emptyArray = []; // 创建空数组
// 2. 构造函数语法
const numbers = new Array(1, 2, 3); // 创建包含1,2,3的数组
const anotherArray = new Array(5); // 创建长度为5的空数组(元素为undefined)
// 3. 使用 Array.of() 和 Array.from()
const digits = Array.of(0, 1, 2); // 创建包含0,1,2的数组
const fromString = Array.from('hello'); // 从可迭代对象创建数组:['h', 'e', 'l', 'l', 'o']
关键特性:数组可存储任意类型数据,如
const mixArr = ['小明', 20, true, 3.14];
1.2 元素访问与索引
数组通过索引(下标) 访问元素,索引从 0 开始(对应第一个元素):
const arr = ['red', 'green', 'blue'];
console.log(arr[0]); // 输出 'red'(第一个元素)
console.log(arr[3]); // 输出 undefined(索引超出范围,无对应元素)
索引规则:索引为非负整数,若为小数或负数,会被视为对象属性(而非数组元素)。
1.3 数组遍历与长度
遍历即“逐个访问数组元素”,最常用 for 循环结合 length 属性(数组元素个数):
const arr = ['red', 'green', 'blue'];
// 遍历数组:i 从 0 到 arr.length - 1(覆盖所有元素)
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]); // 依次输出 'red'、'green'、'blue'
}
length属性的特性
- 可读可写:不仅能获取长度,还能修改长度(谨慎使用):
const arr = [1, 2, 3]; arr.length = 5; // 扩展数组,新增位置元素为 undefined → [1, 2, 3, undefined, undefined] arr.length = 2; // 截断数组,删除索引 ≥2 的元素 → [1, 2]
- 动态更新:数组元素新增/删除时,
length自动更新。
1.4 数组新增元素
通过两种核心方式新增元素,避免直接给数组名赋值(会覆盖原数组):
-
修改索引新增(指定位置添加):
const arr = [1, 2, 3]; arr[3] = 4; // 索引 3 不存在,新增元素 → [1, 2, 3, 4] arr[5] = 6; // 索引 4 会留白(值为 undefined)→ [1, 2, 3, 4, undefined, 6] -
利用
length新增(尾部添加):const arr = [1, 2, 3]; arr[arr.length] = 4; // arr.length 为 3,索引 3 是尾部位置 → [1, 2, 3, 4]
length属性是可读写的,可以修改length长度来实现数组扩容。
2. 函数:代码封装与复用
函数是封装可重复执行代码块的工具,通过函数名调用,实现代码复用与逻辑解耦。
2.1 函数的基本使用
函数使用分“声明”和“调用”两步,缺一不可(函数不调用则不执行)。
- 声明函数
// 语法:function 函数名(参数列表) { 函数体(执行逻辑) }
function getSum() {
let sum = 0;
for (let i = 1; i <= 100; i++) {
sum += i; // 计算 1-100 累加和
}
console.log(sum); // 输出 5050
}
函数名命名:建议用动词(如
getSum、printInfo),遵循小驼峰命名法。
- 调用函数
通过“函数名 + 括号”调用,括号内可传参数,声明函数本身并不会执行代码,只有调用函数时才会执行函数体代码。
getSum(); // 调用函数,执行累加逻辑并输出 5050
- 函数封装的意义
将重复逻辑(如计算、验证)封装为函数,减少代码冗余,便于维护。
- 例如:封装“判断是否为偶数”的逻辑:
function isEven(num) {
return num % 2 === 0; // 返回判断结果
}
// 多次复用
console.log(isEven(4)); // true
console.log(isEven(5)); // false
2.2 函数的参数
参数让函数更灵活(同一函数可处理不同输入),分为“形参”和“实参”:
| 类型 | 定义 | 位置 | 示例 |
|---|---|---|---|
| 形参(参数) | 函数声明时定义的“占位符”,表示需要接收的输入 | 函数名后的括号内 | function add(a, b) { ... } 中的 a、b |
| 实参(参数) | 函数调用时传入的“实际值”,会传递给形参 | 函数调用后的括号内 | add(2, 3) 中的 2、3 |
示例:带参数的加法函数
// 形参:a 和 b(接收两个数字)
function add(a, b) {
console.log(a + b); // 利用形参计算
}
// 实参:10 和 20(传递给 a 和 b)
add(10, 20); // 输出 30
但是 JavaScript 允许形参和实参数量不匹配,此时结果由规则决定:
- 实参多:多余实参被忽略(可通过
arguments获取); - 实参少:未匹配的形参值为
undefined(计算可能出NaN)。
function add(a, b) {
console.log(a + b);
}
add(1); // 实参少:b 为 undefined → 输出 NaN
add(1, 2, 3); // 实参多:3 被忽略 → 输出 3
2.3 函数的返回值
return 用于将函数执行结果“返回”给调用者,同时终止函数执行(return 后的代码不执行)。
// 声明带返回值的函数
function add(a, b) {
return a + b; // 返回 a+b 的结果,终止函数
console.log('这里不会执行'); // 被 return 阻断
}
// 调用函数并接收返回值
const result = add(10, 20);
console.log(result); // 输出 30
- 若无
return,函数默认返回undefined; return可返回任意类型(数字、字符串、对象等),但只能返回一个值(若需返回多个,可包装为数组或对象);return与break/continue的区别:break:终止循环;continue:跳过本次循环;return:终止函数(无论是否在循环内)。
2.4 arguments 处理不确定参数
当函数参数数量不确定时,可用 arguments(函数内置对象)获取所有实参,其本质是伪数组(有 length、按索引存储,但无数组方法如 push)。
示例:计算任意个数的和
function getTotal() {
let total = 0;
// 遍历 arguments,累加所有实参
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// 传入任意个数的参数
console.log(getTotal(1, 2)); // 3
console.log(getTotal(1, 2, 3, 4)); // 10
2.5 函数的两种声明方式
| 声明方式 | 语法 | 特点 | 适用场景 |
|---|---|---|---|
| 命名函数(自定义函数) | function fn() { ... } |
函数名可见,可在声明前调用(函数提升) | 全局函数、复用性高的函数 |
| 匿名函数(函数表达式) | const fn = function() { ... }; |
函数无名称,赋值给变量;需在声明后调用(变量提升仅提升声明,不提升赋值) | 临时函数、回调函数(如定时器、事件处理) |
// 1. 命名函数:可在声明前调用
fn1(); // 输出 '命名函数'
function fn1() {
console.log('命名函数');
}
// 2. 匿名函数:不可在声明前调用(会报错)
// fn2(); // 报错:fn2 is not a function
const fn2 = function() {
console.log('匿名函数');
};
fn2(); // 输出 '匿名函数'
3. 作用域:变量的访问规则
通常来说,一段程序代码中所用到的名字并不总是有效和可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。作用域的使用提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。
3.1 ES6 之前的作用域的分类
JavaScript(ES6 前)仅有两种作用域:
1)全局作用域
- 范围:整个
script标签或独立 JS 文件; - 变量:在全局作用域声明的变量(全局变量),可在任意位置访问;
- 特殊情况:函数内未用
var/let/const声明的变量,会隐式成为全局变量(不推荐,易污染全局)。
const globalVar = '全局变量'; // 全局变量
function fn() {
innerVar = '隐式全局变量'; // 未声明,成为全局变量
console.log(globalVar); // 可访问全局变量 → '全局变量'
}
fn();
console.log(innerVar); // 可访问 → '隐式全局变量'
2)局部作用域(函数作用域)
- 范围:仅函数内部;
- 变量:在函数内声明的变量(局部变量),仅能在函数内访问;
- 注意:函数的形参也属于局部变量。
function fn() {
const localVar = '局部变量'; // 局部变量
console.log(localVar); // 函数内可访问 → '局部变量'
}
fn();
console.log(localVar); // 报错:localVar is not defined(函数外不可访问)
3.2 ES6 新增 块级作用域
在 ES6 之前,JavaScript 中只有函数作用域和全局作用域,{} 包裹的代码块(如 if、for、while 等)并不形成独立的作用域。这导致用 var 声明的变量会“穿透”块结构,泄露到外部环境中。即块内用 var 声明的变量,在块外仍可访问。
// for 循环中的 var 泄露
for (var i = 0; i < 3; i++) {
// 循环内的 i 会泄露到外部
}
console.log(i); // 输出 3(循环结束后仍可访问)
// 嵌套块中的 var 共享同一作用域
{
var a = 10;
{
var a = 20; // 覆盖外部的 a
}
}
console.log(a); // 输出 20
ES6 引入的 let 和 const 支持块级作用域,变量仅在当前 {} 块内有效,避免泄露:
if (true) {
let blockLet = '块内 let 变量';
const blockConst = '块内 const 变量';
}
console.log(blockLet); // 报错:blockLet is not defined
console.log(blockConst); // 报错:blockConst is not defined
// for 循环中用 let 声明计数器,不会泄露
for (let j = 0; j < 3; j++) {
// j 仅在循环块内有效
}
console.log(j); // 报错:j is not defined
因此,ES6 后推荐使用 let/const 替代 var,以避免变量泄露和意外覆盖,使代码更可控。
3.3 作用域链:变量查找规则
作用域链(Scope Chain)是用于查找变量和函数的机制,决定了代码在执行时如何访问不同作用域中的变量。当访问一个变量时,JavaScript 会从当前作用域开始向上查找,直到找到变量或抵达全局作用域,这个由多个嵌套作用域组成的链式结构就是作用域链。
1)作用域链的形成
作用域链的结构由代码的嵌套关系决定:
- 每个函数或块级作用域(
let/const声明的块)都会创建一个新的作用域。 - 嵌套的作用域会形成层级关系(如函数内部包含另一个函数)。
- 每个作用域都会记录其父级作用域,最终指向全局作用域,形成链式结构。
// 全局作用域(最顶层)
let globalVar = "全局变量";
function outer() {
// outer 函数作用域(父级是全局作用域)
let outerVar = "outer 变量";
function inner() {
// inner 函数作用域(父级是 outer 作用域)
let innerVar = "inner 变量";
console.log(innerVar); // 自身作用域查找
console.log(outerVar); // 向上查找父级作用域
console.log(globalVar); // 继续向上查找全局作用域
}
inner();
}
outer();
-------------------------------
inner 变量
outer 变量
全局变量
inner作用域的作用域链:inner 作用域 → outer 作用域 → 全局作用域- 访问变量时,按这个链条依次向上查找,找到则停止,未找到则报错(
ReferenceError)。
2)作用域链的核心规则
-
变量查找方向:从内到外
访问变量时,优先在当前作用域查找,找不到则沿作用域链向上查找,直到全局作用域。如果全局作用域也没有,就会报错。let a = 10; function fn() { let a = 20; console.log(a); // 输出 20(当前作用域找到,停止查找) } fn(); -
作用域链的不可逆性
内部作用域可以访问外部作用域的变量,但外部作用域无法访问内部作用域的变量(单向访问)。function outer() { let innerVar = "内部变量"; } outer(); console.log(innerVar); // 报错:innerVar is not defined(外部无法访问内部) -
作用域链在函数定义时确定(静态作用域)
JavaScript 是静态作用域(词法作用域),作用域链在函数定义时就已确定,而非执行时。也就是说,函数的作用域链取决于它在代码中声明的位置,而非调用的位置。let x = 10; function fn() { console.log(x); // 作用域链在定义时已确定:fn 作用域 → 全局作用域 } function outer() { let x = 20; fn(); // 调用 fn 时,仍从定义时的作用域链查找 x } outer(); // 输出 10(而非 20,因为 fn 定义在全局作用域下)
作用域链是 JavaScript 变量查找的核心机制,其特点可概括为:
- 由嵌套作用域组成,方向是“内部→外部→全局”。
- 静态确定(定义时形成),与调用位置无关。
- 支持单向访问(内部可访问外部,反之不行)。
理解作用域链有助于解释变量的访问规则、避免作用域污染。
4. 预解析:代码执行的前置步骤
预解析(Hoisting)是 JavaScript 引擎在代码正式执行前的一个“准备阶段”,主要工作是将代码中声明的变量和函数“提升”到其所在作用域的顶部(但赋值操作留在原地)。
这一机制使得可以在变量或函数声明前使用它们(
不推荐这样做)。
4.1 预解析的核心规则
预解析针对两种核心语法:变量声明和函数声明,处理方式不同:
1)变量预解析(var/let/const)
-
var声明:变量名会被提升到作用域顶部,默认赋值为undefined。console.log(a); // 输出 undefined(不会报错) var a = 10; // 预解析后相当于: // var a; (提升到顶部,默认 undefined) // console.log(a); // a = 10; (赋值留在原地) -
let/const声明:变量名也会被提升,但存在“暂时性死区(TDZ)”——在声明语句前访问会报错,无法使用。console.log(b); // 报错:Cannot access 'b' before initialization let b = 20; // 变量 b 被提升了,但在声明前的区域(TDZ)禁止访问
2)函数预解析(函数声明式)
-
函数声明:整个函数体都会被提升到作用域顶部,因此可以在声明前调用函数。
foo(); // 输出 "函数被调用"(正常执行) function foo() { console.log("函数被调用"); } // 预解析后相当于: // function foo() { console.log("函数被调用"); } // foo(); -
函数表达式:仅变量名会被提升(同
var/let规则),函数体不会提升。bar(); // 报错:bar is not a function var bar = function() { console.log("函数表达式"); }; // 预解析后相当于: // var bar; (变量提升,默认 undefined) // bar(); (此时 bar 是 undefined,不是函数) // bar = function() { ... }
4.2 预解析的作用域范围
预解析是按作用域进行的:
- 全局作用域中的声明,提升到全局作用域顶部。
- 函数作用域中的声明,提升到该函数作用域顶部(不会影响外部作用域)。
var a = 100;
function fn() {
console.log(a); // 输出 undefined(访问的是函数内提升的 a)
var a = 200;
// 函数内预解析后:
// var a;
// console.log(a);
// a = 200;
}
fn();
console.log(a); // 输出 100(全局 a 不受函数内声明影响)
4.3 常见预解析问题
-
var变量覆盖问题:var允许重复声明,后声明的变量会覆盖前面的,但预解析可能导致意外结果,依然undefined。var x = 10; function fn() { console.log(x); // undefined(函数内 var x 提升,覆盖全局 x) var x = 20; } fn(); -
函数声明与变量声明的优先级:函数声明的提升优先级高于变量声明(变量声明会被忽略,但赋值仍有效):
console.log(foo); // 输出函数体(函数声明优先提升) function foo() {} var foo = 10; // 预解析后: // function foo() {} // var foo; (被忽略,因为函数已声明) // console.log(foo); // foo = 10; -
let/const重复声明报错:即使在预解析阶段,let/const也不允许同一作用域内重复声明(包括与函数名重复):let a = 10; function a() {} // 报错:Identifier 'a' has already been declared
预解析虽然允许 “先使用后声明”,但会降低代码可读性,还是建议变量和函数先声明,后使用(遵循 “正常执行顺序”)。
5. 对象:结构化数据存储
对象是无序的属性和方法集合,用于存储“事物的特征(属性)”和“行为(方法)”,比数组更适合存储结构化数据(如用户信息、商品信息)。
5.1 为什么需要对象?
数组存储数据需通过索引,语义不清晰;对象通过“属性名”访问数据,表意更明确:
// 数组存储用户信息(语义模糊,需记忆索引)
const userArr = ['张三', 20, '男'];
// 对象存储用户信息(属性名明确,易理解)
const userObj = {
name: '张三',
age: 20,
gender: '男'
};
5.2 创建对象的三种方式
1)字面量语法(推荐,简洁高效)
用 {} 包裹属性和方法,属性/方法以“键值对”形式存在(键:属性名,值:属性值/方法):
const star = {
name: 'pink', // 字符串属性
age: 18, // 数字属性
isMale: true, // 布尔属性
sayHi: function() { // 方法(函数作为属性值)
console.log('大家好!');
}
};
- 访问属性:两种方式,推荐“
对象.属性名”(更简洁);console.log(star.name); // 输出 'pink'(点语法) console.log(star['age']); // 输出 18(方括号语法,属性名需加引号) - 调用方法:“
对象.方法名()”(必须加括号);star.sayHi(); // 输出 '大家好!'
2)new Object() 构造函数
先创建空对象,再通过“对象.属性”添加属性和方法:
// 1. 创建空对象
const andy = new Object();
// 2. 添加属性和方法
andy.name = 'pink';
andy.age = 18;
andy.sayHi = function() {
console.log('大家好!');
};
// 使用
console.log(andy.name); // 'pink'
andy.sayHi(); // '大家好!'
3)自定义构造函数(批量创建对象)
当需要创建多个结构相同的对象(如多个用户、多个商品)时,可封装构造函数,实现批量初始化:
- 构造函数语法
// 构造函数(首字母大写,约定)
function Person(name, age, gender) {
this.name = name; // this 指向新创建的对象
this.age = age;
this.gender = gender;
this.sayHi = function() {
console.log(`你好,我是${this.name}`);
};
}
// 用 new 调用构造函数,创建对象
const person1 = new Person('张三', 20, '男');
const person2 = new Person('李四', 22, '女');
// 使用
console.log(person1.name); // '张三'
person2.sayHi(); // '你好,我是李四'
new关键字的作用
- 创建一个空对象;
- 将
this指向该空对象;- 执行构造函数代码(给对象添加属性和方法);
- 返回该对象(无需手动写
return)。
5.3 遍历对象(for...in)
用 for...in 循环遍历对象的所有属性(包括方法),语法:
for (let key in 对象) {
console.log(key); // 输出属性名(键)
console.log(对象[key]); // 输出属性值(需用方括号语法)
}
示例:遍历用户对象
const user = {
name: '张三',
age: 20,
gender: '男'
};
for (let key in user) {
console.log(`${key}: ${user[key]}`);
}
// 输出:
// name: 张三
// age: 20
// gender: 男
5.4 变量、属性、函数、方法的区别
| 概念 | 定义 | 访问方式 |
|---|---|---|
| 变量 | 独立存储数据的容器 | 直接用变量名(如 num) |
| 属性 | 对象内的“变量”,描述对象特征 | 对象.属性名(如 user.name) |
| 函数 | 独立的代码块,实现特定功能 | 函数名()(如 fn()) |
| 方法 | 对象内的“函数”,描述对象行为 | 对象.方法名()(如 user.sayHi()) |
6. 内置对象:高效开发工具
内置对象是指由 ECMAScript 标准定义的,在运行环境中自动存在的对象,它们提供了一系列实用的属性和方法,方便开发者处理各种数据类型、执行常见操作。
JavaScript 提供多个内置对象,封装了常用功能(如数学计算、日期处理、数组/字符串操作),无需重复开发,直接调用即可。
6.1 常用内置对象概览
| 内置对象 | 功能 | 核心用途 |
|---|---|---|
Math |
数学计算 | 求绝对值、取整、随机数等 |
Date |
日期与时间 | 获取当前时间、格式化时间、计算时间差 |
Array |
数组操作 | 数组排序、增删元素、查找索引等 |
String |
字符串操作 | 查找字符、截取字符串、替换内容等 |
6.2 Math 对象(数学计算)
Math 不是构造函数,直接通过 Math.属性/方法 调用,常用方法:
| 方法/属性 | 功能 | 示例 |
|---|---|---|
Math.PI |
圆周率(约 3.14159) | console.log(Math.PI); // 3.14159... |
Math.abs(x) |
求 x 的绝对值 | Math.abs(-5); // 5 |
Math.floor(x) |
向下取整(小于等于 x 的最大整数) | Math.floor(3.9); // 3 |
Math.ceil(x) |
向上取整(大于等于 x 的最小整数) | Math.ceil(3.1); // 4 |
Math.round(x) |
四舍五入取整(注意:-3.5 取 -3) | Math.round(3.5); // 4 |
Math.max(...x) |
求多个数的最大值 | Math.max(1, 3, 2); // 3 |
Math.min(...x) |
求多个数的最小值 | Math.min(1, 3, 2); // 1 |
Math.random() |
生成 [0,1) 之间的随机小数 | Math.random(); // 如 0.345 |
- Math.max() / Math.min() 中,如果参数有不是数字的情况,返回值时
NaN;- Math.max() / Math.min() 中,如果参数为空的话,返回值时
-Infinity;- 注意:上面的方法使用时必须带括号
实用案例:生成指定范围的随机整数
// 生成 [min, max] 之间的随机整数
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(getRandom(1, 10)); // 如 5
6.3 Date 对象(日期与时间)
Date 是构造函数,需用 new 实例化后使用,用于处理日期和时间。
- 创建
Date实例
const now = new Date(); // 无参数:获取当前时间
const future = new Date('2024-12-31'); // 传字符串:指定时间
const past = new Date(2023, 0, 1); // 传数字:年、月(0-11)、日(注意:月从 0 开始)
- 常用方法(获取日期时间)
| 方法 | 功能 | 示例(当前时间 2024-05-20) |
|---|---|---|
getFullYear() |
获取年份 | now.getFullYear(); // 2024 |
getMonth() |
获取月份(0-11) | now.getMonth(); // 4(5月) |
getDate() |
获取日期(1-31) | now.getDate(); // 20 |
getDay() |
获取星期(0-6,0 是周日) | now.getDay(); // 1(周一) |
getHours() |
获取小时(0-23) | now.getHours(); // 14 |
getMinutes() |
获取分钟(0-59) | now.getMinutes(); // 30 |
getSeconds() |
获取秒(0-59) | now.getSeconds(); // 45 |
- 案例:格式化时间(2024年5月20日 周一 14:30:45)
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1; // 月份+1(转为 1-12)
const date = now.getDate();
const weekArr = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const week = weekArr[now.getDay()];
const hours = now.getHours();
const minutes = now.getMinutes();
const seconds = now.getSeconds();
// 拼接格式化字符串
const formatTime = `${year}年${month}月${date}日 ${week} ${hours}:${minutes}:${seconds}`;
console.log(formatTime); // 2024年5月20日 周一 14:30:45
6.4 Array 对象(数组操作)
Array 对象提供丰富的数组操作方法,简化数组管理,常用方法分类如下:
1)增删元素
| 方法 | 功能 | 示例 |
|---|---|---|
push(元素1, ...) |
尾部添加元素,返回新长度 | const arr = [1,2]; arr.push(3); // arr 变为 [1,2,3] |
unshift(元素1, ...) |
头部添加元素,返回新长度 | arr.unshift(0); // arr 变为 [0,1,2,3] |
pop() |
尾部删除元素,返回删除的元素 | arr.pop(); // 返回 3,arr 变为 [0,1,2] |
shift() |
头部删除元素,返回删除的元素 | arr.shift(); // 返回 0,arr 变为 [1,2] |
// 初始购物车
let cart = ["苹果", "香蕉"];
// 1. push():添加新商品到购物车末尾
const newLength = cart.push("橙子");
console.log(cart); // ["苹果", "香蕉", "橙子"]
console.log("当前商品总数:", newLength); // 3
// 2. unshift():在购物车开头添加"牛奶"
cart.unshift("牛奶");
console.log(cart); // ["牛奶", "苹果", "香蕉", "橙子"]
// 3. pop():删除最后一个商品(橙子)
const removedItem = cart.pop();
console.log("删除的商品:", removedItem); // "橙子"
console.log(cart); // ["牛奶", "苹果", "香蕉"]
// 4. shift():删除第一个商品(牛奶)
cart.shift();
console.log(cart); // ["苹果", "香蕉"]
2)排序与反转
| 方法 | 功能 | 示例 |
|---|---|---|
sort() |
排序(默认按字符串排序,需传比较函数) | const arr = [3,1,2]; arr.sort((a,b) => a-b); // 升序 [1,2,3] |
reverse() |
反转数组元素顺序 | arr.reverse(); // [3,2,1] |
// 学生成绩数组
let scores = [85, 92, 78, 95, 88];
// 1. sort() 升序排序(按分数从低到高)
scores.sort((a, b) => a - b);
console.log("成绩升序:", scores); // 输出:[78, 85, 88, 92, 95]
// 2. sort() 降序排序(按分数从高到低)
scores.sort((a, b) => b - a);
console.log("成绩降序:", scores); // 输出:[95, 92, 88, 85, 78]
// 3. reverse() 反转数组(模拟“倒序展示排名”)
scores.reverse();
console.log("反转后成绩:", scores); // 输出:[78, 85, 88, 92, 95](回到升序,但逻辑上是“从后往前展示”)
3)查找索引
| 方法 | 功能 | 示例 |
|---|---|---|
indexOf(元素) |
从头部查找元素,返回索引(无则 -1) | arr.indexOf(2); // 1 |
lastIndexOf(元素) |
从尾部查找元素,返回索引(无则 -1) | arr.lastIndexOf(2); // 1 |
// 用户标签数组
const tags = ["普通用户", "VIP", "活跃用户", "VIP", "新用户"];
// 1. indexOf():查找“VIP”首次出现的索引
const firstVIPIndex = tags.indexOf("VIP");
console.log("VIP首次出现索引:", firstVIPIndex); // 输出:1
// 2. lastIndexOf():查找“VIP”最后出现的索引
const lastVIPIndex = tags.lastIndexOf("VIP");
console.log("VIP最后出现索引:", lastVIPIndex); // 输出:3
// 3. 判断是否有重复标签
if (firstVIPIndex !== lastVIPIndex) {
console.log("存在重复的VIP标签"); // 输出:存在重复的VIP标签
} else if (firstVIPIndex === -1) {
console.log("无VIP标签");
} else {
console.log("仅1个VIP标签");
}
4)数组转字符串
| 方法 | 功能 | 示例 |
|---|---|---|
toString() |
转为字符串(元素用逗号分隔) | arr.toString(); // "3,2,1" |
join(分隔符) |
转为字符串(用指定分隔符连接) | arr.join('-'); // "3-2-1" |
// 订单ID数组
const orderIds = [1001, 1002, 1003];
// 1. toString():转为逗号分隔的字符串(用于接口参数,如?id=1001,1002,1003)
const orderIdStr = orderIds.toString();
console.log("接口传参格式:", `?id=${orderIdStr}`); // 输出:?id=1001,1002,1003
// 2. join():用横线连接,生成展示用的订单号
const displayOrderIds = orderIds.join("-");
console.log("页面展示订单号:", displayOrderIds); // 输出:1001-1002-1003
// 3. 特殊场景:用空字符串连接(合并为一个字符串)
const mergedStr = orderIds.join("");
console.log("合并为单个字符串:", mergedStr); // 输出:100110021003
6.5 String 对象(字符串操作)
String 对象是 JavaScript 中用于处理文本数据的内置对象,它封装了字符串(原始值)并提供了丰富的方法,用于字符串的查找、截取、转换、替换等操作。在实际开发中,既可以直接使用字符串原始值调用这些方法(JavaScript 会自动装箱为 String 对象),也可以显式创建 String 对象。
// 1. 字符串原始值(推荐,自动装箱为 String 对象)
const str1 = "Hello World";
// 2. 显式创建 String 对象(不推荐,因可能导致类型判断问题)
const str2 = new String("Hello JavaScript");
console.log(typeof str1); // "string"(原始值)
console.log(typeof str2); // "object"(对象)
1)字符串不可变
字符串的值一旦创建,无法修改(修改时会创建新字符串,原字符串不变):
let str = 'abc';
str[0] = 'A'; // 无效,字符串不可变
console.log(str); // 仍为 'abc'
2)常用方法
| 方法 | 功能 | 示例 |
|---|---|---|
charAt(索引) |
获取指定索引的字符 | 'abc'.charAt(1); // 'b' |
indexOf(字符) |
查找字符首次出现的索引(无则 -1) | 'abcab'.indexOf('a'); // 0 |
lastIndexOf(字符) |
查找字符最后出现的索引(无则 -1) | 'abcab'.lastIndexOf('a'); // 3 |
slice(开始索引, 结束索引) |
截取字符串(含开始,不含结束,支持负数) | 'abcde'.slice(1,3); // 'bc' |
substring(开始索引, 结束索引) |
截取字符串(同 slice,但不支持负数) | 'abcde'.substring(1,3); // 'bc' |
replace(旧字符, 新字符) |
替换字符(仅替换首次匹配) | 'aaa'.replace('a', 'b'); // 'baa' |
split(分隔符) |
按分隔符分割字符串,返回数组 | 'a-b-c'.split('-'); // ['a','b','c'] |
以下是结合实际开发场景的字符串方法使用示例,通过具体业务需求展示这些方法的应用价值:
- 场景1:用户输入验证(
indexOf/lastIndexOf)
需求:检查用户输入的密码是否包含至少一个大写字母和一个数字(通过查找特定字符的索引实现)。
function checkPassword(password) {
// 检查是否包含大写字母(A-Z)
let hasUpper = false;
for (let i = 65; i <= 90; i++) { // ASCII 码中 A=65,Z=90
const upperChar = String.fromCharCode(i);
if (password.indexOf(upperChar) !== -1) {
hasUpper = true;
break;
}
}
// 检查是否包含数字(0-9)
const hasNumber = password.indexOf('0') !== -1 ||
password.indexOf('1') !== -1 ||
// ... 简化写法:直接判断是否有任意数字
/\d/.test(password);
return { hasUpper, hasNumber };
}
const result = checkPassword("Pass123");
console.log("包含大写字母:", result.hasUpper); // true
console.log("包含数字:", result.hasNumber); // true
- 场景2:截取文件扩展名(
slice/lastIndexOf)
需求:从文件名中提取扩展名(如从 “image.jpg” 中得到 “jpg”)。
function getFileExtension(filename) {
// 找到最后一个 "." 的位置
const dotIndex = filename.lastIndexOf('.');
// 若没有 "." 或 "." 在末尾,则无扩展名
if (dotIndex === -1 || dotIndex === filename.length - 1) {
return "无扩展名";
}
// 从 "." 后一位截取到结尾
return filename.slice(dotIndex + 1);
}
console.log(getFileExtension("document.pdf")); // "pdf"
console.log(getFileExtension("image.png.bak")); // "bak"(取最后一个 "." 后的内容)
console.log(getFileExtension("notes")); // "无扩展名"
- 场景3:格式化用户昵称(
charAt/slice)
需求:对过长的用户昵称进行截断,并在末尾添加省略号(如 “张晓明同学” → “张晓…”)。
function formatNickname(nickname, maxLength = 2) {
if (nickname.length <= maxLength) {
return nickname;
}
// 取前 maxLength 个字符,拼接省略号
return nickname.slice(0, maxLength) + "...";
}
// 结合 charAt 获取首字符(用于头像字母)
function getAvatarChar(nickname) {
return nickname.charAt(0).toUpperCase(); // 取首字符并转为大写
}
const nickname = "张晓明同学";
console.log("格式化昵称:", formatNickname(nickname)); // "张晓..."
console.log("头像字符:", getAvatarChar(nickname)); // "张"
- 场景4:解析URL参数(
split)
需求:从URL中提取查询参数(如从 “?name=张三&age=20” 中解析出 { name: “张三”, age: “20” })。
function parseUrlParams(url) {
const params = {};
// 1. 分割出查询字符串部分(去掉 "?")
const queryStr = url.split('?')[1];
if (!queryStr) return params;
// 2. 按 "&" 分割成键值对数组
const keyValuePairs = queryStr.split('&');
// 3. 遍历数组,按 "=" 分割键和值
keyValuePairs.forEach(pair => {
const [key, value] = pair.split('=');
params[key] = decodeURIComponent(value); // 解码特殊字符(如空格→%20)
});
return params;
}
const url = "https://example.com/user?name=张三&age=20&city=%E5%8C%97%E4%BA%9A";
const params = parseUrlParams(url);
console.log(params.name); // "张三"
console.log(params.city); // "北京"(已解码)
- 场景5:敏感信息替换(
replace)
需求:隐藏手机号中间4位(如 “13812345678” → “138****5678”)。
function hidePhone(phone) {
if (phone.length !== 11) return "手机号格式错误";
// 截取前3位和后4位,中间用"****"替换
const prefix = phone.slice(0, 3);
const suffix = phone.slice(7);
return prefix + "****" + suffix;
// 也可直接用 replace + 正则实现
// return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
console.log(hidePhone("13812345678")); // "138****5678"
这些示例覆盖了表单验证、数据格式化、URL解析等常见开发场景,展示了字符串方法如何配合解决实际问题。核心是理解每个方法的“输入/输出”特性,再根据需求组合使用。
6.6 简单与复杂数据类型
在 JavaScript 中,数据类型按存储方式和特性可分为 简单数据类型(原始值类型) 和 复杂数据类型(引用值类型),二者的核心区别在于 存储位置 和 赋值/传递方式,这直接影响代码的执行结果。
1)简单数据类型(原始值类型)
简单数据类型是 不可分割的基本数据单元,存储在 栈内存 中(栈内存特点:存储容量小、访问速度快,数据直接存储值)。
- JavaScript 中的简单数据类型共 7 种:
| 类型 | 说明 | 示例 |
|---|---|---|
string |
字符串(文本数据) | "Hello"、'JS' |
number |
数字(整数/小数/NaN/Infinity) | 123、3.14、NaN |
boolean |
布尔值(逻辑判断) | true、false |
undefined |
未定义(变量声明未赋值) | let a;(a 为 undefined) |
null |
空值(主动表示“无值”) | let b = null; |
symbol |
唯一标识(ES6 新增,不重复) | Symbol('id') |
bigint |
大整数(ES11 新增,存超大数字) | 123n、BigInt(999999999999999999) |
- 值直接存储:栈内存中直接存储“值”,而非地址。
- 赋值/传递时复制值:将一个简单类型变量赋值给另一个变量,会 复制一份完整的值,两个变量独立,修改一个不影响另一个。
let a = 10; let b = a; // 复制 a 的值(10)给 b b = 20; // 修改 b,不影响 a console.log(a); // 10(a 仍为原值) - 不可修改:原始值本身不可变(如字符串不能直接修改某一位字符),修改时实际是创建了新值。
let str = "abc"; str[0] = "A"; // 尝试修改字符串,无效 console.log(str); // "abc"(原字符串未变)
2)复杂数据类型(引用值类型)
复杂数据类型是 由多个简单类型组成的集合,存储在 堆内存 中(堆内存特点:存储容量大、访问速度慢,栈内存中仅存储“堆内存的地址”,而非值本身)。
- JavaScript 中的复杂数据类型主要是 对象及其衍生类型:
| 类型 | 说明 | 示例 |
|---|---|---|
Object |
普通对象(键值对集合) | { name: "张三" } |
Array |
数组(有序数据集合) | [1, 2, 3] |
Function |
函数(可执行代码块) | function add() {} |
Date |
日期对象 | new Date() |
RegExp |
正则对象 | /abc/ |
- 值存堆,地址存栈:创建复杂类型时,堆内存存储实际的“值(如对象的键值对)”,栈内存存储“堆内存的地址”(类似“指针”)。
- 赋值/传递时复制地址:将一个复杂类型变量赋值给另一个变量,仅复制“栈中的地址”,两个变量指向 同一块堆内存,修改一个会影响另一个。
let obj1 = { age: 20 }; let obj2 = obj1; // 复制地址,obj2 和 obj1 指向同一堆内存 obj2.age = 30; // 修改堆内存中的值 console.log(obj1.age); // 30(obj1 也受影响) - 可修改:复杂类型的“值”(堆内存中的内容)可直接修改(如添加/删除对象属性、修改数组元素)。
let arr = [1, 2, 3]; arr.push(4); // 修改数组,添加元素 console.log(arr); // [1, 2, 3, 4](数组被修改)
3)核心区别对比
| 对比维度 | 简单数据类型(原始值) | 复杂数据类型(引用值) |
|---|---|---|
| 存储位置 | 栈内存(直接存值) | 堆内存存值,栈内存存地址 |
| 赋值/传递方式 | 复制值(变量独立) | 复制地址(变量指向同一值) |
| 可修改性 | 不可修改(修改时创建新值) | 可修改(直接修改堆内存中的内容) |
| 比较方式 | 比较“值”是否相等 | 比较“地址”是否相等(是否指向同一对象) |
| 类型判断 | typeof 可区分(除 null 显式 object) |
typeof 均显式 object,需用 instanceof 区分 |
- 关键示例:比较方式差异
// 1. 简单类型:比较值
let num1 = 10;
let num2 = 10;
console.log(num1 === num2); // true(值相等)
// 2. 复杂类型:比较地址
let obj1 = { name: "A" };
let obj2 = { name: "A" };
console.log(obj1 === obj2); // false(地址不同,是两个不同对象)
let obj3 = obj1;
console.log(obj1 === obj3); // true(地址相同,指向同一对象)
- 常见误区与注意事项
typeof null结果为object:这是 JavaScript 的历史 bug,null本质是简单类型,需单独判断(value === null)。- 函数的特殊性:
typeof function() {}结果为function(而非object),但函数本质是引用类型,赋值时仍复制地址。- 浅拷贝与深拷贝:复杂类型赋值时仅复制地址,若需“完全独立的新对象”,需实现浅拷贝(如
Object.assign)或深拷贝(如递归复制、JSON.parse(JSON.stringify()))。
本文覆盖的数组、函数、作用域、对象及内置对象,是 JavaScript 开发的核心基石:
- 数组:管理有序批量数据,通过索引和
length实现遍历与更新; - 函数:封装复用代码,通过参数和返回值实现灵活交互,
arguments处理不确定参数; - 作用域:定义变量访问规则,作用域链遵循就近查找原则;
- 预解析:变量和函数提升影响代码执行顺序,需注意函数表达式的特殊性;
- 对象:存储结构化数据,通过构造函数实现批量创建,
for...in实现遍历; - 内置对象:
Math/Date/Array/String封装常用功能,大幅提升开发效率。
掌握这些概念后,可进一步学习 ES6+ 特性(如 let/const、箭头函数、类)、DOM 操作、异步编程等进阶内容,逐步构建完整的 JavaScript 技术体系。
更多推荐

所有评论(0)