JS进阶

JS进阶

_

一,作用域

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

作用域分为:

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

1.1 局部作用域

局部作用域分为函数作用域和块作用域

  1. 函数作用域

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

    • 函数的参数也是函数内部的局部变量
    • 不同函数内部声明的变量无法互相访问
    • 函数执行完毕后,函数内部的变量实际被清空了
  2. 块作用域

    在 JavaScript 中使用 { } 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。

    • let 声明的变量会产生块作用域,var 不会产生块作用域
    • const 声明的常量也会产生块作用域
    • 不同代码块之间的变量无法互相访问
    • 推荐使用 let 或 const

1.2 全局作用域

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

全局作用域中声明的变量,任何其它作用域都可以被访问

注意:

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

1.3 作用域链

作用域链本质上是底层的变量查找机制。

  • 在函数被执行时,会优先查找当前函数作用域中查找变量
  • 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域

总结:

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

1.4 JS垃圾回收机制

什么是垃圾回收机制?

垃圾回收机制 (Garbage Collection) 简称 GC ,JS 中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。 正因为垃圾回收器的存在,许多人认为 JS 不用太关心内存管理的问题 但如果不了解 JS 的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况 不再用到的内存,没有及时释放,就叫做内存泄漏

内存的生命周期

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

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
  4. 说明
    • 全局变量一般不会回收(关闭页面回收);
    • 一般情况下局部变量的值, 不用了, 会被自动回收掉

JS垃圾回收机制-算法说明

堆栈空间分配区别:

  1. 栈(操作系统): 由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
  2. 堆(操作系统): 一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。 下面介绍两种常见的浏览器垃圾回收算法: 引用计数法 和 标记清除法

引用计数

  • IE 采用的引用计数算法, 定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象

  • 算法:

    1. 跟踪记录被引用的次数
    2. 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
    3. 如果减少一个引用就减1 --
    4. 如果引用次数是0 ,则释放内存
  • 缺点:嵌套引用(循环引用)

    如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露

标记清除法

  • 现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
  • 核心:
    1. 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
    2. 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。
    3. 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

1.5 闭包

概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域

简单理解:闭包 = 内层函数 + 外层函数的变量

function outer(){
    const a = 1
    function f(){
        console.log(a)
    }
    f()
}
outer()//输出1

闭包作用:封闭数据,提供操作,外部也可以访问函数内部的变量

闭包的基本格式:

function outer(){
    let i = 1
    function fn(){
        console.log(i)
    }
    return fn
}
const fun = outer()
fun()//输出1
// 外层函数使用内部函数的变量

//简化写法
function outer(){
    let i = 1
    return function (){
        console.log(i)
    }
}
const fun = outer()
fun()

闭包应用:实现数据的私有

比如,我们要做个统计函数调用次数,函数调用一次,就 ++

function fn(){
    count++
    console.log(`函数被调用${count}次`)
}
fn()//2
fn()//3

但是,这个 count 是个全局变量,很容易被修改

用闭包就变成了

function fn(){
    let count=1
    return function (){
        count++
        console.log(`函数被调用${count}次`)
    }
}
fn()//2
fn()//3

这样实现了数据私有,无法直接修改 count

1.6 变量提升

变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于 var 声明变量)

注意:

  1. 变量在未声明即被访问时会报语法错误
  2. 变量在var声明之前即被访问,变量的值为 undefined
  3. let/const 声明的变量不存在变量提升
  4. 变量提升出现在相同作用域当中
  5. 实际开发中推荐先声明再访问变量

二,函数进阶

2.1 函数提升

函数提升与变量提升比较类似,是指函数在声明之前即可被调用。

foo()
function foo(){
    console.log('函数提升')
}
// 函数表达式不存在函数提升!
bear()
var bar = function(){
    console.log('函数提升')
}

总结

  • 函数提升能够使函数的声明调用更灵活
  • 函数表达式不存在提升的现象
  • 函数提升出现在相同作用域当中

2.2 函数参数

动态参数:arguments

arguments 是一个类数组对象,它存在于所有非箭头函数中,包含了调用函数时传入的所有参数。

特征

  • 结构:具有 length 属性和索引(0, 1, 2...),但不是数组实例。
  • 可用性:在箭头函数中不可用(会抛出引用错误)。
  • 与形参绑定:在非严格模式下,arguments 元素与对应命名形参存在联动关系(修改会互相影响);严格模式下为独立副本。

语法示例

function logParams() {
  console.log(arguments[0]); // 第一个参数
  console.log(arguments.length); // 传入参数的总数
}

logParams('A', 'B', 'C'); 
// 输出: 'A' , 3

转换为真数组

由于 arguments 不是数组,无法直接使用 map、forEach 等方法,需借助 Array.from() 或展开运算符转换:

function sum() {
  const args = Array.from(arguments); // 转换为真数组
  // 或 const args = [...arguments];
  return args.reduce((total, n) => total + n, 0);
}

注意:arguments 存在性能损耗且语法冗长,在 ES6 之后的新项目中已不推荐使用。

剩余参数 :Rest Parameters

剩余参数语法允许我们将一个不定数量的参数表示为一个真正的数组。

语法

function func(a, b, ...theArgs) {
  // theArgs 是一个数组,包含从第三个参数开始的所有实参
}

特征

  • 真实数组:可直接调用 map、filter、reduce 等数组方法。
  • 位置限制:剩余参数必须是函数定义的最后一个参数,否则会抛出语法错误。
  • 不包含初始参数:它只收集“剩余”的部分,清晰区分必填参数与动态参数。

代码示例

// 场景:计算商品总价(基本价 + 多项附加费)
function calculateTotal(basePrice, ...fees) {
  const totalFees = fees.reduce((acc, fee) => acc + fee, 0);
  return basePrice + totalFees;
}

console.log(calculateTotal(100, 10, 20, 5)); // 输出: 135
// basePrice = 100, fees = [10, 20, 5]

核心对比总结

特性arguments 对象剩余参数(...rest)
数据类型类数组(非数组)真正的 Array 实例
适用函数非箭头函数所有函数(含箭头函数)
包含范围包含全部传入的实参仅包含未单独命名的实参
位置限制无限制必须放在形参列表末尾
数组方法不支持,需手动转换原生支持(.reduce等)
推荐程度遗留特性,不推荐ES6+ 标准推荐

展开运算符

展开运算符(Spread Syntax),以 ... 表示,允许将一个可迭代对象(如数组、字符串、Map、Set)或对象自身属性在语法层面“展开”为独立的元素列表或键值对。

技术本质:它在语法层面将集合数据结构“拆包”(Unpack)为单个元素,常用于函数调用、数组构造和对象合并。

