闭包与作用域

Author Avatar
Hongxu 7月 10, 2018

变量的作用域

闭包与 JavaScript 变量的作用域息息相关。要理解闭包就要先理解 JavaScript 变量作用域。

变量的作用域有两种: 一种是全局的另一种是局部的。

var a = 1;
var n = 9;

function foo() {
    var a = 0;
    b = 2;
    var c = 1;
    console.log(a);
    console.log(n);
}

foo(); 
// 0
// 9
console.log(a); // 1
console.log(b); // 2  解释:如果局部变量没有用 var 命令声明,那么这个变量会被当做全局变量
console.log(c); // ReferenceError: c is not defined

如上函数变量 首先我们声明了一个全局的 var a = 1 ,然后又声明了一个全局的函数 foo

foo 函数内部声明了函数内部的变量 var a = 0, 以及没有用 var 命令声明的变量 b = 2

此时全局作用域即外部的 JavaScript 所执行的环境而局部作用域是 foo 函数内部。上面代码的执行结果应该很明显的表现了作用域的范围。

全局变量可以被局部作用域和全局作用域访问,然而局部变量却不可以被外部作用域访问。

可以被欺骗的作用域

《你不懂 JS —— 作用域和闭包》 里看到了一段有趣的东西。我们可以欺骗词法作用域但是欺骗词法作用于会导致性能下降。因为引擎需要动态去改变某个变量的作用域环境。

第一种是 eval

var b = 2;

function foo(str, a) {
    eval(str);
    console.log(a, b);
}

foo("var b = 3;", 1); // 1, 3

上面的函数在 eval 执行完之后,引擎不会关心前面做了什么依然会乖乖的去寻找 console.log 所需要的两个变量 ab, 首先 a 在函数的参数中找到了,其次找 b 由于执行了 eval 所有在函数内部存在了变量 b = 3。于是打印出 1, 3

Note: 当 eval 被用于 strict 模式的时候,eval 内部的声明不会改变包围它的作用域

function foo(str) {
    "use strict";
    eval(str);
    console.log(a); // ReferenceError: a is not defined
}
foo("var a = 1;");

第二种是 with

好吧已经废除了不想多说 MDN with

对性能的影响

JavaScript 引擎在编译阶段运行许多性能优化的工作。其中的一些优化原理都归结为实质在进行词法分析时可以静态地分析代码,并提前决定所有的变量和函数声明都在什么位置,这样在执行期间就可以少花些力气来解析标识符。

如果引擎在代码中发现一个 evalwith,它实质上就不得不假设自己知道所有的标识符的位置可能是无效的,因为他不可能在词法分析时就知道你会向 evalwith 传递什么样的代码来修改词法作用域。

换句话说,如果 evalwith 出现,那么引擎做的几乎所有优化都会变得没有意义。所以他简单地不做任何优化。代码会趋于运行的更慢(没有优化,代码就运行的更慢)

——《你不知道的 JavaScript —— 作用域与闭包》

什么是闭包 (Closure)

闭包是函数和声明该函数的词法环境的组合

考虑如下函数

function makeAdder(x) {
    return function(y) {
        return x + y;
    };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

上述函数中我们定义了一个 makeAdder(x) 函数,它接受一个参数 x 并返回一个新的函数。返回的函数接受一个参数 y 并返回 x + y 的值。

add5add10 都是闭包他们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中 x 是 5 而在 add10x 是 10。

上述只是闭包的一种用来简略的介绍闭包的特性。

实用的闭包

闭包可以用来做很多事情,比如模拟面向对象的代码风格,提升代码执行效率。

闭包的优点就是可以读取函数内部的变量,还可以让这些变量始终保持在内存当中。

正好前些日子在看 Sizzle 的源码,其中也用到了很多闭包相关的东西。就用 Sizzle 源码作为例子吧。

1. 结果缓存

/**
 * Create key-value caches of limited size
 * @returns {function(string, object)} Returns the Object data after storing it on itself with
 *    property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
 *    deleting the oldest entry
 */
function createCache() {
    var keys = [];

    function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if ( keys.push( key + " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            delete cache[ keys.shift() ];
        }
        return (cache[ key + " " ] = value);
    }
    return cache;
}

var tokenCache = createCache();

tokenCache( selector, groups );

createCache() 内部有一个 keys 数组用来维护需要缓存的关键字,缓存结果被当做属性存储在 cache 函数上,当 keys 的数量超出可缓存的数量就会删掉最开始缓存的内容。

这里 keys 只在函数执行时创建了,并且不会被回收掉。形成了一个闭包,外部通过 cache 函数依然可以访问到 createCache 内部的 keys

2. 模拟私有方法

var Person = function() {
    var name = "noName";

    return {
        getName: function() {
            return name;
        },
        setName: function(newName) {
            name = newName
        }
    };
};


var person = Person();
person.getName(); // noName
person.setName("LiLei");
person.getName(); // LiLei

Person 函数中的 name 作为一个私有变量只有调用 getName 方法才能访问的到,如果直接用 person.name 访问会得到 undefined

小心的使用闭包

闭包的缺点也很明显:由于变量都被保存下来了,那么内存的消耗肯定相比于没有闭包要来的大。如果不小心使用很容易就造成内存泄漏。

意料之外的闭包

下面是 MDN 上关于闭包的一段代码,许多的面试题也喜欢用 for 循环 + setTimeout 来考察求职者对于闭包的理解。

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();

毫无疑问上述代码不管哪个元素被 focus 都只会显示关于 age 的 help 信息,解法也大致那么几种。

  1. 用 ES6 的 let
let item = helpText[i]
  1. 创建一个匿名闭包
  for (var i = 0; i < helpText.length; i++) {

    (function(item) {
        document.getElementById(item.id).onfocus = function() {
            showHelp(item.help);
        };
    }(helpText[i]);

  }
  1. 使用更多的闭包
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(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 = makeHelpCallback(item.help);
  }
}

setupHelp();