日期对象

日期对象用于处理日期和时间。

在 JavaScript 中,日期时间处理主要通过 Date 对象实现,它用于表示和操作时间。

Date 对象属性

属性 描述
constructor 返回对创建此对象的 Date 函数的引用。
prototype 原型链,所以我们可以为日期对象对象添加属性和方法。

但是由于原生 Date 方法在格式化、时区处理等场景不够便捷,所以我一般使用Day.js

创建 Date 对象

Date 是一个构造函数,需通过 new 关键字创建实例,常见创建方式有 4 种:

  • 无参数

    创建当前时间的 Date 对象(基于系统本地时间):

    1
    2
    const now = new Date();
    console.log(now); // 例如:2025-11-14T08:30:00.123Z(UTC 时间)
  • 时间戳参数

    milliseconds 参数是一个 Unix 时间戳(Unix Time Stamp),它是一个整数值,表示自 1970 年 1 月 1 日 00:00:00 UTC(the Unix epoch)以来的毫秒数。

    1
    2
    const timestamp = 1760409600000; // 对应 2025-11-14 00:00:00 UTC
    const date = new Date(timestamp);
  • 日期字符串参数

    传入符合规范的日期字符串(如 ISO 8601 格式 YYYY-MM-DDTHH:mm:ss.sssZ):

    1
    2
    3
    const date1 = new Date('2025-11-14'); // 默认为当天 00:00:00(本地时区)
    const date2 = new Date('2025-11-14T12:30:00'); // 带时分秒
    const date3 = new Date('2025-11-14T12:30:00+08:00'); // 带时区偏移(东八区)
  • 年月日等参数

    传入 年, 月, 日, 时, 分, 秒, 毫秒(注意月份从 0 开始,0 代表 1 月,11 代表 12 月):

    其中它使用当前时区中的给定组件创建日期。只有前两个参数是必须的。

    1
    const date = new Date(2025, 10, 14, 15, 30, 0); // 2025年11月14日15:30:00

注意:

  1. 月份从 0 开始:这是最容易出错的点,getMonth() 返回 0-11,对应 1-12 月。
  2. 时区问题Date 对象内部存储的是 UTC 时间,但 getXxx() 方法返回本地时区的结果,getUTCXxx() 返回 UTC 时间。
  3. 不可变性误区Date 对象是可变的,设置时间的方法(如 setHours)会直接修改原对象,而非返回新对象。
  4. 字符串解析差异:不同浏览器对非标准日期字符串的解析可能不一致,建议使用 ISO 格式或参数列表创建日期。

获取日期时间的方法

Date 对象提供了一系列方法获取具体的时间信息,分为本地时间(基于系统时区)和UTC 时间(世界协调时间)两类:

本地时间方法 UTC 时间方法 说明(返回值范围)
getFullYear() getUTCFullYear() 获取年份(4 位数字,如 2025)
getMonth() getUTCMonth() 获取月份(0-11,0 代表 1 月)
getDate() getUTCDate() 获取日期(1-31)
getDay() getUTCDay() 获取星期(0-6,0 代表周日,6 代表周六)
getHours() getUTCHours() 获取小时(0-23)
getMinutes() getUTCMinutes() 获取分钟(0-59)
getSeconds() getUTCSeconds() 获取秒(0-59)
getMilliseconds() getUTCMilliseconds() 获取毫秒(0-999)
getTime() - 获取时间戳(毫秒级,UTC)
getTimezoneOffset() - 获取本地时区与 UTC 的偏移分钟数(如东八区返回 -480)
1
2
3
4
5
6
const date = new Date(2025, 10, 14, 15, 30, 0);
console.log(date.getFullYear()); // 2025
console.log(date.getMonth()); // 10(即11月)
console.log(date.getDate()); // 14
console.log(date.getHours()); // 15
console.log(date.getTime()); // 对应的毫秒时间戳

注意:

  • 获取年份一般都是getFullYear(),当然,很多 JavaScript 引擎都实现了一个非标准化的方法 getYear()。不推荐使用这个方法。它有时候可能会返回 2 位的年份信息。永远不要使用它。要获取年份就使用 getFullYear()
  • getDay()是获取一周中的第几天,从 0(星期日)到 6(星期六)。第一天始终是星期日,在cn不是这样的习惯,但是这不能被改变。
  • 当然,也有与当地时区的 UTC 对应项,它们会返回基于 UTC+0 时区的日、月、年等:getUTCFullYear()getUTCMonth()getUTCDay()。只需要在 "get" 之后插入 "UTC" 即可。除了上述给定的方法,还有两个没有 UTC 变体的特殊方法

设置日期时间的方法

通过以下方法修改 Date 对象的时间(同样分本地时间和 UTC 时间):

本地时间方法 UTC 时间方法 说明
setFullYear(year) setUTCFullYear(year) 设置年份
setMonth(month) setUTCMonth(month) 设置月份(0-11)
setDate(day) setUTCDate(day) 设置日期(1-31)
setHours(hour) setUTCHours(hour) 设置小时(0-23)
setMinutes(min) setUTCMinutes(min) 设置分钟(0-59)
setSeconds(sec) setUTCSeconds(sec) 设置秒(0-59)
setMilliseconds(ms) setUTCMilliseconds(ms) 设置毫秒(0-999)
setTime(timestamp) - 通过时间戳设置时间
1
2
3
4
5
const date = new Date();
date.setFullYear(2026); // 设置年份为2026
date.setMonth(0); // 设置为1月
date.setDate(1); // 设置为1日
console.log(date); // 2026-01-01T...

以上方法除了 setTime() 都有 UTC 变体,例如:setUTCHours()

日期格式化方法

