您的位置:首页 > Web前端 > JavaScript

Javascript闭包

2016-01-14 14:43 471 查看
摘要:“如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂。”好吧,我试着向一个27岁的朋友就是JS闭包(JavaScript closure)却彻底失败了。

  越来越觉得国内没有教书育人的氛围,为了弄懂JS的闭包,我使出了我英语四级吃奶的劲去google上搜寻着有关闭包的解释,当我看到stackoverflow上这一篇解答,我脑中就出现了一句话:就是这货没跑了!

  不才译文见下,见笑了。

  Peter Mortensen问:

就像老Albert所说的,“如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂。”好吧,我试着向一个27岁的朋友就是JS闭包(JavaScript closure)却彻底失败了。

你们会怎么把它解释给一个充满好奇心的六岁孩子听呢?

注:我看过StackOverflow上给出的示例,但根本没用。

  Ali的回答:

  当function里嵌套function时,内部的function可以访问外部function里的变量。

function foo(x) {

var tmp
= 3;

function bar(y) {

alert(x + y
+ (++tmp));

}

bar(10);

}

foo(2)

  不管执行多少次,都会alert 16,因为bar能访问foo的参数x,也能访问foo的变量tmp。

  但,这还不是闭包。当你return的是内部function时,就是一个闭包。内部function会close-over外部function的变量直到内部function结束。

function foo(x) {
var tmp
= 3;

return function (y) {

alert(x + y
+ (++tmp));

}

}

var bar
= foo(2);
// bar 现在是一个闭包

bar(10);

  上面的脚本最终也会alert 16,因为虽然bar不直接处于foo的内部作用域,但bar还是能访问x和tmp。

  但是,由于tmp仍存在于bar闭包的内部,所以它还是会自加1,而且你每次调用bar时它都会自加1.

  (考虑到六岁这个限制:我们其实可以建立不止一个闭包方法,比如return它们的数组,也可以把它们设置为全局变量。它们全都指向相同的x和相同的tmp,而不是各自有一份副本。)

  注:现在来整点儿七岁的内容。

  上面的x是一个字面值(值传递),和JS里其他的字面值一样,当调用foo时,实参x的值被复制了一份,复制的那一份作为了foo的参数x。

  那么问题来了,JS里处理object时是用到引用传递的,那么,你调用foo时传递一个object,foo函数return的闭包也会引用最初那个object!

function foo(x) {

var tmp
= 3;

return function (y) {

alert(x + y
+ tmp);

x.memb = x.memb
? x.memb
+ 1 :
1;

alert(x.memb);

}

}

var age
= new Number(2);

var bar
= foo(age);
// bar 现在是一个引用了age的闭包

bar(10);

  不出我们意料,每次运行bar(10),x.memb都会自加1。但需要注意的是x每次都指向同一个object变量——age,运行两次bar(10)后,age.memb会变成2.

  这和HTML对象的内存泄漏有关,呃,不过貌似超出了答题的范围。

  JohnMerlino 对Ali说:

  这里有一个不用return关键字的闭包例子:

function closureExample(objID, text, timedelay) {

setTimeout(function() {

document.getElementById(objID).innerHTML = text;

}, timedelay);

}

closureExample(‘myDiv’, ‘Closure is created’, 500);

  深夜1:37 John Pick这样回答:

  JS里的function能访问它们的:

  1. 参数

  2. 局部变量或函数

  3. 外部变量(环境变量?),包括

3.1 全局变量,包括DOM。

3.2 外部函数的变量或函数。

  如果一个函数访问了它的外部变量,那么它就是一个闭包。

  注意,外部函数不是必需的。通过访问外部变量,一个闭包可以维持(keep alive)这些变量。在内部函数和外部函数的例子中,外部函数可以创建局部变量,并且最终退出;但是,如果任何一个或多个内部函数在它退出后却没有退出,那么内部函数就维持了外部函数的局部数据。

  一个典型的例子就是全局变量的使用。

  mykhal这样回答:

  Wikipedia对闭包的定义是这样的:

In computer science, a closure is a function together with a referencing environment for the nonlocal names (free variables) of that function.

  从技术上来讲,在JS中,每个function都是闭包,因为它总是能访问在它外部定义的数据。

  Since scope-defining construction in Javascript is a function, not a code block like in many other languages,
what we usually mean by closure in Javascript is a
fuction working with nonlocal variables defined in already executed surrounding function
.

  闭包经常用于创建含有隐藏数据的函数(但并不总是这样)。