代码样例

  • 样例一:函数调用中展开数组(替代 apply)

    当你想把数组元素作为独立的参数传递给函数时,展开运算符是最简洁的方式。

    const scores = [85, 92, 78];
    
    // 传统方式(ES5)
    Math.max.apply(null, scores); // 92
    
    // 展开运算符方式(ES6+)
    Math.max(...scores); // 92
    // 相当于执行 Math.max(85, 92, 78)
    

    常见场景:

    • Math.max() / Math.min()
    • Date 构造函数传参:new Date(...[2025, 0, 1])
  • 样例二:数组字面量中展开(合并 / 拷贝数组)

    可以在数组内部展开另一个数组,实现浅拷贝和无缝拼接,且不影响原数组。

    const arr1 = [1, 2, 3];
    const arr2 = [4, 5, 6];
    
    // 拼接数组(替代 concat)
    const merged = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
    
    // 在任意位置插入元素
    const newArr = [0, ...arr1, 99]; // [0, 1, 2, 3, 99]
    
    // 浅拷贝(替代 slice)
    const copy = [...arr1]; // [1, 2, 3],修改 copy 不会影响 arr1(仅限第一层)
    

    注意:展开运算符只能进行浅拷贝(Shallow Copy)。如果数组元素是对象,拷贝的是对象的引用,修改嵌套对象会影响原数组。

  • 样例三:对象字面量中展开(ES2018+,合并 / 拷贝对象)

    这是现代 JavaScript 中最常用的模式之一,用于不可变数据操作(Redux / React 状态管理核心技巧)。

    const user = { name: '张三', age: 25 };
    const address = { city: '北京', district: '海淀' };
    
    // 合并对象(后面的属性会覆盖前面的同名属性)
    const merged = { ...user, ...address };
    // { name: '张三', age: 25, city: '北京', district: '海淀' }
    
    // 浅拷贝对象(用于更新部分属性)
    const updated = { ...user, age: 26 }; 
    // { name: '张三', age: 26 }  —— 原 user 对象未被修改
    
    // 覆盖/添加属性(常用于状态管理)
    const userWithToken = { ...user, token: 'abc123' };
    

与剩余参数(Rest Parameters)的区别

这两个语法长得一模一样,初学者极易混淆。用一句话记住:

语法作用方向位置用途
剩余参数 (...rest)收集(Collect)函数定义的形参列表末尾将零散参数聚合为数组
展开运算符 (...spread)打散(Spread)函数调用、数组/对象字面量中将集合拆解为独立元素

对比示例:

// 剩余参数(函数定义中,收集)
function sum(...numbers) {  // 这里的 ... 是“剩余参数”
  return numbers.reduce((a, b) => a + b);
}
sum(1, 2, 3); // numbers = [1, 2, 3]

// 展开运算符(函数调用中,打散)
const nums = [1, 2, 3];
sum(...nums); // 这里的 ... 是“展开运算符”,等价于 sum(1, 2, 3)

2.4 箭头函数

基本语法

箭头函数是 ES6(ECMAScript 2015)引入的一种简化函数定义语法。它使用 =>(箭头)符号,不仅提供了更简洁的书写方式,更重要的是,它拥有词法作用域的 this 绑定,且不能作为构造函数使用。

一句话总结:箭头函数是为了解决 JavaScript 中 this 指向混乱问题而设计的“轻量级函数”。

// 1. 零参数:必须写空括号
const greet = () => console.log('Hello');

// 2. 单参数:括号可选
const double = x => x * 2;

// 3. 多参数:必须加括号
const sum = (a, b) => a + b;

// 4. 函数体多行:必须加花括号,并显式 return
const process = (a, b) => {
  const result = a + b;
  return result * 2;
};

// 5. 返回对象字面量:必须用括号包裹,否则会和函数体花括号混淆
const createUser = name => ({ name: name, age: 18 });

当函数体只有一个表达式时,箭头函数会隐式返回该表达式的值,无需写 return 关键字。

const add = (a, b) => a + b; // 自动返回 a + b

// 相当于传统写法:
function add(a, b) { return a + b; }

箭头函数参数

普通函数有 arguments 动态参数, 箭头函数没有 arguments 动态参数,但是有 剩余参数 ..args

const getSum = (...args)=>{
    let sum=0
    for(let i=0;i<args.length;i++){
        sum += args[i]
    }
    return sum
}
console.log(getSum(1,2,3))

箭头函数this

这是箭头函数最核心、最革命性的改变。

  • 普通函数:this 指向调用时的上下文(动态绑定),谁调用指向谁。
  • 箭头函数:this 指向定义时的上下文(静态绑定),它捕获的是外层(通常是父级作用域)的 this,且不可被 call、apply、bind 改变。
const obj = {
  name: '前端文档',
  // 普通函数:this 指向 obj
  normalFunc: function() {
    console.log(this.name); 
  },
  // 箭头函数:this 指向全局(严格模式下为 undefined)或外层 this
  arrowFunc: () => {
    console.log(this.name); 
  }
};

obj.normalFunc(); // 输出: '前端文档'
obj.arrowFunc();  // 输出: undefined (因为定义时外层是全局对象)

经典应用场景(React 类组件 / Vue Options API 中的事件处理):

// 普通函数在 setTimeout 中丢失 this(指向 window)
const obj = {
  name: '张三',
  sayLater: function() {
    setTimeout(function() {
      console.log(this.name); // undefined(this 指向 window)
    }, 1000);
  }
};

// 箭头函数修复(捕获外层 this)
const obj2 = {
  name: '张三',
  sayLater: function() {
    setTimeout(() => {
      console.log(this.name); // '张三'(箭头函数捕获了外层的 this)
    }, 1000);
  }
};

三,解构赋值

解构赋值是一种 JavaScript 表达式,允许将数组中的值或对象中的属性提取出来,赋值给不同的变量。

一句话总结:解构赋值是“数据拆包器”,它让左侧的变量结构匹配右侧的数据结构,并完成自动赋值。

3.1 数组解构

数组解构按照位置顺序进行匹配,变量名可以随意命名。

基础用法

const colors = ['红', '绿', '蓝'];

// 传统写法
// const red = colors[0];
// const green = colors[1];

// 解构写法
const [red, green, blue] = colors;
console.log(red);   // '红'
console.log(green); // '绿'
console.log(blue);  // '蓝'

跳过元素(使用逗号占位)

const [, , third] = ['A', 'B', 'C'];
console.log(third); // 'C'(跳过前两个)

剩余元素收集(结合剩余参数 ...)

const [first, ...rest] = [10, 20, 30, 40];
console.log(first); // 10
console.log(rest);  // [20, 30, 40]

默认值(防止 undefined)

const [a = 1, b = 2] = [100];
console.log(a); // 100(覆盖默认值)
console.log(b); // 2(使用默认值)

交换变量(无需临时变量)

let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1

3.2 对象解构

对象解构按照属性名称进行匹配,与顺序无关。

基础用法

const user = { name: '张三', age: 25, city: '杭州' };

const { name, age } = user;
console.log(name); // '张三'
console.log(age);  // 25