Date 对象提供了几个将日期转换为字符串的方法:

  1. toString():返回本地时间的完整字符串表示(如 Fri Nov 14 2025 15:30:00 GMT+0800 (中国标准时间))。
  2. toUTCString():返回 UTC 时间的字符串表示(如 Fri, 14 Nov 2025 07:30:00 GMT)。
  3. toISOString():返回 ISO 8601 格式的字符串(UTC 时间,如 2025-11-14T07:30:00.000Z),常用于接口传输。
  4. toLocaleString():返回本地时区的本地化字符串(如 2025/11/14 15:30:00,格式因地区而异)。
  5. toLocaleDateString():仅返回本地化的日期部分(如 2025/11/14)。
  6. toLocaleTimeString():仅返回本地化的时间部分(如 15:30:00)。

自动校准

自动校准Date 对象的一个非常方便的特性。我们可以设置超范围的数值,它会自动校准

1
2
let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!?
alert(date); // ……是 1st Feb 2013!

超出范围的日期组件将会被自动分配。

假设我们要在日期 “28 Feb 2016” 上加 2 天。结果可能是 “2 Mar” 或 “1 Mar”,因为存在闰年。但是我们不需要考虑这些,只需要直接加 2 天,剩下的 Date 对象会帮我们处理:

1
2
3
4
let date = new Date(2016, 1, 28);
date.setDate(date.getDate() + 2);

alert( date ); // 1 Mar 2016

这个特性经常被用来获取给定时间段后的日期。例如,我们想获取“现在 70 秒后”的日期:

1
2
3
4
let date = new Date();
date.setSeconds(date.getSeconds() + 70);

alert( date ); // 显示正确的日期信息

我们还可以设置 0 甚至可以设置负值。例如:

1
2
3
4
5
6
7
let date = new Date(2016, 0, 2); // 2016 年 1 月 2 日

date.setDate(1); // 设置为当月的第一天
alert( date );

date.setDate(0); // 天数最小可以设置为 1,所以这里设置的是上一月的最后一天
alert( date ); // 31 Dec 2015

日期转化为数字,日期差值

JavaScript 中,Date 对象在参与数值运算(如加法、减法)或被强制类型转换时,会自动转换为对应的毫秒级时间戳(等同于 date.getTime() 的结果)。

Date 对象被转化为数字时,得到的是对应的时间戳,与使用 date.getTime() 的结果相同

可以使用一元加号 + 转换

这是最简洁的方式,本质是触发 Date 对象的 valueOf() 方法(该方法返回时间戳):

1
2
3
const date = new Date(2025, 10, 14);
console.log(+date); // 1760409600000(对应 2025-11-14 00:00:00 本地时间的时间戳)
console.log(date.getTime() === +date); // true(完全等价)

有一个重要的副作用:日期可以相减,相减的结果是以毫秒为单位时间差。

这个作用可以用于时间测量:

1
2
3
4
5
6
7
8
9
10
let start = new Date(); // 开始测量时间

// do the job
for (let i = 0; i < 100000; i++) {
let doSomething = i * i * i;
}

let end = new Date(); // 结束测量时间

alert( `The loop took ${end - start} ms` );

如果我们仅仅想要测量时间间隔,我们不需要 Date 对象。

有一个特殊的静态方法 Date.now(),它会返回当前的时间戳。

它相当于 new Date().getTime(),但它不会创建中间的 Date 对象。因此它更快,而且不会对垃圾回收造成额外的压力。

这种方法很多时候因为方便,又或是因性能方面的考虑而被采用

1
2
3
4
5
6
7
8
9
10
let start = Date.now(); // 从 1 Jan 1970 至今的时间戳

// do the job
for (let i = 0; i < 100000; i++) {
let doSomething = i * i * i;
}

let end = Date.now(); // 完成

alert( `The loop took ${end - start} ms` ); // 相减的是时间戳,而不是日期

Date.parse() 是另一个静态方法,用于将日期字符串解析为对应的毫秒级时间戳。如果解析失败,返回 NaN

支持的字符串格式:ISO 8601 标准格式

YYYY-MM-DDYYYY-MM-DDTHH:mm:ssYYYY-MM-DDTHH:mm:ss.sssZ(带时区)等。

1
2
3
Date.parse('2025-11-14'); // 1760409600000(本地时区的 2025-11-14 00:00:00 时间戳)
Date.parse('2025-11-14T12:30:00'); // 1760453400000(本地时区的 12:30)
Date.parse('2025-11-14T12:30:00+08:00'); // 1760418600000(东八区 12:30 对应 UTC 04:30)

函数类型

在JavaScript中,函数(Function)是个重要的内容,它在开发中会经常被用到。这几年函数式编程越来越火,如React,在React中,一个组件就是一个函数,React许多Hook也是一个函数。

因为我们经常需要在脚本的许多地方执行很相似的操作。

函数是程序的主要“构建模块”。函数使该段代码可以被调用很多次,而不需要写重复的代码。

函数基础

函数声明

使用 函数声明 创建函数。

1
2
3
function showMessage() {
alert( 'Hello everyone!' );
}

function 关键字首先出现,然后是 函数名,然后是括号之间的 参数 列表,最后是花括号之间的代码(即“函数体”)。

所以,js 你会看到通篇内容都长一个样,使用 function 关键字声明的函数具有函数提升特性(可在定义前调用)。

1
2
3
4
function 函数名(参数1, 参数2, ...) {
// 函数体
return 返回值; // 可选,无return则返回undefined
}

函数也是一种类型,所以我们也可以通过 new Function() 动态创建函数,参数为字符串形式(知道就够,极少使用,性能差且不利于调试,通篇这玩意能给我看似)。

1
2
const subtract = new Function('a', 'b', 'return a - b');
console.log(subtract(5, 2)); // 3

局部和外部变量

和其他语言一样,在函数中声明的变量只在该函数内部可见,是局部变量

函数也可以访问外部变量,这是外部变量,而且函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。

1
2
3
4
5
6
7
8
9
function showMessage() {
let message = "Hello, I'm JavaScript!"; // 局部变量

alert( message );
}

showMessage(); // Hello, I'm JavaScript!

alert( message ); // <-- 错误!变量是函数的局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let userName = 'John';

function showMessage() {
userName = "Bob"; // (1) 改变外部变量

let message = 'Hello, ' + userName;
alert(message);
}