var db
= (function() {

// 创建一个隐藏的object, 这个object持有一些数据

// 从外部是不能访问这个object的

var data
= {};

// 创建一个函数, 这个函数提供一些访问data的数据的方法

return function(key, val) {

if (val
=== undefined) {
return data[key] }
// get

else {
return data[key]
= val }
// set

}

// 我们可以调用这个匿名方法

// 返回这个内部函数,它是一个闭包

})();

db('x');
// 返回 undefined

db('x',
1);
// 设置data['x']为1

db('x');
// 返回 1

// 我们不可能访问data这个object本身

// 但是我们可以设置它的成员

  看了这么多外国大牛的解答,不知道你懂还是不懂,反正我是懂了。

  P.S. 发布文章之后看到@xiaotie的一篇文章,觉得有必要推荐一下,因为其剖析得更为深入。有人说应该在文章结尾对闭包进行总结,可惜小弟才疏学浅,不能给出一个精辟的总结。

  @xiaotie对闭包的总结如下:

(1)闭包是一种设计原则,它通过分析上下文,来简化用户的调用,让用户在不知晓的情况下,达到他的目的;

(2)网上主流的对闭包剖析的文章实际上是和闭包原则反向而驰的,如果需要知道闭包细节才能用好的话,这个闭包是设计失败的;

(3)尽量少学习。

  大家学习学习。

什么是闭包,我的理解

首先,我觉得,一个概念,如果不理解也不影响使用的话,那么,就没必要去理解它、去学习它。闭包就是这样一个概念,你不理解它也能很好的用它。俺这两年写as3程序,是天天在和它打交道,甚至有过一个function套一个,一个方法中套了20多个function的极端例子,但从未深究过它是怎么实现的,它就像水和空气一样,我们不需要知道水是H2O,空气是氧气氮气二氧化碳等的混合物,也活的好好的。

其次,我觉得,网上对闭包概念的解释都太狭隘了,看得人蛋疼,就像回到了i++,++i时代一样。如果非要去理解这个概念,像那样去理解,则收获太小,不值得。

维基百科上对闭包的解释就很经典:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

Peter J. Landin 在1964年将术语闭包定义为一种包含环境成分和控制成分的实体。

下面是我理解的闭包概念。

先看看数学上的闭包。

(1,5) 是一个区间,但对这个区间做分析、计算什么的,经常会用到1和5这两个不属于这个区间的值,[1,5]就是(1,5)的闭包。

在生活上,我们办事情,找A部门,A部门说,你先得找B部门盖个章,B部门说,你先得找C部门盖个章,C部门说,这个东西不是我们的职权范围…… 踢皮球,这就是非闭包。闭包就是负责到底,你找到A部门,A部门接待的那个人负责到底,他/她去协调B部门和C部门。

在工程上,闭包就是项目经理,负责调度项目所需要的资源。老板、客户有什么事情,直接找项目经理即可,不用再去找其它的人。

在程序语言中,闭包就是一种语法糖,它以很自然的形式,把我们的目的和我们的目的所涉及的资源全给自动打包在一起,以某种自然、尽量不让人误解的方式让人来使用。至于其具体实现,我个人意见,在不影响使用的情况下,不求甚解即可。在很多情况下,需要在一段代码里去访问外部的局部变量,不提供这种语法糖,需要写非常多的代码,有了闭包这个语法糖,就不用写这么多代码,自然而然的就用了。

这样一来,可以把闭包从一个语法机制提升为一种设计原则:

闭包是从用户角度考虑的一种设计概念,它基于对上下文的分析,把龌龊的事情、复杂的事情和外部环境交互的事情都自己做了,留给用户一个很自然的接口。

在这个原则下,函数式语言中,那种所谓的闭包只是一种“闭包”,还有大量的其它类型的“闭包”等待发现和实现。

下面举出一些闭包设计原则的正例和反例。

正例:

Flex中的数据绑定语法就是一种“闭包”。x="{b.num + c.num}",对于这个语法,编译器自动去上下文中寻找叫 b 和 c 的变量,然后再找他们内部 num 变量,如果他们都是可绑定的话,则自动给它们添加上绑定链,当 b, c, num 等有任一变动时,更新 x 的值。

反例:

Winform 中的设计就违反了闭包原则,当不是在该UI线程中,更新某些控件的值时,会抛出异常。只能去invoke调用,而invoke的接口很难用,相信很多人对这东东极其反感。

