0%

JavaScript函数

JavaScript的函数算是在写代码的过程中出现频率最高的了。但是有些细节的地方还是比较含糊,用这篇文章来整理下。

声明函数的方式

有两种方式声明函数,用function命令,或者用函数表达式。两者之间的区别只是在函数名提升上,其他基本都一样。

function声明

1
2
3
function test() {
console.log('function:test');
}

如果一个函数声明多次,那么后面的声明会覆盖前面的声明。前一次声明在任何时候都是无效的。

函数表达式

采用变量赋值的方式声明函数,也叫匿名函数。

1
2
3
var test = function(){
console.log('function:test')
};

当使用函数表达式声明函数时,function后面不能带有函数名。如果带有函数名,该函数名只能在函数体的内部有效,指代函数本身,其他地方都不可用。这样的好处是:方便在函数体内部调用自己,2是方便除错。

1
2
3
4
5
6
7
var test = function test(i){    
console.log(i);
if (i == 0) {
return;
}
return test(i-1);
};

JavaScript语言将函数看做是一种特殊的值,凡是可以使用值的地方,就可以使用函数。

函数只是一个可执行的值,此外并无特殊之处

1
2
3
4
5
6
7
8
function add (x, y) {
return x + y;
}
function test(option) {
return option;
}
var result = test(add)(1, 2);
console.log(result);// 3

定义了一个方法add,返回两者之和,定义了个方法test,接受一个参数,并返回这个参数。

最后调用的时候,我们给test传递了个add方法,当test返回方法的时候,1和2作为参数传给返回的add方法,并计算出结果。

特别说明的是,圆括号()也是运算符,需要调用函数的时候,使用圆括号运算符,圆括号之中,加入函数的参数。上面的代码中,test返回的是个函数,正好后面匹配了圆括号,所以会调用这个函数,并接收传递过来的参数,这就是为啥最后会返回个3。

同样的,如果返回的不是个function,那么程序会报错:

1
TypeError: test(...) is not a function

函数名提升

JavaScript将函数名视同为变量名,所以当我们采用function命令声明函数时,整个函数会像变量一样,被声明到代码的头部。所以下面的代码是不会报错的:

1
2
3
4
test();
function test(){
console.log('function:test');
}

但是下面用函数表达式声明的方式就会报错:

1
2
3
4
test();
var test = function(){
console.log('function:test');
};

因为用变量名赋值的方法声明函数时,被提升的是变量名,等于说还没有赋值,变量名还只是个undefined的变量,所以先调用的话当然会报错。

所以,如果同时采用fuction命令和函数表达式来声明一个方法,那么最后采用的总会是函数表达式的定义。因为变量名和函数都会被提升,但是程序运行到赋值的时候,会覆盖掉原先用function声明的函数。

由于变函数的提升,最好不要在控制语句中声明函数。如果有需要,最好用函数表达式的方式来声明。

函数作用域

JavaScript只有两种作用域:全局作用域、函数作用域。全局作用域顾名思义,就是在整个程序中一直存在。函数作用域只在函数内部存在。

在函数内部,也会发生变量提升的现象,在内部创建的变量都会提升到函数头部。函数的作用域就是其声明时所在的作用域,与其运行时所在的作用域无关。

例如:函数a调用函数b,函数b不会引用函数a里面的内部变量。

闭包

上面说到,在函数外部无法读取函数内部的变量。但由于种种原因,需要得到函数内部的局部变量,那就需要在函数的内部,再定义一个函数。

JavaScript特有的“链式作用域”结构,子对象会一级一级向上寻找父级对象的变量,所有父级对象是对子对象可见的,反之则不成立。

闭包就是能够读取其他函数内部变量的函数。可以简单理解为“定义在一个函数内部的函数”。闭包最大的一个特性,就是闭包可以记住自己的诞生环境。

闭包最大的作用有两个:一个是可以读取函数内部的变量,另一个是可以让这些变量始终保存在内存中。即闭包可以使他诞生环境一直存在。

1
2
3
4
5
6
7
8
9
10
function test(num) {
function add () {
return num += 1;
}
return add;
}
var result = test(1);
result();
result();
console.log(result());// 4

上面的代码,num是方法test的内部变量,通过闭包,num状态被保留了,每一次调用都是在上一次调用的基础上进行计算。闭包使得函数test的内部环境一直存在。

为什么会这样?

原因在于result变量一直存在于内存里面,而result是被test方法赋值存在的,所以result的存在依赖于方法test,因此test调用时的状态也存在于内存中,不会在调用结束后,被垃圾回收机制回收。

闭包的另一个用处,是封装对象的私有属性和私有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name){
var _age;
function setAge(age) {
_age = age;
}
function getAge(){
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge,
};
}
var p = new Person('tony');
p.setAge(24);
console.log(p.getAge());

立即调用的函数表达式

前面说过,圆括号运算符()跟在函数名后,表示调用该函数。比如test();。有时候我们需要在定义函数后,立即调用该函数。

function这个关键字既可以当语句,也可以当表达式。为了避免解析上的歧义,JavaScript规定,当function出现在行首,一律解释成语句。因此,当行首为function的时候,JavaScript会认为这是函数的定义,不能以圆括号结尾。

解决的方式是,让JavaScript的引擎将其理解成为一个表达式,我们可以将函数的声明放在一个圆括号里面,这样再在表达式的后面加上圆括号,就可以让函数立即运行了。

1
2
3
4
5
6
7
(function test(){
console.log(1);
})();
// 或者
(function test(){
console.log(1);
}());

上面两种写法都是圆括号打头,所以引擎会认为这是一个表达式,而不是定义语句。这就叫做“立即调用的函数表达式(Immediately-Invoked Function Expression, IIFE)”。注意末尾的分号是必须的。

同理,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果:

1
2
3
4
5
6
7
8
9
10
11
var test = function(){
console.log('function:test');
}();

new function(){
console.log('new function');
};

+ function test(){
console.log('+function test');
}();
码字辛苦,打赏个咖啡☕️可好?💘