初探函数使用
文章目录
- 一、函数基础
- (一)函数类型定义
- 1. 函数类型的基础定义方式
- (1)函数变量添加类型注解
- (2)使用接口来定义函数类型
- (3)使用类型别名定义函数类型
- 2. 参数类型的详细定义
- (1)必需参数
- (2)可选参数
- (3)默认参数
- (4)剩余参数
- 3. 返回值类型的定义
- (1)显式指定返回值类型
- (2)void 返回值类型
- (3)never 返回值类型
- 4. 函数类型的应用场景
- (1)作为参数类型
- (2)作为返回值类型
- (二)函数重载
- 1. 为什么需要函数重载
- 2. 函数重载的语法
- 3. 重载签名 vs 实现签名
- 4. 重载签名的匹配规则
- 5. 常见应用场景
- 5.1 参数可选 / 数量不同
- 5.2 返回不同类型
- 6. 与联合类型的对比
- 7. 注意事项
- (三)泛型函数
- 1. 泛型函数的基本概念
- (1)基础语法
- (2)调用方式
- 2. 泛型类型变量的命名规范
- 3. 泛型约束
- (1)使用接口实现约束
- (2)多个类型变量的约束
- 4. 泛型函数与重载
- 2. **选择策略**
- 5. 泛型函数与非泛型函数的对比
- (1)非泛型函数
- 类型安全失效
- (2)泛型函数
- 保留类型信息
- 支持任意类型
- 灵活应用
- 6. 泛型默认类型参数
- 7. 注意事项
- (1)避免过度使用泛型
- (2)确保类型变量使用正确
- (四)高阶函数
- 1. 高阶函数的类型定义
- (1)函数作为参数
- (2)函数作为返回值
- 2. 泛型高阶函数
- (1)泛型函数作为参数
- (2)泛型函数作为返回值
- 3. 高阶函数的应用场景
- (1)数组处理
- (2)异步操作
- (3)函数装饰器
- (4)柯里化
- 4. 高阶函数的类型约束
- 5. 高阶函数的实践
- (1)明确类型定义
- (2)使用泛型提高复用性
- (3)避免过度使用
- (五)this参数
- 1. 全局作用域中的 `this`(普通函数调用)
- 2. 方法调用
- **2.1 实例方法中的 `this`**
- **2.2 静态方法中的 `this`**
- 3. `call`、`apply` 和 `bind` 方法
- (1)显式声明 `this` 类型
- (2)使用箭头函数
- (3)使用类语法
- 4.函数参数的`this`
- **4.1类型为`void `的`this`**
- **4.2类型为非`void `的`this`**
- 5. 箭头函数
- **5.1实例方法中的 `this`**
- **5.2对象中的 `this`**
- 6. `this` 的类型注解与常见问题
- 6.1 类方法的 `this` 类型
- 7. 实践
- 7.1 使用箭头函数保留上下文
- 7.2 在构造函数中绑定方法
- **5. 常见陷阱与解决方案**
- **5.1 回调函数中的 `this` 丢失**
- **1. 问题示例:`this` 丢失**
- **2. 解决方案一:使用箭头函数**
- **3. 解决方案二:使用 `bind` 方法**
- **4. 在构造函数中绑定**
- **5.2 静态方法误用 `this`**
- (六)回调函数
- **1. 核心定义与特点**
- **定义**
- **本质**
- **2. 回调函数的常见形式**
- **2.1 同步回调**
- **2.2 异步回调**
- 3. 回调函数的参数类型
- (1)可选参数
- (2)泛型回调
- 4. 回调函数的返回值类型
- (1)void 返回值
- (2)非 void 返回值
- 5. 回调函数的实际应用
- (1)数组方法中的回调
- **(2)异步编程**
- **(3)函数组合与抽象**
- 6. 回调函数的最佳实践
- (七)函数的作用域
- 1. 全局作用域(Global Scope)
- 示例:
- 2. 函数作用域(Function Scope)
- 示例:
- 3. 块级作用域(Block Scope)
- 示例:
- 4. 闭包(Closure)
- 示例:
- 5. 词法作用域(Lexical Scope)
- 示例:
- 6. 箭头函数与作用域
- 示例:
- 7. 作用域链(Scope Chain)
- 8. 变量提升(Hoisting)
- 9. TypeScript 特有的作用域规则
- (1)模块作用域
- (2)命名空间(Namespace)
- (八)函数的闭包
- 1. 闭包的基本概念
- 2. 闭包的作用
- (1)数据私有化
- (2)记忆函数
- 3. 闭包与箭头函数
- 4. 闭包与作用域链
- 5. 闭包的注意事项
- (1)内存管理
- (2)作用域混淆
一、函数基础
(一)函数类型定义
在 TypeScript 里,函数类型定义能让你清晰地规定函数的参数类型以及返回值类型,进而增强代码的可靠性与可维护性。
1. 函数类型的基础定义方式
(1)函数变量添加类型注解
这段代码定义了一个函数类型的变量 add
,它接收两个 number
类型的参数,返回值也是 number
类型。随后,将一个符合该类型定义的函数赋值给 add
变量。
let add: (x: number, y: number) => number;add = function(a: number, b: number) {return a + b;
};
(2)使用接口来定义函数类型
借助接口,也能对函数类型进行定义:
interface AddFunction {(x: number, y: number): number;
}let add: AddFunction = function(a, b) {return a + b;
};
这里,AddFunction
接口把函数的形状给定义好了,要求函数必须接收两个 number
类型的参数,并且返回 number
类型的值。
(3)使用类型别名定义函数类型
类型别名同样可用于函数类型的定义:
type AddFunction = (x: number, y: number) => number;let add: AddFunction = (a, b) => a + b;
AddFunction
类型别名和前面接口的作用是一样的,都对函数的参数类型和返回值类型做了约束。
2. 参数类型的详细定义
(1)必需参数
在调用函数时,必须为这些参数提供值:
function greet(name: string): string {return `Hello, ${name}`;
}greet("Alice"); // 正确
// greet(); // 错误,缺少必需参数
(2)可选参数
在参数名后面加上 ?
,将其设置为可选参数:
function greet(name?: string): string {return name ? `Hello, ${name}` : "Hello";
}greet(); // 正确,返回 "Hello"
greet("Bob"); // 正确,返回 "Hello, Bob"
需要注意的是,可选参数必须放在必需参数的后面。
(3)默认参数
为参数设置默认值后,在调用函数时如果没有为该参数传值,就会使用这个默认值:
function greet(name: string = "Guest"): string {return `Hello, ${name}`;
}console.log(greet()); // 返回 "Hello, Guest"
console.log(greet("Eve")); // 返回 "Hello, Eve"
(4)剩余参数
剩余参数可以用 ...args: Type[]
的形式来表示,它允许函数接收任意数量的同类型参数:
function sum(...numbers: number[]): number {return numbers.reduce((ans, num) => ans + num, 0);
}sum(1, 2, 3); // 返回 6
sum(1, 2, 3, 4); // 返回 10
3. 返回值类型的定义
(1)显式指定返回值类型
明确地为函数指定返回值类型:
function getFullName(firstName: string, lastName: string): string {return `${firstName} ${lastName}`;
}
(2)void 返回值类型
当函数没有返回值时,使用 void
类型:
function logMessage(message: string): void {console.log(message);
}
(3)never 返回值类型
如果函数永远不会正常结束执行,就使用
never
类型。比如抛出异常的函数:function throwError(message: string): never { throw new Error(message); }
4. 函数类型的应用场景
(1)作为参数类型
函数类型可以作为其他函数的参数类型:
function applyOperation(a: number, b: number, operation: (x: number, y: number) => number): number {return operation(a, b);
}let result = applyOperation(3, 4, (x, y) => x * y); // 结果为 12console.log(result);
(2)作为返回值类型
函数类型也能作为其他函数的返回值类型:
function createAdder(base: number): (num: number) => number {return (nums: number): number => base + nums;
}let add5 = createAdder(5);
add5(3); // 结果为 8
(二)函数重载
函数重载(Function Overloading)是 TypeScript 中允许一个函数接受不同参数类型和数量,并返回不同类型值的特性。它通过为同一个函数提供多个调用签名来实现类型安全的多态行为。
1. 为什么需要函数重载
- 静态类型检查:TypeScript 要求函数参数和返回值类型明确。
- 多态需求:同一个函数可能需要处理不同类型的输入。
- 代码可读性:避免为相似功能创建多个函数名。
2. 函数重载的语法
TypeScript 的函数重载由两部分组成:
- 重载签名:定义多个函数类型(参数和返回值)。
- 实现签名:实际实现函数逻辑的单一版本。
// 重载签名(多个)
function add(a: number, b: number): number;
function add(a: string, b: string): string;// 实现签名(一个)
function add(a: number | string, b: number | string): number | string {if (typeof a === 'number' && typeof b === 'number') {return a + b; // 数字相加} else if (typeof a === 'string' && typeof b === 'string') {return a + b; // 字符串拼接}throw new Error("参数类型不匹配");
}// 使用示例
const numResult = add(1, 2); // 返回 number
const strResult = add("a", "b"); // 返回 string
// 函数重载的调用必须严格匹配重载签名(overload signatures)中的某一个,而不考虑函数的实现签名(implementation signature)。
// add(1, "b"); // 错误:重载签名不允许此调用
3. 重载签名 vs 实现签名
- 重载签名:
- 只声明函数的参数和返回值类型。
- 可以有多个,用于描述不同的调用方式。
- 不包含函数体。
- 实现签名:
- 必须兼容所有重载签名的类型。
- 参数类型需为所有重载参数的联合类型(如
number | string
)。 - 返回值类型需为所有重载返回值的联合类型。
4. 重载签名的匹配规则
TypeScript 会按重载签名的声明顺序依次匹配:
split()
方法用于将字符串按指定的分隔符(separator)分割成子字符串数组。语法:str.split(separator, limit);
separator
:指定分割位置的字符串或正则表达式。limit
(可选):限制返回数组的最大长度。
分隔符 示例 结果 ''
"hello".split('')
["h", "e", "l", "l", "o"]
','
"a,b,c".split(',')
["a", "b", "c"]
' '
"hello world".split(' ')
["hello", "world"]
''
(含空格)"a b c".split(' ')
["a", "", "b", "", "c"]
正则 /\s+/
"a b\tc".split(/\s+/)
["a", "b", "c"]
function reverse(str: string): string;
function reverse(arr: any[]): any[];
function reverse(arg: string | any[]): string | any[] {return typeof arg === 'string'? arg.split('').reverse().join('') // 反转字符串: arg.slice().reverse(); // 反转数组
}reverse("hello"); // 返回 "olleh"
reverse([1, 2, 3]); // 返回 [3, 2, 1]
// reverse(123); // 错误:没有匹配的重载签名
5. 常见应用场景
5.1 参数可选 / 数量不同
// 重载签名:0个参数(返回默认值)
function add(): number;
// 重载签名:1个参数(返回参数本身)
function add(a: number): number;
// 重载签名:2个参数(返回两数之和)
function add(a: number, b: number): number;
// 重载签名:3个参数(返回三数之和)
function add(a: number, b: number, c: number): number;// 实现签名(使用可选参数)
function add(a?: number, b?: number, c?: number): number {if (a === undefined) return 0; // 无参数if (b === undefined) return a; // 1个参数if (c === undefined) return a + b; // 2个参数return a + b + c; // 3个参数
}// 使用示例
console.log(add()); // 0
console.log(add(5)); // 5
console.log(add(3, 4)); // 7
console.log(add(1, 2, 3)); // 6
5.2 返回不同类型
function parseDate(input: string): Date;
function parseDate(input: number): Date;
function parseDate(input: string | number): Date {return typeof input === 'string'? new Date(input): new Date(input);
}
6. 与联合类型的对比
函数重载 vs 联合类型参数:
// 使用重载
function process(input: string): string;
function process(input: number): number;
function process(input: string | number): string | number {// 必须在函数体内手动区分类型return input;
}// 使用联合类型参数(非重载)
function process2(input: string | number): string | number {// 无需重载,但调用者无法获得精确类型提示return input;
}
选择原则:
- 当函数逻辑因参数类型而异时,使用重载。
- 当函数逻辑不依赖参数类型时,使用联合类型。
总的来说,重载适用于函数根据不同参数类型有不同的处理逻辑的情况,而联合类型适用于函数对不同类型参数可以进行统一处理的情况。
7. 注意事项
-
实现签名需兼容所有重载:
function fn(x: string): number; function fn(x: number): string; function fn(x: string | number): string | number {// 必须处理两种类型return typeof x === 'string' ? x.length : x.toString(); }
-
重载签名顺序很重要:
function fn(x: any): any; // 错误:覆盖了其他重载,使其无法访问 function fn(x: string): number;
-
箭头函数无法重载:
// 错误:箭头函数不能有重载签名 const fn = (x: string) => x.length;
(三)泛型函数
在 TypeScript 里,泛型函数是一种非常强大的特性,它能让函数在保持类型安全的同时,灵活处理多种数据类型。
1. 泛型函数的基本概念
泛型函数的核心在于使用类型变量(通常用 T
表示),这个类型变量可以捕获调用函数时传入的类型参数,从而让函数在不同的调用场景下处理不同的类型,同时保证类型的一致性。
(1)基础语法
function identity<T>(arg: T): T {return arg;
}
这里的 <T>
就是类型变量,它代表一个未知的类型。在函数内部,参数 arg
的类型是 T
,返回值的类型同样是 T
,这就保证了函数输入和输出的类型是一致的。
(2)调用方式
// 方式一:显式指定类型参数
let output1 = identity<string>("myString");// 方式二:利用类型推断自动确定类型参数
let output2 = identity(100);
2. 泛型类型变量的命名规范
虽然类型变量通常用 T
来表示,但你也可以使用更具描述性的名称,这样能让代码的含义更加清晰:
function loggingIdentity<Type>(arg: Type): Type {console.log(arg.length); // 这里会报错,因为并非所有类型都有 length 属性return arg;
}
如果要操作数组,可以这样写:
function loggingIdentity<Type>(arg: Type[]): Type[] {console.log(arg.length); // 数组肯定有 length 属性,所以不会报错return arg;
}
或用 TypeScript 中的泛型约束语法,用于限制泛型类型必须包含 length
属性(类型为 number
)。
function loggingIdentity<Type extends {length: number}>(arg: Type): Type {console.log(arg.length); // 这里会报错,因为并非所有类型都有 length 属性return arg;
}//console.log(loggingIdentity(1));//报错,1没有length属性
3. 泛型约束
有时候你可能需要对类型变量进行一些限制,确保它符合某些条件,这时候就需要用到泛型约束。
(1)使用接口实现约束
interface Lengthwise {length: number;
}function loggingIdentity<T extends Lengthwise>(arg: T): T {console.log(arg.length); // 因为有约束,所以可以安全访问 length 属性return arg;
}loggingIdentity({ length: 10, value: 3 }); // 正确
loggingIdentity([1, 2, 3, 4, 5]);
// loggingIdentity(3); // 错误,数字类型没有 length 属性
(2)多个类型变量的约束
keyof T
:- 返回类型
T
的所有公共属性名组成的联合类型。 - 例如,若
T
是{ a: number; b: string }
,则keyof T
是'a' | 'b'
。
- 返回类型
K extends keyof T
:- 约束泛型
K
必须是T
的某个属性名(即K
必须属于keyof T
的联合类型)。 - 确保
key
参数是obj
对象实际存在的键。
- 约束泛型
function getProperty<T, K extends keyof T>(obj: T, key: K) {return obj[key];
}let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(getProperty(x, "a")); // 正确
// getProperty(x, "m"); // 错误,"m" 不是 x 对象的键const user = { name: "Alice", age: 30 };
console.log(getProperty(user, "name")); // ✅ 类型为 string
// const email = getProperty(user, "email"); // ❌ 类型错误
4. 泛型函数与重载
泛型函数适用于类型参数化的场景,而函数重载适用于固定类型的多种实现:
泛型实现:
function identity<T>(arg: T): T {return arg;
}
重载实现:
function identity(arg: string): string;
function identity(arg: number): number;
function identity(arg: any): any {return arg;
}
2. 选择策略
- 当函数逻辑完全相同时,优先使用泛型。
- 当函数针对不同类型有不同实现时,使用重载。
5. 泛型函数与非泛型函数的对比
(1)非泛型函数
function identity(arg: any): any {return arg;
}let output = identity("myString"); // 返回 any 类型,丢失了类型信息
//可能在后续代码中引入类型错误(如 output.toUpperCase() 可能意外调用不存在的方法)。
类型安全失效
let result = identity(123);
result.split(','); // ❌ 运行时错误(number 没有 split 方法),但 TypeScript 不会报错,any绕过了类型检查,允许任何操作。
(2)泛型函数
function identity<T>(arg: T): T {return arg;
}let output = identity("myString"); // 返回 string 类型,保留了类型信息
保留类型信息
let num = identity(123); // num 类型为 number
num.toFixed(2); // ✅ 类型安全
// num.split(','); // ❌ TypeScript 报错:number 没有 split 方法
支持任意类型
identity(true); // boolean 类型
identity([1, 2, 3]); // number[] 类型
identity({ id: 1 }); // { id: number } 类型
灵活应用
// 手动指定类型(通常不需要)
identity<Date>(new Date()); // 等价于 identity(new Date())
6. 泛型默认类型参数
你可以为泛型类型参数设置默认值:
function createArray<T = string>(length: number, value: T): T[] {let result: T[] = [];for (let i = 0; i < length; i++) {result[i] = value;}return result;
}let arr = createArray(3, "x"); // 默认类型为 string
let arr2 = createArray(3, 1);
console.log(arr);
console.log(arr2);
7. 注意事项
(1)避免过度使用泛型
如果一个函数只处理单一的具体类型,就没有必要使用泛型:
// 这种情况不需要泛型
function greet(name: string): string {return `Hello, ${name}`;
}
(2)确保类型变量使用正确
要保证类型变量在函数中被正确使用,避免出现无意义的泛型:
// 无意义的泛型,T 没有被使用
function printHello<T>(name: string): void {console.log("Hello, " + name);
}
(四)高阶函数
在 TypeScript 中,高阶函数(Higher-Order Function)的概念与 JavaScript 一致,但 TypeScript 为其提供了更严格的类型系统,使高阶函数的使用更加安全和可靠。
在编程中,高阶函数(Higher-Order Function) 是指满足以下任意一个条件的函数:
- 接受一个或多个函数作为参数
- 返回一个函数作为结果
高阶函数的核心是将函数视为一等公民(First-Class Citizen),允许它们像普通数据一样被传递和操作。这种特性是 函数式编程 的基础,在 JavaScript、TypeScript、Python 等多种语言中广泛应用。
高阶函数的作用:
- 抽象和复用逻辑:将通用行为封装在高阶函数中。
- 创建可配置的函数:通过参数定制函数行为。
- 实现异步控制流:如回调、Promise、async/await。
- 简化代码:减少重复,提高可读性。
1. 高阶函数的类型定义
(1)函数作为参数
// 定义一个接受函数作为参数的高阶函数
function applyOperation(a: number, b: number, operation: (x: number, y: number) => number): number {return operation(a, b);
}// 使用示例
const sum = applyOperation(3, 4, (x, y) => x + y); // 类型为 number
const product = applyOperation(3, 4, (x, y) => x * y); // 类型为 number
(2)函数作为返回值
// 定义一个返回函数的高阶函数
function createAdder(base: number): (num: number) => number {return (num: number) => base + num;
}// 使用示例
const add5 = createAdder(5);
console.log(add5(3)); // 输出 8,类型为 number
2. 泛型高阶函数
泛型可以让高阶函数处理多种类型的数据,同时保持类型安全:
(1)泛型函数作为参数
function processItems<T>(items: T[], callback: (item: T) => void): void {
//array.forEach(callback(currentValue, index, array), thisArg);items.forEach(callback);
}// 使用示例
processItems([1, 2, 3], (num) => console.log(num.toFixed(2))); // 处理数字
processItems(["a", "b"], (str) => console.log(str.toUpperCase())); // 处理字符串
(2)泛型函数作为返回值
function createMapper<T, U>(mapper: (item: T) => U): (items: T[]) => U[] {return (items: T[]) => items.map(mapper);
}// 使用示例
const stringToLength = createMapper((str: string) => str.length);
console.log(stringToLength(["apple", "banana"])); // 输出 [5, 6],类型为 number[]
3. 高阶函数的应用场景
(1)数组处理
TypeScript 的数组方法(如 map
、filter
、reduce
)都是泛型高阶函数:
高阶函数在数组处理中广泛应用,通过将逻辑抽象为参数化函数,实现代码复用和声明式编程。常见应用包括:
- 映射(Map):将数组每个元素通过函数转换为新值,生成新数组。例如:对所有元素乘以 2、提取对象特定属性。
- 过滤(Filter):根据条件筛选元素,保留符合条件的元素。例如:筛选偶数、查找长度超过 5 的字符串。
- 归约(Reduce):累积计算数组元素,将数组合并为单个值。例如:求和、查找最大值、对象数组属性合并。
- 排序(Sort):通过自定义比较函数实现灵活排序。例如:按对象属性排序、倒序排列。
优势:避免手动循环,代码更简洁、易读,减少错误。
const numbers: number[] = [1, 2, 3, 4];// map 方法的类型定义
const doubled: number[] = numbers.map((num: number) => num * 2);// filter 方法的类型定义
const evens: number[] = numbers.filter((num: number) => num % 2 === 0);// reduce 方法的类型定义
const sum: number = numbers.reduce((acc: number, num: number) => acc + num, 0);
(2)异步操作
高阶函数是处理异步逻辑的关键工具,通过回调、Promise、async/await 等机制实现非阻塞编程:
- 回调函数:将函数作为参数传递给异步操作,在操作完成后执行。例如:网络请求完成后处理数据、文件读取回调。
- Promise 封装:高阶函数可封装异步操作并返回 Promise,支持链式调用。例如:封装 setTimeout 为 delay 函数。
- 异步控制流:实现重试机制、并发限制、串行执行等复杂流程。例如:自动重试失败的请求、限制同时执行的任务数量。
- 事件监听:将回调函数注册到事件,事件触发时执行。例如:DOM 事件处理、消息队列订阅。
优势:解决回调地狱问题,使异步代码更清晰、可维护。
(3)函数装饰器
函数装饰器是一种特殊的高阶函数,用于增强或修改函数行为,而不改变原函数代码:
- 日志记录:自动记录函数调用时间、参数、返回值。例如:性能监控、操作审计。
- 权限验证:在函数执行前检查权限,阻止未授权访问。例如:登录验证、角色权限检查。
- 缓存机制:缓存函数结果,避免重复计算。例如:记忆化(Memoization)优化纯函数。
- 错误处理:统一捕获函数异常,提供默认值或重试逻辑。例如:网络请求失败时自动重试。
- 参数校验:在函数执行前验证参数合法性,提前抛出错误。
优势:实现横切关注点(如日志、权限)的复用,遵循单一职责原则。
(4)柯里化
柯里化是将多参数函数转换为一系列单参数函数的技术,每次调用只接收一个参数并返回新函数,直到所有参数被收集完毕:
- 参数复用:固定部分参数,生成新函数。例如:创建专门处理美元的货币转换函数。
- 延迟计算:分阶段收集参数,在需要时统一计算。例如:配置函数在最终调用时生效。
- 动态生成函数:根据不同参数组合生成特定功能的函数。例如:根据环境配置生成不同的日志函数。
- 函数复用:将多元函数转换为一元函数,便于复用和组合。
优势:提高函数灵活性和复用性,适配只接受单参数的场景(如事件处理、数组方法)。
4. 高阶函数的类型约束
你可以使用类型约束来限制高阶函数的参数类型:
interface Lengthwise {length: number;
}function loggingIdentity<T extends Lengthwise>(arg: T): T {console.log(arg.length); // 因为有约束,所以可以安全访问 length 属性return arg;
}// 使用示例
loggingIdentity("hello"); // 正确,字符串有 length 属性
loggingIdentity([1, 2, 3]); // 正确,数组有 length 属性
// loggingIdentity(100); // 错误,数字没有 length 属性
5. 高阶函数的实践
(1)明确类型定义
始终为高阶函数的参数和返回值定义明确的类型:
// 好的做法
function filter<T>(array: T[], predicate: (item: T, index: number, array: T[]) => boolean): T[] {// ...
}// 不好的做法(缺少类型定义)
function filter(array, predicate) {// ...
}
(2)使用泛型提高复用性
泛型可以让高阶函数处理多种类型的数据:
// 泛型版本
function map<T, U>(array: T[], mapper: (item: T) => U): U[] {return array.map(mapper);
}// 非泛型版本(只能处理 number 类型)
function mapNumbers(array: number[], mapper: (item: number) => number): number[] {return array.map(mapper);
}
(3)避免过度使用
虽然高阶函数很强大,但过度使用可能会使代码变得复杂难懂。在使用时,要确保代码的可读性和可维护性。
(五)this参数
在JavaScript 和 TypeScript 中,this
是一个特殊的关键字,它的值在函数调用时动态确定,指向调用函数的对象。this
的行为在不同的函数调用方式(普通函数、方法、构造函数等)下有所不同。
- 全局对象:在全局作用域中,非严格模式下
this
指向全局对象(如浏览器中的window
)。 - 类实例:在类的实例方法中,
this
指向调用该方法的对象。 - 构造函数:在构造函数中,
this
指向新创建的实例。 - 静态方法:在静态方法中,
this
指向类本身。 - 箭头函数:箭头函数不绑定自己的
this
,而是继承外层作用域的this
。
1. 全局作用域中的 this
(普通函数调用)
在普通函数调用中,this
的值取决于函数的调用方式。在非严格模式下,this
指向全局对象(在浏览器中是 window
,在 Node.js 中是 global
);在严格模式下,this
的值为 undefined
。
- ES5:
this
类型为any
,指向全局对象。 - ES6+:
this
类型为undefined
(严格模式)。
function greet() {console.log(this);
}// 非严格模式
greet(); // 在浏览器中,输出 window 对象;在 Node.js 中,输出 global 对象// 严格模式
function strictGreet() {'use strict';console.log(this);
}strictGreet(); // 输出 undefined
2. 方法调用
2.1 实例方法中的 this
在类的实例方法中,this
自动绑定到调用该方法的对象实例。
class Person {name: string;constructor(name: string) {this.name = name;//构造函数中的 this 自动绑定到新创建的 Person 实例(即 alice)。}greet() {console.log(`Hello, ${this.name}`); // this 指向 Person 实例}
}const alice = new Person("Alice");
alice.greet(); // 输出: "Hello, Alice"const greetFn = alice.greet;
greetFn(); // 严格模式下:this → undefined(报错)// 非严格模式下:this → window(浏览器环境)
//当方法被赋值到变量并单独调用时,this的绑定会丢失。
2.2 静态方法中的 this
在静态方法中,this
指向类本身,可以访问类的静态属性和方法。
- 静态属性(如
PI
)是类本身的属性,而非类实例的属性。 - 静态成员可直接通过类名访问(如
Calculator.PI
),无需创建实例。
class Calculator {static PI = 3.14;static calculateCircleArea(radius: number) {return this.PI * radius * radius; // this 指向 Calculator 类}
}console.log(Calculator.calculateCircleArea(5));
与实例方法的对比
若 calculateCircleArea
为实例方法(非静态):
this
将指向调用该方法的实例(需先创建new Calculator()
)。- 实例方法无法直接访问静态属性,需通过类名引用(如
Calculator.PI
)。
3. call
、apply
和 bind
方法
call
和 apply
方法允许显式指定函数调用时的 this
值,它们的区别在于参数的传递方式。bind
方法创建一个新的函数,该函数在调用时 this
值固定。
(1)显式声明 this
类型
interface Context {x: number;
}function add(this: Context, a: number, b: number): number {return this.x + a + b;
}const obj = { x: 1 };//function.call(thisArg, arg1, arg2, ...);
//thisArg:函数执行时 this 指向的对象。
//arg1, arg2, ...:传递给函数的参数(逗号分隔)。
const result1 = add.call(obj, 2, 3); // 6
const result2 = add.apply(obj, [2, 3]); // 6
const result3 = add.bind(obj, 2, 3)(); // 6
function greet(this: any, message: string) {console.log(`${message}, ${this.name}`);
}const person = { name: "Alice" };
greet.call(person, "Hello"); /
function calculate(this: any, a: number, b: number) {return this.operation + (a + b);
}const calculator = { operation: "Result:" };// 使用 apply 调用 calculate 函数,参数以数组形式传递
const result = calculate.apply(calculator, [3, 5]);
console.log(result); // 输出: "Result: 8"/
function sayHi() {console.log(`Hi, ${this.name}`);
}const user = { name: "Bob" };// 使用 bind 创建一个新函数,其 this 始终指向 user
const boundSayHi = sayHi.bind(user);// 调用绑定后的函数
boundSayHi(); // 输出: "Hi, Bob"
(2)使用箭头函数
const greetArrow = () => {console.log(`Hello, ${this.name}`);
};const obj = { name: "Charlie" };// 无效:箭头函数的 this 继承自全局作用域(非严格模式下为 window)
greetArrow.call(obj); // 输出: "Hello, undefined"
(3)使用类语法
this
的类型:- 在类方法中,
this
自动绑定为类的实例类型(即Calculator
)。 - TypeScript 会确保
this.x
存在且类型正确。
- 在类方法中,
class Animal {constructor(public name: string) {}speak(phrase: string) {console.log(`${this.name} says: ${phrase}`);}
}const cat = new Animal("Whiskers");
const dog = new Animal("Buddy");// 使用 call 让 dog 借用 cat 的 speak 方法
cat.speak.call(dog, "Woof!"); // 输出: "Buddy says: Woof!"
4.函数参数的this
TypeScript 允许通过 this
参数显式声明函数的 this
类型,增强类型安全。(仅用于类型检查,不影响实际参数)。
4.1类型为void
的this
在 TypeScript 中,用于禁用函数内部对 this 的访问。这种设计主要用于回调函数或工具函数,确保它们不会意外依赖调用上下文的 this。
const button: UIElement = {// 定义一个计算器函数,禁止使用 this
function calculate(this: void, a: number, b: number): number {// this.a + this.b; // 错误:this 类型为 voidreturn a + b;
}// 安全调用
const result = calculate(1, 2); // 3// 尝试使用 this 调用(TypeScript 编译错误)
// calculate.call({ a: 1, b: 2 }, 1, 2);
// 报错:"类型 '{ a: number; b: number; }' 的参数不能赋给类型 'void' 的参数"
4.2类型为非void
的this
通过 this
参数作为函数的第一个参数
interface Counter {value: number;increment(this: Counter): void;
}const counter: Counter = {value: 0,increment() {this.value++; // 确保 this 类型为 Counter}
};counter.increment();
console.log(counter.value); // 输出: 1
5. 箭头函数
箭头函数没有自己的 this
,它的 this
继承自外层作用域。
5.1实例方法中的 this
class Timer {seconds = 0;start() {setInterval(() => {this.seconds++; // 箭头函数继承 start 方法的 this(Timer 实例)//start() 方法的 this 指向 Timer 实例(因为是通过实例调用的)。//箭头函数 () => { ... } 继承了 start() 方法的 this,因此 this.seconds 正确指向实例的 seconds 属性。console.log(this.seconds);}, 1000);}
}const timer = new Timer();
timer.start(); // 每秒递增 seconds
5.2对象中的 this
在 getName
方法中定义的箭头函数 innerFunction
,其 this
指向 getName
方法的 this
,而 getName
方法作为 person
对象的方法,this
指向 person
对象。所以 innerFunction
中的 this.name
实际上访问的是 person
对象的 name
属性。
const person = {name: 'Bob',getName() {// 箭头函数中的 this 继承自外层的 person 对象const innerFunction = () => {console.log(this.name);};innerFunction();}
};person.getName(); // 输出:Bob
6. this
的类型注解与常见问题
6.1 类方法的 this
类型
在类方法中,this
的类型默认是类实例类型,当一个方法返回 this
时,它返回的是当前对象实例,从而允许链式调用(当子类继承基类的方法时,返回的 this 会自动调整为子类类型)。
class Animal {move(distance: number) {console.log(`Animal moved ${distance}m.`);return this; // 返回 this 实现链式调用}
}class Dog extends Animal {bark() {console.log("Woof!");return this;}
}const dog = new Dog();
dog.move(10).bark(); // 正确:链式调用
7. 实践
7.1 使用箭头函数保留上下文
在需要传递回调函数的场景中,优先使用箭头函数确保 this
指向正确。
class Form {fields = ["name", "email"];//无论submit方法如何被调用,其内部的this始终指向Form实例。submit = () => {// 箭头函数继承 Form 实例的 thisthis.fields.forEach(field => {console.log(`Processing ${field} with ${this}`);});}
}
// 创建 Form 类的实例
const myForm = new Form();// 调用 submit 方法
myForm.submit();const a = myForm.submit();setTimeout(myForm.submit, 100);
7.2 在构造函数中绑定方法
对于需要传递的普通方法,在构造函数中使用 bind
方法绑定 this
。
this.log.bind(this)
:- 左边的
this.log
:指向当前实例的log
属性(初始为原型上的方法)。 - 右边的
this.log
:指向原型链上的原始log
方法(Logger.prototype.log
)。 bind(this)
:创建一个新的绑定函数,将this
永久锁定为当前实例(即logger
),并将这个新函数赋值给实例的log
属性。
- 左边的
- 结果:
实例的log
属性被覆盖为绑定后的新函数,而原型上的原始方法保持不变。
class Logger {prefix: string;constructor(prefix: string) {this.prefix = prefix;// 绑定 log 方法的 thisthis.log = this.log.bind(this);}log(message: string) {console.log(`${this.prefix}: ${message}`);}
}const logger = new Logger("DEBUG");
setTimeout(logger.log, 1000, "Logging..."); // 正确:输出 "DEBUG: Logging..."
5. 常见陷阱与解决方案
5.1 回调函数中的 this
丢失
问题:将方法作为回调传递时,this
指向全局对象或 undefined
。
解决方案:使用箭头函数或 bind
方法。
1. 问题示例:this
丢失
- 方法分离:当你将
logger.log
作为参数传递时,它与logger
实例的绑定被切断,此时this
不再指向logger
。 - 回调函数特性:回调函数通常在新的上下文中执行,
this
的指向由调用方式决定,而非定义位置。
class Logger {prefix = "DEBUG";log(message: string) {console.log(`${this.prefix}: ${message}`);}
}const logger = new Logger();// 错误:将 log 方法单独传递时,this 指向全局对象(非严格模式)或 undefined(严格模式)
//setTimeout接收的是log函数的引用,而非绑定到looger的方法。
setTimeout(logger.log, 1000, "Logging...");
// 输出: "undefined: Logging..."(严格模式)
2. 解决方案一:使用箭头函数
箭头函数继承外层作用域的 this
,确保 this
指向 Logger
实例。
class Logger {prefix = "DEBUG";log(message: string) {console.log(`${this.prefix}: ${message}`);}
}const logger = new Logger();// 正确:箭头函数捕获并保留外层的 this(Logger 实例)
setTimeout(() => logger.log("Logging..."), 1000);
// 输出: "DEBUG: Logging..."
3. 解决方案二:使用 bind
方法
通过 bind
方法创建一个新函数,永久绑定 this
到 Logger
实例。
class Logger {
prefix = "DEBUG";log(message: string) {console.log(`${this.prefix}: ${message}`);
}
}const logger = new Logger();// 正确:使用 bind 绑定 this 到 logger 实例
const boundLog = logger.log.bind(logger);
setTimeout(boundLog, 1000, "Logging...");
// 输出: "DEBUG: Logging..."
4. 在构造函数中绑定
在类的构造函数中使用 bind
,确保所有实例方法的 this
始终指向实例。
class Logger {prefix = "DEBUG";constructor() {// 在构造函数中绑定 log 方法的 thisthis.log = this.log.bind(this);//一旦使用bind绑定了this,就无法再被覆盖,哪怕使用call、apply或者箭头函数也不行。}log(message: string) {console.log(`${this.prefix}: ${message}`);}
}const logger = new Logger();// 安全:无论 log 方法如何被传递,this 都指向 logger 实例
setTimeout(logger.log, 1000, "Logging...");
// 输出: "DEBUG: Logging..."
总结
this
的动态性:TypeScript 中this
的指向取决于函数的调用方式,而非定义位置。- 回调函数陷阱:将方法作为回调传递时,需显式绑定
this
或通过箭头函数捕获上下文。
5.2 静态方法误用 this
问题:在静态方法中使用 this
访问实例属性。
解决方案:静态方法只能访问类的静态成员。
class Person {name: string;gender: number;static age: number = 30;constructor(name: string) {this.name = name;}static printInfo() {// 错误:静态方法中不能访问实例属性//TS2564: Property 'gender' has no initializer and is not definitely assigned in the constructor.console.log(this.gender);// 只能访问静态成员,如 this.age 是可以的console.log(this.age);console.log(this.name);// 如果你的环境(如浏览器控制台或某些工具链)中// 类对象(Person)的 name 属性默认返回类的名称(即 "Person")// 则 this.name 会打印出 "Person"。console.log(Person.name); // 输出 "Person"(类对象的 name 属性)}
}Person.printInfo();
//输出:
//undefined
//Person
//30
//Person
(六)回调函数
在 TypeScript 中,回调函数的概念与 JavaScript 一致,但 TypeScript 为其提供了更严格的类型系统,使回调函数的使用更加安全和可靠。回调函数是编程中的一个核心概念,它本质上是一种函数传递机制。具体来说,就是把一个函数作为参数传递给另一个函数,这个被传递的函数会在某个特定的时刻或者条件下被调用执行。
1. 核心定义与特点
定义
回调函数是一种函数传递模式,其核心特点是:
- 作为参数传递:将一个函数作为参数传递给另一个函数(称为高阶函数)。
- 延迟执行:回调函数不会立即执行,而是在某个条件满足或事件发生时被调用。
本质
回调函数本质上是将代码执行逻辑的控制权委托给另一个函数。常见场景包括:
- 异步操作完成后(如网络请求、定时器)。
- 事件触发时(如点击按钮、数据加载)。
- 循环或递归中的每一步处理。
2. 回调函数的常见形式
2.1 同步回调
在函数执行过程中立即调用的回调函数。
// 数组的 forEach 方法接收回调函数
const numbers = [1, 2, 3];
numbers.forEach((num) => {console.log(num); // 回调函数在遍历到每个元素时立即执行
});
2.2 异步回调
在异步操作完成后调用的回调函数(如定时器、Promise)。
// setTimeout 函数的基本语法是:setTimeout(callback, delay, arg1, arg2, ...),其中:
//callback(回调函数):在 delay 毫秒后要执行的函数。
//delay(延迟时间):指定多少毫秒后执行 callback 函数。
//arg1, arg2, ...(可选):要传递给 callback 函数的参数。
setTimeout(() => {console.log("2秒后执行"); // 回调函数在2秒后执行
}, 2000);
3. 回调函数的参数类型
(1)可选参数
function delayedAction(callback: (message?: string) => void) {setTimeout(() => callback("Action completed"), 1000);
}delayedAction((msg) => console.log(msg.toUpperCase())); // 正确
delayedAction(() => console.log("Done")); // 正确,message 是可选参数
(2)泛型回调
- 回调函数类型:
callback: (item: T) => void
表示传入的回调函数接收一个T
类型的参数- 回调函数的返回值类型为
void
,意味着它不返回任何有意义的值(尽管实际上可以返回任意值,但会被忽略)
function processItems<T>(items: T[], callback: (item: T) => void) {items.forEach(callback);
}processItems([1, 2, 3], (num) => console.log(num.toFixed(2))); // 处理数字
processItems(["a", "b"], (str) => console.log(str.toUpperCase())); // 处理字符串
4. 回调函数的返回值类型
(1)void 返回值
function forEach(items: any[], callback: (item: any) => void) {for (let i = 0; i < items.length; i++) {callback(items[i]);}
}// 回调函数的返回值被忽略
forEach([1, 2, 3], (item) => {console.log(item);return item * 2; // 返回值被忽略 但 TypeScript 允许这样写
});
这种设计主要是为了提高代码的灵活性:
- 兼容性:允许回调函数实现者返回有用的值(如中间结果),即使调用方不需要这些值。
- 函数重载:同一个回调函数可能在不同场景下被不同函数调用,有些调用者可能使用返回值,而有些则忽略。
(2)非 void 返回值
function map<T, U>(items: T[], callback: (item: T) => U): U[] {const result: U[] = [];for (let i = 0; i < items.length; i++) {result.push(callback(items[i]));}return result;
}const squared = map([1, 2, 3], (num) => num * num); // 返回 number[]
5. 回调函数的实际应用
(1)数组方法中的回调
const numbers: number[] = [1, 2, 3, 4];// map 方法的回调
const doubled: number[] = numbers.map((num: number) => num * 2);// filter 方法的回调
const evens: number[] = numbers.filter((num: number) => num % 2 === 0);// reduce 方法的回调
const sum: number = numbers.reduce((acc: number, num: number) => acc + num, 0);
(2)异步编程
// 模拟异步请求
function fetchData(callback: (data: string) => void) {setTimeout(() => {callback("数据加载完成"); // 请求完成后调用回调函数}, 1000);
}fetchData((data) => {console.log(data); // 输出: "数据加载完成"
});
(3)函数组合与抽象
通过高阶函数实现代码复用。
// 自定义高阶函数
function processArray(arr: number[], callback: (num: number) => number) {return arr.map(callback);
}const result = processArray([1, 2, 3], (num) => num * 2);
console.log(result); // 输出: [2, 4, 6]
6. 回调函数的最佳实践
- 明确类型定义:始终为回调函数定义明确的参数类型和返回值类型。
- 使用泛型:当回调函数处理特定类型的数据时,使用泛型提高类型安全性。
- 注意 this 指向:使用箭头函数或显式绑定保持正确的 this 上下文。
回调函数中的 this
可能指向全局对象或 undefined
。
class Form {submit() {setTimeout(function() {console.log(this); // 错误:this 指向全局对象(非严格模式)或 undefined}, 1000);}
}
解决方案:
-
箭头函数:继承外层的this。
setTimeout(() => {console.log(this); // 正确:this 指向 Form 实例 }, 1000);
-
bind 方法:显式绑定this。
setTimeout(function() {console.log(this); // 正确:this 指向 Form 实例 }.bind(this), 1000);
(七)函数的作用域
在 TypeScript 中,函数的作用域(Scope)决定了变量和函数的可见性与生命周期。理解作用域是编写高质量、无错误代码的基础。以下是关于函数作用域的详细介绍:
1. 全局作用域(Global Scope)
- 定义:在所有函数和类外部定义的变量和函数具有全局作用域。
- 特点:
- 可以在代码的任何位置访问。
- 可能导致命名冲突和代码难以维护。
示例:
// 全局变量
const globalVar = 100;function globalFunction() {console.log("这是一个全局函数");
}// 在任何地方都可以访问
console.log(globalVar); // 输出: 100
globalFunction(); // 输出: "这是一个全局函数"
2. 函数作用域(Function Scope)
- 定义:在函数内部定义的变量和函数只在该函数内部可见。
- 特点:
- 使用
var
声明的变量具有函数作用域(而非块级作用域)。 - 不同函数内的同名变量相互独立。
- 使用
示例:
function example() {var localVar = "局部变量";if (true) {var innerVar = "内部变量"; // 仍属于函数作用域}console.log(localVar); // 输出: "局部变量"console.log(innerVar); // 输出: "内部变量"
}console.log(localVar); // 错误:localVar 未定义
3. 块级作用域(Block Scope)
- 定义:使用
let
和const
在代码块(如if
、for
、while
)内声明的变量具有块级作用域。 - 特点:
- 只能在声明它们的块内访问。
- 解决了
var
带来的变量提升问题。
示例:
function blockExample() {if (true) {let blockVar = "块级变量";const constant = 42;}console.log(blockVar); // 错误:blockVar 未定义console.log(constant); // 错误:constant 未定义
}
4. 闭包(Closure)
- 定义:函数及其捕获的外部变量的组合称为闭包。
- 特点:
- 即使外部函数执行完毕,闭包仍能访问外部变量。
- 闭包会保留变量的引用,而非值的副本。这意味着闭包内部访问的变量会反映其外部环境的最新状态。
示例:
function outer() {let count = 0;function inner() {count++; // 闭包捕获了外部的 count 变量console.log(count);}return inner;
}const counter = outer();
counter(); // 输出: 1
counter(); // 输出: 2
5. 词法作用域(Lexical Scope)
- 定义:作用域由代码的书写位置静态决定,而非运行时。
- 特点:
- 内部函数可以访问外部函数的变量,即使外部函数已执行完毕。
- 与动态作用域(如
this
的绑定)形成对比。(有两个对象,通过call方法绑定不同的对象)
示例:
function outer() {const x = 10;//将其词法环境绑定到 outer 的作用域(即使 outer 执行完毕,inner 仍保留对 x 的引用)。function inner() {console.log(x); // 访问外部函数的变量 x}return inner;
}const closure = outer();
//closure 实际是 inner 函数,它仍然可以访问 outer 作用域中的 x。尽管 outer 已执行完毕,但由于 inner 函数形成了闭包(因为它引用了 outer 函数作用域内的变量 x),x 的值不会被立即销毁。
closure(); // 实际上是调用了 inner 函数,输出: 10(即使 outer 已执行完毕)
6. 箭头函数与作用域
- 特点:
- 箭头函数不创建自己的
this
,而是捕获其定义时所在作用域的this
。 - 适合需要保留上下文的场景(如回调函数)。
- 箭头函数不创建自己的
示例:
class Example {private value = 42;getValue = () => {setTimeout(() => {console.log(this.value); // 箭头函数捕获了 Example 实例的 this}, 1000);};
}const instance = new Example();
instance.getValue(); // 输出: 42
7. 作用域链(Scope Chain)
-
定义:当访问一个变量时,TypeScript 会先在当前作用域查找,若找不到则沿作用域链向上查找,直到全局作用域。
-
示例:
const globalVar = "全局变量";function outer() {const outerVar = "外部变量";function inner() {const innerVar = "内部变量";console.log(innerVar); // 直接找到console.log(outerVar); // 沿作用域链找到console.log(globalVar); // 沿作用域链找到}return inner; }const inner = outer(); inner();
8. 变量提升(Hoisting)
-
定义:使用
var
声明的变量会被提升到函数或全局作用域的顶部(这意味着无论var
声明的变量在代码中的位置如何,JavaScript或TypeScript引擎在执行代码之前,会先将var
声明的变量提升到其所在作用域的顶部),但赋值不会提升。 -
示例:
function hoistExample() {console.log(x); // 输出: undefined(变量已提升但未赋值)var x = 10; }// 等价于: function hoistExample() {var x;console.log(x); // undefinedx = 10; }
-
注意:
let
和const
也会被提升,但在声明前访问会导致ReferenceError
(暂时性死区/引用错误)。
9. TypeScript 特有的作用域规则
(1)模块作用域
-
每个
.ts
文件都是一个独立的模块,模块内的变量默认是私有的。 -
使用
export
关键字暴露变量:// utils.ts export const PI = 3.14; // 导出的变量可被其他模块访问const secret = "私有变量"; // 模块内私有
(2)命名空间(Namespace)
-
用于组织代码,避免全局命名冲突(
namespace
(命名空间)是一种用于组织代码、避免命名冲突的机制,它可以将相关的变量、函数、类等组织在一起):namespace MyNamespace {// 定义变量export const message = "Hello, namespace!";// 定义函数export function sayHello() {console.log(message);}// 定义类export class MyClass {constructor() {console.log("MyClass instantiated");}}// 私有成员(外部无法直接访问)const privateVariable = "This is private";function privateFunction() {console.log(privateVariable);} }MyNamespace.sayHello(); // 输出: Hello, namespace! const myObj = new MyNamespace.MyClass(); // 实例化 MyClass // 尝试访问私有成员会报错 MyNamespace.privateFunction(); // 无法访问私有成员
(八)函数的闭包
在 TypeScript 中,闭包是一个非常重要的概念,它涉及到函数和其周围状态的关联。闭包允许函数记住并访问其词法作用域内的变量,即使函数在其原始作用域之外被调用。
闭包的核心特性
- 捕获词法环境:闭包会记住其定义时所在的词法环境(即外部函数的作用域),包括该作用域内的所有变量。
- 延长变量生命周期:即使外部函数执行完毕,其作用域内的变量也不会被垃圾回收,因为闭包仍然引用它们。
- 动态访问变量:闭包捕获的是变量的引用,而非值的副本,因此变量的变化会反映在闭包中。
1. 闭包的基本概念
闭包是指函数和其捕获的外部变量的组合。当一个函数在其词法作用域之外被调用时,它仍然可以访问其定义时所在作用域中的变量。
function outer() {//outer 函数内部创建了变量 count 和函数 innerlet count = 0;function inner() {count++;console.log(count);}return inner;
}const closure = outer();
closure(); // 输出: 1
closure(); // 输出: 2
- 词法作用域:
inner
函数在定义时(即在outer
内部),其作用域链包含outer
的变量(如count
)。 - 闭包的形成:当
inner
函数被返回并在其他地方调用时,它携带了整个词法环境(即outer
的作用域),即使outer
已执行完毕。
在上述代码中,inner
函数形成了一个闭包。inner
函数可以访问 outer
函数作用域内的 count
变量,即使 outer
函数已经执行完毕,count
变量的值仍然被 inner
函数记住。
2. 闭包的作用
(1)数据私有化
闭包可以用来实现数据的私有化,外部无法直接访问闭包内部的变量。
function createCounter() {let value = 0;return {increment() {value++;},getValue() {return value;}};
}const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 输出: 1
在这个例子中,value
变量被封装在闭包中,外部只能通过返回的对象方法来操作 value
,实现了数据的私有化。
(2)记忆函数
闭包可以用来创建记忆函数,记住之前的计算结果。
function memoize(func: (n: number) => number): (n: number) => number {const cache = new Map<number, number>();// 用于存储已计算的结果return function(arg: number): number {// 返回一个闭包函数if (cache.has(arg)) { // 如果缓存中已有结果return cache.get(arg) as number;// 直接从缓存中返回}// 否则计算结果并缓存const result: number = func(arg);cache.set(arg, result);return result;};
}function factorial(n: number): number {return n === 0 ? 1 : n * factorial(n - 1);
}const memoizedFactorial = memoize(factorial);
console.log(memoizedFactorial(5));
console.log(memoizedFactorial(5)); // 从缓存中获取结果,提高性能
memoize
函数返回的新函数形成了闭包,记住了之前的计算结果,避免了重复计算。
3. 闭包与箭头函数
箭头函数也可以形成闭包,但箭头函数本身不创建自己的 this
,它捕获其定义时所在作用域的 this
。
class Example {private value = 42;getValue = () => {setTimeout(() => {console.log(this.value); // 箭头函数捕获了 Example 实例的 this}, 1000);};
}const instance = new Example();
instance.getValue(); // 输出: 42
在这个例子中,getValue
箭头函数形成了闭包,并且捕获了 Example
实例的 this
,即使在 setTimeout
回调中也能正确访问 value
。
4. 闭包与作用域链
闭包依赖于作用域链,当函数访问一个变量时,会先在自己的作用域中查找,如果找不到则沿着作用域链向上查找。
function outer() {let outerVar = "外部变量";function inner() {let innerVar = "内部变量";console.log(innerVar); // 找到内部变量console.log(outerVar); // 沿着作用域链找到外部变量}return inner;
}const closure = outer();
closure();
在这个例子中,inner
函数可以访问其自身作用域内的 innerVar
和外部作用域的 outerVar
,这是通过作用域链实现的。
5. 闭包的注意事项
(1)内存管理
闭包会保留对外部变量的引用,如果不当使用,可能会导致内存泄漏。
function createLargeArray() {const largeArray = new Array(1000000).fill(0);return function() {// 即使 largeArray 不再被直接引用,闭包仍然持有对它的引用console.log(largeArray.length);};
}const closure = createLargeArray();
// 闭包保持对 largeArray 的引用,导致内存占用
在这个例子中,createLargeArray
函数返回的闭包保持了对 largeArray
的引用,即使 createLargeArray
执行完毕,largeArray
也不会被垃圾回收,可能导致内存占用过高。
(2)作用域混淆
过多的闭包嵌套可能会导致作用域混淆,使代码难以理解和维护。
inner
函数可以访问自己作用域内的变量 c
,同时也可以访问其外部作用域(middle
函数的作用域)中的变量 b
,以及更外层作用域(outer
函数的作用域)中的变量 a
。
function outer() {let a = 1;function middle() {let b = 2;function inner() {let c = 3;console.log(a); // 访问外部作用域的 aconsole.log(b); // 访问中间作用域的 bconsole.log(c); // 访问内部作用域的 c}return inner;}return middle;
}const closure = outer()();
closure();
在这个例子中,多层闭包嵌套使得变量的作用域变得复杂,增加了代码理解的难度。