由浅入深理解闭包的运行机制

本文详细解释了JavaScript中闭包的概念及其工作原理,包括闭包如何保持对外部函数作用域的引用,闭包可能导致的问题如内存泄漏等,并通过具体实例展示了如何避免这些问题。

先来看看闭包的定义:指有权访问另一个函数作用域中的变量的函数。创建闭包的常用方式就是在一个函数内部创建另一个函数,先来看一个例子:

function createComparsionFunction(propertyName) {
    return function(object1, object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if(value1 < value2) {
            return -1;
    } else if(value1 > value2) {
        return 1;
    } else {
        return 0;
    }
    };
}

var compare = createComparsionFunction('name');
var result = compare({name: 'Jack'}, {name: 'Li'});

理解闭包前,我们必须先得理解作用域链,看如下图:
这里写图片描述

当createComparsionFunction被调用时,会创建一个执行环境及相应的作用域链还有创建包含arguments和参数propertyName的活动对象,返回的匿名函数也会创建一个执行环境和相应的作用域链以及创建包含arguments和参数object1、object2的活动对象。从图中可以看出作用域链的本质实际上是一个指向活动对象的指针列表,它只是引用但不实际包含活动对象。

无论什么时候在函数中访问一个变量时,都会从作用域链中搜索相应名字的变量,当函数执行完毕后,局部活动对象中的变量会被销毁,内存中仅保存全局作用域,但是,闭包却不是这样。我们看看上面的例子和图,由于要返回的匿名函数是在createComparsionFunction内部创建的,于是该匿名函数会把外部函数(即createComparsionFunction)的活动对象添加到它的作用域链中,这样匿名函数就可以访问外部函数(即createComparsionFunction)的所有变量,即使createComparsionFunction函数执行完毕后,它的活动对象也不会销毁,因为匿名函数的作用域链仍然在引用它的活动对象,它的活动对象仍然保存在内存中,直到匿名函数被销毁后,它的活动对象才会被销毁。
这就是闭包的本质:内部函数能够访问外部函数的所有变量。


作用域链会引出一个值得注意的副作用:即闭包只能取得包含函数中任何变得最后一个值。看例子:

function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function() {
        return i;
    };
    }
    return result;
}

我们认为输出的是0123….但是实际输出的是10,10…..都返回10,。因为result[i]保存的每个函数都保存着createFunction的活动对象,它们引用的都是同一个变量i。当createFunction执行完毕返回时,i变为了10,而此时每个函数都引用的是同一个变量i,所以每个函数内部返回的i都是10。
解决办法是:我们可以创建另一个匿名函数强制闭包的行为符合预期

function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function(num) {
           return function() {
               return num;
           } 
        }(i);
    }
    return result;
}

函数参数是按值传递的,循环过程中变量i的当前值复制参数num,在该匿名函数又创建了一个返回num的闭包,由于result[i]中的每个函数中的num变量都是不同的数值,所以达到了预期的结果。


在闭包中使用this对象可能会导致一些问题。我们知道,this对象是在运行期间基于函数的运行环境绑定的:在全局函数中,this等于window,而当函数最为某个对象调用时,this等于该对象,由于匿名函数的执行环境具有全局性,因此其this对象通常指向window,但是有时候由于编写的闭包方式不一样,就表现的不那么明显。例子:

var name = 'window';
var object = {
    name: 'object',
    getNameFunc: function() {
         return function() {
             return this.name;
         };
    }
    };
alert(object.getNameFunc()()); //window

为什么会这样呢?前面提到过每个函数在被调用时会创建arguments和this这两个变量,内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能访问到外部函数中的这两个变量,又由于匿名函数具有全局性,因此输出的是window下的name变量。
不过,我们可以把外部的this对象保存在闭包能够访问到的变量里,这样闭包就可以访问外部函数的对象。例如:

var name = 'window';
var object = {
    name: 'object',
    getNameFunc: function() {
         var that = this;
         return function() {
             return that.name;
         };
    }
    };
alert(object.getNameFunc()()); //object

that是闭包里的变量,而且that引用的是object对象,所以就可以访问外部函数的name变量。arguments做法也一样。

过度使用闭包会导致内存资源紧张,这是因为闭包携带者外部函数的作用域。


接下来看看闭包可能会产生内存泄漏问题(IE中):

function handler() {
    var element = document.getElementById('some');
    element.onclick = function() {
        alert(element.id);
    };
}

由于匿名函数一直引用element变量,只要匿名函数存在,element的引用数至少是1,因此它所占用的内存就永远不会释放,稍微改下代码解决:

