Web前端的知识之旅哟——(补之前遗漏的)函数与作用域与闭包

写完预编译之后才发现忘记写关于函数和闭包部分的知识点了……所以这里补上。

函数部分

我们的函数声明有两种方式:

1.var demo = function () {} 函数表达式

2.function demo () {} 函数声明

3.var demo = function xxx() {} 命名函数表达式

其实第一种和第三种相比较,第一种叫做匿名函数表达式,但是平时我们基本都使用第一种,很少使用第三种,因此第一种我们就简称为函数表达式了。

• 每一个函数里面都有一个类似数组的类数组属性arguments,这个属性里面存的就是实参

arguments[0]就可以查看我们传递的第一个实参了。

函数有一个length属性,这个length储存的是形参的数量

• 每一个函数都会有一个return,如果不写的话函数会自动加上一个return;

return的功能有两个:

1.返回这个函数的执行结果

2.终止函数的执行


1.  
2. function test (a, b) {
3.    console.log(a + b);
4.    return ;
5.    console.log(‘hello’);
6. }
7. test(1, 2); // 3 没有打印hello
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

作用域

定义:变量(变量作用域又称为上下文)和函数生效(能被访问)的区域。

javascript的函数,是可以产生作用域的!!

es5中的作用域大概只有全局作用域和函数作用域两种,es6中新添加了块级作用域。


1.  
2. var demo = 123; //全局变量
3. function test () {
4.   var demo = 234;// 局部变量
5.   console.log(demo);
6.   var demo1 = ‘hello’;
7. }
8. test();//234  就近打印局部变量,没有局部变量的时候才会打印全局的。
9. console.log(demo1); // 报错 我们的全局作用域无法访问函数的局部作用域
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这个函数作用域就好像一个屋子,我们在屋子里面能够用屋子外面的东西,但是外面却不能闯进屋子里面使用屋子里面的东西。

• 有一点要注意的是,如果在函数作用域里面声明变量没有用var的话,那么就声明了一个全局变量。

• 同时,两个不同作用域(除了全局作用域)之间是不能互相访问的。


1.  
2. function demo1 () {
3.       var str1 = ‘abc’;
4. }
5. function demo2 () {
6.       console.log(str1);
7. }
8. demo2(); // 报错
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

作用域链

既然函数存在函数作用域,函数又可以嵌套,那么作用域之间自然就会产生嵌套关系,这个时候就产生了作用域链。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)来保证对执行环境有权访问的变量和函数的有序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象。


1.  
2. function demo () {
3. var demo_a = 1;
4. function test () {
5. var demo_a = 2;
6. console.log(demo_a);
7. }
8. test();
9. }
10. demo();
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

在这个例子中,demo1运行的时候,首先创建了一个demo的作用域,但是window本身还有一个全局作用域,这就让demo产生了一个作用域链。本着对执行环境的有权和有序访问,每个函数的自身的作用域总是在作用域链的最顶层,下一层是这个函数的父级函数的作用域,再下面是父级的父级的作用域,直到全局作用域。

因此这个例子中的test函数执行时候打印的demo_a是它本身的作用域中的demo_a,而不是demo函数作用域下的demo_a,如果test函数作用域中没有demo_a这个变量的话,系统才会沿着作用域链向下找到demo作用域中的demo_a变量。

闭包

闭包是一个非常非常重要的知识点,我在网上看到过许多的定义,但是都有点繁琐,这里的知识点是我简单化的总结的。

什么是闭包?

我的理解是,闭包就是能够读取其他函数内部变量的函数

我们前面提到过,不同作用域之间不能够互相访问,但是我们如果在一个函数内部再定义一个函数,并且这个内部函数与外部函数的变量有关联,那么我们就可以通过返回这个内部的函数,然后来访问外部函数里面的变量

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。


1.  
2. function a () {
3.       var demo1 = 123;
4.       add = function () {
5.             demo1++;
6.       }
7.       return function () {
8.             console.log(demo1);
9.       };
10. }
11. var demo = a();
12. demo(); // 123
13. add();
14. demo(); // 124
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

当函数执行完之后,函数的执行上下文就会被销毁,自然我们就无法访问里面的变量了,但是我们这个函数返回了一个依赖于这个函数的新函数,也就是说这个没有被销毁的新函数的作用域链中还存在着对原本函数的作用域的引用,就导致我们原本的函数的上下文不会被销毁,我们称返回的这个新函数是原本函数的闭包函数

在上面的例子中,a函数内部有一个全局的函数add和一个局部变量demo1,我们这个把返回函数给了一个全局变量demo进入到了内存中,但是由于这个返回的新函数依赖于本来的a函数,这就导致本来的a函数的上下文不会被销毁。

