【【吐槽 CNode 社区续】续】JavaScript匿名函数、函数自调用、返回函数的函数、闭包《入门》
发布于 1年前 作者 richarddong 2527 次浏览

虽然正经的分享贴反响非常平淡,但本人还是愿意给大家继续分享一下非常入门的 JavaScript 知识,这些知识对于大家写 Node.js 不可或缺。


匿名函数

什么是匿名函数?就是没有名字的函数。上一篇文章中举的例子:

setTimeout(function () {
  // some code...
}, 1000);

其中的function就是一个匿名函数(因为它没有名字)

如果这么写:

setTimeout(function something () {
  // some code...
}, 1000);

或者这么写:

var something = function () {
  // some code...
};

setTimeout(something, 1000);

这样就给这个函数起名为something,所以就不是匿名函数了。而且,因为有了名字,我们就可以在别的地方,比如函数内部调用它,从而实现代码的复用或者递归调用。

**注意:**上面两种写法虽然都给函数起名为something,但是这个名字的作用域不同,倒数第二种写法把something的作用域限制在了他本身,也就是只能在函数内部递归调用,而倒数第一种写法中something的作用域就是最外部的作用域,这也是setTimeout可以把它作为参数的原因。

所以,匿名函数并没有什么神奇,和有名字的函数没什么本质区别。不过,按照Felix’s Node.js Style Guide的推荐,最好给大多数函数都起上名字,这样有利于代码的维护。也就是按照上面倒数第二种写法。


函数自调用

所谓的函数自调用,就是在定义函数的时候直接调用。

比如:

var result = function (x) { return x + 1; }(3); // => result 的值为4

这段代码的本质是:

var plusOne = function (x) {
  return x + 1;
};

var result = plusOne(3);

这样写就很清楚了,我们先定义了一个函数plusOne,然后调用它,并提供实参「3」。

而上面的自调用的写法就是把这两件事放在一起完成了。

通过function (x) { return x + 1; }这一段代码,相当于写了一个「函数字面量」,而后面紧接着的(3)就是给这个「函数字面量」一个参数来调用它。

换句话说,所有函数类对象,不论是一个现场定义的匿名函数,还是一个之前定义的有名字的函数,他们在不加括号的时候都代表了这个函数对象本身,而加了括号就代表调用这个函数,也就是这个函数return的对象。

你可能会问,既然我在定义函数的时候直接调用它,我为什么不直接写函数里的代码呢?

// var result = function (x) { return x + 1; }(3); // => result 的值为4

var result = 3 + 1;

这样不是更好吗?

在有些情况下的确是这样更简洁,但在另一些情况下,我们希望递归调用,或者希望创造一个闭包,就需要使用函数自调用的技巧了。至于什么是闭包,后面会有解释。


返回函数的函数

重温刚才的例子:

var plusOne = function (x) {
  return x + 1;
};

var result = plusOne(3);

这里的函数plusOne返回参数加一以后的值。

实际上一个函数是可以返回任何对象的,我可以给plusOne再套一层:

var plusOne = function () {
  var realFunction = function (x) {
    return x + 1;
  };
  return realFunction; // 这里不能加括号,因为我们要返回`realFunction`函数本身
};

var result = plusOne()(3);

// 等价于
// var newFunction = plusOne();
// var result = newFunction(3);

这个很好理解,plusOne是第一个真正被调用的函数,通过plusOne()加括号,我们得到了plusOne函数return的对象,也就是realFunction函数,再通过plusOne()(3)加括号,我们就得到了realFunction这个函数传了参数「3」以后的return的值,也就是4。(注意对于最后一行plusOne()来说,realFunction这个名字已经不存在)

我们也可以把x放在外面:

var plusOne = function (x) {
  var realFunction = function () {
    return x + 1;
  };
  return realFunction; // 这里不能加括号,因为我们要返回`realFunction`函数本身
};

var result = plusOne(3)(); 

// 等价于
// var newFunction = plusOne(3);
// var result = newFunction();

这里,我们相当于先把「3」告诉plusOne函数,然后由realFunction负责把plusOne获得的参数加一,并返回。所以我们需要在第一个括号里传3,第二个括号为空。

利用上面所说的返回函数的函数,我们可以做一些很有趣的事情。

比如,我们想写一个做两个数相加的工作的函数。传统的方法是:

function plus (a, b) {
  return a + b;
}

plus(3, 4); // => 得到 7

现在我们可以这么写:

function plus (a) {
  return function (b) {
    return a + b;
  };
}

plus(3)(4); // => 得到 7

这样写的好处在于我可以把加法动作分成两部完成,而不一定要同时知道加号两边的数字。这样就可以做到:

var anotherNumberIs = plus(3);

