闭包函数

290 阅读9分钟

导言

闭包是 javaScript 语言强大的特性之一 , 本节将介绍闭包函数的特性

认识闭包函数

闭包简单来说就是 函数能够使用函数外定义的变量
简单描述 , 闭包就是嵌套函数结构, 在一个函数内定义的一个函数或函数表达式。内部函数应该访问外部函数中声明的私有变量,参数或其他内部函数。

其实就是,内层函数可以访问外层函数的变量,就是闭包
下面是经典的闭包结构

function f(x) {
    var a = x;
    var b = function () {
        return a;
    }
    a++;
    return b;
}
var c = f(5);
console.log(c())

如果没有闭包函数的作用, 那么这种数据寄存和传递就无法得意实现

function f(x) {
    var a = x;
    var b = a;
    a++;
    return b;
}
var c = f(5);
console.log(c); // 5

在函数运行时,闭包可以实时访问上一级函数作用域中其他标识符的值。同一个函数中所有闭包都可以引用函数体内相同标识符的值,并且互相影响

使用闭包

初步认识闭包后,我们使用几个实例介绍闭包的简单使用,以便能够更彻底理解什么是闭包

  1. 实例1 使用闭包结构能够跟踪动态环境中数据的实时变化,并即时存储
function f() {
    var a = 1;
    var b = function() {
        return a;
    }
    a++;
    return b;
}
var c = f();
console.log(c()); //  返回2,而不是返回1
  1. 实例2 闭包不会因为外部函数环境的注销而消失,并始终存在
function f() {
        var a =  1;
        b = function() {
            console.log(a)
        }
        c = function() {
            a++;
        }
        d = function(x) {
            a = x;
        }
    }

普通函数 f() 中定义了3个闭包函数,他们分别指向并寄存器局部变量a的值,并根据不同的操作动态跟踪变量 a 的值。 当调用函数 f() ,将生成 3个闭包, 3个闭包函数, 他们分别指向并寄存器局部变量 a 的值, 并根据不同的操作动态跟踪变量 a 的值。

  1. 实例3 如何利用闭包存储变量所有变化的值
function f(x) {
    // 定义功能函数,把参数数组的元素以闭包体分别封装在数组中并返回
    var a = []; // 定义临时数组
    for(var i = 0 ; i < x.length ; i ++) { // 遍历参数数组
        var temp = x[i]; // 临时存储每个数组元素的值
        a.push(
            function () {
                console.log(temp + '' + x[i])
            }
        )
    }
    return a;
}
function e() {
    var a = f(['a','b','c']);
    for(var i = 0 ; i < a.length ; i ++) {
        a[i]() 
        // cundefined
        // cundefined
        // cundefined
    }
}
e()

在这个实例中, 函数 f() 功能是 : 把数组类型的参数中每个元素的值分别封装在闭包结构中,然后把闭包存储在一个数组中 , 并返回这个数组。
但是, 在函数中每个元素的值都是 "c undefined" 原来闭包中变量 temp 并不是固定的 , 它会随着根据函数运行环境中的变量 temp 的值变化而更新 ,导致临时数组元素的值都是字符 c , 而不是 a , b , c, 同时由于循环变量 i 递增之后, 最后的值是3, 则 x[3] 超出了数组的长度, 结果就是 undefined

解决方法 :可以为闭包再包裹一层函数,然后运行该函数,并把边界动态值传递给它,当这个函数接收到这些值,然后传递给内部的闭包函数,自己就注销了,从而阻断了闭包与最外层函数的实时联系

function f(x) {
    var a = [];
    for(var i = 0 ; i < x.length ; i ++) {
        var temp = x[i];
        a.push(
            (function (temp , i) {
                return function() {
                    console.log(temp , i)
                }
            })(temp,i)
        )
    }
    return a;
}

实例4 利用同一个闭包体声明多个闭包。同一个闭包,通过分别引用,能够在当前环境中生成多个闭包, 多个闭包之间的变量不能共享

function f(x) {
    var num = 10;
    return function (x) {
        console.log(num)
        num--;
    }
}
var a = f(1)
a() // 10
a() // 9
b() // 10

闭包作用域和声明周期

  • 使用var 语句声明的全局变量,则在全局范围内可见
  • 不使用var 语句在任意位置隐式声明的变量,则在全局范围内可见
  • 使用var 语句在函数体内显式声明的变量,则仅仅在函数体内可见
  • 如果变量在函数体内有效,则其所有内嵌函数中都有效
  1. 实例1 参数变量x 是函数 f() 的私有变量, 该变量将在函数 f()内部所有内嵌函数中都是可见的
function f(x) {
    return function() {
        return function() {
            return function() {
                return x;
            }
        }
    }
}
var a = f(1);
a()()() // 1

函数内声明的局部变量能够覆盖外部同名变量

  1. 实例2 内部函数的同名局部变量会逐层覆盖, 并显示最里层的变量值
function f() {
    var x = 1;
    return function() {
        var x = 2;
        return function() {
            var x = 3;
            return function() {
                return x;
            }
        }
    }
}
var a = f();
a()()() // 3
  1. 函数内的私有变量具有较大的访问优先级
function f(x) {
    return function () {
        var x;
        return x;
    }
}
var a = f(1);
a(); // undefined

函数被解析时 , 会把内部的所有使用var 语句声明的变量放入调用对象的局部变量列表中, 然后根据作用域链逐层上访变量。 如果在当前作用域内发现了该变量,则会使用该变量,否则就会向上访问同名变量。 如果变量为隐式声明使用,则将作为全局变量被放入全局作用域内的全局对象变量列表中。

