您的位置:首页 > Web前端 > JavaScript

js(三)---函数的作用域与闭包

2017-12-22 15:32 218 查看
函数作用域

函数是js比较重要的概念,函数里面是一个执行体,只有当你去调用这个函数是,这个执行提才会产生作用。当你不去调用它时,它就是一个封闭的空间。

函数是声明有三种:

匿名函数(函数表达式):
var demo = function () {}


函数:
function demo () {}


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


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

var demo = function(a,b){
console.log(arguments);
console.log(demo.length);
}
demo(1,2,3,4)//=>Arguments(1,2,3,4),2(a,b就是形参)


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

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

每一个函数都会有一个return,如果不写的话函数会自动函数提的最后加上一个return,来终结函数体的运行。

return的功能有两个:

1.返回这个函数的执行结果。(需要一个变量来接收)

2.终止函数的执行。

function test (a, b) {
console.log(a + b);
return ;
console.log('hello');
}
test(1, 2); // 3 没有打印hello


作用域

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

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

var demo = 123; //全局变量
function test () {
var demo = 234;// 局部变量
console.log(demo);
var demo1 = 'hello';
}
test();//234  就近打印局部变量,没有局部变量的时候才会打印全局的。
console.log(demo1); // 报错 我们的全局作用域无法访问函数的局部作用域


作用域有一个特点,就是函数外部的可以访问内部,内部的却不可以访问外部的。

• 有一点要注意的是,如果在函数作用域里面声明变量没有用var的话,那么就声明了一个全局变量。(相当于直接定义一个全局的变量,就像是Window.某一个变量一样的,所以是全局的。)

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

function demo1 () {
var str1 = 'abc';
}
function demo2 () {
console.log(str1);
}
demo2(); // 报错


它们并不是一点都不能联系的,可以通过作用域链进行联系的,接下来就会介绍到。

作用域链

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

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

function A(){
var  a=1;
function B(){  //在A函数内部,声明了函数B,这就是所谓的函数嵌套。
var b=2;
}
}


对于A来说,A函数在执行的时候,会创建其A函数的作用域, 那么函数B在创建的时候,会引用A的作用域,类似下面这样:



函数B在执行的时候,其作用域类似于下面这样:



从上面的两幅图中可以看出,函数B在执行的时候,是会引用函数A的作用域的。所以,像这种函数作用域的嵌套就组成了所谓的函数作用域链。当在自身作用域内找不到该变量的时候,会沿着作用域链逐步向上查找,若在全局作用域内部仍找不到该变量,则会抛出异常。

闭包

什么是闭包?

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

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

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

var funB,
funC;
(function() {
var a = 1;
funB = function () {
a = a + 1;
console.log(a);
}
funC = function () {
a = a + 1;
console.log(a);
}
}());
funB();  //2
funC();  //3.


对于 funB和funC两个闭包函数,无论是哪个函数在运行的时候,都会改变匿名函数中变量a的值,这种情况就会污染了a变量。

两个函数的在运行的时候作用域如下图:



这这幅图中,变量a可以被函数funB和funC改变,就相当于外部作用域链上的变量对内部作用域来说都是静态的变量,这样,就很容易造成变量的污染。

上面的代码,可以表示无论运行哪一个函数,都会改变变量a。

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

只要把这个函数return一下,把函数中的变量给返回出来,这样在函数执行结束之后才不会被销毁。就这样形成了新的作用域链,就可以继续使用了。

下面这两点一定要注意:

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

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

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

用接下来的例子,加深一下了解吧:

var name = 'global';
var obj = {
name: 'obj',
getName: function () {
return function () {
console.log(this.name);
}
}
}
obj.getName()(); // global (有点像this的指向问题)


function a () {
var num = 1;
function addNum () {
num++;
console.log(num);
}
return addNum;
}
var demo = a();
demo(); // 2
demo(); // 3
var demo1 = a();
demo1(); // 2
demo1(); // 3


从这两个总结一下:当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的闭包 ,如果内部函数引用了位于外部函数的变量,当外部函数调用完毕后,这些变量在内存不会被释放(还一直存在着,可以继续调用),因为闭包需要它们。

立即执行函数

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

立即执行函数的写法:

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

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

因为立即执行函数可以封闭作用域,所以到以后需要些模块的时候,经常使用立即执行函数,就是为了使里面的变量,不相互影响。

function returnB() {
var arr = [];
for (var i = 0; i < 10; i ++) {
arr[i] = function () {
console.log(i);
}
}
return arr;
}
var save = returnB();
for (var i = 0; i < 10; i ++) {
save[i]();
}


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

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

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

function returnB() {
var arr = [];
for (var i = 0; i < 10; i ++) {
(function (n) {
arr
= function () {
console.log(n)
};
}(i))
}
return arr;
}
var save = returnB();
console.log(save);
for (var i = 0; i < save.length; i ++) {
save[i]();
}


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

var funB,funC;
(function () {
var a = 1;
(function () {
funB = function () {
a = a + 1;
console.log(a);
}
}(a));
(function (a) {
funC = function () {
a = a + 1;
console.log(a);
}
}(a));
}());
funB()||funC();  //输出结果全是2 另外也没有改变作用域链上a的值。


在函数执行时,内存的结构如图所示:



加入立即执行函数之后,这个变量a就不是一个静态量了,他在立即执行函数里面也会产生一个新的变量,这样就不会影响全局的变量了。因此立即执行函数是解决闭包(上文也说了不是消除闭包)的一种方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息