// some code...

anotherNumberIs(4); // => 得到 7

把一个动作分成两步完成还可以实现一定的私密性。想象一下,我们需要两个密码打开一把锁,但是我们又不希望其中任何一个人知道另一个人的密码。如果我们用类似传统的加法方式实现,那必定会有一个环境,是两个密码的作用域重叠的,比如:

function unlock(pw1, pw2) {
  // unlocking
  if (pw1 + pw2 === 'niubi') return true;
  else return false;
}

var password1 = 'niu';
var password2 = 'bi';

unlock(password1, password2); // => true

而如果我们这么写:

function unlock (pw1) {
  return function (pw2) {
    if (pw1 + pw2 === 'niubi') return true;
    else return false;
  };
}

var myLock;

function () {
  var password1 = 'niu';
  myLock = unlock(password1);
}();

function () {
  var password2 = 'bi';
  myLock = myLock(password2);
}()

myLock; // => true

就能把两个密码的作用域完全分隔,保证两个密码的独立和安全。当然,这只是一个比喻,真正的安全问题比这复杂的多。


闭包(Closure)

闭包是经常被复杂化的概念。如果你仔细阅读并理解了上面的所有内容,其实你已经明白了闭包是怎么回事。

在上面的锁的例子中,我们用myLock = unlock(password1)把密码1包在了此时的myLock函数中,这个函数走到哪里,就把password1带到哪里。比如带到了这里:myLock = myLock(password2)。但是在用密码2解锁的时候,这个环境下无法得知myLock里装着的password1是什么,却可以把password2交给myLock函数来解锁。所以我们称它为「闭包」。

当然闭包不一定要那么复杂。非常粗略地说,如果一个函数包含着一些变量,函数走到哪里变量就走到哪里,但是这个变量从外面无法读取,那这就是一个闭包。

举个最简单的例子:

function incrementClosure () {
  var current = 0;
  return function () {
    current = current + 1;
    return current;
  };
}

var inc = incrementClosure();

inc(); // => 1
inc(); // => 2
inc(); // => 3
inc(); // => 4
// ......

这就是最简单的一个闭包的例子,current这个变量会一直跟着inc函数走,所以每次inc()都会对这个current执行加一的操作,但是在调用inc的时候,完全看不到里面发生了什么。所以使用闭包十分安全。

这时候,我们再回去看刚才的一个例子:

var plusOne = function (x) {
  var realFunction = function () {
    return x + 1;
  };
  return realFunction; // 这里不能加括号,因为我们要返回`realFunction`函数本身
};

var result = plusOne(3)(); 

这里其实也创造了一个闭包,通过plusOne(3)把「3」这个参数存了下来,之后只要直接加()就可以给3加1了。

闭包也有不足的地方,那就是这个变量会长期占用内存,因为运行环境不知道哪年哪月又会调用inc(),所以这个current就只能一直保留着,以备不时之需。


上面这几个概念都是 JavaScript 中非常重要也很常用的概念,在这里总结一下,供初学者参考。欢迎交流!

6 回复

不举多一个循环的实例…来展示闭包的使用?..个人感觉…js新手,往往会在循环的时候采坑…

做个补充吧,最后楼主提到的闭包最简单的那个例子没有说清楚为什么函数走到哪变量就走到哪。我来说下我个人的理解吧,incrementClosure是匿名函数的父函数,匿名函数相当于声明了一个全局变量,它依赖于父函数,就像一家人一样,父函数始终存在于内存中,子匿名函数也会存在,因此current就变为了全局变量,不会被垃圾回收机制所回收,因此也解释了为什么函数走到哪变量就走到哪。欢迎吐槽~

在javascript中可以认为“()”为函数调用符,括号里为参数列表 有名称的函数 通过 函数名() 使函数发生作用即执行函数内容 匿名函数为 匿名函数体+() 使匿名函数发生作用。

全局变量这个概念是依托作用域的概念的,一个东西即使一直存在内存里,但是当前的上下文无法访问,还是不能称为全局变量。

而且即使父函数不存在了这个current应该还是会存在的。

比如:

var inc = function () {
  var current = 0;
  return function () {
    current = current + 1;
    return current;
  };
}();

inc(); // => 1
inc(); // => 2
inc(); // => 3
inc(); // => 4
// ......

欢迎继续讨论~

@richarddong 关于这个问题我网上查了下资料,正如你所说,即使父函数不存在,current仍然会存在于内存中。我的感觉是原来current是inc()的局部变量,使用了闭包后“升级”为全局变量,作用域是整个js代码段,在外部是可以改变current的值,“权重”与inc()是等同的。因此,内存开销会非常大,如果不用了就要把它们干掉。

回到顶部