alert( userName ); // John 在函数调用之前

showMessage();

alert( userName ); // Bob,值被函数修改了

如果在函数内部声明了同名变量,那么函数会 遮蔽 外部变量。例如,在下面的代码中,函数使用局部的 userName,而外部变量被忽略:

1
2
3
4
5
6
7
8
9
10
11
12
13
let userName = 'John';

function showMessage() {
let userName = "Bob"; // 声明一个局部变量

let message = 'Hello, ' + userName; // Bob
alert(message);
}

// 函数会创建并使用它自己的 userName
showMessage();

alert( userName ); // John,未被更改,函数没有访问外部变量。

任何函数之外声明的变量,例如上述代码中的外部变量 userName,都被称为 全局 变量。全局变量在任意函数中都是可见的,除非被遮蔽,这也和其他语言一样。

参数和返回值

我们可以通过参数将任意数据传递给函数

函数定义时的参数为 “形参”,调用时传入的为 “实参”。

和其他语言不同的是,JS 允许实参数量与形参不同,多余参数会被忽略,不足则形参为 undefined。也就是说,如果一个函数被调用,但有参数(argument)未被提供,那么相应的值就会变成 undefined

我们可以使用 = 为函数声明中的参数指定所谓的“默认”(如果对应参数的值未被传递则使用)值(ES6+)

1
2
3
4
function greet(name = "Guest") {
return `Hello, ${name}`;
}
console.log(greet()); // "Hello, Guest"
  • 注意,和 Java 一样,在 JavaScript 中,出现如果函数在没带个别参数的情况下被调用的情况,默认参数会被计算出来。

    • 当函数定义了默认参数时,默认参数的 “计算过程” 并不是在函数定义时执行,而是在函数被调用的时刻,并且只有在该参数未被传入(或传入 undefined 时,才会触发计算并使用默认值。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function greet(name = "Guest") {
      console.log(`Hello, ${name}`);
      }

      // 1. 未传入参数时,默认值 "Guest" 被使用
      greet(); // 输出:Hello, Guest(此时才计算并使用默认值)

      // 2. 传入参数时,默认值不生效
      greet("Alice"); // 输出:Hello, Alice(默认值未被计算)
    • 如果默认参数是一个表达式(比如函数调用、运算等),这个表达式的计算也会延迟到函数调用时,且仅在参数缺失时执行:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // 定义一个返回当前时间的函数(作为默认参数的表达式)
      function getCurrentTime() {
      console.log("计算默认时间...");
      return new Date().toLocaleTimeString();
      }

      // 函数默认参数使用 getCurrentTime() 的返回值
      function logActivity(time = getCurrentTime()) {
      console.log(`活动时间:${time}`);
      }

      // 1. 未传入参数时,默认参数的表达式会被计算(调用 getCurrentTime())
      logActivity();
      // 输出:
      // 计算默认时间...
      // 活动时间:15:30:00(假设当前时间)

      // 2. 传入参数时,默认参数的表达式不会执行
      logActivity("10:00:00");
      // 输出:活动时间:10:00:00(没有 "计算默认时间..." 的输出)

还可通过 剩余参数...)接收不定数量的参数(返回数组):

