专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

老生常谈--"闭包"不完全解析

历史

彼得·J·兰丁(Peter J. Landin)于1964年 在一个编程实验中将一些表达式定义为闭包,并且认为闭包具有一定的环境部分和控制部分。(彼得·兰丁在1964年提出了闭包的概念)

后来,乔尔·摩西(Joel Moses)引用了兰丁(Landin)提出的闭包概念,用来指代一个lambda表达式,该表达式的变量被绑定到指定的词法环境中,从而生成了一个封闭的表达式 即闭包。

匿名函数

闭包一词通常用作匿名函数的同义词,尽管严格来讲,匿名函数是不带名称的函数,而闭包是函数(即值)的实例,该函数的非局部变量已绑定到 值或存储位置。

    function f(x) {
        function g(y) { return x + y; }
        return g;
    }

    function h(x) { return function (y) { return x + y } }

    a = f(1);
    b = h(1);

    console.log('---a(5)===6')
    console.log(a(5) === 6)

    console.log('---b(5)===6')
    console.log(b(5) === 6)

    console.log('---f(1)(5) == 6')
    console.log(f(1)(5) == 6)

    console.log('---h(1)(5) == 6')
    console.log(h(1)(5) == 6)

在两种情况下,a和b的值都是闭包,是通过从嵌套函数返回带有自由变量(参考底部标注)的嵌套函数而产生的。 a和b中的闭包在功能上相同。实现上的唯一区别是,在第一种情况下,我们使用了名称为g的嵌套函数,而在第二种情况下,我们使用了匿名嵌套函数。

闭包是与其他任何值一样的值。如示例的最后两行所示,不需要将其分配给变量,而是可以直接使用它。可以将这种用法视为“匿名闭包”。

嵌套函数定义本身不是闭包:因为它们的自由变量尚未被绑定。只有在使用参数的值对封闭函数进行求值之后,嵌套函数绑定的自由变量才会创建闭包,然后从封闭函数返回封闭函数。

    x = 1;
    nums = [1, 2, 3]

    function f(y) { return x + y }

    // var b = Array.prototype.map.call(nums, f);
    b = nums.map(f)

    console.log(b)

实现闭包的最常见的一种方式是返回一个函数,

应用

闭包的使用的前提条件是 编程言的函数是一等公民,即函数可以作为高阶函数的结果返回,也可以作为参数传递给其他函数调用;如果具有自由变量的函数是一类(First-class)的,则返回将创建一个闭包。闭包还经常与回调一起使用,尤其是对于事件处理程序(例如JavaScript)而言,在闭包中用于与动态网页进行交互。

闭包也可以以连续传递样式使用以隐藏状态。因此,可以使用闭包来实现诸如对象和控制结构之类的构造。在某些语言中,在一个函数中定义另外一个函数并且内部函数引用外部函数的局部变量时,可能会发生闭包。在运行时,当外部函数执行时,将形成一个闭包,该闭包由内部函数的代码和对该闭包所需的外部函数的变量的引用组成。

1. First-class function

闭包通常出现在 支持 First-class function 的编程语言中。换句话说,此类编程语言可以使函数可以作为

  • 参数传递
  • 函数做为值并返回,
  • 函数还可以赋值给某个变量

```javascript var bookList = [ { name: ‘JavaScript高级程序设计’, sales: 100 }, { name: ‘CSS世界’, sales: 50 }, { name: ‘深入浅出Node’, sales: 100 }, { name: ‘白鹿原’, sales: 200 }, ]

function bestSellingBooks(threshold) {
    return bookList.filter(
        function (book) { return book.sales >= threshold; }
    );
}

var result = bestSellingBooks(90)
console.log(result)

_代码中的闭包由一个 匿名函数(function (book){})和对threshold 变量的引用组成。_<br />_然后将闭包传递了给 filter 函数,_<br />_因为闭包引用了threshold 变量, 因此每次filter执行都能调用threshold 变量_<br />_<br />_一个函数被创建为闭包函数后,也可以作为值被返回_
```javascript
function derivative(f, dx) {
  return function (x) {
    return (f(x + dx) - f(x)) / dx;
  };
}

在支持闭包的编程语言中,就算外部函数执行完毕了, 但只要闭包函数引用了某些自由变量,这些自由变量就会一直存在,不会被销毁。

