日期对象
日期对象用于处理日期和时间。
在 JavaScript 中,日期时间处理主要通过 Date
对象实现,它用于表示和操作时间。
Date 对象属性
| 属性 | 描述 |
|---|---|
| constructor | 返回对创建此对象的 Date 函数的引用。 |
| prototype | 原型链,所以我们可以为日期对象对象添加属性和方法。 |
但是由于原生 Date
方法在格式化、时区处理等场景不够便捷,所以我一般使用Day.js
创建 Date 对象
Date 是一个构造函数,需通过 new
关键字创建实例,常见创建方式有 4 种:
无参数
创建当前时间的 Date 对象(基于系统本地时间):
1
2const 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
2const timestamp = 1760409600000; // 对应 2025-11-14 00:00:00 UTC
const date = new Date(timestamp);日期字符串参数
传入符合规范的日期字符串(如 ISO 8601 格式
YYYY-MM-DDTHH:mm:ss.sssZ):1
2
3const 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
注意:
- 月份从 0
开始:这是最容易出错的点,
getMonth()返回 0-11,对应 1-12 月。 - 时区问题:
Date对象内部存储的是 UTC 时间,但getXxx()方法返回本地时区的结果,getUTCXxx()返回 UTC 时间。 - 不可变性误区:
Date对象是可变的,设置时间的方法(如setHours)会直接修改原对象,而非返回新对象。 - 字符串解析差异:不同浏览器对非标准日期字符串的解析可能不一致,建议使用 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 | const date = new Date(2025, 10, 14, 15, 30, 0); |
注意:
- 获取年份一般都是
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 | const date = new Date(); |
以上方法除了 setTime() 都有 UTC
变体,例如:setUTCHours()。
日期格式化方法
Date 对象提供了几个将日期转换为字符串的方法:
toString():返回本地时间的完整字符串表示(如Fri Nov 14 2025 15:30:00 GMT+0800 (中国标准时间))。toUTCString():返回 UTC 时间的字符串表示(如Fri, 14 Nov 2025 07:30:00 GMT)。toISOString():返回 ISO 8601 格式的字符串(UTC 时间,如2025-11-14T07:30:00.000Z),常用于接口传输。toLocaleString():返回本地时区的本地化字符串(如2025/11/14 15:30:00,格式因地区而异)。toLocaleDateString():仅返回本地化的日期部分(如2025/11/14)。toLocaleTimeString():仅返回本地化的时间部分(如15:30:00)。
自动校准
自动校准 是 Date
对象的一个非常方便的特性。我们可以设置超范围的数值,它会自动校准
1 | let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!? |
超出范围的日期组件将会被自动分配。
假设我们要在日期 “28 Feb 2016” 上加 2 天。结果可能是 “2 Mar” 或 “1
Mar”,因为存在闰年。但是我们不需要考虑这些,只需要直接加 2 天,剩下的
Date 对象会帮我们处理:
1 | let date = new Date(2016, 1, 28); |
这个特性经常被用来获取给定时间段后的日期。例如,我们想获取“现在 70 秒后”的日期:
1 | let date = new Date(); |
我们还可以设置 0 甚至可以设置负值。例如:
1 | let date = new Date(2016, 0, 2); // 2016 年 1 月 2 日 |
日期转化为数字,日期差值
JavaScript 中,Date
对象在参与数值运算(如加法、减法)或被强制类型转换时,会自动转换为对应的毫秒级时间戳(等同于
date.getTime() 的结果)。
当 Date 对象被转化为数字时,得到的是对应的时间戳,与使用
date.getTime() 的结果相同
可以使用一元加号 + 转换
这是最简洁的方式,本质是触发 Date 对象的
valueOf() 方法(该方法返回时间戳):
1 | const date = new Date(2025, 10, 14); |
有一个重要的副作用:日期可以相减,相减的结果是以毫秒为单位时间差。
这个作用可以用于时间测量:
1 | let start = new Date(); // 开始测量时间 |
如果我们仅仅想要测量时间间隔,我们不需要 Date 对象。
有一个特殊的静态方法
Date.now(),它会返回当前的时间戳。
它相当于 new Date().getTime(),但它不会创建中间的
Date
对象。因此它更快,而且不会对垃圾回收造成额外的压力。
这种方法很多时候因为方便,又或是因性能方面的考虑而被采用
1 | let start = Date.now(); // 从 1 Jan 1970 至今的时间戳 |
Date.parse()
是另一个静态方法,用于将日期字符串解析为对应的毫秒级时间戳。如果解析失败,返回
NaN。
支持的字符串格式:ISO 8601 标准格式
YYYY-MM-DD、YYYY-MM-DDTHH:mm:ss、YYYY-MM-DDTHH:mm:ss.sssZ(带时区)等。
1 | Date.parse('2025-11-14'); // 1760409600000(本地时区的 2025-11-14 00:00:00 时间戳) |
函数类型
在JavaScript中,函数(Function)是个重要的内容,它在开发中会经常被用到。这几年函数式编程越来越火,如React,在React中,一个组件就是一个函数,React许多Hook也是一个函数。
因为我们经常需要在脚本的许多地方执行很相似的操作。
函数是程序的主要“构建模块”。函数使该段代码可以被调用很多次,而不需要写重复的代码。
函数基础
函数声明
使用 函数声明 创建函数。
1 | function showMessage() { |
function 关键字首先出现,然后是
函数名,然后是括号之间的 参数
列表,最后是花括号之间的代码(即“函数体”)。
所以,js 你会看到通篇内容都长一个样,使用 function
关键字声明的函数具有函数提升特性(可在定义前调用)。
1 | function 函数名(参数1, 参数2, ...) { |
函数也是一种类型,所以我们也可以通过 new Function()
动态创建函数,参数为字符串形式(知道就够,极少使用,性能差且不利于调试,通篇这玩意能给我看似)。
1 | const subtract = new Function('a', 'b', 'return a - b'); |
局部和外部变量
和其他语言一样,在函数中声明的变量只在该函数内部可见,是局部变量
函数也可以访问外部变量,这是外部变量,而且函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。
1 | function showMessage() { |
1 | let userName = 'John'; |
如果在函数内部声明了同名变量,那么函数会 遮蔽
外部变量。例如,在下面的代码中,函数使用局部的
userName,而外部变量被忽略:
1 | let userName = 'John'; |
任何函数之外声明的变量,例如上述代码中的外部变量
userName,都被称为 全局
变量。全局变量在任意函数中都是可见的,除非被遮蔽,这也和其他语言一样。
参数和返回值
我们可以通过参数将任意数据传递给函数
函数定义时的参数为 “形参”,调用时传入的为 “实参”。
和其他语言不同的是,JS
允许实参数量与形参不同,多余参数会被忽略,不足则形参为
undefined。也就是说,如果一个函数被调用,但有参数(argument)未被提供,那么相应的值就会变成
undefined。
我们可以使用 =
为函数声明中的参数指定所谓的“默认”(如果对应参数的值未被传递则使用)值(ES6+)
1 | function greet(name = "Guest") { |
注意,和 Java 一样,在 JavaScript 中,出现如果函数在没带个别参数的情况下被调用的情况,默认参数会被计算出来。
当函数定义了默认参数时,默认参数的 “计算过程” 并不是在函数定义时执行,而是在函数被调用的时刻,并且只有在该参数未被传入(或传入
undefined) 时,才会触发计算并使用默认值。1
2
3
4
5
6
7
8
9function 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 | function sumAll(...nums) { |
函数的返回值也通过 return 语句返回,若未写
return 或 return 后无值,默认返回
undefined。return 会立即终止函数执行。
只使用 return
但没有返回值也是可行的。但这会导致函数立即退出。
空值的 return 或没有 return 的函数返回值为
undefined
如果函数无返回值,它就会像返回 undefined 一样:
1 | function doNothing() { /* 没有代码 */ } |
空值的 return 和 return undefined
等效:
1 | function doNothing() { |
闭包
和 Python 一样,JS函数嵌套时,内层函数引用外层函数的变量,且内层函数被外部引用,导致外层变量不被销毁。
这样就能实现封装私有变量、实现函数记忆等。
1 | function createCounter() { |
在 JavaScript
中,所有函数都是天生闭包的,只有一个例外,new Function创建的,它们会通过隐藏的[[Environment]]属性,记住自己创建时的词法环境,即便在创建环境外执行,也能访问该环境中的外部变量。
[[Environment]]属性其值就是
“当前所在的词法环境”,即创建函数时,周围的变量环境
闭包的本质就是函数通过[[Environment]]“抓住” 了自己的
“诞生环境”,即便后来跑到其他环境执行,也能顺着[[Environment]]找到诞生环境中的变量
—— 这就是 “记住外部变量并访问” 的能力。
闭包不是 “没用的理论”,实际开发中经常用,这是模块化的基础
1 | function createModule() { |
this 关键字
this
指向函数的调用者,具体指向取决于调用方式(箭头函数无自己的
this,继承外层作用域的 this):
普通函数直接调用:
this指向全局对象(浏览器中为window,Node.js 中为global;严格模式下为undefined)。作为对象方法调用:
this指向该对象。1
2
3
4
5
6
7const 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
8function 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 | function sayHi() { |
但它依然是一个值,所以我们可以像使用其他类型的值一样使用它。
我们可以复制函数到其他变量:
1 | function sayHi() { // (1) 创建 |
函数表达式允许我们在任何表达式的中间创建一个新函数。它将函数赋值给变量,无函数提升(必须先定义后调用)。
1 | const 变量名 = function(参数1, 参数2, ...) { |
1 | // 函数表达式(匿名函数赋值给变量) |
若给函数表达式命名(具名函数表达式),那么它的名称仅在函数内部可见
1 | const factorial = function fn(n) { |
函数的词法环境
函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。一旦代码执行到赋值表达式
const factorial = function....
的右侧,此时就会开始创建该函数,并且可以从现在开始使用(分配,调用等)。
而函数声明则不同。它在函数声明被定义之前,它就可以被调用。所以一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。
这是内部算法的缘故。当 JavaScript 准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。
严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。
1 | let age = prompt("What is your age?", 18); |
就这样,这是因为函数声明只在它所在的代码块中可见。、
但是我们怎么才能让 welcome 在 if
外可见呢?
正确的做法是使用函数表达式,并将 welcome 赋值给在
if 外声明的变量,并具有正确的可见性。
1 | let age = prompt("What is your age?", 18); |
那么这涉及到词法环境的内容,它是 JS 引擎在函数创建和执行时自动管理的 “变量存储容器”。
词法环境的结构:每个词法环境包含两部分
- 环境记录(Environment
Record):存储当前作用域的变量、函数声明(比如
let a = 1、function fn() {})。 - 外部引用(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!)- 环境记录(Environment
Record):存储当前作用域的变量、函数声明(比如
有关于这个内容其实就是JS的内外部词法环境的相关内容,在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。
在这个函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局):
- 内部词法环境与
say的当前执行相对应。它具有一个单独的属性:name,函数的参数。我们调用的是say("John"),所以name的值为"John"。 - 外部词法环境是全局词法环境。它具有
phrase变量和函数本身。
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。
回调函数
回调函数(Callback Function)指一个被作为参数传递给另一个函数(称为 “主函数”)的函数,当主函数执行到特定阶段或满足特定条件时,会 “回过来调用” 这个函数。
简单来说:你定义一个函数 A,把 A 传给函数 B,让 B 在需要的时候执行 A,A 就是回调函数。
1 | // 定义回调函数:打印消息 |
JS 中大量异步操作(如定时器、网络请求、文件读写)需要通过回调函数来处理结果。因为异步操作不会立即完成,无法直接用返回值获取结果,只能通过回调在操作完成后触发处理逻辑。
示例:定时器中的回调
1 | // 1秒后执行回调函数 |
而且在浏览器中,事件(如点击、输入、加载)的响应逻辑本质上都是回调函数。当事件触发时,浏览器会调用我们预先定义的回调函数。
回调函数可分为两类:
同步回调
回调函数在主函数执行过程中立即同步执行,不会涉及异步操作。
示例:
Array.prototype.map的回调(同步执行)1
2
3
4
5
6const 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
10console.log("开始");
setTimeout(() => {
console.log("异步回调执行"); // 1秒后执行
}, 1000);
console.log("结束");
// 输出顺序:
// 开始
// 结束
// (1秒后)异步回调执行
回调地狱(Callback Hell)
对没错这个词貌似是 JS 这边发明的
也就是当多个异步操作需要按顺序执行时(如 “先请求 A 数据,再用 A 的结果请求 B 数据,再用 B 的结果请求 C 数据”),会导致回调函数嵌套层级过深,代码可读性和可维护性变差,这种现象称为 “回调地狱”。
1 | // 模拟三个依赖的异步请求 |
解决这个就可以祭出神秘的 Promise 异步编程 了
对了,普通回调函数中的 this
指向可能不符合预期(如在对象方法中作为回调传递时,this
可能指向全局或 undefined)。所以用箭头函数(继承外层
this)或 bind 绑定 this。
1 | const obj = { |
Rest 参数
在 JavaScript 中,无论函数是如何定义的,你都可以在调用它时传入任意数量的参数。
1 | function sum(a, b) { |
虽然这里这个函数不会因为传入过多的参数而报错。但是,当然,只有前两个参数被求和了。
我们可以在函数定义中声明一个数组来收集参数,它是这样写的,...变量名,一般写作...rest或者...args,我倾向于后者
这三个点的语义就是“收集剩余的参数并存进指定数组中”。
注意,Rest 参数会收集剩余的所有参数,因此下面这种用法没有意义,并且会导致错误:
1 | function f(arg1, ...rest, arg2) { // arg2 在 ...rest 后面?! |
...rest 必须写在参数列表最后。
而且有一个名为 arguments
的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。
1 | function showName() { |
尽管 arguments
是一个类数组,也是可迭代对象,但它终究不是数组。它不支持数组方法,因此我们不能调用
arguments.map(...) 等方法。
此外,它始终包含所有参数,我们不能像使用 rest 参数那样只截取参数的一部分。
想起来,箭头函数没有自身的 this,它们也没有特殊的
arguments 对象。
Spread 语法
Spread 语法用...表示,能 “展开”
可迭代对象(数组、字符串、Set 等)或对象的可枚举属性,将其拆分为独立元素
/ 键值对,常用于复制、合并、传参等场景。
所以说 Spread 语法有适用对象,他不是谁都能用,你直接直接展开非可迭代对象(如 null、undefined、数字、布尔值)会报错。
- 可迭代对象(Iterable):数组、字符串、Set、Map、arguments 对象、NodeList 等。
- 普通对象:ES2018 后支持,可展开其可枚举属性(原型链上的属性不会展开)。
最常用的就是和 rest参数 相反的事情,展开数组,用于复制数组、合并数组、向数组插入元素,避免修改原数组。
复制数组(浅拷贝):
1
2
3
4
5const 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
3const arr = [1, 4];
const newArr = [1, ...arr, 5]; // 在arr前后插入元素
console.log(newArr); // [1,1,4,5]合并对象(同名属性覆盖):
1
2
3
4const objA = { name: "张三", age: 20 };
const objB = { age: 21, gender: "男" };
const mergedObj = { ...objA, ...objB }; // 后面的对象覆盖前面的同名属性
console.log(mergedObj); // { name: "张三", age: 21, gender: "男" }合并时添加 / 修改属性:
1
2
3const obj = { name: "张三" };
const newObj = { ...obj, age: 20, name: "李四" };
console.log(newObj); // { name: "李四", age: 20 }(修改name,新增age)
当函数需要接收多个独立参数时,可直接展开数组 /
可迭代对象,替代Function.apply()。
1 | // 求数组最大值(Math.max需要多个独立参数,不能直接传数组) |
多参数 + 展开可以这么写
1 | function sum(a, b, c) { |
除了数组和对象,字符串、Set、Map 等可迭代对象也能展开,不演示了。
注意,上述的操作都是浅拷贝,只拷贝对象 / 数组的 “表层”,如果内部有嵌套对象 / 数组,拷贝的是引用(修改嵌套内容会影响原对象)。
很多人会把 Spread 语法和 Rest 参数搞混,核心区别:
- Spread 语法(…):用于 “展开”,把可迭代对象 / 对象拆分为独立元素 / 属性。
- Rest 参数(…):用于 “收集”,把多个独立参数收集为数组。
箭头函数
创建函数还有另外一种非常简单的语法,并且这种方法通常比函数表达式更好。
它极大简化函数表达式的语法,无自己的 this、arguments,不能作为构造函数。
1 | // 基本形式:参数 => 函数体 |
1 | const sum = (a, b) => a + b; |
箭头函数可以像函数表达式一样使用。
箭头函数对于简单的单行行为(action)来说非常方便,尤其是当我们懒得打太多字的时候。
让我们深入研究一下箭头函数。
正如我们上面说的那样,箭头函数没有 this。如果访问
this,则会从外部获取。
1 | const user = { |
- 这是因为箭头函数的
this是 “继承” 外部作用域的this,而常规函数的this取决于调用方式。
而且不能对箭头函数进行 new 操作
1 | // 常规函数可以作为构造函数 |
不具有 this
自然也就意味着另一个限制:箭头函数不能用作构造器(constructor)。不能用
new 调用它们。
箭头函数 => 和使用 .bind(this)
调用的常规函数之间有细微的差别:
1 | const obj = { |
.bind(this)创建了一个该函数的“绑定版本”。也就是创建一个 “固定this的新函数- 箭头函数
=>没有创建任何绑定。箭头函数只是没有this。this的查找与常规变量的搜索方式完全相同:在外部词法环境中查找。
继续我们上面所说,箭头函数也没有 arguments 变量。
当我们需要使用当前的 this 和 arguments
转发一个调用时,这对装饰器(decorators)来说非常有用。
1 | // 常规函数有arguments变量(类数组,包含所有参数) |
- 箭头函数没有
arguments,但可以用剩余参数...args获取参数列表,且args是真正的数组,更方便操作。
它们也没有 super,但是这都是类继承的相关内容了
函数对象
我们已经知道,在 JavaScript 中,函数也是一个值。
而 JavaScript 中的每个值都有一种类型,那么函数是什么类型呢?
对象
一个容易理解的方式是把函数想象成可被调用的“行为对象(action object)”。我们不仅可以调用它们,还能把它们当作对象来处理,增/删属性,按引用传递等。
所以说,函数在 JavaScript 中是
“一等公民”,不仅可以被调用,本身也是对象(Function
类型的实例),因此拥有自己的属性和方法。这些属性不仅包含语言内置的特性,还允许开发者自定义扩展,极大增强了函数的灵活性。
函数对象的属性
函数对象包含一些便于使用的属性。
比如,一个函数的名字可以通过属性 “name” 来访问:
1 | function sayHi() { |
更有趣的是,名称赋值的逻辑很智能。即使函数被创建时没有名字,名称赋值的逻辑也能给它赋予一个正确的名字,然后进行赋值:
1 | let sayHi = function() { |
这就是 JS 的上下文命名。如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。
当然有时也会出现无法推测名字的情况。此时,属性 name
会是空
1 | // 立即执行函数表达式(IIFE) |
还有另一个内建属性
“length”,它返回其声明时的形参数量,不包含剩余参数(...rest)和默认参数之后的参数。
1 | function f1(a) {} |
自定义函数属性
除了内置属性,开发者可以为函数添加自定义属性,用于存储元数据、状态或辅助信息,而无需污染全局作用域。
这里我们添加了 counter 属性,用来跟踪总的调用次数:
1 | function countCalls() { |
这个特性可以去做缓存,下面我们尝试缓存计算结果(记忆化)
1 | function factorial(n) { |
函数的全局对象
全局对象是 JavaScript 运行时环境中一个特殊的对象,它在代码执行前就已存在,提供全局可访问的变量和函数。不同环境中全局对象的名称不同,但核心作用一致。
全局对象提供可在任何地方使用的变量和函数。默认情况下,这些全局变量内建于语言或环境中。
浏览器环境:全局对象名为
window1
2console.log(window === this); // 在全局作用域中:true
window.alert("Hello"); // 等同于 alert("Hello")Node.js 环境:全局对象名为
global1
2console.log(global === this); // 在模块顶层:false(模块有独立作用域)
global.console.log("Hello"); // 等同于 console.log("Hello")通用标准:ES2020 引入
globalThis,统一各环境的全局对象访问(推荐使用)1
2
3// 浏览器中:globalThis === window
// Node.js 中:globalThis === global
console.log(globalThis); // 指向当前环境的全局对象
全局对象具有如下的特性
全局变量的宿主:在全局作用域中声明的变量和函数,会成为全局对象的属性
1
2
3
4
5const globalVar = "I'm global";
function globalFunc() {}
console.log(window.globalVar); // "I'm global"(浏览器中)
console.log(globalThis.globalFunc); // 函数本身内置对象的容器:JavaScript 内置的构造函数(
Object、Array)、工具函数(console、setTimeout)等都挂载在全局对象上1
2console.log(globalThis.Object === Object); // true
console.log(globalThis.setTimeout === setTimeout); // true严格模式下的差异:在严格模式(
'use strict')中,全局作用域的this不再指向全局对象,而是undefined1
2;
console.log(this); // undefined(全局作用域)
在编写跨环境代码时,globalThis
是访问全局对象的统一方式,无需判断运行环境。
函数的垃圾收集
这是 JS 内存管理的核心,如果 JS 没有一个很好的内存回收机制,他的过度灵活就会害了他
函数的垃圾收集本质是 “释放不再被引用的函数相关资源”
JS 垃圾收集器(GC)会自动回收 “不再被任何引用指向” 的函数对象及其关联资源(如闭包捕获的词法环境),回收时机由引擎决定,关键是判断 “函数是否还能被访问”。
JS 采用 “自动垃圾收集” 机制,不需要手动释放内存,核心逻辑是:
- 内存中对象(包括函数对象)如果没有任何可达引用(即无法通过代码访问到),会被 GC 标记为 “垃圾”。
- 引擎在空闲时(如代码执行间隙)清理这些垃圾对象,释放内存。
函数作为 “函数对象”,遵循同样规则 —— 但因为函数可能关联闭包、原型链等,回收场景比普通对象更复杂。
函数对象本身(比如定义的函数、箭头函数)的回收,和普通对象一致:当所有指向它的引用被清除,就会被 GC 回收。
函数对象的基础回收场景
函数变量被重新赋值 / 销毁
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 会在合适时机回收它函数作为参数 / 返回值的回收
函数作为参数传入后,如果没有被内部保存引用,调用结束后会被回收:
1
2
3
4
5
6
7
8
9function useFunc(callback) {
callback(); // 调用回调函数
// 没有将 callback 保存到其他地方
}
// 传入匿名函数,调用结束后,匿名函数对象无引用,会被回收
useFunc(function() {
console.log("临时回调");
});函数作为返回值,但没有被接收,会被回收:
1
2
3
4
5
6
7
8
9function createFunc() {
return function() {
console.log("返回的函数");
};
}
// 调用 createFunc,但没有保存返回的函数对象
createFunc();
// 返回的匿名函数无可达引用,会被 GC 回收
函数特有的回收难点:闭包对回收的影响
这是函数垃圾收集最核心的场景 —— 闭包会让 “本应销毁的词法环境” 被保留,进而影响函数相关资源的回收。
闭包导致的 “引用保留”
之前讲闭包时提到,内层函数会通过
[[Environment]]引用创建时的词法环境(外层函数的变量环境)。只要内层函数还能被访问,这个词法环境就不会被回收:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function 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 会回收两者误区:外层函数执行完,其词法环境不一定销毁
很多人以为 “函数执行完,内部变量就没了”,但闭包会打破这个逻辑:
- 外层函数执行时创建的词法环境,默认在执行完后会被 GC 回收。
- 但如果内层函数(闭包)被外部引用,外层词法环境会被闭包的
[[Environment]]引用,从而保留下来。
多层闭包的回收逻辑
如果有多层嵌套闭包,只有当 “最外层的闭包引用被清除”,所有关联的词法环境才会逐步回收:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function 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;
影响函数垃圾收集的其他因素
除了闭包,还有几个场景会让函数相关资源无法被回收,需要特别注意:
全局函数几乎不会被回收
全局作用域的函数(如
function globalFunc() {}),其引用会一直存在于全局对象(window/globalThis)上,只要页面 / 程序不结束,就不会被回收:1
2
3
4
5
6
7
8// 全局函数:引用一直存在于全局对象上
function globalFunc() {
console.log("全局函数");
}
// 除非手动清除全局引用(不推荐,可能影响其他代码)
window.globalFunc = null;
// 此时函数对象无可达引用,才可能被回收被原型链 / 对象属性引用的函数
如果函数被赋值给对象的属性或原型方法,只要对象可达,函数就不会被回收:
1
2
3
4
5
6
7
8
9
10
11
12const obj = {
method: function() {
console.log("对象方法");
}
};
// obj 可达 → obj.method 指向的函数对象可达 → 不会被回收
obj.method();
// 清除对象引用,或清除 method 属性,函数才可能被回收
obj.method = null; // 仅清除函数引用,obj 仍存在
// 或 obj = null; // 清除对象引用,函数也会被回收事件监听 / 定时器中的函数
如果函数被用作事件监听回调或定时器回调,只要监听没移除、定时器没清除,函数就不会被回收:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function 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);
其他内容
剩下太多的进阶内容,我只提及一下