1
2
3
4
function sumAll(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
console.log(sumAll(1, 2, 3)); // 6

函数的返回值也通过 return 语句返回,若未写 returnreturn 后无值,默认返回 undefinedreturn 会立即终止函数执行。

只使用 return 但没有返回值也是可行的。但这会导致函数立即退出。

空值的 return 或没有 return 的函数返回值为 undefined

如果函数无返回值,它就会像返回 undefined 一样:

1
2
3
function doNothing() { /* 没有代码 */ }

alert( doNothing() === undefined ); // true

空值的 returnreturn undefined 等效:

1
2
3
4
5
function doNothing() {
return;
}

alert( doNothing() === undefined ); // true

闭包

和 Python 一样,JS函数嵌套时,内层函数引用外层函数的变量,且内层函数被外部引用,导致外层变量不被销毁。

这样就能实现封装私有变量、实现函数记忆等。

1
2
3
4
5
6
7
8
9
10
function createCounter() {
let count = 0; // 外层变量
return function() {
count++; // 内层函数引用外层变量
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2(count 未被销毁)

在 JavaScript 中,所有函数都是天生闭包的,只有一个例外,new Function创建的,它们会通过隐藏的[[Environment]]属性,记住自己创建时的词法环境,即便在创建环境外执行,也能访问该环境中的外部变量。

[[Environment]]属性其值就是 “当前所在的词法环境”,即创建函数时,周围的变量环境

闭包的本质就是函数通过[[Environment]]“抓住” 了自己的 “诞生环境”,即便后来跑到其他环境执行,也能顺着[[Environment]]找到诞生环境中的变量 —— 这就是 “记住外部变量并访问” 的能力。

闭包不是 “没用的理论”,实际开发中经常用,这是模块化的基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createModule() {
// 私有变量(外部无法直接访问)
let privateVar = "我是私有变量";

// 暴露公共方法(通过闭包访问私有变量)
return {
getPrivateVar: () => privateVar,
setPrivateVar: (val) => privateVar = val
};
}

const module = createModule();
console.log(module.getPrivateVar()); // 访问私有变量
module.setPrivateVar("修改后的私有变量");
console.log(module.getPrivateVar()); // 修改成功
console.log(module.privateVar); // undefined(无法直接访问,实现封装)

this 关键字

this 指向函数的调用者,具体指向取决于调用方式(箭头函数无自己的 this,继承外层作用域的 this):

  • 普通函数直接调用:this 指向全局对象(浏览器中为 window,Node.js 中为 global;严格模式下为 undefined)。

  • 作为对象方法调用:this 指向该对象。

    1
    2
    3
    4
    5
    6
    7
    const obj = {
    name: "Alice",
    sayHi() {
    console.log(`Hi, ${this.name}`); // this 指向 obj
    }
    };
    obj.sayHi(); // "Hi, Alice"
  • 构造函数调用(new):this 指向新创建的实例。

  • 通过 call/apply/bind 改变 this 指向:

    1
    2
    3
    4
    5
    6
    7
    8
    function introduce() {
    console.log(`I'm ${this.name}`);
    }
    const person = { name: "Bob" };
    introduce.call(person); // "I'm Bob"(call 直接调用,参数逐个传入)
    introduce.apply(person); // 同上,参数以数组传入
    const boundFn = introduce.bind(person); // 返回绑定后的函数,需手动调用
    boundFn(); // "I'm Bob"

函数命名

JS 中我们有这样的一个习惯,就是用动词前缀来开始一个函数,这个前缀模糊地描述了这个行为。

JS的函数通常是行为的封装,所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能清楚地知道这个函数的功能,使得 JS 不会通篇让人头大

而且一个函数一般只对应一个行为,一个函数应该只包含函数名所指定的功能,而不是做更多与函数名无关的功能。

两个独立的行为通常需要两个函数,即使它们通常被一起调用(在这种情况下,我们可以创建第三个函数来调用这两个函数)。

常用的函数有时会有非常短的名字。

例如,jQuery 框架用 $ 定义一个函数。LoDash 库的核心函数用 _ 命名。

这些都是例外,一般而言,函数名应简明扼要且具有描述性。

那么,函数应该简短且只有一个功能。如果这个函数功能复杂,那么把该函数拆分成几个小的函数是值得的。有时候遵循这个规则并不是那么容易,但这绝对是件好事。

一个单独的函数不仅更容易测试和调试 —— 它的存在本身就是一个很好的注释

把 JS 函数变得想注释一样容易阅读

函数表达式

首先要注意,在 JavaScript 中,函数不是“神奇的语言结构”,而是一种特殊的值。

所以我们可以用 alert 显示这个变量的值:这个值是什么呢?就是它本身,即函数的源码的字符串值。

1
2
3
4
5
function sayHi() {
alert( "Hello" );
}

alert( sayHi ); // 显示函数代码

但它依然是一个值,所以我们可以像使用其他类型的值一样使用它。

我们可以复制函数到其他变量:

1
2
3
4
5
6
7
8
function sayHi() {   // (1) 创建
alert( "Hello" );
}

let func = sayHi; // (2) 复制

func(); // Hello // (3) 运行复制的值(正常运行)!
sayHi(); // Hello // 这里也能运行(为什么不行呢)

函数表达式允许我们在任何表达式的中间创建一个新函数。它将函数赋值给变量,无函数提升(必须先定义后调用)。

1
2
3
const 变量名 = function(参数1, 参数2, ...) {
// 函数体
};
1
2
3
4
5
6
// 函数表达式(匿名函数赋值给变量)
const multiply = function(a, b) {
return a * b;
};

console.log(multiply(2, 3)); // 6

若给函数表达式命名(具名函数表达式),那么它的名称仅在函数内部可见

1
2
3
4
const factorial = function fn(n) {
if (n <= 1) return 1;
return n * fn(n - 1); // 内部可通过fn调用自身
};

函数的词法环境

函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。一旦代码执行到赋值表达式 const factorial = function.... 的右侧,此时就会开始创建该函数,并且可以从现在开始使用(分配,调用等)。

而函数声明则不同。它在函数声明被定义之前,它就可以被调用。所以一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。

这是内部算法的缘故。当 JavaScript 准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。

严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let age = prompt("What is your age?", 18);

// 有条件地声明一个函数
if (age < 18) {

function welcome() {
alert("Hello!");
}

} else {

function welcome() {
alert("Greetings!");
}

}

// ……稍后使用
welcome(); // Error: welcome is not defined

就这样,这是因为函数声明只在它所在的代码块中可见。、

但是我们怎么才能让 welcomeif 外可见呢?

正确的做法是使用函数表达式,并将 welcome 赋值给在 if 外声明的变量,并具有正确的可见性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let age = prompt("What is your age?", 18);

let welcome;

if (age < 18) {

welcome = function() {
alert("Hello!");
};

} else {

welcome = function() {
alert("Greetings!");
};

}

welcome(); // 现在可以了

那么这涉及到词法环境的内容,它是 JS 引擎在函数创建和执行时自动管理的 “变量存储容器”。

  • 词法环境的结构:每个词法环境包含两部分

    1. 环境记录(Environment Record):存储当前作用域的变量、函数声明(比如let a = 1function fn() {})。
    2. 外部引用(Outer Reference):指向 “父级词法环境”(比如函数内部的环境,外部引用指向函数所在的全局 / 外层函数环境)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 1. 全局词法环境创建:存储a=10,外部引用null
    let a = 10;

    function outer() {
    // 2. outer创建时,记住全局词法环境(通过[[Environment]])
    let b = 20; // outer的词法环境:存储b=20,外部引用指向全局环境

    function inner() {
    // 3. inner创建时,记住outer的词法环境(通过[[Environment]])
    console.log(a + b); // 访问a(全局环境)、b(outer环境)
    }
    return inner; // 把inner函数返回出去
    }

    // 4. 调用outer,得到inner(此时outer的执行已经结束,其词法环境本应销毁?)
    const innerFunc = outer();

    // 5. 在全局环境中调用innerFunc(创建时的环境是outer内部)
    innerFunc(); // 输出30(居然能访问到已经“结束”的outer中的b!)

有关于这个内容其实就是JS的内外部词法环境的相关内容,在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。

image-20251114204538861

在这个函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局):

  • 内部词法环境与 say 的当前执行相对应。它具有一个单独的属性:name,函数的参数。我们调用的是 say("John"),所以 name 的值为 "John"
  • 外部词法环境是全局词法环境。它具有 phrase 变量和函数本身。

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。

如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。

回调函数

回调函数(Callback Function)指一个被作为参数传递给另一个函数(称为 “主函数”)的函数,当主函数执行到特定阶段或满足特定条件时,会 “回过来调用” 这个函数。

简单来说:你定义一个函数 A,把 A 传给函数 B,让 B 在需要的时候执行 A,A 就是回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义回调函数:打印消息
function callback(message) {
console.log("回调执行:", message);
}

// 定义主函数:接收一个函数作为参数,并在内部调用它
function mainFunction(callbackFn, text) {
console.log("主函数开始执行");
// 主函数执行到某个阶段时,调用回调函数
callbackFn(text);
console.log("主函数执行结束");
}

// 调用主函数,传入回调函数
mainFunction(callback, "Hello, Callback!");

// 输出顺序:
// 主函数开始执行
// 回调执行: Hello, Callback!
// 主函数执行结束

JS 中大量异步操作(如定时器、网络请求、文件读写)需要通过回调函数来处理结果。因为异步操作不会立即完成,无法直接用返回值获取结果,只能通过回调在操作完成后触发处理逻辑。

示例:定时器中的回调

1
2
3
4
// 1秒后执行回调函数
setTimeout(function() {
console.log("1秒后执行的回调");
}, 1000);

而且在浏览器中,事件(如点击、输入、加载)的响应逻辑本质上都是回调函数。当事件触发时,浏览器会调用我们预先定义的回调函数。

回调函数可分为两类:

  • 同步回调

    回调函数在主函数执行过程中立即同步执行,不会涉及异步操作。

    示例:Array.prototype.map 的回调(同步执行)

    1
    2
    3
    4
    5
    6
    const numbers = [1, 2, 3];
    // map 的回调会同步遍历每个元素
    const doubled = numbers.map(function(num) {
    return num * 2;
    });
    console.log(doubled); // [2,4,6](同步完成)
  • 异步回调

    回调函数不会立即执行,而是在主函数触发异步操作(如定时器、网络请求)完成后才执行,执行时机不确定。

    示例:setTimeout 或网络请求的回调(异步执行)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    console.log("开始");
    setTimeout(() => {
    console.log("异步回调执行"); // 1秒后执行
    }, 1000);
    console.log("结束");

    // 输出顺序:
    // 开始
    // 结束
    // (1秒后)异步回调执行

回调地狱(Callback Hell)

对没错这个词貌似是 JS 这边发明的

也就是当多个异步操作需要按顺序执行时(如 “先请求 A 数据,再用 A 的结果请求 B 数据,再用 B 的结果请求 C 数据”),会导致回调函数嵌套层级过深,代码可读性和可维护性变差,这种现象称为 “回调地狱”。

1
2
3
4
5
6
7
8
9
10
11
// 模拟三个依赖的异步请求
fetchDataA(function(dataA) {
console.log("获取到A数据:", dataA);
fetchDataB(dataA.id, function(dataB) {
console.log("获取到B数据:", dataB);
fetchDataC(dataB.id, function(dataC) {
console.log("获取到C数据:", dataC);
// ... 更多嵌套
});
});
});

解决这个就可以祭出神秘的 Promise 异步编程 了

对了,普通回调函数中的 this 指向可能不符合预期(如在对象方法中作为回调传递时,this 可能指向全局或 undefined)。所以用箭头函数(继承外层 this)或 bind 绑定 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {
name: "Alice",
sayHi: function() {
console.log(`Hi, ${this.name}`);
}
};

// 直接传递 obj.sayHi 作为回调,this 会指向全局(浏览器中为 window)
setTimeout(obj.sayHi, 1000); // 输出:Hi, undefined

// 用 bind 绑定 this 到 obj
setTimeout(obj.sayHi.bind(obj), 1000); // 输出:Hi, Alice

// 用箭头函数(继承外层 this,即 obj 所在的作用域)
setTimeout(() => obj.sayHi(), 1000); // 输出:Hi, Alice

Rest 参数

在 JavaScript 中,无论函数是如何定义的,你都可以在调用它时传入任意数量的参数。

1
2
3
4
5
function sum(a, b) {
return a + b;
}

alert( sum(1, 2, 3, 4, 5) );

虽然这里这个函数不会因为传入过多的参数而报错。但是,当然,只有前两个参数被求和了。

我们可以在函数定义中声明一个数组来收集参数,它是这样写的,...变量名,一般写作...rest或者...args,我倾向于后者

这三个点的语义就是“收集剩余的参数并存进指定数组中”。

注意,Rest 参数会收集剩余的所有参数,因此下面这种用法没有意义,并且会导致错误:

1
2
3
function f(arg1, ...rest, arg2) { // arg2 在 ...rest 后面?!
// error
}

...rest 必须写在参数列表最后。

而且有一个名为 arguments 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );

// 它是可遍历的
// for(let arg of arguments) alert(arg);
}

