深入理解:函数、匿名函数、自执行函数、闭包

深入理解:函数、匿名函数、自执行函数、闭包

原文:https://www.jianshu.com/p/a72395741c50

1 定义函数的方式

  1. 函数的声明
  2. 函数表达式

1.1 函数声明

下面是函数声明的结构:

function sum(x, y) {
    alert(x+y);
}
sum(1, 2);

由于javascript具有“函数声明提升”的特性,即执行代码之前,先读取函数声明,意味着函数声明可以放在调用它的语句之后。如下代码可以正常执行:

sum(1, 2);
function sum(x, y) {
    alert(x+y);
}

1.2 函数表达式

函数表达式中有几种不同的语法。最常见和最具代表性的一种如代码所示:

var ss = function(x, y) {
    alert(x+y);
}
ss(1, 2);

这种形式看起来好像是常规的变量赋值语句。但是函数表达式和函数声明的区别在于,函数表达式在使用前必须先赋值。所以一下代码执行的时候就会出错:

ss(1, 2); // 报错,显示undefined is not a function
var ss = function(x, y) {
    alert(x+y);
}

造成这种现象是因为解析器在向执行环境中加载数据时,解析器会率先读取函数声明,并使其在执行任何代码前可用;至于函数表达式,则必须等到解析器执行到它的所在的的代码行,才会真正的被解析。
函数表达式中,创建的函数叫做匿名函数,因为function关键字后面没有标识符。

2 匿名函数

匿名函数,顾名思义就是没有名字的函数。上面的函数表达式中的创建,即创建一个匿名函数,并将匿名函数赋值给变量ss,用ss来进行函数的调用,调用的方式就是在变量ss后面加上一对括号(),如果有参数传入的话就是ss(1,2),这就是匿名函数的一种调用方式。

2.1 匿名函数的调用方式

还有一种匿名函数的调用方式是:1)将匿名函数用()括起来;2)然后在后面加一对小括号(包含参数列表)。我们再看一下以下例子:

alert((function(x, y){return x+y;})(2, 3));
// Function 对象(类),其中的每个arg都是一个参数,最后一个参数是函数主体,且参数必须是字符串。 这种函数定义方式的场景是在NodeJS和浏览器的全局环境中
alert((new Function('x', 'y', 'return x+y;'))(2, 3)); 

在javascript中,是没有块级作用域这种说法的,以上代码的这种方式就是模仿了块级作用域(通常成为私有作用域),语法如下所示:

function() {
    // 代码块级作用域
}();

以上代码定义并立即调用了一个匿名函数。经函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随期后的另一个圆括号会立即调用这个函数。

function() {
    // 代码块级作用域
}();

注意:以上代码会报错Uncaught SyntaxError: Unexpected token (
因为Javascript将function关键字当作一个函数声明的开始,而函数声明后面不能加圆括号,如果你不显示告诉编译器,它会默认生成一个缺少名字的function,并且抛出一个语法错误,因为function声明需要一个名字。有趣的是,即便你为上面那个错误的代码加上一个名字,他也会提示语法错误,只不过和上面的原因不一样。在一个表达式后面加上括号(),该表达式会立即执行,但是在一个语句后面加上括号(),是完全不一样的意思,他的只是分组操作符

// 这个function在语法上是没问题的,但是依然只是一个语句
// 加上括号()以后依然会报错,因为分组操作符需要包含表达式  
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )  
   
// 但是如果你在括弧()里传入一个表达式,将不会有异常抛出  
// 但是foo函数依然不会执行  
function foo(){ /* code */ }( 1 );  
   
// 因为它完全等价于下面这个代码,一个function声明后面,又声明了一个毫无关系的表达式:   
function foo(){ /* code */ }  
   
( 1 );  

因此,以上代码要想正确实现,就必须要实现赋值,如a = function(){}(),"a="这个告诉了编译器这是一个函数表达式,而不是函数声明,因为函数表达式后面可以跟()。因此下面两段代码是等价的。

