乐趣区

js闭包探秘

译者:闭包都被讨论烂了,不理解闭包都不好意思说自己会 js,但我看到这篇文章还是感觉眼前一亮,也让我对闭包有了一些新的理解,并且涉及了一些类和原型链的知识,这是一篇 2012 年的文章,稍微有点早,内容也略微基础,但是很明晰,希望能给读者带来新的理解。
闭包 (Closure) 是 javascript 这门语言中有些复杂并且充满误解的特性。简言之,闭包是一个对象,这个对象包含一个方法(function)和该方法创建时环境的引用(reference to the enviroment)。为了完全理解闭包,我们还需要理解两个 js 中的特性,一个是一级方法 (first-class function),另一个是内部方法 (inner function)。
一级方法 /First-Class Functions
在 js 中,方法是头等公民,因为它可以被轻易转换成其他数据类型。比如,一级方法可以实时构建并且赋值给一个变量。也可以传递给其他方法,或者通过其他方法返回。除了满足这些标准以外,方法也拥有自己的属性和方法。通过下述例子,我们来看一下一级方法的能力。
var foo = function() {
alert(“Hello World!”);
};

var bar = function(arg) {
return arg;
};

bar(foo)();
译者注:省略原文对代码的文字解释,这里体现的是一级方法可以返回参数,参数可以是另外一个一级函数,返回的结果还可以调用。
内部方法 /Inner Functions
内部方法或者说嵌套方法,是指定义在其他方法内部的方法,每当外部方法被唤起,内部方法的实例就被创建。下面的例子反应内部方法的使用,add 方法是外部方法,doAdd 是内部方法。
function add(value1, value2) {
function doAdd(operand1, operand2) {
return operand1 + operand2;
}

return doAdd(value1, value2);
}

var foo = add(1, 2);
// foo equals 3
这个例子中,一个重要的特性是,内部方法获取到了外部方法的作用域,这意味着内部方法能够使用外部方法的变量,参数等。例子中 add() 的参数 value1,value2 传递给 doAdd() 的 operand1,operand2 参数。然而这并没有必要,因为 doAdd 可以直接获取 value1,value2。所以上面的例子我们还可以这么写:
function add(value1, value2) {
function doAdd() {
return value1 + value2;
}

return doAdd();
}

var foo = add(1, 2);
// foo equals 3
创建闭包 /Creating Closures
内部方法获取外部方法的作用域,便形成了一个闭包。典型的场景是外部函数将其内部方法返回,内部方法保持了外部环境的引用,并保存了作用域下的所有变量。一下例子展示闭包如何创建并使用。
function add(value1) {
return function doAdd(value2) {
return value1 + value2;
};
}

var increment = add(1);
var foo = increment(2);
// foo equals 3
说明:

add 返回了内部方法 doAdd,doAdd 调用了 add 的参数,闭包创建。
value1 是 add 方法的本地变量,对 doAdd 来说是非本地变量(非本地变量指变量既不在函数体本身,也不在全局),value2 是 doAdd 的本地变量。
当 add(1) 被调用,一个闭包被创建并储存在 increment 中,在该闭包的引用环境中,value1 绑定了 1,被绑定的 1 相当于“封锁”在这个函数中,这也是“闭包”这个名字的由来。
当 increment(2) 被调用,进入闭包函数,这意味着携带着 value1 为 1 的 doAdd 被调用,因此该闭包本质上可以当做如下函数:

function increment(value2) {
return 1 + value2;
}
何时使用闭包?
闭包可以实现很多功能。比如将回调函数绑定指定参数。我们说两个让你的生活和开发变得更简单的场景。
配合定时器
闭包结合 setTimeout 和 setInterval 非常有用,闭包允许你向回调函数传入指定参数,比如下面的例子,每秒钟在给指定 dom 插入字符串。
<!DOCTYPE html>
<html lang=”en”>
<head>
<title>Closures</title>
<meta charset=”UTF-8″ />
<script>
window.addEventListener(“load”, function() {
window.setInterval(showMessage, 1000, “some message<br />”);
});

function showMessage(message) {
document.getElementById(“message”).innerHTML += message;
}
</script>
</head>
<body>
<span id=”message”></span>
</body>
</html>
遗憾的是,IE 不支持向 setInterval 的回调传参,IE 中页面不会展现“some message”而是“undefined”(无值传入 showMessage()), 解决这个问题,可以通过闭包将期望值绑定于回调函数里,我们可以改写如上代码:
window.addEventListener(“load”, function() {
var showMessage = getClosure(“some message<br />”);

window.setInterval(showMessage, 1000);
});