// 依次显示:2,Julius,Caesar
showName("Julius", "Caesar");

// 依次显示:1,Ilya,undefined(没有第二个参数)
showName("Ilya");

尽管 arguments 是一个类数组,也是可迭代对象,但它终究不是数组。它不支持数组方法,因此我们不能调用 arguments.map(...) 等方法。

此外,它始终包含所有参数,我们不能像使用 rest 参数那样只截取参数的一部分。

想起来,箭头函数没有自身的 this,它们也没有特殊的 arguments 对象。

Spread 语法

Spread 语法用...表示,能 “展开” 可迭代对象(数组、字符串、Set 等)或对象的可枚举属性,将其拆分为独立元素 / 键值对,常用于复制、合并、传参等场景。

所以说 Spread 语法有适用对象,他不是谁都能用,你直接直接展开非可迭代对象(如 null、undefined、数字、布尔值)会报错。

  • 可迭代对象(Iterable):数组、字符串、Set、Map、arguments 对象、NodeList 等。
  • 普通对象:ES2018 后支持,可展开其可枚举属性(原型链上的属性不会展开)。

最常用的就是和 rest参数 相反的事情,展开数组,用于复制数组、合并数组、向数组插入元素,避免修改原数组。

  • 复制数组(浅拷贝)

    1
    2
    3
    4
    5
    const arr1 = [1, 2, 3];
    const arr2 = [...arr1]; // 展开arr1,生成新数组
    console.log(arr2); // [1,2,3]
    arr2.push(4);
    console.log(arr1); // [1,2,3](原数组不受影响)
  • 向数组中间插入元素

    1
    2
    3
    const arr = [1, 4];
    const newArr = [1, ...arr, 5]; // 在arr前后插入元素
    console.log(newArr); // [1,1,4,5]
  • 合并对象(同名属性覆盖)

    1
    2
    3
    4
    const objA = { name: "张三", age: 20 };
    const objB = { age: 21, gender: "男" };
    const mergedObj = { ...objA, ...objB }; // 后面的对象覆盖前面的同名属性
    console.log(mergedObj); // { name: "张三", age: 21, gender: "男" }
  • 合并时添加 / 修改属性

    1
    2
    3
    const obj = { name: "张三" };
    const newObj = { ...obj, age: 20, name: "李四" };
    console.log(newObj); // { name: "李四", age: 20 }(修改name,新增age)