闭包不一定是语法糖。当我们不能直接扩展编译器时,我们就无法增加语法糖来实现闭包机制,这时,就要用现有的语言机制来实现了。

下面,我们来对winform的invoke方法进行改造,使它满足闭包原则。下面是代码:

public class ControlFuncContext

{

public Control Control { get; private set; }

public Delegate Delegate { get; private set; }

public ControlFuncContext(Control ctl, Delegate d)

{

this.Control = ctl;

this.Delegate = d;

}

public void Invoke0()

{

if (Control.IsHandleCreated == true)

{

try

{

Delegate.DynamicInvoke();

}

catch(ObjectDisposedException ex)

{

}

}

}

public void Invoke1<T>(T obj)

{

if (Control.IsHandleCreated == true)

{

try

{

Delegate.DynamicInvoke(obj);

}

catch (ObjectDisposedException ex)

{

}

}

}

public void Invoke2<T0,T1>(T0 obj0, T1 obj1)

{

if (Control.IsHandleCreated == true)

{

try

{

Delegate.DynamicInvoke(obj0, obj1);

}

catch (ObjectDisposedException ex)

{

}

}

}

}

public static class FormClassHelper

{

public static void InvokeAction(this Control ctl, Action action)

{

if (ctl.IsHandleCreated == true)

{

ControlFuncContext fc = new ControlFuncContext(ctl, action);

ctl.Invoke(new Action(fc.Invoke0));

}

}

public static void InvokeAction<T>(this Control ctl, Action<T> action, T obj)

{

if (ctl.IsHandleCreated == true)

{

ControlFuncContext fc = new ControlFuncContext(ctl, action);

ctl.Invoke(new Action<T>(fc.Invoke1<T>), obj);

}

}

public static void InvokeAction<T0, T1>(this Control ctl, Action<T0, T1> action, T0 obj0, T1 obj1)

{

if (ctl.IsHandleCreated == true)

{

ControlFuncContext fc = new ControlFuncContext(ctl, action);

ctl.Invoke(new Action<T0, T1>(fc.Invoke2<T0, T1>), obj0, obj1);

}

}

}

使用起来很简单,直接调用扩展方法 InvokeAction 即可,不必去考虑跨线程还是不跨线程这些“环境因素”,跨线程调用,我们已经通过用户不必知晓的方式,把它封装起来了。

再举个例子,写程序经常需要这样一个功能:打开一个图像文件,然后进行处理。正常写法很麻烦,比如,那个filter格式就很容易忘记,那么,我们就把它闭包化,把不该让用户知道,不该让用户敲键盘的都给它封装起来:

public static void OpenFile(this Form element, Action<String> callbackOnFilePath, String filter = "所有文件|*.*")

{

String filePath;

OpenFileDialog dlg = new OpenFileDialog();

dlg.Filter = filter;

dlg.FileOk += (object sender, CancelEventArgs e) =>

{

filePath = dlg.FileName;

if (callbackOnFilePath != null)

callbackOnFilePath(filePath);

};

dlg.ShowDialog();

}

public static void OpenImageFile(this Form element, Action<String> callbackOnFilePath, String filter = "图像文件|*.bmp;*.jpg;*.gif;*.png")

{

OpenFile(element, callbackOnFilePath, filter);

}

再举一个例子,这个例子是as3中的。在Flex中,控件有一个callLater 方法,在下一帧时进行调用。这个方法非常有用,很多时候,非Flex项目也需要这样的一个方法。下面,我们进行模拟:

package orc.utils

{

import flash.display.Stage;

import flash.events.Event;

public class CallLaterHelper

{

public function CallLaterHelper(stage:Stage, callback:Function)

{

this.callback = callback;

this.stage = stage;

stage.addEventListener(Event.ENTER_FRAME, onStageEnterFrame);

}

private var stage:Stage;

private var callback:Function;

private function onStageEnterFrame(event:Event):void

{

stage.removeEventListener(Event.ENTER_FRAME, onStageEnterFrame);

if(callback != null)

{

callback();

}

}

}

}

然后在基础控件中,提供callLater方法:

public function callLater(callback:Function):void

{

new CallLaterHelper(this.stage,callback);

}

总结:

(1)闭包是一种设计原则,它通过分析上下文,来简化用户的调用,让用户在不知晓的情况下,达到他的目的;

(2)网上主流的对闭包剖析的文章实际上是和闭包原则反向而驰的,如果需要知道闭包细节才能用好的话,这个闭包是设计失败的;

(3)尽量少学习。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: