JavaScript的函数算是在写代码的过程中出现频率最高的了。但是有些细节的地方还是比较含糊,用这篇文章来整理下。
声明函数的方式
有两种方式声明函数,用function命令,或者用函数表达式。两者之间的区别只是在函数名提升上,其他基本都一样。
function声明
1 | function test() { |
如果一个函数声明多次,那么后面的声明会覆盖前面的声明。前一次声明在任何时候都是无效的。
函数表达式
采用变量赋值的方式声明函数,也叫匿名函数。
1 | var test = function(){ |
当使用函数表达式声明函数时,function后面不能带有函数名。如果带有函数名,该函数名只能在函数体的内部有效,指代函数本身,其他地方都不可用。这样的好处是:方便在函数体内部调用自己,2是方便除错。
1 | var test = function test(i){ |
JavaScript语言将函数看做是一种特殊的值,凡是可以使用值的地方,就可以使用函数。
函数只是一个可执行的值,此外并无特殊之处
1 | function add (x, y) { |
定义了一个方法add,返回两者之和,定义了个方法test,接受一个参数,并返回这个参数。
最后调用的时候,我们给test传递了个add方法,当test返回方法的时候,1和2作为参数传给返回的add方法,并计算出结果。
特别说明的是,圆括号()
也是运算符,需要调用函数的时候,使用圆括号运算符,圆括号之中,加入函数的参数。上面的代码中,test返回的是个函数,正好后面匹配了圆括号,所以会调用这个函数,并接收传递过来的参数,这就是为啥最后会返回个3。
同样的,如果返回的不是个function,那么程序会报错:
1 | TypeError: test(...) is not a function |
函数名提升
JavaScript将函数名视同为变量名,所以当我们采用function命令声明函数时,整个函数会像变量一样,被声明到代码的头部。所以下面的代码是不会报错的:
1 | test(); |
但是下面用函数表达式声明的方式就会报错:
1 | test(); |
因为用变量名赋值的方法声明函数时,被提升的是变量名,等于说还没有赋值,变量名还只是个undefined的变量,所以先调用的话当然会报错。
所以,如果同时采用fuction命令和函数表达式来声明一个方法,那么最后采用的总会是函数表达式的定义。因为变量名和函数都会被提升,但是程序运行到赋值的时候,会覆盖掉原先用function声明的函数。
由于变函数的提升,最好不要在控制语句中声明函数。如果有需要,最好用函数表达式的方式来声明。
函数作用域
JavaScript只有两种作用域:全局作用域、函数作用域。全局作用域顾名思义,就是在整个程序中一直存在。函数作用域只在函数内部存在。
在函数内部,也会发生变量提升的现象,在内部创建的变量都会提升到函数头部。函数的作用域就是其声明时所在的作用域,与其运行时所在的作用域无关。
例如:函数a调用函数b,函数b不会引用函数a里面的内部变量。
闭包
上面说到,在函数外部无法读取函数内部的变量。但由于种种原因,需要得到函数内部的局部变量,那就需要在函数的内部,再定义一个函数。
JavaScript特有的“链式作用域”结构,子对象会一级一级向上寻找父级对象的变量,所有父级对象是对子对象可见的,反之则不成立。
闭包就是能够读取其他函数内部变量的函数。可以简单理解为“定义在一个函数内部的函数”。闭包最大的一个特性,就是闭包可以记住自己的诞生环境。
闭包最大的作用有两个:一个是可以读取函数内部的变量,另一个是可以让这些变量始终保存在内存中。即闭包可以使他诞生环境一直存在。
1 | function test(num) { |
上面的代码,num是方法test的内部变量,通过闭包,num状态被保留了,每一次调用都是在上一次调用的基础上进行计算。闭包使得函数test的内部环境一直存在。
为什么会这样?
原因在于result变量一直存在于内存里面,而result是被test方法赋值存在的,所以result的存在依赖于方法test,因此test调用时的状态也存在于内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的另一个用处,是封装对象的私有属性和私有方法。
1 | function Person(name){ |
立即调用的函数表达式
前面说过,圆括号运算符()
跟在函数名后,表示调用该函数。比如test();
。有时候我们需要在定义函数后,立即调用该函数。
function这个关键字既可以当语句,也可以当表达式。为了避免解析上的歧义,JavaScript规定,当function出现在行首,一律解释成语句。因此,当行首为function的时候,JavaScript会认为这是函数的定义,不能以圆括号结尾。
解决的方式是,让JavaScript的引擎将其理解成为一个表达式,我们可以将函数的声明放在一个圆括号里面,这样再在表达式的后面加上圆括号,就可以让函数立即运行了。
1 | (function test(){ |
上面两种写法都是圆括号打头,所以引擎会认为这是一个表达式,而不是定义语句。这就叫做“立即调用的函数表达式(Immediately-Invoked Function Expression, IIFE)”。注意末尾的分号是必须的。
同理,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果:
1 | var test = function(){ |