当函数需要接收多个独立参数时,可直接展开数组 / 可迭代对象,替代Function.apply()

1
2
3
4
// 求数组最大值(Math.max需要多个独立参数,不能直接传数组)
const nums = [1, 5, 3, 9];
const max = Math.max(...nums); // 等价于Math.max(1,5,3,9)
console.log(max); // 9

多参数 + 展开可以这么写

1
2
3
4
5
6
function sum(a, b, c) {
return a + b + c;
}
const arr = [1, 2];
const total = sum(...arr, 3); // 等价于sum(1,2,3)
console.log(total); // 6

除了数组和对象,字符串、Set、Map 等可迭代对象也能展开,不演示了。

注意,上述的操作都是浅拷贝,只拷贝对象 / 数组的 “表层”,如果内部有嵌套对象 / 数组,拷贝的是引用(修改嵌套内容会影响原对象)。

很多人会把 Spread 语法和 Rest 参数搞混,核心区别:

  • Spread 语法(…):用于 “展开”,把可迭代对象 / 对象拆分为独立元素 / 属性。
  • Rest 参数(…):用于 “收集”,把多个独立参数收集为数组。

箭头函数

创建函数还有另外一种非常简单的语法,并且这种方法通常比函数表达式更好。

它极大简化函数表达式的语法,无自己的 this、arguments,不能作为构造函数。

1
2
3
4
5
6
7
// 基本形式:参数 => 函数体
const 函数名 = (参数1, 参数2, ...) => {
// 函数体
};

// 单参数可省略括号,单语句返回可省略大括号和return
const double = x => x * 2;
1
2
3
4
5
6
7
8
const sum = (a, b) => a + b;
console.log(sum(2, 3)); // 5

// 多语句需用大括号和return
const greeting = name => {
console.log("Hello");
return `Hello, ${name}`;
};

箭头函数可以像函数表达式一样使用。

箭头函数对于简单的单行行为(action)来说非常方便,尤其是当我们懒得打太多字的时候。

让我们深入研究一下箭头函数。

正如我们上面说的那样,箭头函数没有 this。如果访问 this,则会从外部获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const user = {
name: "张三",
// 常规函数作为方法,this指向当前对象
sayHi() {
console.log("常规函数:", this.name); // 输出:常规函数: 张三

// 嵌套箭头函数:没有自己的this,会向上找(这里找到sayHi的this)
const arrowFunc = () => {
console.log("箭头函数:", this.name); // 输出:箭头函数: 张三
};
arrowFunc();

// 嵌套常规函数:有自己的this(默认指向全局对象,严格模式下是undefined)
const normalFunc = function() {
console.log("嵌套常规函数:", this?.name); // 输出:嵌套常规函数: undefined
};
normalFunc();
}
};

user.sayHi();
  • 这是因为箭头函数的 this 是 “继承” 外部作用域的 this,而常规函数的 this 取决于调用方式。

而且不能对箭头函数进行 new 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 常规函数可以作为构造函数
function Person(name) {
this.name = name;
}
const person1 = new Person("张三"); // 正常工作,person1是Person实例

// 箭头函数不能作为构造函数
const ArrowPerson = (name) => {
this.name = name; // 这里的this其实是外部的this(比如全局)
};

// 尝试用new调用箭头函数:报错!
const person2 = new ArrowPerson("李四");
// 错误信息:TypeError: ArrowPerson is not a constructor

不具有 this 自然也就意味着另一个限制:箭头函数不能用作构造器(constructor)。不能用 new 调用它们。

箭头函数 => 和使用 .bind(this) 调用的常规函数之间有细微的差别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const obj = {
value: 100,
// 常规函数
getValue() {
return this.value;
}
};

// 用bind创建绑定版本:生成一个新函数,this固定为obj
const boundFunc = obj.getValue.bind(obj);
console.log(boundFunc()); // 100

// 箭头函数:没有自己的this,直接用外部的this(这里外部是全局,假设全局没有value)
const arrowFunc = () => obj.getValue();
// 等价于:() => { return obj.getValue() }(this从外部找,这里外部this不影响结果)
console.log(arrowFunc()); // 100

// 关键区别:bind创建的函数可以被再次修改this(虽然不推荐)
const anotherObj = { value: 200 };
console.log(boundFunc.call(anotherObj)); // 还是100(bind绑定后无法修改)

// 箭头函数的this由外部决定,修改外部this会影响结果
const outer = {
value: 300,
getArrow() {
return () => this.value; // 箭头函数的this继承自getArrow的this(即outer)
}
};
const arrowFromOuter = outer.getArrow();
console.log(arrowFromOuter()); // 300
  • .bind(this) 创建了一个该函数的“绑定版本”。也就是创建一个 “固定 this 的新函数
  • 箭头函数 => 没有创建任何绑定。箭头函数只是没有 thisthis 的查找与常规变量的搜索方式完全相同:在外部词法环境中查找。

继续我们上面所说,箭头函数也没有 arguments 变量。

