在Javascript中使用闭包的好例子

5

我最近学习了Javascript中的闭包。

虽然我认为它的概念非常神奇,但我自己还没有找到一个好的应用。

在所有的博客文章和教程中,我都得到了很好的解释,知道了它们是什么以及如何使用它们。

但我找不到任何让我想:“哇!你可以用闭包做这个?太棒了!!!”的例子。所有我找到的例子都像这个一样纯理论性的。

function say667() {
  // Local variable that ends up within closure
  var num = 666;
  var sayAlert = function() { alert(num); }
  num++;
  return sayAlert;
}

var sayNumber = say667();
alert(sayNumber());

所以,我在想你们中是否有人能分享一些与这些特殊函数相关的令人惊叹的经历。

我知道这是一个开放式问题,但我会将答案归因于让我感到最惊叹的人。

谢谢


所有函数在JavaScript中都会创建闭包。你能具体描述一下你所谈论的概念吗? - user1106925
好的,请发布一些学术示例... - Šime Vidas
2
您可能需要重新措辞您的问题。目前的方式非常开放,没有“正确”的答案。 - user1106925
虽然不是非常惊人,但我想到了一个现实世界的用例,它与你引用的学术示例相差不多。这个用例是一个 setTimeout() / clearTimeout() 包装对象,可以为您管理计时器 ID。 - nnnnnn
7个回答

7
闭包常常与回调函数一起使用,因为它们允许您访问调用函数的本地变量,或者可以用于将本地变量的值“冻结”到特定回调的私有变量中,当本地变量在代码继续执行但在调用回调之前将更改为另一个值时。以下是我在Stack Overflow上提供答案时使用闭包的示例:
从setTimeout回调中访问父级本地变量:https://dev59.com/jFrUa4cB1Zd3GeqPhS1R#7032671 将非静态信息传递给延迟的回调:https://dev59.com/WV7Va4cB1Zd3GeqPMbYK#8660518 我知道仅在上个月里在SO答案中就使用了数十次闭包(我只是不确定如何快速搜索找到更多示例而不必浏览大量帖子)。
此外,这是一个创建私有变量的有用闭包:
function slides(images) {
    var slideImages = images || [];

    // because of this closure, the variable slideImages is available
    // to the method defined in here even though the slides function
    // has already finished executing
    this.addSlide = function(url) {
        slideImages.push(url);
    }
    this.clearSlides = function() {
        slideImages = [];
    }
}

// the slideImages variable is not available out here
// it is truly private inside the clsoure
var slideshow = new slides(imgArray);
slideshow.addSlide("xxx.jpeg");

5

那么,你可以做的一个好玩的事情是使用私有变量:

function Thing() {
  var x = 10;
  this.getX = function () {
    return x;
  }

  this.increment = function () {
    x++;
  }
}

现在,当您创建一个新事物时,它将具有getXincrement方法,但没有降低x值的方法。每个Thing实例的x值也将是唯一的。
Crockford有一个关于此模式的页面:http://javascript.crockford.com/private.html

5
一个基本的例子:
var getDay = (function () {
    var days = [
        'Monday',
        'Tuesday',
        'Wednesday',
        'Thursday',
        'Friday',
        'Saturday',
        'Sunday'
    ];

    return function ( n ) {
        return days[ n - 1 ];
    };
}());

在这里,我们的思路是将一个返回函数的IIFE分配给一个变量。完成此操作后,该变量保存了从IIFE返回的函数。由于该函数嵌套在IIFE内部,因此即使IIFE本身不再存在,该函数仍然可以访问其所有局部变量和参数。
因此,在上面的示例中,IIFE 的整个目的是定义一个 days 数组,它充当 getDay 函数的私有变量。

5

柯里化变量

由于“闭包”只是一种表达函数始终保留其原始变量范围的方式,因此有许多方法可以利用它。

柯里化似乎是人们喜欢的东西。


创建柯里化值操作器

在这里,我创建了一个名为curry的函数,该函数将返回一个函数,该函数将用于生成与原始柯里化值一起工作的新函数。

function curry() {
    var args = Array.prototype.slice.call(arguments);
    return function(fn) {
        return function() {
            var args2 = Array.prototype.slice.call(arguments);
            return fn.apply(this,args.concat(args2));
        };
    };
}

创建一个柯里化字符串的函数

如果我想创建处理字符串的函数,我可以将字符串进行柯里化...

var workWithName = curry("Bubba");

创建一个函数,将柯里化后的字符串放入句子中


创建一个函数,将柯里化后的字符串放到句子中

在这里,我创建了一个名为talkToName的函数,该函数会根据传入的参数将名称合并到不同的句子中...

var talkToName = workWithName(function(curried_str, before, after) {
    return before + curried_str + after;
});

现在我有一个talkToName函数,它接受两个字符串,这些字符串包装了柯里化的字符串。

talkToName("Hello there ", ". How are you?"); // "Hello there Bubba. How are you?"
talkToName("", " is really super awesome.");  // "Bubba is really super awesome."