重命名变量(提取后换个名字)

const { name: userName, age: userAge } = user;
console.log(userName); // '张三'
console.log(userAge);  // 25

默认值

const { gender = '未知', city } = user;
console.log(gender); // '未知'(原对象中没有 gender)
console.log(city);   // '杭州'

同时重命名 + 默认值

const { country: userCountry = '中国' } = user;
console.log(userCountry); // '中国'

嵌套对象解构

const person = {
  name: '李四',
  address: {
    province: '浙江',
    city: '杭州'
  }
};

const { address: { city } } = person;
console.log(city); // '杭州'

四,js对象

4.1 创建对象三种方式

  1. 对象字面量(最常用、最推荐)

    使用花括号 {} 直接创建。这是最简洁、性能最好的方式。

    // 直接定义属性和方法
    const person = {
      name: '小明',
      age: 25,
      sayHello: function() {
        console.log('你好,我是' + this.name);
      }
    };
    
    // ES6 简写(方法可以省略 function 关键字)
    const person2 = {
      name: '小红',
      age: 23,
      sayHello() {
        console.log(`你好,我是${this.name}`);
      }
    };
    
    person.sayHello(); // 输出:你好,我是小明
    
    • 优点:代码简洁、直观,适合创建单个独立对象。

    • 缺点:如果需要创建多个结构相同的对象(比如 100 个用户),就得重复写很多重复代码。

  2. 使用 new Object() 构造函数

    通过内置的 Object 构造函数来创建对象,然后动态添加属性。

    const person = new Object();
    person.name = '小刚';
    person.age = 28;
    person.sayHello = function() {
      console.log('你好,我是' + this.name);
    };
    
    person.sayHello(); // 你好,我是小刚
    
    • 优点:语义上明确表示“我在构造一个对象”。

    • 缺点:写法比字面量繁琐,且与字面量本质完全一样(字面量底层也会调用 Object),所以实际开发中基本不用这种写法,除非特殊场景(比如需要动态决定创建哪种对象时)。

  3. 自定义构造函数

    通过 function 定义构造函数,配合 new 关键字批量创建对象。这是实现“类”概念的基础。

    // 1. 定义构造函数(首字母大写是约定)
    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.sayHello = function() {
        console.log(`你好,我是${this.name}`);
      };
    }
    
    // 2. 使用 new 创建实例
    const p1 = new Person('张三', 30);
    const p2 = new Person('李四', 22);
    
    p1.sayHello(); // 你好,我是张三
    p2.sayHello(); // 你好,我是李四
    
    // 检查实例类型
    console.log(p1 instanceof Person); // false
    
    • 优点:可以像工厂流水线一样批量生产对象,且能通过 instanceof 识别类型。

    • 缺点(注意):每个实例的 sayHello 方法都会在内存中创建一份副本(p1.sayHello === p2.sayHello 为 false),浪费内存。优化方案:把共用方法挂载到原型上

  4. 现代 JavaScript 的最佳实践(ES6 Class)

    实际上,现在最常用的是 ES6 的 Class 语法,它本质上是构造函数的“语法糖”,但写法更优雅,且自动解决了原型共享问题。

    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
      
      // 方法自动挂在原型上,所有实例共享,节省内存
      sayHello() {
        console.log(`你好,我是${this.name}`);
      }
    }
    
    const p1 = new Person('王五', 26);
    p1.sayHello();
    

三种方式的对比总结

方式适用场景是否共享方法
字面量 {}创建单个简单对象、配置项、JSON 数据否(独立)
new Object()极少使用,除非动态创建否
构造函数 / Class需要批量创建相同类型的对象可共享(挂载原型)

4.2 实例成员&静态成员

核心定义

  • 实例成员 (Instance Members):属于具体的实例对象。每个实例独有一份,只能通过 实例.属性 访问。
    • 实例对象的属性和方法即为实例成员
    • 为构造函数传入参数,动态创建结构相同但值不同的对象
    • 构造函数创建的实例对象彼此独立互不影响。
  • 静态成员 (Static Members):属于构造函数/类本身。所有实例共享一份,只能通过 类名.属性 访问。
    • 构造函数的属性和方法被称为静态成员
    • 一般公共特征的属性或方法静态成员设置为静态成员
    • 静态成员方法中的this 指向构造函数本身

代码示例

  • 构造函数写法

    // 定义构造函数
    function Person(name) {
      // 【实例成员】—— 挂在 this 上,每个实例独有
      this.name = name;
      this.sayHi = function() {
        console.log('Hi, ' + this.name);
      };
    }
    
    // 【静态成员】—— 直接挂在函数对象上
    Person.species = '人类';          // 静态属性
    Person.describe = function() {    // 静态方法
      console.log('我们都是' + this.species);
    };
    
    // ----- 使用 -----
    const p1 = new Person('小明');
    const p2 = new Person('小红');
    
    console.log(p1.name);            // '小明' (实例成员)
    console.log(p2.name);            // '小红' (实例成员)
    console.log(Person.species);     // '人类' (静态成员,通过类名访问)
    
    p1.sayHi();                      // 实例方法,通过实例调用
    Person.describe();               // 静态方法,通过类名调用
    
    // ❌ 错误示范:实例访问不到静态成员
    console.log(p1.species);         // undefined (实例上根本没有)
    // ❌ 错误示范:类名访问不到实例成员
    console.log(Person.name);        // undefined (构造函数本身没有 name 属性)
    
  • ES6 Class 写法

    class Person {
      // 【实例成员】写在 constructor 里
      constructor(name) {
        this.name = name;
        this.sayHi = function() {
          console.log('Hi, ' + this.name);
        };
      }
    
      // 【静态成员】用 static 关键字
      static species = '人类';
      static describe() {
        console.log('我们都是' + this.species);
      }
    }
    
    // 使用方式完全一样
    const p1 = new Person('小明');
    console.log(p1.name);          // 实例成员
    console.log(Person.species);   // 静态成员
    

4.3 编程思想

面向过程编程:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次 调用就可以了。

面向对象编程:把事务分解成为一个个对象,然后由对象之间分工与合作。

  • 在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。
  • 面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
  • 面向对象的特性:
    • 封装性
    • 继承性
    • 多态性

五,内置构造函数

在 JavaScript 中最主要的数据类型有 6 种:

  • 基本数据类型:字符串、数值、布尔、undefined、null
  • 引用类型: 对象

但是,我们会发现有些特殊情况:

const str ='abc'
console.log(str.length)//3

其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型,JS 中几乎所有的数据都可以基于构成函数创建。

内置构造函数分为:

  • 引用类型: Object,Array,RegExp,Date 等
  • 包装类型: String,Number,Boolean 等

5.1 Object

Object 是内置的构造函数,用于创建普通对象。

const user = new Object({name:"张三",age:13})

推荐使用字面量方式声明对象,而不是 Object 构造函数