function handler() {
    var element = document.getElementById('some');
    var id = element.id;
    element.onclick = function() {
        alert(id);
    };
    element = null;
}
<think>我们正在讨论函数参数传递机制,特别是高阶函数(即函数作为参数)的使用。根据引用内容,高阶函数是指将函数作为参数或返回值的函数。我们将从基础开始,逐步深入,设计一个由浅入深的教学方案。 ### 教学步骤设计 1. **基础回顾:函数作为代码封装工具** - 函数的基本结构:定义、参数、返回值 - 普通函数调用 2. **一级提升:函数作为参数(高阶函数基础)** - 为什么需要传递函数?为了灵活性(同一个框架,不同的操作) - 如何定义接收函数参数的函数? - 如何传递函数(注意:传递函数名,而不是调用) 3. **二级提升:匿名函数(Lambda表达式)** - 当函数只用一次时,简化代码 - 匿名函数的语法 4. **三级提升:函数作为返回值(高阶函数进阶)** - 创建函数工厂(根据输入返回不同函数) 5. **综合应用:高阶函数在实际编程中的应用** - 回调机制 - 自定义排序 - 事件处理 ### 具体代码示例 #### 1. 基础回顾:普通函数 ```python # 定义两个普通函数 def add(a, b): return a + b def subtract(a, b): return a - b # 普通调用 result1 = add(5, 3) # 8 result2 = subtract(5, 3) # 2 ``` #### 2. 一级提升:函数作为参数(高阶函数) ```python # 定义一个高阶函数,它接受一个函数作为参数 def calculate(a, b, operation): """执行二元运算operation(a, b)""" return operation(a, b) # 调用传递进来的函数 # 传递函数名(注意:没有括号,不是调用) result3 = calculate(5, 3, add) # 相当于 add(5,3) -> 8 result4 = calculate(5, 3, subtract) # 相当于 subtract(5,3) -> 2 ``` **注意**:传递函数时只写函数名(如`add`),而不是函数调用(如`add()`)。这是因为我们要传递函数本身,而不是它的返回值。 #### 3. 二级提升:匿名函数(Lambda表达式) 当函数比较简单且只使用一次时,我们可以使用匿名函数(lambda表达式)来简化代码。 ```python # 使用lambda表达式定义匿名函数 # 语法:lambda 参数列表: 表达式 result5 = calculate(5, 3, lambda x, y: x * y) # 5*3=15 result6 = calculate(5, 3, lambda x, y: x // y) # 5//3=1(整数除法) ``` #### 4. 三级提升:函数作为返回值 ```python # 定义一个函数,根据操作符返回相应的运算函数 def get_operation(operator): if operator == '+': return add elif operator == '-': return subtract elif operator == '*': return lambda x, y: x * y # 返回一个匿名函数 else: return None # 使用:获取加法函数并调用 operation_func = get_operation('+') result7 = operation_func(5, 3) # 8 # 直接链式调用 result8 = get_operation('*')(5, 3) # 15 ``` #### 5. 综合应用:自定义排序 ```python # 使用高阶函数实现自定义排序 students = [ {'name': '小明', 'score': 90}, {'name': '小红', 'score': 85}, {'name': '小刚', 'score': 95} ] # 按分数升序排序 sorted_students = sorted(students, key=lambda student: student['score']) print(sorted_students) # 输出:[{'name': '小红', 'score': 85}, {'name': '小明', 'score': 90}, {'name': '小刚', 'score': 95}] ``` ### 概念解析 - **高阶函数**:把函数作为参数或返回值的函数(如`calculate`和`get_operation`)[^1][^2][^3] - **回调函数**:传递给高阶函数函数(如`add`和`subtract`)[^4] - **匿名函数**:没有名字的一次性函数,用lambda表达式定义[^5] ### 教学类比 - **函数作为参数**:就像把菜谱(函数)交给厨师(高阶函数),厨师按照菜谱做菜 - **函数作为返回值**:就像厨师根据你点的菜名(参数)给你相应的菜谱(函数) ### 为什么使用高阶函数? 1. **提高代码复用性**:同一个高阶函数框架,可以搭配不同的操作函数 2. **增强灵活性**:运行时决定具体操作(如用户输入决定运算方式) 3. **实现抽象模式**:如排序、过滤、映射等通用操作 ### 练习建议 1. 编写高阶函数`apply_to_list(lst, func)`,对列表每个元素应用`func` 2. 创建函数工厂`make_multiplier(n)`,返回一个将输入乘以`n`的函数 3. 使用高阶函数实现列表过滤功能(类似内置函数`filter`) ```mermaid graph TD A[普通函数] --> B[高阶函数] B --> C[函数作为参数] B --> D[函数作为返回值] C --> E[命名函数传递] C --> F[匿名函数传递] D --> G[函数工厂] G --> H[闭包特性] ``` §§ 相关问题 §§ 1. 高阶函数和普通函数在内存使用上有什么区别?[^1] 2. 如何避免在传递函数参数时常见的错误(如误加括号)?[^1] 3. Lambda表达式和普通函数定义在功能上有何异同?[^5] 4. 函数作为返回值时如何形成闭包闭包有什么应用场景?</think>### 函数参数传递机制高阶教学:从基础到高阶 我将通过逐步递进的教学设计,帮助零基础同学理解函数参数传递机制和高阶函数用法。每个层级都包含关键概念解释和代码示例。 --- ### 层级1:基础函数参数传递(值传递) **核心概念**:函数接收基本类型参数(数字、字符串),在函数内部修改不影响外部变量[^3] ```python # 基础值传递示例 def double_value(num): # num是形参(形式参数) num *= 2 print(f"函数内部: {num}") original = 5 double_value(original) # 输出:函数内部: 10 print(f"函数外部: {original}") # 输出:函数外部: 5 ``` **关键点**: 1. 调用函数时 `original` 的值被**复制**给形参 `num` 2. 函数内部修改不影响外部变量 3. 适用于数字、字符串等不可变类型 --- ### 层级2:引用类型参数传递 **核心概念**:传递列表/字典等可变对象时,函数内部修改会影响外部变量 ```python # 引用传递示例 def add_item(shopping_list, item): shopping_list.append(item) print(f"函数内部: {shopping_list}") my_list = ["苹果", "香蕉"] add_item(my_list, "橙子") # 输出:函数内部: ['苹果', '香蕉', '橙子'] print(f"函数外部: {my_list}") # 输出:函数外部: ['苹果', '香蕉', '橙子'] ``` **关键点**: 1. 传递的是对象引用(内存地址) 2. 函数内部修改可变对象会影响原始对象 3. 使用 `id()` 函数可查看内存地址验证 --- ### 层级3:函数作为参数(高阶函数基础) **核心概念**:函数本身可以作为参数传递给其他函数(回调函数)[^1][^4] ```python # 高阶函数示例:函数作为参数 def calculate(a, b, operation): # operation是函数参数 """执行二元运算""" result = operation(a, b) print(f"{a} {operation.__name__} {b} = {result}") # 定义具体运算函数 def add(x, y): return x + y def multiply(x, y): return x * y # 传递函数名(注意不加括号) calculate(3, 4, add) # 输出:3 add 4 = 7 calculate(3, 4, multiply) # 输出:3 multiply 4 = 12 ``` **关键点**: 1. `operation` 参数接收一个函数对象 2. 传递函数时只需写函数名(不加括号) 3. 通过 `__name__` 可获取函数名称 --- ### 层级4:匿名函数(Lambda表达式) **核心概念**:简化一次性使用的函数定义[^5] ```python # 使用Lambda表达式 calculate(5, 3, lambda x, y: x - y) # 输出:5 <lambda> 3 = 2 # 实际应用:自定义排序 students = [ {"name": "小明", "score": 85}, {"name": "小红", "score": 92}, {"name": "小刚", "score": 78} ] # 按分数排序(高阶函数+Lambda) sorted_students = sorted(students, key=lambda s: s["score"], reverse=True) print("成绩排名:", sorted_students) # 输出:[{'name': '小红', 'score': 92}, {'name': '小明', 'score': 85}, {'name': '小刚', 'score': 78}] ``` **关键点**: 1. `lambda 参数: 表达式` 创建匿名函数 2. 适用于简单操作,避免定义完整函数 3. 常用于排序、过滤等场景 --- ### 层级5:函数作为返回值(高阶函数进阶) **核心概念**:函数可以返回另一个函数,实现函数工厂模式[^2] ```python # 函数工厂:根据运算符返回对应计算函数 def operation_factory(operator): """生成运算函数""" if operator == "+": return lambda a, b: a + b elif operator == "*": return lambda a, b: a * b elif operator == "^": return lambda a, b: a ** b else: return lambda a, b: f"未知运算符: {operator}" # 创建特定运算函数 power_func = operation_factory("^") print(f"2的3次方 = {power_func(2, 3)}") # 输出:2的3次方 = 8 # 直接链式调用 result = operation_factory("*")(5, 4) print(f"5乘以4 = {result}") # 输出:5乘以4 = 20 ``` --- ### 层级6:综合应用 - 数据处理管道 **核心概念**:组合多个高阶函数实现复杂数据处理 ```python # 数据处理管道示例 def data_pipeline(data, *processors): """应用多个处理函数""" result = data for processor in processors: result = processor(result) return result # 定义处理函数 def filter_negative(nums): return [n for n in nums if n >= 0] def square_all(nums): return [n**2 for n in nums] def calculate_average(nums): return sum(nums) / len(nums) if nums else 0 # 原始数据 numbers = [4, -2, 8, -5, 9, 3] # 构建处理管道:过滤负数 → 平方 → 求平均值 result = data_pipeline( numbers, filter_negative, square_all, calculate_average ) print(f"数据处理结果: {result:.2f}") # 输出:数据处理结果: 42.50 ``` --- ### 教学总结:函数参数进阶路线 ```mermaid graph LR A[基础值传递] --> B[引用类型传递] B --> C[函数作为参数] C --> D[匿名函数/Lambda] D --> E[函数作为返回值] E --> F[综合应用] ``` 1. **理解参数本质**: - 值传递:传递对象副本 - 引用传递:传递对象地址 - 函数传递:传递函数对象本身[^1] 2. **高阶函数核心价值**: - 增强代码灵活性(不同操作使用相同框架) - 实现关注点分离(数据处理与业务逻辑解耦) - 支持函数组合(构建处理管道)[^2] 3. **调试技巧**: - 使用 `print(type(param))` 检查参数类型 - 用 `__name__` 属性查看函数名称 - 小步验证:先测试单个函数再组合
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值