function getClosure(message) {
function showMessage() {
document.getElementById(“message”).innerHTML += message;
}

return showMessage;
}
2. 模拟私有属性绝大多数面向对象的程序语言支持对象的私有属性,然而 js 不是纯正的面向对象的语言,因此也没有私有属性的概念。不过,我们可以通过闭包来模拟私有属性。回想一下,闭包包含了一份其创建环境的引用,这份引用已经不在当前作用域中了,因此这份引用只能在闭包中访问,这本质上就是私有属性。看如下例子(译者:省略对代码的文字描述):
function Person(name) {
this._name = name;

this.getName = function() {
return this._name;
};
}
这里有一个严重的问题,因为 js 不支持私有属性,所以我们没法阻止别人修改实例的 name 字段,比如我们创建一个 Person 实例叫 Colin,然后可以将他的名字改成 Tom。
var person = new Person(“Colin”);

person._name = “Tom”;
// person.getName() now returns “Tom”
没有人愿意不经同意就被别人改名字,为了阻止这种情况的发生,通过闭包让_name 字段变成私有。看如下代码,注意这里的_name 是 Person 构造器的本地变量,而不是对象的属性,闭包形成了,因为外层方法 Person 对外暴露了一个内部方法 getName。
function Person(name) {
var _name = name;// 注:区别在这里

this.getName = function() {
return _name;
};
}
现在,当 getName 被调用,能够保证返回的是最初传入类构造器的值。我们依然可以为对象添加新的_name 属性,但这并不影响闭包 getName 最初绑定的值,下面的代码证明,_name 字段,事实私有。
var person = new Person(“Colin”);

person._name = “Tom”;
// person._name is “Tom” but person.getName() returns “Colin”
什么时候不要用闭包?
正确理解闭包如何工作何时使用非常重要,而理解什么时候不应该用它也同样重要。过度使用闭包会导致脚本执行变慢并消耗额外内存。由于闭包太容易创建了,所以很容易发生你都不知道怎么回事,就已经创建了闭包的情况。本节我们说几种场景要注意避免闭包的产生。1. 循环中循环中创建出闭包会导致结果异常。下例中,页面上有三个按钮,分别点击弹出不同的话术。然而实际运行,所有的按钮都弹出 button4 的话术,这是因为,当按钮被点击时,循环已经执行完毕,而循环中的变量 i 也已经变成了最终值 4.
<!DOCTYPE html>
<html lang=”en”>
<head>
<title>Closures</title>
<meta charset=”UTF-8″ />
<script>
window.addEventListener(“load”, function() {
for (var i = 1; i < 4; i++) {
var button = document.getElementById(“button” + i);

button.addEventListener(“click”, function() {
alert(“Clicked button ” + i);
});
}
});
</script>
</head>
<body>
<input type=”button” id=”button1″ value=”One” />
<input type=”button” id=”button2″ value=”Two” />
<input type=”button” id=”button3″ value=”Three” />
</body>
</html>
去解决这个问题,必须在循环中去掉闭包(译者:这里的闭包指的是 click 事件回调函数绑定了外层引用 i),我们可以通过调用一个引用新环境的函数来解决。下面的代码中,循环中的变量传递给 getHandler 函数,getHandler 返回一个闭包(译者:这个闭包指的是 getHandler 返回的内部方法绑定传入的 i 参数),独立于原来的 for 循环。
function getHandler(i) {
return function handler() {
alert(“Clicked button ” + i);
};
}

window.addEventListener(“load”, function() {
for (var i = 1; i < 4; i++) {
var button = document.getElementById(“button” + i);

button.addEventListener(“click”, getHandler(i));
}
});
2. 构造函数里的非必要使用类的构造函数里,也是经常会产生闭包的错误使用。我们已经知道如何通过闭包设置类的私有属性,而如果当一个方法不需要调用私有属性,则造成的闭包是浪费的。下面的例子中,Person 类增加了 sayHello 方法,但是它没有使用私有属性。
function Person(name) {
var _name = name;

this.getName = function() {
return _name;
};

this.sayHello = function() {
alert(“Hello!”);
};
}
每当 Person 被实例化,创建 sayHello 都要消耗时间,想象一下有大量的 Person 被实例化。更好的实践是将 sayHello 放入 Person 的原型链里(prototype),原型链里的方法,会被所有的实例化对象共享,因此节省了为每个实例化对象去创建一个闭包(译者:指 sayHello),所以我们有必要做如下修改:
function Person(name) {
var _name = name;

this.getName = function() {
return _name;
};
}

Person.prototype.sayHello = function() {
alert(“Hello!”);
};
需要记得一些事情

闭包包含了一个方法,以及创建它的代码环境引用
闭包会在外部函数包含内部函数的情况下形成
闭包可以轻松的帮助回调函数传入参数
类的私有属性可以通过闭包模拟
类的构造器中使用闭包不是一个好主意,将它们放到原型链中

退出移动版