2. 模拟块级作用域&私有变量的共有方法

闭包可用于将函数与一组“私有”变量相关联,这些变量在函数的多次调用中持续存在。 该变量的范围仅包含封闭函数,因此无法从其他程序代码访问它。 这类似于面向对象编程中的私有变量,实际上,闭包类似于具有单个公共方法(函数调用)的对象类型,尤其是函数对象,并且可能还有许多私有变量(绑定变量)。

块级作用域

<body>
    <p id='pp'>第一</p>
    <p id='pp'>第2</p>
    <p id='pp'>第3</p>
    <p id='pp'>第4</p>
    <p id='pp'>第5</p>
    <p id='pp'>第6</p>
</body>

    var pp = document.getElementsByTagName('p')

    function test() {
        for (var i = 0; i < 6; i++) {
            pp[item].addEventListener('click', function () {
              // 打印的全是6
                console.log(item)
            })
        }
        // for循环外部还可以访问
        console.log(i)
    }

因为 ES6之前 没有块级作用域的概念, 并且var声明的变量会导致 变量提升到 函数声明顶部, 类似于以下代码


function test1() { // 变量提升, var i; for ( i = 0; i < 6; i++) { pp[item].addEventListener('click', function () { // 打印的全是6 console.log(item) }) } // for循环外部还可以访问 console.log(i) }
// 解决方式一  ES6方式
    function test0() {
        for (let i = 0; i < 6; i++) {
            pp[item].addEventListener('click', function () {
                    // 打印的0-5
                console.log(item)
            })
        }
                // Uncaught ReferenceError: i is not defined
        console.log(i)   
    }
    test0()

// 解决方式二  闭包
    function test1() {
        for (var i = 0; i < 6; i++) {
            (function (item) {
                pp[item].addEventListener('click', function () {
                        // 打印的0-5
                    console.log(item)
                })
            })(i)
        }
        // for循环外部还可以访问
        console.log(i)
    }

解决方式二中 使用了一个 立即执行的匿名函数,该匿名函数接受一个 自由变量(i),从而形成了一个闭包函数。 因此每次匿名函数执行时都会将最新的 变量 i的值保存到内存中,不会被销毁。最终导致 在匿名函数中使用的变量(item) ,每次都是最新的值。实质上是使用闭包模拟了块级作用域

#

私有变量的公有方法

JavaScript中也是有私有变量的概念的。任何在函数中定义的变量,都是私有变量,因为是外部是访问不到这些变量的。
私有变量包括

  • 函数的参数
  • 局部变量
  • 函数内部定义的其他函数
    function MyObject() {
        var privateVariable = '这是MyObject的私有变量之一';

        var privateCounter = 0;

        function privateFunction() {
            var res = '这是MyObject的私有函数,也是MyObject的私有变量之一';
            console.log(res);
            return res;
        }

        function changeCount(val) {
            privateCounter += val;
        }

        this.increment = function () {
            changeCount(1);
        }
        this.decrement = function () {
            changeCount(-1);
        }
        this.getCount = function () {
            console.log('privateCounter:' + privateCounter);
            return privateCounter;
        }

        this.publicMethod = function () {
            privateVariable = '通过共有方法改变了私有变量';
            console.log(privateVariable);
            return privateFunction();
        }
    }

    const NewMyObject = new MyObject();
    NewMyObject.publicMethod();

    const NewMyObject1 = new MyObject();
    NewMyObject1.getCount();
    NewMyObject1.increment();
    NewMyObject1.increment();
    NewMyObject1.getCount();

    const NewMyObject2 = new MyObject();
    NewMyObject2.getCount();
    NewMyObject2.increment();
    NewMyObject2.increment();
    NewMyObject2.getCount();

上面的代码 MyObject 构造函数中,定义了所有私有变量和函数,然后继续创建能够访问私有变量的公有方法
this.increment, this.decrement,this.getCount,this.publicMethod
(这些方法形成了一个闭包函数)

示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量

请注意两个计数器 NewMyObject1NewMyObject2是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

在语义中的不同之处

词法环境

    var f, g;
    function foo() {
        var x;
        f = function () { return ++x; };
        g = function () { return --x; };
        x = 1;
        alert('inside foo, call to f(): ' + f());
    }
    foo();  // 2
    alert('call to g(): ' + g());  // 1 (--x)
    alert('call to g(): ' + g());  // 0 (--x)
    alert('call to f(): ' + f());  // 1 (++x)
    alert('call to f(): ' + f());  // 2 (++x)

每当创建了一个闭包函数,都会在内存中保存,对自由变量的引用-不会随着外部函数执行完毕而销毁。在上面的代码中创建了 f , g 两个闭包函数,因此这个两个函数分别保存对 变量 X的引用!又因为, f,g 是两个不同的****词法环境, 因此引用的变量也不会相互影响! 上诉的前提是,词法环境已经确定了(在指定的 foo函数中)

在某些情况,开发者可能希望自定义词法环境。

引用一个未绑定的变量

var module = {
  x: 42,
  getX: function() {return this.x; }
}
var unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// emits undefined as 'x' is not specified in global scope.

var boundGetX = unboundGetX.bind(module); // specify object module as the closure
console.log(boundGetX()); // emits 42

#

意外引用一个绑定的变量

<body>
    <p id='p'>第一</p>
    <p id='pp'>第2</p>
    <p id='ppp'>第3</p>
    <p id='pppp'>第4</p>
    <p id='ppppp'>第5</p>
    <p id='pppppp'>第6</p>
</body>
<script>
    var pp = document.getElementsByTagName('p')
    function test0() {
        for (var i = 0; i < 5; i++) {
            pp[i].addEventListener('click', function () {
              // 全是 pppppp
                console.log(pp[i].getAttribute('id'))
            })
        }

    }
    test0()
</script>

在此示例中,预期的行为是每个P标签在单击时都应该打印处对应的id属性。 但是由于变量’i’ 被提升到了作用域的顶部,并且在单击时进行了惰性计算,因此实际发生的情况是,每个on click事件都会在for循环结束时发出’elements’中最后一个元素的ID。 这里同时涉及到this的指向性问题,我将另外开一篇文章讲解

如何解决: 变量 i应该被绑定到 for循环的块级作用域中去

using handle.bind(this) or the let keyword

   var pp = document.getElementsByTagName('p')
    function test0() {
        for (var i = 0; i < 5; i++) {
            function handle(item) {
                console.log(item.getAttribute('id'))
            }
            pp[i].addEventListener('click', handle.bind(this, pp[i]))
        }
    }
    test0()

#

性能考量

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响

之前

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

之后

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

**

术语

1.free variable(自由变量)

在计算机编程中,术语“自由变量”是指函数中使用的既不是局部变量也不是该函数的参数的变量。 在这种情况下,术语“非局部变量”通常是同义词。

2. nested function(嵌套函数)

在计算机编程中,嵌套函数是在另一个函数(封闭函数)中定义的函数**

3. enclosing function(封闭函数)

_

4. First-class citizen(一等公民)

在编程语言设计中,给定编程语言中的一等公民(也就是类型,对象,实体或值)。 这些一等公民通常可以作为参数传递,从函数返回,修改并分配给变量。在JavaScript中 函数是一等公民,意思说 函数可以做为参数,也可以作为值被返回

5. First-class function

在计算机科学中,如果将一种编程语言中的函数视为一等公民(First-class citizen),那么它就具有一级函数(First-class function)。
这意味着该语言

  • 支持将函数作为参数传递给其他函数,
  • 将函数作为其他函数的值返回,并将其分配给变量或存储在数据结构中。

在具有一级函数的语言中,函数名称没有任何特殊状态。它们被视为具有函数类型的普通变量。该词是克里斯托弗·斯特拉奇(Christopher Strachey)在1960年代中期“作为一流公民的职能”的背景下创造的。

First-class function 是函数式编程风格的必要条件,其中使用高阶函数是一种标准做法。高阶函数的一个简单示例是map函数,它以函数和列表作为参数,并返回通过将函数应用于列表的每个成员而形成的列表。

6. higher-order functions(高阶函数)

参考文档

1、 Closure_(computer_programming)
2、 First-class_function
3、 什么是 First-class function?
4、 Free_variables_and_bound_variables
5、 MDN-闭包

文章永久链接:https://tech.souyunku.com/31790

未经允许不得转载:搜云库技术团队 » 老生常谈--"闭包"不完全解析

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们