比较函数调用和引用

函数引用与调用是两个不同的概念。当引用函数时, 多个变量存储的是函数的地址。因此,对于同一个函数来说,不管有多少个变量引用该函数,他们的值都是相同的,即为该函数的入口指针地址。

function f() {
    var x = 5;
    return x;
}
var a = f;
var b = f;
a === b ; // true
function f() {
    var x= 5;
    return function() {
        return x;
    }
}
var a = f();
var b = f();
a === b; // false

判断下面变量 a 和 b 是否相同

function F() {
    this.x = 5;
}
var a = new F();
var b = new F();
a === b; // false

通过new 运算符可以克隆函数的结构, 从而实现函数实例化的目的。 实例化的过程实际上就是对函数结构进行复制和初始化的操作过程

比较闭包函数和函数实例

函数实例就是对函数结构的克隆,函数实例与原函数(初次定义函数)是两个不同的对象,因此应把函数实例与函数引用区分开来。实际上,闭包函数就是函数实例的一种应用形式

function f() {
    var x = 5;
    return x;
}
var a = new f();
a.x // undefined

function f() {
    this.x = 5;
}
var a = new f();
a.x ; // 返回5

也就是说,可以使用 new 运算符能够对任意函数进行实例化。但是只要构造函数才能执行有效实例化操作

  1. 使用不同的方法创建的函数对象,他们是否包含闭包结构还需要具体分析,这里主要观察函数对象被实例化之后,是否生成了实例化方法
function F(){}
F.prototype.y = function() {
    return 5;
}
var a = new F();
var b = new F();
a === b; // false
a.y === b.y; // true

保护闭包数据的独立性

先看一个实例,该实例设想在一个循环结构中通过闭包来自动更改一个数组内所有元素的值,代码如下

var a = [1,2,3];
for(var i in a) {
    a[i] = function () {
        return i * i;
    }
}

运行结果发现,数组的值都是相同,为最后一个元素下标数的平方。原来在遍历操作中,闭包中变量i的值共享外部变量i的值,他们都是对于同一个值的不同引用。 如果检测3个函数实例,结果发现他们并不相等,说明3个函数实例是互相独立的

a[0] === a[1]; // false
a[1] === a[2]; // false
a[2] === a[3]; // false

那该怎么更改呢
在前面的实例中也曾经讲解过,解决这个问题的方法是为闭包函数再包裹一层函数体,因为我们都知道函数结构具有存储数据的天性。把局部变量作为参数传递给函数,则函数就会把该参数作为私有数据进行保护,从而防止闭包内的数据与外层数据建立动态联系。

var a = [1,2,3];
for(var i in a) {
    a[i] = function(i) {
        return function() {
            return i * i;
        }
    }(i)
}

通过在闭包外面包裹一层函数来实现闭包数据的单独存储,也存在一定问题。因为多了一层闭包函数,将增大系统的负担。

var a = [1,2,3];
for(var i in a) {
    a[i] = function() {
        var i = arguments.caller.value;
        return i * i;
    }
    a[i].value = i;
}

闭包应用

下面通过几个实例介绍函数式编程中几个经典的应用,以提高用户灵活使用函数的能力

柯里化

柯里化是把接收多个参数的函数变换成接收一个单一参数的函数, 并且返回一个新函数,这个新函数能够接收原函数的参数。

下面举一个例子

function add(x , y) {
    return x + y;
}

add(1)(2) => 3;

function curry(fn) {
    // 参数数组
    var args = Array.from(arguments).slice(1);
    if(args.length === fn.length) {
        return fn.apply(this,args);
    }
    return function next() {
        args = args.concat(Array.from(arguments));
        if(args.length < fn.length) {
            return next;
        } else if(args.length === fn.length) {
            return fn.apply(this,args);
        }
    }
}

记忆

函数可以利用对象去记住先前操作的结果,从而能避免无谓的运算。这种优化称为记忆。javaScript的对象和数组要实现这种优化是非常方便的。

  1. 实例使用地递归函数计算 fibonacci 数列。 一个 fibonacci 数字是之前两个 数字之和。 最前面的两个数字是 0 和 1.
var fi = function(n) {
    return n < 2 ? n : fi(n - 1) + fi(n - 2);
}

去计算可能已被刚计算的值。如果是该函数具备记忆功能,就可以显著减少它的运算次数。
先使用一个临时数组保存存储结果,存储结果可以隐藏在闭包中。当函数被调用时,先看是否已经知道存储结果,如果已经知道,就立即返回这个存储结果。

var fi = (function () {
    var memo = [0,1];
    return function fib (n) {
        var result = memo[n];
        if(typeof result !== 'number') {
            result = fib(n - 1) + fib(n - 2);
            memo[n] = result;
        }
        return result;
    }
})()

当然我们可以把这种函数形式抽象化,以构造带记忆功能的函数。
memoizer() 函数将取得一个初始化 memo 数组和 fundamental 函数。memoizer()函数返回一个管理 memo 存储和在需要时调用 fundamental 函数的 shell 函数。 memoizer()函数传递这个 shell函数和该函数的参数给fundamental 函数。

var memoizer = function (memo , formula) {
    return function recur(n) {
        var result = memo[n];
        if(typeof result !== 'number') {
            result = formula(recur ,n);
            memo[n] = result;
        }
        return result;
    }
}