背景
Javascript中函数有两个重要特征。
第一,函数是第一类对象(first-class object),可以作为带有属性和方法的值以及参数进行传递。
第二,函数提供了局部作用域。Javascript中仅存在函数作用域,花括号{}
并不提供局部作用域,因此如果在if
条件语句或在for
以及while
循环中,使用var
关键字定义一个变量,这并不意味着该变量对于if
或for
来说是局部变量。它仅对于包装函数来说是局部变量,并且如果没有包装函数,它将称为一个全局变量。
此外,函数中声明的局部变量可被提升到局部作用域的顶部。
函数创建方式
1、命名函数表达式。
1 | var add = function add(a, b){...}; |
注:不要将命名函数表达式分配给一个具有不同名称的变量,因为可能在某些浏览器下不支持。如var foo = function bar(){};
2、函数表达式。
与上面的相同,但缺少一个名字,通常也称为匿名函数。
1 | var add = function(a, b){...}; |
唯一的区别在于该函数对象的name
属性将会成为一个空字符串或undefined
。name
属性是Javascript语言的一个扩展(它并不是ECMA标准的一部分),可用于debug时错误定位,也可用于在自身内部递归调用同一个函数。
3、函数声明。
1 | function foo(){...} |
就语法而言,命名函数表达式与函数声明看起来很相似,尤其是如果不将函数表达式的结果分配给变量。
在尾随的分号中,这两者之间存在语法差异。函数声明中并不需要分号结尾,但在函数表达式中需要分号,并且应该总是使用分号(在压缩js文件时会出问题)。
函数的提升
对于所有变量,无论在函数体的何处进行声明,都会在后台被提升到函数顶部。而这对于函数同样适用,其原因在于函数只是分配给变量的对象。当使用函数声明时,函数定义也被提升,而不仅仅是函数声明被提升。
example.
1 | function hoistMe(){ |
API模式-回调模式
example.普通写法
1 | var findNodes = function(){ |
实现低效,因为hide()
必须再次遍历由findNodes()
所返回的数组节点。如果在findNodes()
中实现hide
逻辑,由于检索和修改逻辑耦合,那么它不再是一个通用函数。对这种问题的解决办法是采用回调模式。
example.回调模式
1 | var findNodes = function(callback){ |
注:回调函数可以是一个已有的函数,也可以是一个匿名函数,可以在调用主函数时创建它。
虽然在许多情况下这种方法都是简单而且有效的,但经常存在一些场景,其回调并不是一次性的匿名函数或全局函数,而是对象的方法。如果该回调方法使用this
来引用它所属的对象,就会导致问题。
解决办法是传递回调函数,并且另外还传递该回调函数所属的对象。
example.
1 | var findNodes = function(callback, callback_obj){ |
API模式-配置对象
将函数参数包装成一个对象,如addPerson(param)
配置对象的优点在于:不需要记住众多的参数以及其顺序;可以安全忽略可选参数;易于阅读和维护;易于添加和删除参数。
配置对象的不利之处在于:需要记住参数名称;属性名称无法被压缩。
API模式-返回函数
1 | var setup = function(){ |
由于setup()
包装了返回函数,它创建了一个闭包,可以使用这个闭包存储一些私有数据,而这些数据仅可被该返回函数访问,但外部代码却无法访问。
API模式-Curry化
例子,有function add(a, b){...}
函数应用:add.apply(null, [1, 2]);
部分应用:(add.apply(null, [1]))(2);
注:部分应用中,add.apply(null, [1])
仅应用了第一个参数,当执行部分应用时,并不会获得具体结果,而是会获得另一个函数,随后再以其他参数调用该返回函数。这种运行方式实际上与add(1)(2)
有些相似,这是由于add(1)
返回了一个可在后来用(2)
来调用的函数。
example.
1 | function add(x, y){ |
使函数理解并处理部分应用的过程就称为Curry过程。
example.通用Curry化函数
1 | function schonfinkelize(fn){ |
注:Javascript中arguments
并不是一个真实的数组,从Array.prototype
中借用slice()
方法可以帮助我们将arguments
变成一个数组,并且使用该数组工作更加方便。
1 | //普通函数 |
转换函数schonfinkelize()
并不局限于单个参数或者单步Curry化。
当发现正在调用同一个函数,并且传递的参数绝大多数都是相同的,那么该函数可能是用于Curry化的一个很好的候选参数。可以通过将一个函数集合部分应用到函数中,从而动态创建一个新函数。这个新函数将会保存重复的参数(因此不必每次都传递这些参数),并且还会使用预填充原始函数所期望的完整参数列表。
初始化模式-即时函数
即时函数模式是一种可以支持在定义函数后立即执行该函数的语法。(function(){...}());
或 (function(){...})();
这种模式非常有用,因为它为初始化代码提供了一个作用域沙箱,不会污染全局变量。
一般情况下,全局对象是以参数方式传递给即时函数的,这样将使得代码在浏览器环境之外时具有更好的互操作性。
1 | (function(global){ |
一般来说,不应该传递过多的参数到即时函数中。
正如任何其他函数一样,即时函数可以返回值,并且这些返回值也可以分配给变量。
即时函数模式得到了广泛的使用,它可以帮助包装许多想要执行的工作,且不会在后台留下任何全局变量。该模式还支持将个别功能包装在自包含模块中。
初始化模式-即时对象初始化
example.
1 | ({ |
注:如果想在init()
完毕之后保存对该对象的一个引用,可以通过在init()
尾部添加return this;
语句来实现该功能。
就语法而言,这种模式就像在使用对象字面量创建一个普通的对象。将字面量包装到括号中(分组操作符),它指示Javascript引擎将大括号作为对象字面量,而不是作为一个代码块。在结束该括号之后,可以立即调用init()
方法。({...}).init();
或 ({...}.init());
这种模式的优点与即时函数模式的优点是相同的,可以在执行一次性的初始化任务时保护全局命名空间。
初始化模式-初始化时分支
初始化时分支,也称为加载时分支,是一种优化模式。当知道某个条件在整个程序生命周期内都不会发生改变的时候,仅对该条件测试一次是很有意义的。浏览器嗅探(或功能检测)就是一个典型的例子。
性能模式-备忘模式
函数是对象,因此它们具有属性。事实上,它们确实还有属性和方法。例如,对于每一个函数,无论使用什么样的语法来创建它,它都会自动获得一个length
属性,其中包含了该函数期望的参数数量。
可以在任何时候将自定义属性添加到你的函数中。自定义属性的其中一个用例是缓存函数返回值,也被称为备忘。
example.
1 | var myFunc = function(param){ |
上述代码假定该函数只需要一个参数param
,并且它是一个基本数据类型。如果有更多更复杂的参数,对此的通用解决方案是将它们序列化。例如,可以将参数对象序列化为一个JSON字符串,并使用该字符串作为cache
对象的键。
1 | var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)); |
注意:在序列化过程中,对象的“标识”将会丢失。如果有两个不同的对象并且恰好都具有相同的属性,则它们序列化后的结果相同。
性能模式-自定义函数
example.
1 | var scareMe = function(){ |
当你的函数有一些初始化准备工作要做,并且仅需要执行一次,那么这种模式就非常有用。因为并没有任何理由去执行本可以避免的重复工作,即函数的一些部分可能并不再需要。在这种情况下,自定义函数可以更新自身的实现。
这种模式的另一个名称是“惰性函数定义”(lazy function definition),因为该函数直到第一次使用时才被正确地定义,并且具有后向惰性,执行了更少的工作。该模式的其中一个缺点在于,当它重定义自身时,已经添加到原始函数的任何属性都会丢失。此外,如果该函数使用了不同的名称,比如分配给不同的变量或者以对象的方法来使用,那么重定义部分将永远不会发生,并且将会执行原始函数体。
example.接上
1 | //1、添加一个新的属性 |
正如看到的,当将该函数分配给一个新的变量时,函数的自定义并没有发生。每次当调用prank()
时,它都通知’Boo!’消息,同时它还覆盖了全局scareMe()
函数,但是prank()
自身保持了可见旧函数,其中还包括属性。当该函数以spooky
对象的boo()
方法使用时,也发生了同样的情况。所有这些调用不断地重写全局scareMe()
指针,以至于当它最终被调用时,它才第一次具有更新函数主体并通知’Double boo!’消息的权利。此外,它也不能访问scareMe.property
属性。