函数是支撑一门编程语言的重要内容,在JavaScript(下面简称js)中,函数有多种声明和调用方式,而且函数的位置还和作用域息息相关。同时在ES2015中新增了箭头函数的用法,他们之间有很多易于混淆并且平时很难注意到的小的知识点,这篇文章希望能够全面总结关于函数声明的那些问题。
词法作用域
在js中,尤其是ES2015之前,只存在两种词法作用域,一种是全局作用域,一种是函数作用域。变量在不同的作用域具有屏蔽的效应。
```javascript
var info = 'global info';
function fun() {
var info = 'local info';
console.log(info); //local info;
}
console.log(info); //glocal info;
```
在函数作用域内会屏蔽在全局作用域上的相同名称的变量。
声明方式
ES2015之前,js中函数有两种声明方式。一种是通过function关键字进行函数声明,一种是将变量命名为函数。
```javascript
var fun1 = function () {
console.log('this is fun1');
}
function fun2 () {
console.log('this is fun2');
}
```
上面就是两种声明函数的方式,两种方式都能进行函数的声明,之后再进行函数的调用。
```javascript
...
fun1(); //this is fun1
fun2(); //this is fun2
```
那么这两种方式都有哪些区别呢?
1. 匿名与非匿名问题
使用变量赋值函数的声明方式,后面的函数默认是个匿名函数,它是将一个匿名函数赋值给一个变量。从而能通过变量引用函数。
使用函数关键字定义的声明方式,函数的名称就是定义时的名称。
平日使用时,可能这两种方式区别不是很明显,当使用console.log时,他们区别就得以体现:
```javascript
...
console.log(fun1)
/*
* function () {
* console.log('this is fun1');
* }
*/
console.log(fun2)
/*
* function fun2 () {
* console.log('this is fun2');
* }
*/
```
所以,我们在通过变量定义函数时,可以同时加上函数名称,这样能够在报错或者使用console的函数输出时,能够得到最全的信息。
```javascript
var fun1 = function fun1 () {
...
}
```
上面的这种写法,就能让console输出时同时打印出函数的名称,方便程序调试。
2. 定义位置的问题
想上面屏蔽变量一样,函数名称在全局环境中也是一个变量,它们是否接受这样的设定呢?在不同的作用域中有屏蔽的现象?
答案是肯定的,而且这种屏蔽的机制或许更复杂。
在这里,我们所需要具备的js基础知识不仅有上面作用域的问题,还有变量提升,变量改变问题。
相同作用域下的变量改变:
ES2015之前,我们只能通过var关键字定义变量。var a = 1;
即在stack内存定义了一块区域存放1的值。如果在接下来的相同作用域环境内重新出现了一个名称为a的变量,那么这个区域的值就会改变。
```javascript
var a = 1;
console.log(a); //1
var a = 2;
console.log(a); //2
```
实际上,上面的代码在引擎解析成为抽象语法树时与接下来的代码是相同的:
```javascript
var a = 1;
console.log(a); //1
a = 2;
console.log(a); //2
```
变量提升:
同样的,使用var关键字还会遇到变量提升的问题。引擎在解析时,会首先将所有使用var定义的关键字放到前面。
```javascript
console.log(a); //undefined
var a = 1;
```
上面的代码可能会让初学者震惊,为什么变量在之后定义,但是却没有抛出Refference Error(引用错误),而是输出undefined。
这就是var关键字的神奇之处,它能够将本作用域中所有使用var关键字的变量提升到作用域最前方。
简单来说,上面的代码和下面的代码是等效的:
```javascript
var a;
console.log(a);
a = 1;
```
同样的,无论多少个变量,都会提升到作用域的开头。
有了上面基础知识的铺垫,接下来关于函数声明的部分就变得容易很多。因为函数变量声明函数就用到了var关键字。
所以在使用多个var关键字进行函数声明时,语句中的函数名称总是指向最近的函数,这句话并不好理解,但是代码却很好理解:
```javascript
var fun = function fun() {console.log(1);}
fun(); //1
fun = function fun() {console.log(2);}
fun(); //2
```
但是,这里有一个常错的地方,就是在函数的提前调用。
```javascript
fun(); //Type Error
var fun = function fun() {console.log(1);}
```
上面这种情况下就会报错,如果改变一下代码结构就会清晰很多:
```javascript
var fun; //变量提升
fun(); //函数调用
fun = function fun() {console.log(1);} //函数声明
```
因为var关键字有变量提升,所以没有抛出一个引用错误,而在调用时,fun的值因为变量提升为undefined,此时却将它作为函数调用的方法来进行使用,所以抛出类型错误。
这也就告诉我们,使用变量名称进行函数声明时,必须在声明结束后才能使用函数。
但是另一方面,使用function关键字进行函数声明就不会遇到这样的问题,而且function也会进行变量提升。
```javascript
fun(); //1
function fun () {console.log(1)}
```
多次定义时也不尽相同,总是以最后一次为准。
```javascript
function fun () {console.log(1)}
fun(); //2
function fun () {console.log(2)}
fun(); //2
```
所以,在没有ES2015标准之前,这种写法就是错误的:
```javascript
if (something) {
function fun () {
console.log(1);
}
} else {
function fun () {
console.log(2);
}
}
```
因为函数会同时被提升到当前词法作用域最顶层,有的浏览器会报错,有的浏览器只会使用fun的第二个定义。
以上就是使用关键字function和关键字var定义函数的区别。
那么,接下来的代码输出什么呢?
```javascript
print();
var print = function () {
console.log(1);
};
print();
function print () {
console.log(2);
}
print();
function print () {
console.log(3);
}
print = function () {
console.log(4);
}
```
上面一共有三次调用print的地方。上面的结果是 3 1 1
,这就说明function关键字的提升是先于var的提升的。
箭头函数
ES2016中新定义了一种叫做箭头函数的函数,不同于上面两种声明方式,箭头函数只能通过定义变量的方式进行声明,且必须是匿名函数。
```javascript
var arrow = () => {
console.log('this is arrow function');
}
```
在仅有一行结果时,可以省略return关键字和大括号
```javascript
var arrow = () => console.log(1);
arrow(); //1
```
同时,箭头函数,内部的this指针永远指向调用该函数的词法作用域。
自执行函数
自执行函数全称是Immediately-invoked Function Expression,中文翻译是立即执行函数表达式。它将匿名函数放在一条语句中,在里面声明一个匿名函数,从而能够封装出一个函数作用域,然后在定义时执行这个匿名函数。
能这么做的原因就是因为在js中,所有的程序语句都是表达式。所以下面两个语句快是等价的:
```javascript
//第一种
var fun1 = function () {console.log(1)}
fun1();
//第二种
( function fun2 () {console.log(2)} )()
```
上面两种写法都能执行定义的函数,第二种写法将函数定义在由括号封装的语句中,再加上使用()
结尾进行调用函数,从而使函数执行。同样的道理,我们可以仿造第二种执行函数的方式,制造出一种只执行一次的自执行函数出来。
```javascript
( function autoRun() {
console.log('auto run here!');
} )()
```
这样,当程序执行到这里时函数就会自执行,同样的,我们可以不给函数命名,声明一个匿名函数,从而不会污染全局变量。
```javascript
( function () {
console.log('also auto run!');
} )()
```
还可以将执行的括号放在里面,不至于变的混乱,增强程序可读性。
```javascript
( function () {
console.log('auto run here!');
} ())
```
但是,使用ES2015箭头函数定义的函数不能像上面这样将执行的括号放在表达式里面,只能通过放在表达式外面进行函数自执行。
```javascript
( () => console.log('auto run') )()
```
这样做的好处也很多,在ES2015标准之前,仅存在全局作用域和函数作用域,没有传统编程语言上面的块级作用域,这个时候IIFE就派上用场了,他能创建出一个函数作用域出来,但是这个函数仅仅会在运行到这一步执行一次,实际上就等同于一个简易的块级作用域出来。从而解决很多问题,但是在ES2015的块级作用域出现之后,IIFE现在很少能派上用场了,不过有些场景下还是会使用到。