JavaScript 闭包学习笔记

本文简单记录学习 JavaScript 闭包的知识点。由于闭包和函数是密不可分的,所以先简单总结下 JavaScript 函数的使用方法,然后介绍闭包的定义、闭包的应用和使用闭包常见误区。

0. JavaScript 函数

JavaScript 函数的几种使用方法:

  • 函数指针;
  • 构造方法;
  • 正常定义;
  • 自调用;
  • 函数的嵌套;

函数指针

var func = function (a, b) {
    return a * b;
}

构造方法

var func = new Function (a, b) {
    return a * b;
}

正常定义

function func(a, b) {
    return a * b;
}

var a = 1;
var b = 2;
console.log(func(a, b));

自调用

(function (a, b) {
    return a * b;
}).(1, 2);

函数嵌套

function func (a, b) {
    return function (a, b) {
        return a * b;
    }
}

1. 变量的作用域

1.1 全局变量

通常来说,“全局”是指变量所在的文件。如:

var iAmGlobal;
function func() {
    iAmGlobal++;
}

iAmGlobal++;

1.2 局部变量

局部变量是指,只在某个语句块里面可用的变量。如:

function func() {
    var iAmLocal = "u cannot get me outside func";
}

1.3 计数器

那么问题来了:我们如何实现一个计数器?
下面的方式是可以的:

var counter = 0;
var count = function () {
    return counter++;
}

只要调用 count() 便可以得到计数器的值。但是,counter 是全局变量,即该文件的任何地方都可以操作它,这显然是不安全的。我们想做下限制,限制用户只能通过 count() 函数得到计数器的值,而不能直接访问 counter。

下面的代码可以达到这个目的吗?

var count = function () {
    var counter = 0;
    return counter++;
}

这样做将 counter 由全局变量改为函数内的局部变量,无法在函数外部对其进行访问,但是每次调用 count() 都会对 counter 赋初值 0,返回值总为 1。显然,这是不行的。

下面这种方式可以吗?

var outer = function () {
    var counter = 0;
    return function () {
        return counter++;
    };
};

var count = outer();

或者

var count = (function () {
    var counter = 0;
    return function () {
        return counter++;
    };    
})();

这种方式是可以,我们只要调用 count() 就可以得到对应的计数器的值。

首先,变量 count 的值是一个函数表达式:

function () {
    return counter++;
}

count() 则是上述函数表达式的自调用。

1. 什么是闭包

对于“母语”是 C语言或 Java 的人来说,上面计数器的实现是诡异的,因为该计数器正确的基础是返回的函数表达式“顺走”了 counter 变量,而该变量是个局部变量。从而出现局部变量脱离其所在语句块存在的情况,这太 bug 了。

JavaScript 的世界里没有单身狗,万事万物都是对象(简直比 Java 还具有更彻底的面向对象性质)。变量是对象,函数是对象,对象更是对象。所以,闭包是对象,这个对象由两部分组成:函数和其附带的外部环境

以上述计数器的实现为例。

函数是表达式 function () {return counter++;},附带的外部环境是 counter 变量。

这就解释了为什么局部变量 counter 能脱离其作用域而存在了,其实并没有脱离,而是连同作用域被一起带走了。

来体会一下原味的定义(引自https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures):

Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure ‘remembers’ the environment in which it was created.

A closure is a special kind of object that combines two things: a function, and the environment in which that function was created. The environment consists of any local variables that were in-scope at the time that the closure was created.

2. 如何使用闭包

下面总结一下闭包的使用场景。

2.1 场景一:动态设置网页属性

如下代码可以动态设置网页字体的大小(查看运行效果请点击 jsfiddle)。

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>

<script> function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16); document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16; </script>

效果请点击 jsfiddle。

2.2 场景二:模拟私有变量

JavaScript 没有提供原生的对私有变量的支持,但是我们可以通过闭包来实现这个效果。在下面的代码中,我们没法直接访问局部变量 privateCounter,只能通过 increment()decrement()value() 函数来操作和访问,从而达到了私有变量的效果。

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* Alerts 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* Alerts 2 */
counter1.decrement();
alert(counter1.value()); /* Alerts 1 */
alert(counter2.value()); /* Alerts 0 */

3. 常见误区

在循环中使用闭包,是一个常见的误区。举例如下:

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

上述代码的初衷是在每个输入框获得焦点是都出现对应的提示语。但实际运行结果是提示语始终是“Your age (you must be over 16)”(实际效果请点击 jsfiddle)。

究其原因,在执行完 setupHelp() 后,三个方法公用的外部环境中的变量 item 已经是“{‘id’: ‘age’, ‘help’: ‘Your age (you must be over 16)’}”,当输入框获得焦点时触发对应的 showHelp() 方法,该方法获得的 item 均为上面的值,导致结果始终相同。

显然,上述问题的原因是三个方法使用了相同的外部环境变量所致,解决的思路是使三者各自有自己的外部环境变量:

function makeShowHelp(item) {
    return function() {
      showHelp(item.help);
    }
}

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ];

  for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeShowHelp(item); }
}

setupHelp();

这样,每个 showHelp(item) 的外部环境变量都是 makeShowHelp() 函数中的变量 item,这个 item 是三个不同的值。

4. 一点联想

Java 中没有闭包的概念,但是却有类似的用法,在使用非静态嵌套类(内部类)时,内部类实例可以使用其外部类实例的成员变量和成员方法。举2个例子:
示例1:

final MyClass myObj = new MyClass();
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(build(myObj));

View.OnClickListener build(final MyClass myObj) {

    myObj.member1 = "i am a local variable";
    return new View.OnClickListener() {
        update(myObj);
    };
}

实例2:

public class Outer {
    private String mem1 = "i am Outer's private property";

    private void method1() {
        System.out.println(mem1);
    }

    public Inner getInnerInstance() {
        return new Inner();
    }

    class Inner {
        public void print() {
            method1();
        }
    }
}

public class Main {

    public static void main(String[] args) {
        Outer o = new Outer();
        o.getInnerInstance().print();
    }
}

5. 参考文献

  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

你可能感兴趣的:(JavaScript,闭包)