js中函数的函数

函数是支撑一门编程语言的重要内容,在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现在很少能派上用场了,不过有些场景下还是会使用到。

你可能感兴趣的:(js中函数的函数)