loose heart
一日一钱,千日千钱,绳锯木断,水滴石穿

一、介绍

本次博客主要介绍函数表达式的内容,主要是闭包。

二、函数表达式

定义函数的两种方式:一个是函数声明,另一个就是函数表达式。


//1.函数声明写法
function fn2(){
    console.log('函数声明');  
}
//2.函数表达式写法
var fn1 = function(){
    console.log('函数表达式');
}

区别:
1.函数声明是用function后面有函数名,函数表达式是赋值形式给一个变量。
2.函数声明可以提升函数,而函数表达式不会提升

函数提升就是函数会被自动提升到最前方,以至于再调用函数后再声明函数也不会有错:

//例子:
//先调用运行
sayName();
//再声明函数
function sayName(){
    console.log('ry');
}

//运行结果
'ry'

函数表达式就不会被提升:

//先调用
sayBye();
//函数表达式
var sayBye = function(){
    console.log('bye bye');
}

//运行报错

但是下面的写法很危险:因为存在函数声明的提升

//书上代码
if(condition){
    function sayHi(){
        console.log('hi');
    }
}
else{
    function sayHi(){
        console.log('yo');
    }
}

解说一下: 这段代码想表达在condition为true时声明sayHi,不然就另一函数sayHi,但是运行结果往往出乎意料,在当前chrome或firefox可能能做到,但是在IE10以下的浏览器(我测试过)往往不会遵循你的意愿,不管condition是true还是false都会输出yo。

这时函数表达式能派上用场了:

//换成函数表达式,没问题因为不会被提升,只有当执行时才赋值
var sayHi = null;
if(condition){
    sayHi = function (){
        console.log('hi');
    }
}
else{
    sayHi = function sayHi(){
        console.log('yo');
    }
}

三、闭包

闭包的定义:有权访问另一个函数作用域中的变量的函数

有人觉得闭包很难理解,一开始我也是这样的,我认为那是对一些概念还不够了解。

定义中说明了什么是闭包,最常见的形式就是在函数中再声明一个函数。

重点理解这里:

1.闭包能访问外围函数的变量是因为其作用域链中有外围函数的活动对象(这个活动对象即使在外围函数执行完还会存在,不会被销毁,因为被闭包引用着)。

2.闭包是函数中的函数,也就是说其被调用时也创建执行上下文,对于执行上下文这部分可以看看这篇:深入理解js执行--创建执行上下文这篇博客。

理解了上面之后我们再来看闭包的例子:

function a(){
    //a函数的变量
    var val_a = "我是a函数里的变量";
    //声明函数b,b能访问函数a的变量
    function b(){
        console.log(val_a);
    }
    //a函数将b返回
    return b;
}

//全局变量fn,a执行返回了b给fn
var fn = a();
//调用fn,能够在全局作用域访问a函数里的变量
fn();  //我是a函数里的变量

这里fn能够访问到a的变量,因为b中引用着a的活动对象,所以即使a函数执行完了,a的活动对象还是不会被销毁的。这也说明过度使用闭包会导致内存泄漏。

再来个常见的例子(给多个li添加点击事件,返回对于li的下标):

<body>
    <ul id="list">
        <li>red</li>
        <li>green</li>
        <li>yellow</li>
        <li>black</li>
        <li>blue</li>
    </ul>
</body>
//获得li标签组
var li_group = document.getElementsByTagName('li');

//错误例子:每个li都会跳出5
function fn(){
    //为每一个li添加点击事件
    var i = 0;
    //使用for来给每个li添加事件
    for(;i<li_group.length;i++){
        //添加点击事件
        li_group[i].addEventListener('click',function(){
            // 输出对应得下标
            console.log(i);
        });
    }
}
fn();


//正确的例子:
//在加一层的函数,作为闭包,用来保存每一次循环的i变量,就可以达到目的
function correctFn(){
    var i = 0;
    for(;i<li_group.length;i++){
        //在外面套一层函数,这层函数会保存每次循环的i变量,也就是闭包了。
        (function(num){
            li_group[num].addEventListener('click',function(){
                console.log(num);
            });               
        }(i));        
    }
}
correctFn();

看下面解释之前我默认你已经知道活动对象是什么了,如果不懂可以看这篇:深入理解js执行--创建执行上下文

解释一下:
1.错误的例子:
屡一下思路,每个li都有click执行的函数,每个函数点击后才会执行是吧,那每个click的函数的外层函数是fn这个函数,那这5个click函数都会保存着fn的活动对象,那这个输出的i变量在fn这函数里面,所以当fn执行完后,i的值是5了,因此当每个里触发click的函数的时候输出的也就是5了。
再简单的说:每个li的click事件触发的函数引用的i在同一个活动对象中,所以值都一样。

2.正确执行的例子:
我们就在外层加了一层匿名函数并立即执行(虽然函数被执行了,如果有函数引用着它的活动对象,那其活动对象将不灭),这时每个li的click函数外层函数是每次循环产生的不同的匿名函数,这匿名也是有活动对象,每个li的click的函数保存着各自的匿名函数的活动对象,num这变量也根据每次循环产生不同的匿名函数传入的i的不同而不同,所以能够输出对应不同的值。

上面说的可能有点啰嗦,请原谅我[捂脸.jpg],我是希望尽可能的表达清楚。如果你看懂了,那对闭包的理解也更深一层了哦。

小结:

1.本篇主要讲的是闭包,闭包是有权访问另一个函数作用域中的变量的函数,主要是函数中的函数,因为能引用外层函数的活动对象所以能够访问其外层的变量。
2.我本篇主要讲的是原理,如果对一些东西不懂,可以看下面几篇。

相关的几篇:
深入理解js执行--单线程的JS
深入学习JS执行--创建执行上下文(变量对象,作用域链,this)
我发现,原来你是这样的JS全部文章汇总(点击此处)

posted on 2017-12-10 10:34  loose-heart  阅读(364)  评论(0编辑  收藏  举报