Object 三个常用静态方法

  • Object.keys 静态方法获取对象中所有属性(键)

    const user = {name : '张三',age:6}
    const arr = Object.keys(user)
    console.log(arr) // ['name','age']
    
  • Object.values 静态方法获取对象中所有属性值

    const user = {name : '张三',age:6}
    const arr = Object.values (user)
    console.log(arr) // ['张三','6']
    
  • Object. assign 静态方法常用于对象拷贝

    const o = {name : '张三',age:6}
    const obj = Object.assign(obj,o)
    console.log(obj) //{name : '张三',age:6}
    
  • Object. assign 给对象添加属性

    const o = {name : '张三',age:6}
    Object.assign(o,{gender:'女'})
    console.log(o) //{name : '张三',age:6,gender:'女'}
    

5.2 Array

Array 是内置的构造函数,用于创建数组

const arr = new Array(3,5)
console.log(arr)// [3,5]

创建数组建议使用字面量创建,不用 Array 构造函数创建

数组常见实例方法 - 核心方法

方法作用说明
forEach遍历数组不返回,用于不改变值,经常用于查找打印输出值
filter过滤数组筛选数组元素,并生成新数组
map迭代数组返回新数组,新数组里面的元素是处理之后的值,经常用于处理数据
reduce累计器返回函数累计处理的结果,经常用于求和等

代码示例

// ------------forEach 代码示例-------------
const nums = [1, 2, 3, 4, 5];

// forEach  打印每个元素
nums.forEach(item => {
  console.log(item); // 输出 1 2 3 4 5
});

// -------------filter代码示例--------------
const scores = [85, 92, 78, 66, 95, 88];

// 场景1:筛选及格(>=60)的成绩
const passed = scores.filter(score => score >= 60);
console.log(passed); // [85, 92, 78, 66, 95, 88] (全及格)

// 场景3:筛选数组中的对象
const users = [
  { name: '小明', age: 18 },
  { name: '小红', age: 25 },
  { name: '小刚', age: 16 }
];
const adults = users.filter(user => user.age >= 18);
console.log(adults); 
// [{ name: '小明', age: 18 }, { name: '小红', age: 25 }]

// 场景4:去重(配合 indexOf)
const arr = [1, 2, 2, 3, 3, 4];
const unique = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(unique); // [1, 2, 3, 4]
//-----------map代码示例------------
const nums = [1, 2, 3, 4, 5];