当我们需要使用当前的 thisarguments 转发一个调用时,这对装饰器(decorators)来说非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 常规函数有arguments变量(类数组,包含所有参数)
function normalFunc() {
console.log("常规函数参数:", arguments); // [1, 2, 3]
}
normalFunc(1, 2, 3);

// 箭头函数没有arguments,访问会报错
const arrowFunc = () => {
console.log("箭头函数参数:", arguments); // 报错:ReferenceError: arguments is not defined
};
arrowFunc(1, 2, 3);

// 箭头函数可以用剩余参数(...args)替代arguments
const arrowWithRest = (...args) => {
console.log("箭头函数剩余参数:", args); // [1, 2, 3](数组,更易用)
};
arrowWithRest(1, 2, 3);
  • 箭头函数没有 arguments,但可以用剩余参数 ...args 获取参数列表,且 args 是真正的数组,更方便操作。

它们也没有 super,但是这都是类继承的相关内容了

函数对象

我们已经知道,在 JavaScript 中,函数也是一个值。

而 JavaScript 中的每个值都有一种类型,那么函数是什么类型呢?

对象

一个容易理解的方式是把函数想象成可被调用的“行为对象(action object)”。我们不仅可以调用它们,还能把它们当作对象来处理,增/删属性,按引用传递等。

所以说,函数在 JavaScript 中是 “一等公民”,不仅可以被调用,本身也是对象(Function 类型的实例),因此拥有自己的属性和方法。这些属性不仅包含语言内置的特性,还允许开发者自定义扩展,极大增强了函数的灵活性。

函数对象的属性

函数对象包含一些便于使用的属性。

比如,一个函数的名字可以通过属性 “name” 来访问:

1
2
3
4
5
function sayHi() {
alert("Hi");
}

alert(sayHi.name); // sayHi

更有趣的是,名称赋值的逻辑很智能。即使函数被创建时没有名字,名称赋值的逻辑也能给它赋予一个正确的名字,然后进行赋值:

1
2
3
4
5
let sayHi = function() {
alert("Hi");
};

alert(sayHi.name); // sayHi(有名字!)

这就是 JS 的上下文命名。如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。

当然有时也会出现无法推测名字的情况。此时,属性 name 会是空

1
2
3
4
// 立即执行函数表达式(IIFE)
(function() {}).name; // ""
// 未赋值的匿名函数
console.log((function() {}).name); // ""

还有另一个内建属性 “length”,它返回其声明时的形参数量,不包含剩余参数(...rest)和默认参数之后的参数。

1
2
3
4
5
6
7
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

自定义函数属性

除了内置属性,开发者可以为函数添加自定义属性,用于存储元数据、状态或辅助信息,而无需污染全局作用域。

这里我们添加了 counter 属性,用来跟踪总的调用次数:

1
2
3
4
5
6
7
8
9
10
11
function countCalls() {
countCalls.counter++; // 访问自定义属性 counter
console.log(`调用次数:${countCalls.counter}`);
}

// 初始化自定义属性
countCalls.counter = 0;

countCalls(); // 调用次数:1
countCalls(); // 调用次数:2
console.log(countCalls.counter); // 2(直接访问属性)