注意,我向“talkToName”函数传递了两个参数,但是传递给“workWithName”的函数接受3个参数。
第一个参数由我们从“workWithName()”创建的函数传递,而我们给“talkToName”提供的两个参数将在原始柯里化参数之后添加。
创建一个函数,递增柯里化字符串的字符 在这里,我使用原始的“workWithName”函数创建一个完全不同的函数,该函数将获取“Bubba”字符串,并返回按给定值递增的字母字符串...
var incrementName = workWithName(function(curried_str, n) {
    var ret = '';
    for(var i = 0; i < curried_str.length; i++) {
        ret += String.fromCharCode(curried_str[i].charCodeAt() + n);
    }
    return ret;
});

所以,我将我的新的incrementName函数传入一个数字,它会递增名称中的字母,并返回新的字符串...

incrementName(3);  // "Exeed"
incrementName(8);  // "J}jji"
incrementName(0);  // "Bubba"

你可以看到,我们给curry()赋了一个值,然后它返回一个函数,可以用来创建新的函数,这些函数可以与原始值一起使用。

再次注意,我向incrementName函数传递了一个参数,但是传递给workWithName的函数接受2个参数。第一个参数是柯里化的。


其他数字示例

下面是一个示例,创建一个函数生成器,可与数字35一起使用。

var workWith3And5 = curry(3, 5);

创建可对已柯里化数字执行各种操作的函数

使用workWith3And5函数,我们可以创建一个新函数来接收一个数字参数,并返回一个由给定数字与已柯里化数字之和组成的数组...

var addNTo3And5 = workWith3And5(function(x, y, n) {
    return [3 + n, 5 + n];
});

addNTo3And5( 8 );  // [11, 13];
addNTo3And5( -4 ); // [-1, 1];

使用相同的workWith3And5函数,对数字35进行柯里化,创建一个3 x 5的数组,其中嵌套的数组给定了一些内容...

var create3By5GridWithData = workWith3And5(function(x, y, data) {
    var ret = []
    for(var i = 0; i < x; i++) {
        ret[i] = [];
        for(var j = 0; j < y; j++) {
           ret[i][j] = data;
        }
    }
    return ret;
});

create3By5GridWithData( 'content' ); // [Array[5], Array[5], Array[5]]

3

这并不是什么惊人的事情。像Java这样的语言没有闭包,但你仍然可以用它们编写好的软件。

话虽如此,能够做出像下面这样的事情确实很方便:

var that = this; // that is defined outside of the function below, but is still in its
                 // lexical scope
arry.each(function(item){
   that.doSomething(item); // which means 'that' is "closed-in" to this function
});

3
我在我正在开发的一个库中使用闭包来实现lambda表达式。
JLinx.Delegate=function() {
  var validateArg=function(arg) {
    if(typeof arg!=="string"&&typeof arg!=="function"&&arg!==null&&arg!==undefined) {
      throw new ArgumentException("arg");
    }
  };
  var funcBody;
  function prepBody(code,returnsResult) {
    var temp=code.trimLeft().trimRight();
    if(returnsResult&&temp.indexOf("return ")== -1) {temp="return "+temp;}
    if(temp.substr(temp.length-1,1)!=";") {temp+=";";}
    return temp;
  }
  function getDelegate(arg,defaultLambda,returnsResult) {
    validateArg(arg);
    if(typeof arg==="function") {return arg;}
    arg=(arg===null||arg===undefined)?defaultLambda:arg;
    if(arg.indexOf("=>")> -1) {
      var parts=arg.split("=>");
      var argList=parts[0];
      funcBody=prepBody(parts[1],returnsResult);
      argList=argList.trimLeft().trimRight()==""?"e":argList;
      argList=(argList.indexOf(",")> -1)?argList.split(","):[argList];
      switch(argList.length) {
        case 1:
          return new Function(argList[0],funcBody);
        case 2:
          return new Function(argList[0],argList[1],funcBody);
        default:
          throw new InvalidOperationException("Invalid number of arguments to action delegate.");
      }
    }
    else {
      funcBody=prepBody(arg,returnsResult);
      return new Function("e",funcBody);
    }
  }
  var factory=
    {
      actionFrom: function(arg) { return getDelegate(arg,"e => return;",false); },
      accumulatorFrom: function(arg) { return getDelegate(arg,"e, v => return v;",true); },
      comparerFrom: function(arg) { return getDelegate(arg,"l,r=>return l<r?-1:l>r?1:0;",true); },
      joinSelectorFrom: function(arg) { return getDelegate(arg,"o, i = { return { o : o, i : i }; };",true); },
      predicateFrom: function(arg) { return getDelegate(arg,"e => return true;",true); },
      selectorFrom: function(arg) { return getDelegate(arg,"e => return e;",true); }
    };
  return factory;
} ();

我知道这看起来不太起眼,但它允许您在库中使用其他方法(实际上提供了LINQ-to-XML)编写以下内容:
var exists = myXmlElement.Any("e.name == 'foo' || e.name == 'bar';');

闭包提供了一个工厂,将字符串转换为在Sequence对象中的每个元素上执行的函数。如果参数已经是函数,则直接返回该函数。
这是闭包可以做的一件事情。

1

我认为这个例子非常适合解释Javascript中的闭包。

      var juice = "Mango";
    var foo = function() {
     var juice = "Apple";
  return function(){
   return juice;
}

    };
var juicebar = foo();
console.log(juice);
console.log(juicebar());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接