// 场景1:每个元素乘以2
const doubled = nums.map(item => item * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// 场景2:提取对象数组中的某个字段(最常用!)
const users = [
  { id: 1, name: '小明' },
  { id: 2, name: '小红' },
  { id: 3, name: '小刚' }
];
const names = users.map(user => user.name);
console.log(names); // ['小明', '小红', '小刚']

// 场景3:添加新字段
const usersWithAge = users.map(user => ({
  ...user,
  age: 18 // 给每个人加个年龄字段
}));
console.log(usersWithAge);
// [{ id: 1, name: '小明', age: 18 }, ...]

// 场景4:格式化数据(转字符串)
const prices = [100, 200, 300];
const formatted = prices.map(price => `¥${price}.00`);
console.log(formatted); // ['¥100.00', '¥200.00', '¥300.00']


//------------reduce------------------
const nums = [1, 2, 3, 4, 5];

// 场景1:求和(最经典)
const sum = nums.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15

// 场景2:求乘积
const product = nums.reduce((acc, cur) => acc * cur, 1);
console.log(product); // 120

// 场景3:求最大值
const max = nums.reduce((acc, cur) => acc > cur ? acc : cur);
console.log(max); // 5

// 场景4:统计数组中每个元素出现次数(面试常考)
const fruits = ['苹果', '香蕉', '苹果', '橙子', '香蕉', '香蕉'];
const count = fruits.reduce((acc, cur) => {
  acc[cur] = (acc[cur] || 0) + 1;
  return acc;
}, {});
console.log(count); // { 苹果: 2, 香蕉: 3, 橙子: 1 }

// 场景5:数组扁平化(将二维数组转一维)
const nested = [[1, 2], [3, 4], [5, 6]];
const flat = nested.reduce((acc, cur) => acc.concat(cur), []);
console.log(flat); // [1, 2, 3, 4, 5, 6]

// 场景6:求和对象数组中某个字段的总价
const cart = [
  { name: '苹果', price: 5, count: 3 },
  { name: '香蕉', price: 3, count: 5 },
  { name: '橙子', price: 4, count: 2 }
];
const total = cart.reduce((acc, cur) => acc + cur.price * cur.count, 0);
console.log(total); // 15 + 15 + 8 = 38

5.3 String

String 是什么?

String 是 JavaScript 的基本包装类型。这意味着:

  • 字符串可以是原始值('hello')
  • 但当你调用方法时('hello'.toUpperCase()),JS 会临时把它包装成 String 对象,执行完再转回原始值
const str = 'hello';
console.log(typeof str); // 'string'
console.log(str.toUpperCase()); // 'HELLO'(自动装箱)

String 方法分类大全

我把常用方法分成 6 大类,每类给出核心示例:


查找类(查位置/查是否存在)

方法作用返回值
indexOf()查找子串第一次出现的位置索引(找不到返回 -1)
lastIndexOf()查找子串最后一次出现的位置索引(找不到返回 -1)
includes()是否包含子串true / false
startsWith()是否以某子串开头true / false
endsWith()是否以某子串结尾true / false
const str = 'Hello, World! Hello, JS!';

console.log(str.indexOf('Hello'));     // 0(第一次出现)
console.log(str.lastIndexOf('Hello')); // 14(最后一次出现)
console.log(str.indexOf('Python'));    // -1(不存在)

console.log(str.includes('World'));    // true
console.log(str.startsWith('Hello'));  // true
console.log(str.endsWith('JS!'));      // true

// 带起始位置的查找
console.log(str.indexOf('Hello', 5));  // 14(从索引5开始找)

截取类(切出子串)

方法作用区别
slice(start, end)截取支持负数索引(从末尾数)
substring(start, end)截取不支持负数(负数当 0)
substr(start, length)截取已废弃,别用了
const str = 'JavaScript';

console.log(str.slice(0, 4));     // 'Java'(从0到4,不包括4)
console.log(str.slice(4));        // 'Script'(从4到末尾)
console.log(str.slice(-6));       // 'Script'(从倒数第6个开始)
console.log(str.slice(-6, -1));   // 'Scrip'(从倒数第6到倒数第1)

console.log(str.substring(0, 4)); // 'Java'
console.log(str.substring(-3));   // 'JavaScript'(负数当0处理)

// ❌ substr 已废弃,别用了
// console.log(str.substr(4, 6)); // 'Script'

拆分/合并类

方法作用返回值
split(分隔符)用分隔符拆成数组数组
join()数组转字符串属于 Array 方法,但常配合用
// split:字符串 → 数组
const str = 'apple,banana,orange';
const fruits = str.split(',');
console.log(fruits); // ['apple', 'banana', 'orange']

// 不带分隔符:拆成每个字符
console.log('hello'.split('')); // ['h', 'e', 'l', 'l', 'o']

// 限制拆分次数
console.log('a-b-c-d'.split('-', 2)); // ['a', 'b']

// 配合 join:数组 → 字符串(复习一下)
const arr = ['2024', '08', '11'];
console.log(arr.join('-')); // '2024-08-11'

大小写转换类

方法作用
toUpperCase()全转大写
toLowerCase()全转小写
const str = 'Hello World';

console.log(str.toUpperCase()); // 'HELLO WORLD'
console.log(str.toLowerCase()); // 'hello world'

// 常见场景:不区分大小写比较
const userInput = 'Admin';
if (userInput.toLowerCase() === 'admin') {
  console.log('欢迎管理员'); // 匹配成功
}

去除空白类

方法作用
trim()去除两端空白
trimStart() / trimLeft()去除开头空白
trimEnd() / trimRight()去除结尾空白
const str = '   Hello World   ';

console.log(str.trim());        // 'Hello World'
console.log(str.trimStart());   // 'Hello World   '
console.log(str.trimEnd());     // '   Hello World'

// 常见场景:用户输入清洗
const username = '  张三  ';
console.log(username.trim()); // '张三'

替换/填充类

方法作用
replace(旧, 新)替换第一个匹配
replaceAll(旧, 新)替换所有匹配(ES2021)
padStart(长度, 填充)在前面填充
padEnd(长度, 填充)在后面填充
repeat(次数)重复字符串
// replace / replaceAll
const str = 'Hello Hello Hello';
console.log(str.replace('Hello', 'Hi'));      // 'Hi Hello Hello'(只替换第一个)
console.log(str.replaceAll('Hello', 'Hi'));   // 'Hi Hi Hi'(全部替换)

// 正则替换(更强大)
console.log(str.replace(/Hello/g, 'Hi'));     // 'Hi Hi Hi'

// padStart / padEnd(补位)
console.log('5'.padStart(3, '0'));   // '005'(常见于日期格式化)
console.log('5'.padEnd(3, '0'));     // '500'
console.log('2024'.padStart(10, '-')); // '------2024'

// repeat
console.log('*'.repeat(5)); // '*****'
console.log('abc'.repeat(3)); // 'abcabcabc'

其他常用方法

方法作用示例
length获取长度(属性)'hello'.length → 5
charAt(索引)获取指定位置字符'hello'.charAt(1) → 'e'
charCodeAt(索引)获取 Unicode 编码'A'.charCodeAt(0) → 65
concat()拼接字符串'hello'.concat(' world') → 'hello world'
match()匹配正则返回匹配结果
const str = 'JavaScript';

console.log(str.length); // 10
console.log(str.charAt(4)); // 'S'(从0开始数)
console.log(str[4]); // 'S'(ES5 支持,更常用)
console.log(str.concat(' is', ' awesome')); // 'JavaScript is awesome'

// match(配合正则)
console.log('abc123def'.match(/\d+/)); // ['123']

String重要相关概念

字符串不可变性(重要!)

字符串是不可变的,所有方法都返回新字符串,不会修改原字符串。

let str = 'hello';
str.toUpperCase(); // 返回 'HELLO'
console.log(str);  // 还是 'hello'(原串没变)

// 必须重新赋值
str = str.toUpperCase();
console.log(str); // 'HELLO'

字符串 vs 数组的转换

// 字符串 → 数组
const str = 'hello';
const arr = str.split(''); // ['h','e','l','l','o']

// 数组 → 字符串
const newStr = arr.join(''); // 'hello'

// 逆序字符串(面试常考)
const reversed = str.split('').reverse().join('');
console.log(reversed); // 'olleh'

提取文件扩展名

function getExtension(filename) {
  return filename.slice(filename.lastIndexOf('.') + 1);
}

console.log(getExtension('photo.jpg')); // 'jpg'
console.log(getExtension('archive.tar.gz')); // 'gz'

快速记忆表

场景用什么方法
找位置indexOf / includes
截取子串slice(推荐,支持负数)
拆成数组split
去空白trim
改大小写toUpperCase / toLowerCase
替换内容replace / replaceAll
补位数padStart / padEnd
重复repeat

5.4 Number

Number 是什么?

Number 是 JavaScript 的基本包装类型,用于表示整数和浮点数。JS 中的数字遵循 IEEE 754 双精度浮点数标准(64 位),这导致了某些 "奇怪" 的行为。

const num = 42;
console.log(typeof num); // 'number'
console.log(num.toFixed(2)); // '42.00'(自动装箱)

Number 常用方法分类

类型检查与转换

方法作用返回值
Number()强制转为数字数字或 NaN
parseInt()转为整数(可指定进制)整数或 NaN
parseFloat()转为浮点数浮点数或 NaN
isNaN()判断是否为 NaNtrue / false
isFinite()判断是否为有限数字true / false
Number.isInteger()判断是否为整数true / false
// 强制转换
console.log(Number('123'));     // 123
console.log(Number('123abc'));  // NaN(不能完全转换)
console.log(Number(true));      // 1
console.log(Number(null));      // 0
console.log(Number(undefined)); // NaN

// parseInt / parseFloat(更宽容)
console.log(parseInt('123abc'));   // 123(只解析数字部分)
console.log(parseInt('123.45'));   // 123
console.log(parseFloat('123.45')); // 123.45
console.log(parseInt('0x10'));     // 16(十六进制)
console.log(parseInt('10', 2));    // 2(指定二进制)

// 判断
console.log(isNaN(NaN));            // true
console.log(isNaN('abc'));          // true('abc' 转数字是 NaN)
console.log(Number.isNaN('abc'));   // false(更严格,不先转换)
console.log(Number.isInteger(42));  // true
console.log(Number.isInteger(42.0));// true
console.log(Number.isInteger(42.5));// false

// 特殊值
console.log(Number.MAX_VALUE);  // 1.7976931348623157e+308(最大数)
console.log(Number.MIN_VALUE);  // 5e-324(最小正数)
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991(安全整数上限)
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991

格式化输出(显示控制)

方法作用注意
toFixed(n)保留 n 位小数返回字符串,会四舍五入
toExponential(n)转为科学计数法返回字符串
toPrecision(n)保留 n 位有效数字返回字符串
toString(进制)转为指定进制字符串默认十进制
const num = 123.456789;

// toFixed:保留小数位数(最常用)
console.log(num.toFixed(2));   // '123.46'(四舍五入)
console.log(num.toFixed(0));   // '123'
console.log((123).toFixed(2)); // '123.00'(补零)

// 金额格式化常用
const price = 9.9;
console.log(`¥${price.toFixed(2)}`); // '¥9.90'

// toPrecision:保留有效数字
console.log(num.toPrecision(4)); // '123.5'
console.log(num.toPrecision(2)); // '1.2e+2'(不够位数用科学计数法)

// toString:进制转换
console.log((255).toString(16)); // 'ff'(转十六进制)
console.log((255).toString(2));  // '11111111'(转二进制)
console.log((255).toString(8));  // '377'(转八进制)

数学运算相关(配合 Math 对象)

虽然 Number 本身方法不多,但Math 对象提供了丰富的数学计算函数:

// Math 常用方法
console.log(Math.round(4.7));   // 5(四舍五入取整)
console.log(Math.floor(4.7));   // 4(向下取整)
console.log(Math.ceil(4.1));    // 5(向上取整)
console.log(Math.trunc(4.7));   // 4(去除小数部分,ES6)

console.log(Math.max(1, 3, 2)); // 3(最大值)
console.log(Math.min(1, 3, 2)); // 1(最小值)
console.log(Math.random());     // 0~1 之间的随机数

console.log(Math.pow(2, 3));    // 8(2 的 3 次方)
console.log(Math.sqrt(16));     // 4(平方根)
console.log(Math.abs(-5));      // 5(绝对值)

// 随机整数公式(牢记)
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(getRandomInt(1, 10)); // 1~10 随机整数

避坑指南

坑 1:浮点数精度问题(经典)

console.log(0.1 + 0.2);        // 0.30000000000000004 ❌
console.log(0.1 + 0.2 === 0.3); // false ❌

// 解决方案 1:使用 toFixed 后比较
console.log(Number((0.1 + 0.2).toFixed(10)) === 0.3); // true

// 解决方案 2:转为整数计算(金额常用)
function add(a, b) {
  const factor = Math.pow(10, 10);
  return (a * factor + b * factor) / factor;
}
console.log(add(0.1, 0.2)); // 0.3

// 解决方案 3:使用第三方库(如 decimal.js、big.js)

坑 2:isNaN vs Number.isNaN

// 全局 isNaN 会先转换再判断
console.log(isNaN('abc'));   // true('abc' → NaN)
console.log(isNaN(undefined)); // true
console.log(isNaN({}));      // true({} → NaN)

// Number.isNaN 更严格:只有值是 NaN 才返回 true
console.log(Number.isNaN('abc'));   // false
console.log(Number.isNaN(undefined)); // false
console.log(Number.isNaN(NaN));     // true

// ✅ 推荐使用 Number.isNaN

坑 3:parseInt 不指定进制的问题

// 旧版浏览器中,parseInt('08') 会被解析为八进制 → 0
// 现代浏览器已修复,但仍建议指定进制
console.log(parseInt('08'));      // 8
console.log(parseInt('08', 10));  // 8 ✅ 明确指定十进制

// 更好的做法:用 Number 或 + 运算符
console.log(Number('08'));   // 8
console.log(+'08');          // 8

坑 4:安全整数范围

// 超过安全范围会丢失精度
console.log(9007199254740992 + 1 === 9007199254740992); // true ❌

// 使用 BigInt 处理大数
const big1 = 9007199254740992n;
const big2 = 1n;
console.log(big1 + big2); // 9007199254740993n ✅

实战场景大全

场景 1:金额格式化(保留两位小数 + 千位分隔符)

function formatMoney(amount) {
  return '¥' + amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

console.log(formatMoney(1234567.89)); // '¥1,234,567.89'
console.log(formatMoney(9.9));        // '¥9.90'

场景 2:生成随机验证码(数字)

function generateCode(length = 6) {
  let code = '';
  for (let i = 0; i < length; i++) {
    code += Math.floor(Math.random() * 10);
  }
  return code;
}
console.log(generateCode()); // '847291'

场景 3:数字补零(日期格式化)

function padZero(num) {
  return String(num).padStart(2, '0');
}

const year = 2024;
const month = 8;
const day = 5;
console.log(`${year}-${padZero(month)}-${padZero(day)}`); // '2024-08-05'

场景 4:判断是否为数字(过滤 NaN)

function isNumeric(value) {
  return !isNaN(parseFloat(value)) && isFinite(value);
}

console.log(isNumeric('123'));   // true
console.log(isNumeric('12.3'));  // true
console.log(isNumeric('abc'));   // false
console.log(isNumeric(''));      // false
console.log(isNumeric(null));    // false

场景 5:百分比计算(防止除零)

function getPercentage(part, total) {
  if (total === 0) return '0%';
  return ((part / total) * 100).toFixed(2) + '%';
}

console.log(getPercentage(35, 200)); // '17.50%'
console.log(getPercentage(0, 200));  // '0.00%'
console.log(getPercentage(100, 0));  // '0%'(防止除以0)

Number 快速参考表

需求方法/方案
字符串 → 数字Number(str) 或 +str 或 parseInt(str, 10)
保留小数位数num.toFixed(n)(返回字符串)
判断是否为整数Number.isInteger(num)
判断是否为 NaNNumber.isNaN(num)(不用全局的)
随机数(0~1)Math.random()
随机整数(min~max)Math.floor(Math.random() * (max-min+1)) + min
取整Math.floor(向下)/ Math.ceil(向上)/ Math.round(四舍五入)/ Math.trunc(去尾)
最大/最小值Math.max() / Math.min()
转进制num.toString(16)(转十六进制)
精度问题用 toFixed 后比较,或转整数计算
大数字使用 BigInt(末尾加 n)

六,原型

6.1 什么是原型 (prototype)

原型本质上就是一个普通的对象。

在 JavaScript 中,每一个函数(除了箭头函数)都有一个默认的属性,叫做 prototype。

function Person(name) {
  this.name = name;
}

console.log(typeof Person.prototype); // 'object'
console.log(Person.prototype); 
// 打印出来:{ constructor: f Person(), __proto__: Object }

这个 prototype 对象用来干啥?
它是用来存放所有实例共享的属性和方法的公共空间。当你把方法挂在 Person.prototype 上时,所有 Person 的实例都能访问到它。

6.2 constructor 属性

既然 prototype 是一个对象,那这个对象里默认放着什么东西?

它默认自带一个非常重要的属性:constructor(构造器)。

function Person(name) {
  this.name = name;
}

console.log(Person.prototype.constructor === Person); // true

constructor 的作用:它指定了这个原型对象所属的构造函数。

有什么用? 当你只有一个实例对象,不知道它是谁创建的时候,可以通过 constructor 反推出来。

const p = new Person('张三');
console.log(p.constructor === Person); // true
// 注意:p本身没有constructor,它是顺着原型链找到的 Person.prototype.constructor

6.3 对象原型

这里有个关键区别:

  • prototype 是函数才有的属性。
  • __proto__(也叫隐式原型)是每一个实例对象都有的属性。

当你用 new 创建一个对象时,JS 会在幕后做一件事:
把实例的 __proto__ 指向构造函数的 prototype。

function Person(name) { this.name = name; }
const p = new Person('张三');

console.log(p.__proto__ === Person.prototype); // true

现代推荐写法:不建议直接使用 __proto__(非标准),推荐使用 Object.getPrototypeOf(p),但理解 __proto__ 对于理解链条至关重要。

6.4 原型继承

这是我们使用原型最核心的目的:让一个对象可以访问另一个对象的属性和方法。

JS 的继承不是像 Java 那样“复制”一份父类过来,而是通过原型链进行委托(Delegation)。

比如我们创建一个 Student 构造函数,想让它继承 Person 的 sayHi 方法:

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log('Hi ' + this.name);
};

function Student(name, grade) {
  Person.call(this, name); // 借用构造函数,继承实例属性
  this.grade = grade;
}

// 核心继承步骤:让 Student.prototype 的原型指向 Person.prototype
Student.prototype = Object.create(Person.prototype);
// 修复 constructor,因为上面一行把原来的 constructor 覆盖了
Student.prototype.constructor = Student; 

const stu = new Student('李四', 3);
stu.sayHi(); // 'Hi 李四' (成功继承到了)

6.5 原型链

当你访问 stu.sayHi 时,JS 引擎的查找顺序是:

  1. 先在 stu 自己身上找(实例属性) -> 没有
  2. 顺着 stu.__proto__ 找到 Student.prototype -> 没有
  3. 顺着 Student.prototype.__proto__ 找到 Person.prototype -> 找到了 sayHi
  4. 如果还没找到,继续往上到 Person.prototype.__proto__(即 Object.prototype)
  5. 如果 Object.prototype 还没有,则到 Object.prototype.__proto__(即 null),停止。

这条层层向上、直到 null 的查找链路,就是原型链。

console.log(stu.__proto__ === Student.prototype);                  // true
console.log(Student.prototype.__proto__ === Person.prototype);     // true
console.log(Person.prototype.__proto__ === Object.prototype);      // true
console.log(Object.prototype.__proto__);                           // null

6.6 ES6 语法糖

ES6 的 class 关键字,本质上就是基于上面这套原型体系的语法糖。

之前写的 Person.prototype.sayHi = ...,在 class 里直接写成方法即可:

// 以前的写法(基于 prototype)
function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() { console.log('Hi'); };

// ES6 Class 写法(底层依然是原型)
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {  // 这个方法会自动挂载到 Person.prototype 上
    console.log('Hi');
  }
}

// 继承也变得更优雅(extends + super)
class Student extends Person {
  constructor(name, grade) {
    super(name); // 相当于 Person.call(this, name)
    this.grade = grade;
  }
}

记住:class 并没有改变 JS 基于原型的本质,它只是让写继承的语法更清晰、更接近其他面向对象语言。

七,深浅拷贝

7.1 浅拷贝

定义:只拷贝对象的第一层属性,如果属性是基本类型,拷贝值;如果属性是引用类型,拷贝引用地址。

常见实现方式

const original = {
  name: '张三',
  age: 18,
  address: {
    city: '北京',
    district: '海淀'
  }
};

// 方法1:扩展运算符(最常用)
const copy1 = { ...original };

// 方法2:Object.assign()
const copy2 = Object.assign({}, original);

// 方法3:数组的 slice()
const arr = [1, 2, [3, 4]];
const copyArr = arr.slice();

浅拷贝的结果:

copy1.name = '李四';        // 修改第一层基本类型
console.log(original.name); // '张三'(没变,因为是独立副本)

copy1.address.city = '上海'; // 修改第二层引用类型的属性
console.log(original.address.city); // '上海'(变了!因为 address 指向同一个对象)

7.2 深拷贝

定义:递归拷贝所有层级,在堆内存中创建一份完全独立的对象。

方法 1:JSON 序列化(最常用,但有缺陷)

const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.address.city = '上海';
console.log(original.address.city); // '北京'(没变!完全独立)

缺陷

  • 无法拷贝 undefined、function、Symbol
  • 无法拷贝循环引用的对象(会报错)
  • 无法拷贝 Date、RegExp 等特殊对象(会变成字符串或空对象)
const obj = {
  func: function() { console.log('hi'); },
  date: new Date(),
  undef: undefined
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy); // { date: '2024-...' } (func 和 undef 丢了)

方法 2:递归手写深拷贝

function deepClone(obj, hash = new WeakMap()) {
  // 处理 null 和基本类型
  if (obj === null || typeof obj !== 'object') return obj;
  
  // 处理循环引用
  if (hash.has(obj)) return hash.get(obj);
  
  // 处理数组和对象
  const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);
  
  // 递归拷贝所有属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash);
    }
  }
  return clone;
}

方法 3:使用第三方库(生产环境推荐)

// lodash 的 cloneDeep
import _ from 'lodash';
const deepCopy = _.cloneDeep(original);

// 或者用 structuredClone(现代浏览器原生支持)
const deepCopy = structuredClone(original);

八,异常处理

JavaScript 的异常处理是保证程序健壮性的核心机制

8.1 核心语法

这是处理异常的主结构,无论是否发生异常,finally 块都会执行(常用于释放资源)。

try {
  // 可能抛出错误的代码
  let data = JSON.parse('{ invalid }');
} catch (error) {
  // 捕获并处理错误
  console.error('出错了:', error.message);
} finally {
  // 无论是否出错,都会执行(如关闭连接、清理状态)
  console.log('清理工作完成');
}

8.2 主动抛出异常

可以抛出任何值,但强烈建议抛出 Error 对象或其子类,这样能保留堆栈信息。

function divide(a, b) {
  if (b === 0) {
    // 抛出错误对象,包含自定义信息
    throw new Error('除数不能为0');
  }
  return a / b;
}

8.3 自定义错误类

通过继承 Error 来扩展特定业务异常,便于区分错误类型。

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field; // 自定义属性
  }
}

// 使用
try {
  throw new ValidationError('邮箱格式无效', 'email');
} catch (err) {
  if (err instanceof ValidationError) {
    console.log(`字段 ${err.field} 校验失败`);
  }
}

九,处理this

9.1 this指向

在绝大多数非箭头函数场景下,this 在函数被调用时确定,指向调用该函数的对象。

核心口诀:谁调用,指向谁(非箭头函数)

function show() {
  console.log(this.name);
}

const person = { name: '小明', show: show };
const admin = { name: '管理员', show: show };

person.show(); // 输出 '小明' (person 调用了 show)
admin.show();  // 输出 '管理员' (admin 调用了 show)

const standalone = show;
standalone();  // 浏览器输出 '' (window.name),Node 输出 undefined (全局对象没有 name)