var aa = function(x) {
    alert(x);
}(5); // 5
(function(x){alert(x);})(5);

有上面对于函数和匿名函数的了解,我们引申出来了一个概念,即自执行函数,让我们更加深入的了解为什么。a = function(){}()这个表示可以让编译器认为这个是一个函数表达式而不是一个函数的声明。

3 自执行函数

我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象。
自执行函数,即定义和调用合为一体。

自执行函数的一些表达方式:

// 下面2个括号()都会立即执行  
(function () { /* code */ } ()); // 推荐使用这个  
(function () { /* code */ })(); // 但是这个也是可以用的

4 闭包

闭包是指有权访问另一个函数作用域的变量的函数。创建闭包的的常用方式,就是一个函数内部创建另一个函数。
闭包主要涉及到js的几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等。

4.1 闭包简单例子

一般情况下,函数内部可以访问函数外部的全局变量

 var a = 1;//全局变量  
 function f1(){  
     alert(a);  
 }  
 f1();//1 

函数外部不能访问函数内部的局部变量,

function f2(){  
    var a = 1 ; //局部变量  
}  
alert(a); //error  

有时候我们想得到函数内部的局部变量,那应该如何实现呢?这就引入了闭包的概念。

function fn1() {
    var n = 1;

    return function() {
        alert(n);
    }
}
result = fn1();
result();

4.2 闭包的作用

  • 读取函数内部的变量
  • 将变量的值始终保存在内存中

一般来讲,当函数执行完毕之后,函数内部的局部活动对象就会被销毁,内存中仅保存全局作用域,即js的内存回收机制。
如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的.并且这个内部函数又使用了外部函数的某些变量的话.这种内存回收机制就会出现问题。如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了.所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来.也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收。

4.3 注意事项

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  2. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

5 自执行函数与闭包的使用

很多情况下,可以利用自执行函数和闭包来保存某个特殊状态中的值。
由于作用域链的配置机制,使得闭包只能取得包含函数中任何变量的最后一个值。即说明了闭包中所保存的是整个变量对象,而不是某一个特殊的变量。我们 用下面这个例子来说明这个问题。
例子1:

function createFunction() {
    var result = new Array();
    for( var i = 0; i<10; i++) {
        result[i] = function() {
            return i;
        };
    }
    return result;
}

var aa = createFunction();
alert(aa[0]()); //10
alert(aa[1]()); //10
  • 在这个函数中,我们直接将闭包赋值给数组。这个函数会返回一个函数数组。表面上来看,似乎每个函数都应该返回自己的索引,即位置0的函数返回0,位置1的函数返回1一次类推。但实际上,如同上面例子,每个函数都返回了10。因为每个函数的作用域链中都保存createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值死10,此时每个函数都引用着保存变量i的同一个变量对象。所以在每个函数内部i的值都是10。
  • 所以,我们可以通过如下例子,创建一个自执行函数(匿名函数)强制让闭包的行为符合预期。
    例子2:
function createFunction() {
    var result = new Array();
    for( var i = 0; i<10; i++) {
        result[i] = function(num) {
            return function() {
                return num;
            };
        }(i);
    }
    return result;
}

var bb = createFunction();
alert(aa[0]()); //0
alert(aa[1]()); //1

在例子2中,我们没有直接将闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。对于立即执行的匿名函数来说,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放。所以这里的匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量i。由于函数是按值传递的,所以会将变量i的当前值赋值给参数num,而这个匿名函数内部,又创建并返回了一个返回num的闭包。这样一来,result数组中的每个函数都有自己num的一个副本,因此就可以返回各自不同的数值了。



作者:CPChen
链接:https://www.jianshu.com/p/a72395741c50
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
posted @ 2019-05-26 20:45  鬼小妞  阅读(2047)  评论(0编辑  收藏  举报