前端函数式编程简史02 闭包

116 阅读6分钟

最早的闭包

最早使用闭包的例子可以追溯到1963年 当时美国计算机科学家John McCarthy(AI之父)在Lisp编程语言中首次实现了闭包的概念

它的定义是:“闭包是一个可以在调用时返回其词法环境的函数” ,这允许函数可以访问在定义时,处在同一范围内定义的变量的值。

这个概念被称为“McCarthy的闭包”

下面是一个用Lisp代码实现的McCarthy的闭包例子:

(define make-adder
  (lambda (n)
          (lambda (x) (+ n x))))

(define add-three (make-adder 3))

(add-three 4)
; returns 7

简单解释一下:

首先,定义一个名为make-adder的函数,它接受一个参数n,并返回一个内部函数,该函数接受一个参数x,并返回两个参数nx的和。

然后,将参数3传递给make-adder,返回一个名为add-three的函数,该函数将n的值设置为3,它的自由变量n定义在函数外部,但是可以在函数内部访问。最后,将4作为参数传递给add-three,返回7,这就是McCarthy的闭包的一个简单例子。

源头 Alonzo Church

(lambda (x y)

    (lambda (z)

      (+ x y z)

    )

)

什么是闭包 ?

闭包是一种特殊的作用域,它允许函数在定义它们的作用域之外访问和操作变量。 也就是说,闭包可以记住并访问它被创建时所在的作用域,即使它们当前所处的作用域已经发生了变化。

以下是javascript中的一个简单闭包的例子:

function createClosure() {
  let privateData = 'This is private data';

  return function() {
    console.log(privateData);
  }
}

let closure = createClosure();
closure(); // Outputs "This is private data"

在上面的代码中,createClosure()函数创建了一个作用域,它将privateData变量存储在其中。 然后,它返回一个函数,该函数可以访问privateData变量,即使它当前所处的作用域已经发生了变化。

个人理解:

闭包是一种函数作用域,它可以让你在一个函数内部定义另一个函数,这个内部函数可以访问外部函数的变量和参数。闭包的作用是使函数的变量和参数能够保持可访问状态,以便在其它函数中重复使用,并且可以在不同的作用域中使用。

简单说明:

闭包是一种能够访问另一个函数作用域中的变量的函数。

最简说明

函数和它引用的环境就是闭包

所以有人说,一个函数就是一个闭包

function closure() {}

为什么需要闭包 ?

JavaScript中的闭包是一个函数,它可以访问另一个函数作用域中的变量。

闭包使得函数可以维持对局部变量的访问,即使函数已经执行完毕。

它允许函数在当前作用域外被调用,而不会影响它所在的作用域。

因此,闭包可以让函数的内部变量和外部变量保持在可访问状态,即使函数已经执行完毕。

闭包可以让函数的作用域变得更加精确,可以让函数访问和修改函数定义的时候不可见的变量,提高代码的可复用

利用闭包可以保护对象属性的私有性

闭包的应用

  1. 属性访问:使用闭包可以访问对象的私有属性,而无需暴露在外部。
  2. 封装函数:使用闭包可以将函数内部的变量封装起来,而不会被外部访问到。
  3. 回调函数:使用闭包可以将回调函数封装起来以便在之后使用。
  4. 定时器:使用闭包可以将定时器内部的变量封装起来,而不会被外部访问到。
  5. 命名空间:使用闭包可以将代码内部的变量封装起来,以避免变量和函数名称冲突。

利用闭包的特性:保护函数内部变量不被外部调用,从而起到私有作用域的作用。

可以实现如下功能

  1. 使用立即执行函数表达式(IIFE)来创建闭包:

(function () {
  var x = 10;
  console.log(x);
})();
  1. 使用函数作为参数传递:

function foo(func) {
  var x = 10;
  func();
}

foo(function () {
  console.log(x);
});
  1. 使用闭包实现私有变量:

function foo() {
  var x = 10;

  return function () {
    return x;
  }
}

var x = foo();
console.log(x());
  1. 使用闭包实现延迟执行:

function foo() {
  var x = 10;

  setTimeout(function () {
    console.log(x);
  }, 1000);
}

foo();
  1. 使用闭包实现模块模式:

var myModule = (function () {
  var privateVar = 10;

  function privateFunction() {
    console.log(privateVar);
  }

  return {
    publicFunction: privateFunction
  };
})();

myModule.publicFunction();

闭包使用的问题

闭包引用的环境变量是“不可见”的,这导致了一系列问题

1. 内存泄漏

由于闭包中的变量始终可以被访问,所以它们需要一直存在于内存中,这可能会导致内存泄漏。

function myFunc() {
  let arr = [];
  for (let i = 0; i < 10; i++) {
    arr.push(function() {
      console.log(i);
    });
  }
  return arr;
}
let myArr = myFunc();

上面的代码中,myFunc 函数会返回一个包含 10 个函数的数组。由于每个函数都会引用 i 变量,因此 i 变量会一直存在于内存中,从而导致内存泄漏

2. 不正确的 this 引用

在闭包中,this 指向的是外部作用域中的变量,如果外部作用域中的变量发生改变,那么 this 的指向也会发生改变。

let myObj = {
  name: "John",
  sayName: function() {
    console.log(this.name);
  }
};
let myFunc = myObj.sayName;
myObj.sayName() // John
myFunc(); // undefined

上面的代码中,myFunc 函数会引用外部作用域中的 myObj 变量,所以 this 指向的是 myObj 对象,但是当我们将 myFunc 函数赋值给另一个变量 myFunc 时,this 指向的就不是 myObj 了,所以 this.name 会返回 undefined

小讲一下 this

在 JavaScript 中,this 关键字是一个指向函数的执行上下文的指针,它的值取决于函数的调用方式。

当一个函数被作为对象的方法调用时,this 的值指向调用该函数的对象。因此,当我们在 myObj 对象上调用 sayName 方法时,this 的值指向 myObj 对象,因此 this.name 的值为 John

然而,当我们以函数形式调用 myFunc 时,this 的值指向全局对象,因为 myFunc 并没有被任何对象调用。因此,this.name 的值为 undefined,因此 myFunc() 会返回 undefined

3. 回调函数的重复调用问题

由于闭包会一直引用外部作用域中的变量,所以如果外部作用域中的变量发生改变,那么闭包中的函数也会发生改变。

let myFunc = function() {
  let count = 0;
  return function() {
    count++;
    console.log(count);
  };
};
let myFunc1 = myFunc();
let myFunc2 = myFunc();

myFunc1(); // 1
myFunc2(); // 1

上面的代码中,myFunc定义了一个函数,这个函数内部定义了一个count变量,并返回一个内部函数,该函数每次调用时,count变量值会加1。

然而,myFunc1myFunc2分别调用myFunc函数,分别得到一个内部闭包,此时,由于myFunc1myFunc2是两个不同的作用域,所以它们的count变量在不同的作用域上面. 接着,myFunc1myFunc2分别调用它们的内部闭包函数,都会将各自作用域上的count变量的值加1,所以最终返回的结果都是1.

想要更加深入了解函数式编程,请听下回讲解