四种绑定规则(由高到低优先级)

  1. new 绑定(最高)

    用 new 调用构造函数时,this 指向新创建的实例对象。

    function User(name) {
      this.name = name; // this 指向新对象
    }
    const u = new User('张三');
    console.log(u.name); // '张三'
    
  2. 显式绑定(call / apply / bind)

    手动强制指定 this 指向。

    function greet() {
      console.log(`Hello, ${this.name}`);
    }
    const user = { name: '李四' };
    greet.call(user);    // Hello, 李四 (立即执行)
    const bound = greet.bind(user);
    bound();             // Hello, 李四 (永久绑定)
    
  3. 隐式绑定(对象方法调用)

    即“谁调用指向谁”,如上面的 person.show()。

  4. 默认绑定(兜底)

    独立函数调用时,浏览器指向 window(严格模式下为 undefined),Node 指向 global(严格模式下为 undefined)。

最容易丢 this 的经典场景

  1. 回调函数中的 this 丢失

    const user = {
      name: '王五',
      getName: function() { console.log(this.name); }
    };
    
    // 错误:把方法当回调传递,丢失了调用对象
    setTimeout(user.getName, 1000); // 输出 undefined (或报错)
    
    // 修复方式1:用箭头函数包裹
    setTimeout(() => user.getName(), 1000);
    
    // 修复方式2:用 bind 永久绑定
    setTimeout(user.getName.bind(user), 1000);
    
  2. 事件监听器中的 this

    button.addEventListener('click', function() {
      console.log(this); // 指向 button 元素(符合“谁调用指向谁”)
    });
    
    button.addEventListener('click', () => {
      console.log(this); // 指向外层作用域(如 window),因为箭头函数不绑定 this
    });
    
  3. 类方法作为回调

    class Logger {
      constructor() { this.name = 'Logger'; }
      log() { console.log(this.name); }
    }
    
    const logger = new Logger();
    document.addEventListener('click', logger.log); // undefined (this 指向 document)
    document.addEventListener('click', logger.log.bind(logger)); // 修复
    

9.2 绑定this

可以这样理解:一个函数本来属于 A 对象,但你可以用这三个方法,强行让这个函数在 B 对象上执行。

核心区别

方法执行时机传参方式返回值
call立即执行参数逐个传递,如 fn.call(obj, a, b, c)函数的返回值
apply立即执行参数以数组形式传递,如 fn.apply(obj, [a, b, c])函数的返回值
bind返回新函数,稍后执行参数逐个传递,如 fn.bind(obj, a, b)绑定了 this 的新函数
  • call —— 立即调用,参数逐个给
```js
function introduce(age, city) {
  console.log(`我叫${this.name},${age}岁,来自${city}`);
}

const person = { name: '张三' };

// 让 introduce 函数中的 this 指向 person
introduce.call(person, 25, '杭州'); 
// 输出:我叫张三,25岁,来自杭州
```

**使用场景**:借用一个对象的方法,参数不多且明确。
  • apply —— 立即调用,参数打包成数组
和 `call` 完全一样,只是第二个参数必须是数组(或类数组)。
```js
const person = { name: '李四' };
const args = [30, '北京'];

introduce.apply(person, args);
// 输出:我叫李四,30岁,来自北京
```

**经典使用场景**:配合 Math 求数组最大/最小值

```js
const numbers = [3, 8, 1, 10];
// 原本 Math.max 不支持数组,用 apply 可以把数组拆成参数
const max = Math.max.apply(null, numbers); 
console.log(max); // 10

// 现在更推荐用展开运算符:Math.max(...numbers)
```
  • bind —— 返回新函数,不立即执行

    bind 不会调用原函数,而是创建一个新的函数,这个新函数的 this 被永久绑定到你指定的对象上。

    const person = { name: '王五' };
    
    // 创建一个新函数,this 永久指向 person
    const boundIntroduce = introduce.bind(person, 28, '深圳');
    
    // 稍后调用时,无需再传 this
    boundIntroduce(); // 我叫王五,28岁,来自深圳
    
    // 甚至可以继续传参(bind 的参数会合并到调用参数前面)
    boundIntroduce('广州'); // 注意:28 被固定了,后续传的会覆盖 city?
    // 实际输出:我叫王五,28岁,来自广州(bind 的参数优先)
    

    经典使用场景:React 类组件中绑定事件处理函数

    class Button extends React.Component {
      constructor() {
        super();
        // 确保 handleClick 中的 this 永远指向组件实例
        this.handleClick = this.handleClick.bind(this);
      }
      handleClick() {
        console.log(this.state);
      }
    }
    

9.3 箭头函数绑定this

箭头函数没有自己的 this,它会捕获定义时外层作用域的 this,且之后无法改变(call/apply/bind 无效)。

const obj = {
  name: '箭头示例',
  // 箭头函数中的 this 指向定义时的外层(此处为全局)
  greet: () => console.log(this.name) 
};
obj.greet(); // 浏览器输出 '' (window.name),不是 '箭头示例'

// 典型正确用法:在对象方法内部返回箭头函数
const obj2 = {
  name: '正确用法',
  getName: function() {
    // 箭头函数捕获了外层 getName 的 this (即 obj2)
    return () => console.log(this.name);
  }
};
obj2.getName()(); // 输出 '正确用法'

十,性能优化

10.1 防抖

核心思想是:在事件连续触发时,只在最后一次触发后的等待时间结束后执行一次。

基础实现(最简版)


function debounce(fn, delay) {
  let timer = null;
  
  return function(...args) {
    // 每次触发都清除之前的定时器
    clearTimeout(timer);
    
    // 重新设置定时器
    timer = setTimeout(() => {
      fn.apply(this, args); // 注意这里的 this 和参数传递
    }, delay);
  };
}

使用示例:

// 原始的搜索函数
function search(keyword) {
  console.log(`搜索: ${keyword}`);
}

// 创建防抖版本(延迟 500ms)
const debouncedSearch = debounce(search, 500);

// 连续输入时,只有停止输入 500ms 后才会执行
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc'); // 最终只会执行 'abc' 的搜索

10.2 节流

节流的核心思想:在事件连续触发时,按固定时间间隔执行函数,保证一定时间内只执行一次。就像水龙头的水流被限制,无论你开多大,水都是一滴一滴地流。

基础实现(时间戳版)

function throttle(fn, interval) {
  let lastTime = 0;
  
  return function(...args) {
    const now = Date.now();
    const context = this;
    
    // 如果距离上次执行超过间隔时间,立即执行
    if (now - lastTime >= interval) {
      fn.apply(context, args);
      lastTime = now;
    }
  };
}

使用示例:

function handleScroll() {
  console.log('滚动位置:', window.scrollY);
}

// 每 200ms 最多执行一次
const throttledScroll = throttle(handleScroll, 200);
window.addEventListener('scroll', throttledScroll);

效果:无论滚动多快,handleScroll 每 200ms 执行一次。

正则表达式 2026-06-09
JS异步编程 2026-08-21

© 2026 日志记录