目录
JavaScript 2. 函数
函数是 JavaScript 的核心组成部分,其实质是可调用的函数对象。掌握函数是理解 JavaScript 作用域链、闭包、异步机制以及面向对象特性的核心基础。
2.1 函数的定义与类型
JavaScript 提供了多种定义函数的方式,其底层初始化时机和作用域表现存在差异。
2.1.1 函数声明
函数声明(Function Declaration)是独立的语句结构,具有作用域提升(Hoisting)特征。
{
// 1. 验证作用域提升:函数声明在初始化阶段即被创建,可以在定义语句之前成功调用
console.log("--- 函数声明提升测试 ---");
greet("Alice", "Prof.");
function greet(name, title) {
console.log(`Hello, ${title} ${name}!`);
}
// 2. 验证严格模式下的块级作用域约束
(() => {
"use strict";
if (true) {
function blockScopedFunc() {
return "Inside Block";
}
console.log(`块内调用: ${blockScopedFunc()}`);
}
try {
blockScopedFunc(); // 尝试在块外部访问
} catch (e) {
// 触发 ReferenceError
console.log(`块外调用捕获异常: ${e.name} - ${e.message}`);
}
})();
}
其中,function 是关键字,greet 是函数名,name 和 title 是形参(Parameters),花括号内的代码为函数体。
底层运行机制与规范:
- 作用域提升:在脚本或代码块开始执行前的环境初始化阶段, JavaScript 引擎会扫描当前作用域,创建并初始化所有的函数声明。因此,函数声明可以在其定义语句之前被成功调用。
- 严格模式下的块级作用域:在
"use strict"模式下,如果函数声明位于一个代码块内(如if语句、for循环内部),该函数将绑定到该块级作用域。块外部无法访问此函数。
2.1.2 函数表达式
函数表达式(Function Expression)是将函数作为右值赋值给变量的行为。
{
console.log("--- 函数表达式初始化时机测试 ---");
try {
greetExpr("Alice", "Prof."); // 尝试在赋值语句前调用
} catch (e) {
// 触发 ReferenceError
console.log(`赋值前调用捕获异常: ${e.name} - ${e.message}`);
}
const greetExpr = function(name, title) {
console.log(`Hello, ${title} ${name}!`);
};
greetExpr("Bob", "Dr."); // 赋值后调用正常
}
底层运行机制与规范:
- 执行时创建:与函数声明不同,函数表达式在执行流运行到该赋值语句时才会被创建并初始化。在语句之前调用,若变量使用
let/const声明会触发ReferenceError;若使用var声明,由于变量已被提升但尚未赋值(此时值为undefined),对其进行函数调用会因尝试对非函数类型执行[[Call]]操作而触发TypeError。 - 具名函数表达式(Named Function Expression, NFE):右侧函数可以包含名称(如
let sayHi = function func(me) {...})。根据 ECMAScript 规范,引擎会为该函数实例化一个独立的外部词法环境,并将名称func以不可变绑定(Immutable Binding)的形式写入该环境中。因此该名称仅在函数内部可见,常用于递归调用或在调用栈调试中标识函数,避免因外部变量引用变更导致递归中断。
{
console.log("--- 具名函数表达式行为测试 ---");
let sayHi = function func(who) {
if (who) {
console.log(`Hello, ${who}`);
} else {
// 内部名称 func 仅在自身函数体内可见,
// 用于确保递归调用不因外部变量改变而失效
func("Guest");
}
};
sayHi("Alice"); // 输出: Hello, Alice
try {
func("Bob"); // 尝试直接在外部作用域调用内部名称
} catch (e) {
// 输出: ReferenceError
console.log(`外部调用内部名捕获异常: ${e.name}`);
}
let welcome = sayHi;
sayHi = null; // 显式置空原引用变量
// 物理结构与规范限制:此时外部 sayHi 变量已不指向该函数,
// 但内部通过特定的词法环境保持了对函数自身的直接引用
welcome(); // 依然成功输出: Hello, Guest
}
2.1.3 箭头函数(Arrow Function)
箭头函数是 ECMAScript 6 引入的语法,旨在提供一种更简洁的单行或简短函数书写形式。
{
console.log("--- 箭头函数基础与字面量返回测试 ---");
const greet = (name) => {
console.log(`Hello, ${name}!`);
};
greet("Alice");
const add = (a, b) => a + b;
console.log(`单行隐式返回 add(1, 2): ${add(1, 2)}`); // 3
// 对象字面量的隐式返回陷阱验证
const f1 = () => { foo: 1 };
console.log(`未包裹圆括号 f1(): ${f1()}`); // undefined
const f2 = () => ({ foo: 1 });
console.log(`包裹圆括号后 f2(): ${f2()}`); // [object Object]
console.log(`包裹圆括号后 f2(): ${JSON.stringify(f2())}`); // {"foo":1}
}
核心限制与设计差异:
-
词法
this绑定(Lexical this): 箭头函数本身没有独立的this存储空间。其内部的this完全取决于该函数被定义时所处的外部词法环境。由于这种静态绑定机制,使用call()、apply()或bind()试图动态改变箭头函数的this指向将被引擎静默忽略。 普通函数的this取决于调用方式(非严格模式下独立调用指向全局对象,严格模式下是undefined)。箭头函数则直接继承外层词法作用域的this。{ console.log("--- 箭头函数 This 词法绑定测试 ---"); const obj = { normalFunc: function() { console.log(`普通函数 Method This 指向 obj: ${this === obj}`); }, arrowFunc: () => { console.log(`箭头函数 Method This 指向 obj: ${this === obj}`); } }; obj.normalFunc(); // true obj.arrowFunc(); // false (向外层寻找词法作用域环境) // 异步回调环境下的 This 锁定制 const timerObj = { id: 42, init: function() { // 普通函数作为异步回调,执行上下文在运行时脱离宿主对象,退化为全局环境(非严格模式下) setTimeout(function() { console.log(`setTimeout 普通函数回调 this.id: ${this.id}`); // undefined }, 10毫秒); // 箭头函数没有自己的 this,锁定定义时的外层词法环境(即 init 方法的 this) setTimeout(() => { console.log(`setTimeout 箭头函数回调 this.id: ${this.id}`); // 42 }, 10毫秒); } }; timerObj.init(); } -
缺少
arguments变量对象: 箭头函数内部不会创建传统的类数组对象arguments。若在其内部访问arguments,实际访问的是外层常规函数的arguments变量。若外层非函数作用域,访问arguments将取决于宿主环境,在标准脚本环境中会触发ReferenceError。{ console.log("--- Arguments 行为差异测试 ---"); function outerNormal() { // 箭头函数直接借用外层普通函数的 arguments 对象 const arrow = () => { console.log(`箭头函数内读取到的外层 arguments: ${JSON.stringify([...arguments])}`); }; arrow(); } outerNormal("Param1", "Param2"); // 输出: ["Param1", "Param2"] // 剩余参数 (Rest Parameters) 替代方案 const logArgs = (...args) => { console.log(`箭头函数配合剩余参数获取自身实参: ${JSON.stringify(args)}`); }; logArgs(1, "Alice", 2); // 输出: [1, "Alice", 2] } -
构造能力缺失(无
[[Construct]]): 普通函数在创建时会拥有内部方法[[Call]](支持正常执行)和[[Construct]](支持通过new实例化)。而箭头函数底层仅具备[[Call]]方法,因此无法作为构造函数被new调用(执行时抛出TypeError),并且其不具备prototype原型属性。 -
无
super绑定:无法在类(Class)的继承体系中通过箭头函数触发super关键字。 -
对象字面量的隐式返回限制: 单行箭头函数若需隐式返回一个对象字面量,必须将该对象包裹在圆括号
()内。否则,引擎会将对象的花括号{}解析为函数的语句块(Block),导致返回undefined或触发语法解析错误。
2.1.4 函数命名规范
函数代表抽象行为,其命名通常应采用动词前缀(如 get、set、calc、create)以概括其内部核心逻辑。若一个函数承载了多项不相关的行为,应将其拆分为多个单一职责的函数。
2.2 参数管理机制
JavaScript 的函数参数处理不强制要求实参与形参的数量及类型完全一致。
2.2.1 形参与实参的传递
- 形参 (Parameters):函数定义中列出的变量名。
- 实参 (Arguments):函数调用时实际传入的具体数值。
- 传递机理: JavaScript 始终遵循值传递 (Pass-by-value) 规则。当实参为引用类型(如对象、数组)时,传递的“值”是该对象的内存地址引用。因此,在函数内部修改对象属性会直接作用于外部对象,但若在内部重写该形参的引用(如
obj = {}),则不会改变外部变量。
2.2.2 默认参数值(Default Parameters)
在定义形参时,可以通过赋值运算符 = 为其指定默认值。
{
console.log("--- 默认参数运行时动态评估测试 ---");
let evaluationCount = 0;
// 默认参数表达式可以是一个带有副作用的函数调用
const getDynamicDefault = () => {
evaluationCount++;
return "Prof.";
};
const greet = function(name, title = getDynamicDefault()) {
console.log(`Hello, ${title} ${name}!`);
};
greet("Alice"); // 未传值,触发评估,评估计数 +1
greet("Bob", "Dr."); // 传值,跳过评估,评估计数不变
greet("Charlie", undefined); // 传入 undefined 视为缺省,触发评估,评估计数 +1
console.log(`动态默认参数执行总次数: ${evaluationCount}`); // 输出: 2
}
默认参数不仅限于字面量,可以是任意复杂的表达式或函数调用。默认参数的评估是在运行时动态触发的,即只有在对应的实参缺省或显式传入 undefined 时,才会执行并计算默认参数对应的表达式。
2.2.3 剩余参数(Rest Parameters)与 arguments
在旧标准中,处理不定数量实参依赖内置的类数组对象 arguments。现代标准推荐使用剩余参数。
{
console.log("--- 剩余参数基本测试 ---");
function sumAll(...args) { // args 是一个标准的 Array 实例
return args.reduce((sum, current) => sum + current, 0);
}
console.log(`sumAll(1, 2, 3) 结果: ${sumAll(1, 2, 3)}`); // 6
}
技术对比:
| 特征 | 剩余参数 (...args) | 传统 arguments 对象 |
|---|---|---|
| 类型 | 标准 Array 实例,支持所有数组原型方法 | 类数组对象 (Array-like Object),仅支持索引与 length |
| 语法限制 | 必须位于形参列表的最末尾 | 可以在任何位置通过关键字直接读取 |
| 箭头函数支持 | 完全支持 | 不支持(箭头函数内部无此绑定) |
2.2.4 参数解构赋值
通过在形参位置直接进行解构,可以清晰地表达复杂对象或数组的内部依赖结构,通过默认值防止解构异常。
- 参数顺序设计:非解构形参若包含默认值,应将带有默认值的参数置于形参列表右侧(Trailing Default Parameters),以确保正常省略实参。
- 解构防御约束:解构对象的属性是无序的,应为可能缺失的属性声明默认初始值,并为整个解构对象赋予默认值
{},避免因未传实参导致运行时报错。
{
console.log("--- 参数解构防御机制测试 ---");
// 防御性写法:声明属性默认值,并赋予形参对象整体默认值 `{}`
function greetFlexibly({ name = "Alice", title = "Visitor" } = {}) {
console.log(`Hello, ${title} ${name}!`);
}
greetFlexibly({ title: "Prof." }); // 属性缺省防御成功 -> Hello, Prof. Alice!
greetFlexibly(); // 实参对象整体缺省防御成功 -> Hello, Visitor Alice!
}
2.3 执行上下文与 this 绑定机制
在 JavaScript 中,this 不是静态绑定的,其具体指向在函数执行(调用)阶段根据调用上下文动态确定。
2.3.1 全局上下文与自由函数调用
- 全局作用域:在全局上下文直接评估
this,其指向全局对象(浏览器中为window, Node.js 环境中为global,通用规范下为globalThis)。 - 自由函数调用:在 ECMAScript 规范中,独立执行未显式绑定宿主的标识符调用时(如
func()),this的指向取决于当前是否启用了严格模式:- 非严格模式:引擎在执行函数上下文的
OrdinaryCallBindThis操作时,会将传入的undefined或null显式替换为全局对象globalThis。 - 严格模式 (
"use strict"):this严格保持为undefined。
- 非严格模式:引擎在执行函数上下文的
2.3.2 方法调用与“引用记录(Reference Record)”丢失风险
当函数作为对象的方法被调用时(如 obj.method()),this 指向该点号前的对象 obj。
底层结构与风险:
JavaScript 引擎在解析 obj.method() 时,属性访问运算符会返回一个 ECMAScript 规范内部专有的 Reference Record (引用记录) 结构,可抽象表达为 [[Base]]: obj, [[ReferencedName]]: "method", [[Strict]]: true。
- 如果紧随其后的是小括号
()触发调用,引擎会直接提取该结构的[[Base]]字段并将其赋予执行上下文的this。 - 如果将该方法提取赋值给另一个变量(如
let f = obj.method),或者作为参数传递,会触发内部的GetValue抽象操作。该操作会丢弃 Reference Record 结构,仅返回纯粹的函数对象引用,导致[[Base]]信息丢失,后续调用回退到自由函数调用的逻辑。
{
console.log("--- Reference Record 丢失测试 ---");
"use strict"; // 开启严格模式以观察稳态表现
const user = {
name: "John",
hi() { console.log(this ? `This 寻址成功: ${this.name}` : "This 丢失,变为 undefined"); }
};
user.hi(); // 点号运算符直接触发调用,保留基准对象 -> This 寻址成功: John
let hi = user.hi; // 赋值操作导致隐式引用记录解构,丢失宿主对象引用
try {
hi();
} catch (e) {
console.log(`独立调用赋值后的函数触发异常: ${e.name} - ${e.message}`); // TypeError
}
// 使用硬绑定锁定上下文
let safeHi = user.hi.bind(user);
safeHi(); // 硬绑定成功 -> This 寻址成功: John
}
2.3.3 显式绑定与硬绑定
为了控制或修正函数的 this 上下文,可利用 Function 原型上的三种方法:
call(thisArg, ...args):立即执行该函数,显式指定this为thisArg,后续参数以逗号分隔传入。apply(thisArg, argsArray):立即执行该函数,显式指定this为thisArg,后续参数以数组或类数组形式传入。bind(thisArg, ...args):不立即执行,而是返回一个全新的绑定函数对象(Exotic Bound Function Object)。该新函数内部锁定了this上下文为thisArg,并支持预设部分参数(柯里化/Currying)。
2.4 返回值约束与控制流
- 默认隐式返回:如果函数没有写明
return语句,或者仅书写了不带表达式的空return;,函数在执行完毕后会默认向调用者返回undefined。 - ASI (自动分号插入) 的换行限制: JavaScript 引擎会在单行
return关键字后自动插入分号。因此,不能在return关键字与待返回的表达式之间换行。若表达式过长需要换行,需使用小括号包裹表达式以表明连续性。
{
console.log("--- ASI 自动分号插入测试 ---");
function badReturn() {
return // 引擎在此处自动补全分号变更为 return; 导致后续代码变为无法触达的语句
{ name: "John" };
}
console.log(`受 ASI 错误截断的返回值: ${badReturn()}`); // undefined
function goodReturn() {
return ( // 使用括号显式声明表达式连续性,阻止 ASI 插入
{ name: "John" }
);
}
console.log(`正确换行的对象返回值: ${JSON.stringify(goodReturn())}`); // {"name":"John"}
}
2.5 高阶函数(Higher-Order Functions)
在 JavaScript 中,函数是一等公民(First-Class Citizens),本质上属于特殊的对象。这意味着函数可以被作为值进行存储、作为实参传递给其他函数(回调函数),或者作为其他函数的返回值。
{
console.log("--- 高阶函数基本测试 ---");
function repeat(n, action) {
for (let i = 0; i < n; i++) action(i);
}
repeat(3, (index) => console.log(`Action trigger index: ${index}`));
}
2.5.1 定时器嵌套延迟限制
宿主环境提供了 setTimeout 与 setInterval 实现异步调度。
- 底层嵌套约束:根据 HTML 标准,当定时器嵌套层级大于 5 (即处于第 6 次及以上的嵌套回调),且设定的延迟时间小于 4 毫秒 时,引擎会自动强制将延迟提升至 4 毫秒(4ms minimum delay)。
- 零延迟定时器:
setTimeout(..., 0)并不意味着立即同步执行,而是将其作为异步宏任务(MacroTask)排入事件循环队列,等待当前同步调用栈彻底清空、微任务队列清空后才会被触发。
{
console.log("--- 零延迟定时器宏任务排队测试 ---");
setTimeout(() => console.log("异步宏任务执行"), 0);
console.log("同步栈清空前"); // 优先打印
}
2.5.2 函数装饰器与柯里化
- 装饰器模式(Decorator Pattern):一种高阶函数技术,用于在不修改原函数的前提下,包装函数以实现状态暂存(Memoization)或控制转发。
- 柯里化(Currying):将一个接收多参数的函数,转换为一系列接收单参数的连续调用结构。
{
console.log("--- 函数装饰器与柯里化测试 ---");
// 1. 缓存装饰器实现(Memoization)
function memoize(func) {
const cache = new Map();
return function(x) {
if (cache.has(x)) return cache.get(x);
const result = func.call(this, x);
cache.set(x, result);
return result;
};
}
const slowSquare = (x) => x * x;
const fastSquare = memoize(slowSquare);
console.log(`装饰器首次计算结果: ${fastSquare(5)}`);
console.log(`装饰器二次命中缓存: ${fastSquare(5)}`);
// 2. 通用自动化柯里化转换器
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(`全参数组合调用: ${curriedSum(1, 2, 3)}`);
console.log(`分步柯里化链式调用: ${curriedSum(1)(2)(3)}`);
}
2.6 词法环境(Lexical Environment)与闭包机制
闭包(Closure)是 JavaScript 基于词法作用域设计衍生出的必然结果。理解闭包必须深入到引擎的底层运行时规范。
2.6.1 词法环境的基本理论
在 JavaScript 中,每个运行中的函数、代码块({...})以及脚本本身,都有一个与之关联的内部隐藏对象,称为词法环境 (Lexical Environment)。
一个词法环境对象由两个部分构成:
- 环境记录 (Environment Record):在函数作用域下特化为函数环境记录(Function Environment Record),专门存储当前作用域内所有局部变量、形参、
this绑定以及函数声明的内部对象。 - 外部词法环境引用 (Outer Lexical Environment Reference):指向其外层父级词法环境的指针,建立了作用域链的物理基础。
2.6.2 函数属性 [[Environment]] 与闭包本质
- 创建阶段:当一个函数在某个词法环境中被声明或创建时,它会获得一个隐藏的、不可直接更改的属性,命名为
[[Environment]]。该属性永久性地保存了它被创建时所处的外部词法环境的引用。 - 执行阶段:当该函数被调用时,引擎会为此次执行自动创建一个全新的函数词法环境。这个全新词法环境的“外部词法环境引用”正是直接从该函数的
[[Environment]]属性中复制而来的。
闭包的计算机定义: 闭包是指一个函数能够记住并持续访问其外部词法环境的能力,即使该函数在其原有的词法作用域之外被触发调用。
{
console.log("--- 闭包运行时状态持久化测试 ---");
function makeCounter() {
let count = 0; // 该变量保存在 makeCounter 的函数环境记录中
// 返回的内部函数通过 [[Environment]] 维持对上级词法环境的物理引用
return function() {
return count++;
};
}
let counter1 = makeCounter();
let counter2 = makeCounter(); // 每次调用 makeCounter 都会生成全新的独立词法环境
console.log(`Counter1 第一次评估: ${counter1()}`); // 0
console.log(`Counter1 第二次评估: ${counter1()}`); // 1
console.log(`Counter2 独立评估: ${counter2()}`); // 0 (环境记录在堆内存中相互隔离)
}
2.6.3 内存管理与引擎优化
- 环境生存期:正常情况下,函数执行完毕后其执行上下文退栈,相关的内部变量随词法环境的孤立而被回收。但如果该函数内部创建并返回了一个新函数(闭包),且这个新函数依然被外部变量引用,那么新函数的
[[Environment]]属性就会持续维持对老函数词法环境的物理引用,导致老函数的词法环境无法被垃圾回收机制(GC)清理,从而实现了状态的持久化。 - 引擎优化(静态作用域分析):现代高效引擎(如 V8)在编译期会对闭包进行静态作用域分析。如果一个外部局部变量在内部所有闭包中均未被引用,引擎会将其留在栈上空间(随外层退栈而销毁),而不会将其捕获到堆内存的
Context对象中。这导致了一个调试现象:在浏览器 DevTools 中下断点调试内部闭包时,那些在外层存在但未在内部闭包中使用的变量会显示为不可查看(无法通过作用域链寻址)。