这个特性可以去做缓存,下面我们尝试缓存计算结果(记忆化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function factorial(n) {
// 检查缓存中是否有结果
if (factorial.cache[n] !== undefined) {
return factorial.cache[n];
}
let result = n === 0 ? 1 : n * factorial(n - 1);
// 存入缓存
factorial.cache[n] = result;
return result;
}

// 初始化缓存对象
factorial.cache = {};

console.log(factorial(5)); // 120(首次计算)
console.log(factorial(5)); // 120(从缓存读取)

函数的全局对象

全局对象是 JavaScript 运行时环境中一个特殊的对象,它在代码执行前就已存在,提供全局可访问的变量和函数。不同环境中全局对象的名称不同,但核心作用一致。

全局对象提供可在任何地方使用的变量和函数。默认情况下,这些全局变量内建于语言或环境中。

  1. 浏览器环境:全局对象名为 window

    1
    2
    console.log(window === this); // 在全局作用域中:true
    window.alert("Hello"); // 等同于 alert("Hello")
  2. Node.js 环境:全局对象名为 global

    1
    2
    console.log(global === this); // 在模块顶层:false(模块有独立作用域)
    global.console.log("Hello"); // 等同于 console.log("Hello")
  3. 通用标准:ES2020 引入 globalThis,统一各环境的全局对象访问(推荐使用)

    1
    2
    3
    // 浏览器中:globalThis === window
    // Node.js 中:globalThis === global
    console.log(globalThis); // 指向当前环境的全局对象

全局对象具有如下的特性

  1. 全局变量的宿主:在全局作用域中声明的变量和函数,会成为全局对象的属性

    1
    2
    3
    4
    5
    const globalVar = "I'm global";
    function globalFunc() {}

    console.log(window.globalVar); // "I'm global"(浏览器中)
    console.log(globalThis.globalFunc); // 函数本身
  2. 内置对象的容器:JavaScript 内置的构造函数(ObjectArray)、工具函数(consolesetTimeout)等都挂载在全局对象上

    1
    2
    console.log(globalThis.Object === Object); // true
    console.log(globalThis.setTimeout === setTimeout); // true
  3. 严格模式下的差异:在严格模式('use strict')中,全局作用域的 this 不再指向全局对象,而是 undefined

    1
    2
    'use strict';
    console.log(this); // undefined(全局作用域)

在编写跨环境代码时,globalThis 是访问全局对象的统一方式,无需判断运行环境。

函数的垃圾收集

这是 JS 内存管理的核心,如果 JS 没有一个很好的内存回收机制,他的过度灵活就会害了他

函数的垃圾收集本质是 “释放不再被引用的函数相关资源”

JS 垃圾收集器(GC)会自动回收 “不再被任何引用指向” 的函数对象及其关联资源(如闭包捕获的词法环境),回收时机由引擎决定,关键是判断 “函数是否还能被访问”。

JS 采用 “自动垃圾收集” 机制,不需要手动释放内存,核心逻辑是:

  • 内存中对象(包括函数对象)如果没有任何可达引用(即无法通过代码访问到),会被 GC 标记为 “垃圾”。
  • 引擎在空闲时(如代码执行间隙)清理这些垃圾对象,释放内存。

函数作为 “函数对象”,遵循同样规则 —— 但因为函数可能关联闭包、原型链等,回收场景比普通对象更复杂。

函数对象本身(比如定义的函数、箭头函数)的回收,和普通对象一致:当所有指向它的引用被清除,就会被 GC 回收。

函数对象的基础回收场景

  1. 函数变量被重新赋值 / 销毁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 1. 定义函数,创建函数对象,变量 func 指向它
    let func = function() {
    console.log("我是一个函数");
    };

    // 2. 此时 func 是可达引用,函数对象不会被回收
    func();

    // 3. 重新赋值:func 不再指向原函数对象
    func = null;
    // (或赋值为其他值:func = 123; / func = anotherFunc;)

    // 4. 原函数对象没有任何可达引用,GC 会在合适时机回收它
  2. 函数作为参数 / 返回值的回收

    • 函数作为参数传入后,如果没有被内部保存引用,调用结束后会被回收:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function useFunc(callback) {
      callback(); // 调用回调函数
      // 没有将 callback 保存到其他地方
      }

      // 传入匿名函数,调用结束后,匿名函数对象无引用,会被回收
      useFunc(function() {
      console.log("临时回调");
      });
    • 函数作为返回值,但没有被接收,会被回收:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function createFunc() {
      return function() {
      console.log("返回的函数");
      };
      }

      // 调用 createFunc,但没有保存返回的函数对象
      createFunc();
      // 返回的匿名函数无可达引用,会被 GC 回收

函数特有的回收难点:闭包对回收的影响

这是函数垃圾收集最核心的场景 —— 闭包会让 “本应销毁的词法环境” 被保留,进而影响函数相关资源的回收。

  1. 闭包导致的 “引用保留”

    之前讲闭包时提到,内层函数会通过 [[Environment]] 引用创建时的词法环境(外层函数的变量环境)。只要内层函数还能被访问,这个词法环境就不会被回收:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function createCounter() {
    let count = 0; // 外层函数的变量(存储在 outer 的词法环境中)

    // 内层函数(闭包)通过 [[Environment]] 引用 outer 的词法环境
    return function() {
    count++;
    return count;
    };
    }

    // counter 指向内层函数(闭包)
    const counter = createCounter();

    // 此时:内层函数可达 → 外层函数的词法环境(包含 count)可达 → 不会被回收
    console.log(counter()); // 1
    console.log(counter()); // 2

    // 只有当 counter 被销毁(清除引用),内层函数和外层词法环境才会被回收
    counter = null;
    // 现在内层函数无可达引用 → 外层词法环境也无引用 → 后续 GC 会回收两者
  2. 误区:外层函数执行完,其词法环境不一定销毁

    很多人以为 “函数执行完,内部变量就没了”,但闭包会打破这个逻辑:

    • 外层函数执行时创建的词法环境,默认在执行完后会被 GC 回收。
    • 但如果内层函数(闭包)被外部引用,外层词法环境会被闭包的 [[Environment]] 引用,从而保留下来。
  3. 多层闭包的回收逻辑

    如果有多层嵌套闭包,只有当 “最外层的闭包引用被清除”,所有关联的词法环境才会逐步回收:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function outer() {
    let a = 1;
    function middle() {
    let b = 2;
    function inner() {
    console.log(a + b);
    }
    return inner;
    }
    return middle();
    }

    const innerFunc = outer(); // innerFunc 指向 inner 闭包

    // 可达链:innerFunc → inner → middle 的词法环境(b=2) → outer 的词法环境(a=1)
    // 所以 a、b 所在的环境都不会被回收
    innerFunc(); // 3

    // 清除引用后,所有关联环境才会被回收
    innerFunc = null;

影响函数垃圾收集的其他因素

除了闭包,还有几个场景会让函数相关资源无法被回收,需要特别注意:

  1. 全局函数几乎不会被回收

    全局作用域的函数(如 function globalFunc() {}),其引用会一直存在于全局对象(window/globalThis)上,只要页面 / 程序不结束,就不会被回收:

    1
    2
    3
    4
    5
    6
    7
    8
    // 全局函数:引用一直存在于全局对象上
    function globalFunc() {
    console.log("全局函数");
    }

    // 除非手动清除全局引用(不推荐,可能影响其他代码)
    window.globalFunc = null;
    // 此时函数对象无可达引用,才可能被回收
  2. 被原型链 / 对象属性引用的函数

    如果函数被赋值给对象的属性或原型方法,只要对象可达,函数就不会被回收:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const obj = {
    method: function() {
    console.log("对象方法");
    }
    };

    // obj 可达 → obj.method 指向的函数对象可达 → 不会被回收
    obj.method();

    // 清除对象引用,或清除 method 属性,函数才可能被回收
    obj.method = null; // 仅清除函数引用,obj 仍存在
    // 或 obj = null; // 清除对象引用,函数也会被回收
  3. 事件监听 / 定时器中的函数

    如果函数被用作事件监听回调或定时器回调,只要监听没移除、定时器没清除,函数就不会被回收:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function handleClick() {
    console.log("点击事件");
    }

    // 函数作为事件回调,被 DOM 元素引用
    document.body.addEventListener("click", handleClick);

    // 此时:DOM 元素可达 → handleClick 可达 → 不会被回收
    // 必须移除监听,函数才可能被回收
    document.body.removeEventListener("click", handleClick);

    // 定时器同理:setInterval 会一直引用回调函数
    const timer = setInterval(function() {
    console.log("定时器");
    }, 1000);

    // 清除定时器,回调函数才会被回收
    clearInterval(timer);

其他内容

剩下太多的进阶内容,我只提及一下

装饰器模式和转发,call/apply

函数绑定