JS闭包精讲

  • 内容
  • 评论
  • 相关

闭包是 JavaScript 的重要特性之一,在函数式编程中有着重要的作用,本节介绍闭包的结构和基本用法。

定义闭包

闭包就是一个能够持续存在的函数上下文活动对象。

形成原理

函数被调用时,会产生一个临时上下文活动对象。它是函数作用域的顶级对象,作用域内所有私有方法有变量、参数、私有函数等都将作为上下文活动对象的属性而存在。

函数被调用后,在默认情况下上下文活动对象会被立即释放,避免占用系统资源。但是,若函数内的私有变量、参数、私有函数等被外界引用,则这个上下文活动对象暂时会继续存在,直到所有外界引用被注销。

但是,函数作用域是封闭的,外界无法访问。那么在什么情况下,外界可以访问到函数内的私有成员呢?

根据作用域链,内部函数可以访问外部函数的私有成员。如果内部函数引用了外部函数的私有成员,同时内部函数又被传给外界,或者对外界开放,那么闭包体就形成了。这个外部函数就是一个闭包体,它被调用后,活动对象暂时不会被注销,其属性会继续存在,通过内部函数可以持续读写外部函数的私有成员。

闭包结构

典型的闭包体是一个嵌套结构的函数。内部函数引用外部函数的私有成员,同时内部函数又被外界引用,当外部函数被调用后,就形成了闭包。这个函数也称为闭包函数。

下面是一个典型的闭包结构。

function f(x) {  //外部函数
    return function (y) {  //内部函数,通过返回内部函数,实现外部引用
        return x + y;  //访问外部函数的参数
    };
}
var c = f(5);  //调用外部函数,获取引用内部函数
console.log(c(6));  //调用内部函数,原外部函数的参数继续存在

解析过程简单描述如下:

  1. 在 JavaScript 脚本预编译期,声明的函数 f 和变量 c,先被词法预解析。
  2. 在 JavaScript 执行期,调用函数 f,并传入值 5。
  3. 在解析函数 f 时,将创建执行环境(函数作用域)和活动对象,并把参数和私有变量、内部函数都映射为活动对象的属性。
  4. 参数 x 的值为 5,映射到活动对象的 x 属性。
  5. 内部函数通过作用域链引用了参数 x,但是还没有被执行。
  6. 外部函数被调用后,返回内部函数,导致内部函数被外界变量 c 引用。
  7. JavaScript 解析器检测到外部函数的活动对象的属性被外界引用,无法注销该活动对象,于是在内存中继续维持该对象的存在。
  8. 当调用 c,即调用内部函数时,可以看到外部函数的参数 x 存储的值继续存在。这样就可以实现后续运算操作,返回 x+y=5=6=11。

如下结构形式也可以形成闭包:通过全局变量引用内部函数,实现内部函数对外开放。

var c;  //声明全局变量
function f(x) {  //外部函数
    c = function (y) {  //内部函数,通过向全局变量开放实现外部引用
        return x + y;  //访问外部函数的参数
    };
}
f(5);  //调用外部函数
console.log(c(6));  //使用全局变量c调用内部函数,返回11

闭包变体

除了嵌套函数外,如果外部引用函数内部的私有数组或对象,也容易形成闭包。

var add;  //全局变量
function f() {  //外部函数
    var a = [1,2,3];  //私有变量,引用型数组
    add = function (x) {  //测试函数,对外开放
        a[0] = x * x;  //修改私有数组的元素值
    }
    return a;  //返回私有数组的引用
}
var c = f();
console.log(c[0]);  //读取闭包内数组,返回1
add(5);  //测试修改数组
console.log(c[0]);  //读取闭包内数组,返回25
add(10);  //测试修改数组
console.log(c[0]);  //读取闭包内数组,返回100

与函数相同,对象和数组也是引用型数据。调用函数 f,返回私有数组 a 的引用,即传值给局部变量 c,而 a 是函数 f 的私有变量,当被调用后,活动对象继续存在,这样就形成了闭包。

这种特殊形式的闭包没有实际应用价值,因为其功能单一,只能作为一个静态的、单向的闭包。而闭包函数可以设计各种复杂的运算表达式,它是函数式变成的基础。

反之,如果返回的是一个简单的值,就无法形成闭包,值传递是直接复制。外部变量 c 得到的仅是一个值,而不是对函数内部变量的引用。这样当函数调用后,将直接注销对象。

function f(x) {  //外部函数
    var a = 1;  //私有变量
    return a;
}
var c = f(5);
console.log(c);  //仅是一个值,返回1

使用闭包

下面结合示例介绍闭包的简单使用,以加深对闭包的理解。

示例1

使用闭包实现优雅的打包,定义存储器。

var f = function () {  //外部函数
    var a = [];  //私有数组初始化
    return function (x) {  //返回内部函数
        a.push(x);  //添加元素
        return a;  //返回私有数组
    };
} ()  //直接调用函数,生成执行环境
var a = f(1);  //添加值
console.log(a);  //返回1
var b = f(2);  //添加值
console.log(b);  //返回1,2

在上面示例中,通过外部函数设计一个闭包,定义一个永久的存储器。当调用外部函数生成执行环境之后,就可以利用返回的匿名函数不断地的向闭包体内的数组 a 传入新值,传入的值会持续存在。

示例2

在网页中事件处理函数很容易形成闭包。

<script>
function f() {
    var a = 1;
    b = function () {
        console.log("a =" + a);
    }
    c = function () {
        a ++;
    }
    d = function () {
        a --;
    }
}
</script>
<button onclick="f()">生成闭包</button>
<button onclick="b()">查看 a 的值</button>
<button onclick="c()">递增</button>
<button onclick="d()">递减</button>

在浏览器中浏览时,首先点击“生成闭包”按钮,生成一个闭包;点击“查看 a 的值”按钮,可以随时查看闭包内私有变量 a 的值;点击“递增”“递减”按钮时,可以动态修改闭包内变量 a 的值,效果如图所示。

本文标题:JS闭包精讲

本文地址:https://www.hosteonscn.com/6923.html

评论

0条评论

发表评论

邮箱地址不会被公开。 必填项已用*标注