这里我们的打印函数一共运行了两次,都能打印出来值,说明a函数的demo1变量在函数执行完之后并没有被销毁而是存到了内存中。

其次,add的值是一个匿名函数,而这个匿名函数本身也是一个闭包,所以add相当于是一个setter叠加器,可以在函数外部对函数内部的局部变量进行操作。

使用闭包的注意点

1.由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露

解决方法是,在退出函数之前,将不使用的局部变量全部删除

2.闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值

这里我们再总结一下什么是闭包:

内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的闭包 ,如果内部函数引用了位于外部函数的变量,当外部函数调用完毕后,这些变量在内存不会被释放,因为闭包需要它们。

当我们能理解下面这个代码的时候,差不多就理解了闭包了:


1.  
2. var name = ‘global’;
3. var obj = {
4.       name: ‘obj’,
5.       getName: function () {
6.             return function () {
7.                   console.log(this.name);
8.             }
9.       }
10. }
11. obj.getName()(); // global
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

累加器的例子:


1.  
2. function a () {
3.        var num = 1;
4.       function addNum () {
5.             num++;
6.             console.log(num);
7.       }
8.       return addNum;
9. }
10. var demo = a();
11. demo(); // 2
12. demo(); // 3
13. var demo1 = a();
14. demo1(); // 2
15. demo1(); // 3
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

立即执行函数

立即执行函数是解闭包的一个重要方法。但是注意闭包是没有办法解除的,我们只能通过另一个新闭包来消除上一个闭包的影响。

定义:立即执行函数不需要被定义,直接执行,执行完毕之后直接释放。

经常被用作初始化。

立即执行函数的写法:

1.(function (a) {})(num);

2.(function (a) {} (num))

传递的参数是a,a的实参值是num,num是我们在外面定义的变量。

这两种写法功能完全一样,但是标准一点的写法是第二种。

• 这里有一个小知识点:函数声明不能被执行,但是函数表达式可以被执行


1.  
2. function test() {} ();// 错误
3. var test = function () {} ();// 这个是可以被执行的
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

第一种加一个括号变成立即执行函数就可以执行了。


1.  
2. (function () {} ());  // 正确
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

因为这种写法不是函数声明,所以不需要函数名字,因此就成为了立即执行函数。

立即执行函数的应用

现在我们有一个要求:我们现在有一个函数returnB,这个函数内部有一个数组arr,这个arr每一个值存的是一个函数并且函数的作用是输出相对应的数组下标值。

我们一开始的写法可能是这个样子的:


1.  
2. function returnB() {
3.       var arr = [];
4.       for (var i = 0; i < 10; i ++) {
5.             arr[i] = function () {
6.             console.log(i);
7.             }
8.       }
9.       return arr;
10. }
11. var save = returnB();
12. for (var i = 0; i < 10; i ++) {
13.       savei;
14. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

但是最后会输出10个10,并不是我们所想的0-9。

这是因为我们打印i的这一行代码所在的函数的自身作用域中不存在i这个变量,而在它的父级函数returnB中才有i这个变量,这个打印函数是一个闭包函数,而且这arr[0]-arr[9]这10个值都是这一个闭包函数。因此当for循环执行完i变成10之后,我们开始后面的依次触发每一个save[i]函数的时候,函数寻找的i是作用域链中的第二层作用域,也就是父级函数returnB的作用域,而在这个作用域中的i都已经变成10了。

这个时候10个打印函数都共用一个i值

那么如何更改可以让它输出0-9呢?这里就需要利用我们的立即执行函数来产生新的闭包以消除我们共用一个闭包值导致的问题。(注意不是消除闭包)


1.  
2. function returnB() {
3.       var arr = [];
4.       for (var i = 0; i < 10; i ++) {
5.             (function (n) {
6.                    arr[n] = function () {
7.                           console.log(n)
8.                    };
9.             }(i))
10.       }
11. return arr;
12. }
13. var save = returnB();
14. console.log(save);
15. for (var i = 0; i < save.length; i ++) {
16.       savei;
17. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

我们立即执行函数执行之后,会产生一个新的作用域,我们把i的具体的0-9这10个数分别作为参数传了进去,也就是说每一个作用域里面的的n都是不一样的。里层的打印函数因为没有n值,所以要向上找立即执行函数的作用域里面的n值。这样最后就会产生10个闭包,因为每一个立即执行函数都是一个新作用域。每一个闭包都是利用刚才立即执行函数的变量n,而每一个立即执行函数的变量n都是不同的,这样就解决了刚才共用闭包导致都打印10的问题了。

今天的闭包和立即执行函数其实很重要的哟~希望对大家能有所帮助!