javascript之理解闭包

js之闭包

1、到底什么是闭包

闭包已经成为近乎神话的概念,它非常重要又难以掌握,而且还难以定义。

1.1 古老的定义

闭包(closure),是指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来。

那这样说来,包含变量的函数就是闭包。这个说法已淘汰。

//按照古老定义,包含变量n的函数foo就是闭包
function foo() {
    var n = 0;
}
console.log(n)//Uncaught ReferenceError: n is not defined

1.2 定义一

闭包是指可以访问其所在作用域的函数。

那这样说来,需要通过作用域链查找变量的函数就是闭包。也不是常规理解。

//按照定义一的说法,需要通过作用域链在全局环境中查找变量n的函数foo()就是闭包
var n = 0;
function foo() {
    console.log(n); // 0
}
foo();

1.3 定义二

闭包是指有权访问另一个函数作用域中的变量的函数。

那这样说来,访问上层函数的作用域的内层函数就是闭包。

//按照定义二的说法,嵌套在foo函数里的bar函数就是闭包
function foo(){
    var a = 2;
    function bar(){
        console.log(a); // 2
    }
    bar();
}
foo();

1.4 定义三

闭包是指在函数声明时的作用域以外的地方被调用的函数。

在函数声明时的作用域以外的地方调用函数,需要通过将该函数作为返回值或者作为参数被传递。

1、返回值

//按照定义三的说法,在foo()函数的作用域中声明,在全局环境的作用域中被调用的bar()函数是闭包
function foo(){
    var a = 2;
    function bar(){
        console.log(a); //2
    }
    return bar;
}
foo()();

function foo(){
    var a = 2;
    return function(){
        console.log(a);//2
    }
}
foo()();

2、参数

//按照定义三的说法,在foo()函数的作用域中声明,在bar()函数的作用域中被调用的baz()函数是闭包
function foo(){
    var a = 2;
    function baz(){
        console.log(a); //2
    }
    bar(baz);
}
function bar(fn){
    fn();
}

因此,无论通过何种手段,只要将内部函数传递到所在的词法作用域以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。

1.5 IIFE

IIFE(Immediately-Invoked Function Expression)(立即执行函数表达式)是不是闭包呢?

foo()函数在全局作用域定义,也在全局作用域被立即调用,如果按照定义一的说法来说,它是闭包。如果按照定义二和定义三的说法,它又不是闭包。

var a = 2;
(function foo(){
    console.log(a);//2
})();

1.6 总结

闭包定义之所以混乱,我觉得与经典书籍的不同解读有关。经典定义是犀牛书的原话,定义二是高程的原话。

这里参考阮一峰老师的理解,原文链接学习javascript中的闭包

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

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

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

他的理解主要从作用或用途去直接的表达。小火柴(参考博文博主)想从原理上去表达闭包函数。

但,归纳起来就是关于一个函数要成为一个闭包到底需要满意几个条件。

严格来说,闭包需要满足三个条件:

  • 访问所在作用域;
  • 函数嵌套;
  • 在所在作用域外被调用。

有些人觉得只满足条件1就可以,所以IIFE是闭包;有些人觉得满足条件1和2才可以,所以被嵌套的函数才是闭包;有些人觉得3个条件都满足才可以,所以在作用域以外的地方被调用的函数才是闭包。

问题是,谁是权威呢?

2、IIFE

IIFE(Immediately-Invoked Function Expression)(立即执行函数表达式)。

2.1 实现

函数跟随一对圆括号()表示函数调用。

//函数声明语句写法
function test(){};
test();

//函数表达式写法
var test = function(){};
test();

但有时需要在定义函数之后,立即调用该函数。这种函数就叫做立即执行函数,全称为立即调用的函数表达式IIFE(Imdiately Invoked Function Expression)。

javascript引擎规定,如果function关键字出现在行首,一律解释成函数声明语句。

1、函数声明语句需要一个函数名,由于没有函数名,所以下面报错。

//SyntaxError: Unexpected token (
function(){}();

2、函数声明语句后面加上一对圆括号,只是函数声明语句与分组操作符的组合而已。由于分组操作符不能为空,所以下面报错。

//SyntaxError: Unexpected token )
function foo(){}();

//等价于
function foo(){};
();//SyntaxError: Unexpected token )

3、函数声明语句加上一对有值的圆括号,也仅仅是函数声明语句与不报错的组合而已。

function foo(){}(1);

//等价于
function foo(){};
(1);

解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。

常用的两种办法这样:

(function(){ /* code */ }()); 
(function(){ /* code */ })(); 

其他的写法:

var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();

new function(){ /* code */ };
new function(){ /* code */ }(); 

2.2 作用域

对于IIFE来说,通过作用域链来查找变量与普通函数有一些不同的地方。

2.2.1 with

with语句中的IIFE会先在with语句中查找,然后再向上查找。在下列代码中,标准浏览器下f()函数和IIFE都返回'bar',但IE10-浏览器中的f()函数返回'abc'。

var foo = "abc";
with({
    foo:"bar"
}){
    function f(){
        console.log(foo);
    };
    (function(){
        console.log(foo);
    })();
    f();
}

2.2.2 try-catch

在下列代码中,标准浏览器下f()函数和IIFE都返回'error',但IE10-浏览器中的f()函数返回'10'。

try{
    var e = 10;
    throw new Error();
}catch(e){
    function f(){
        console.log(e);
    }
    (function (){
        console.log(e);
    })();
    f();
}

2.2.3 具名函数表达式

下面的代码,在标准浏览器中a()函数返回1,而IIFE返回a函数代码。但IE8浏览器全部返回1。

function a(){
    a = 1;
    console.log(a);
};
a();
(function a(){
    a = 1;
    console.log(a);
})();

2.3 用途

IIFE一般用于构造私有变量,避免全局污染。

接下来,下面有一个更直观的需求来说明IIFE的用途。假设有一个需求,每次调用函数,都返回加1的一个数字(数字初始值为0)。

1、全局变量

一般情况下,我们会使用全局变量来保存该数字状态

var a = 0;
function add(){
    return ++a;
}
console.log(add());//1
console.log(add());//2

2、上面的方法局限在于变量a只和add函数有关,却声明为全局变量,不太合适。

将变量a更改为函数的自定义属性更为恰当。

function add(){
    return ++add.count;
}
add.count = 0;
console.log(add());//1
console.log(add());//2

3、然后这样做可能还会存在问题,有些时代代码可能会无意间将add.count重置。使用IIFE把计数器变量保存为私有变量更安全,同时也减少全局变量的污染。

var add = (function(){
    var counter = 0;
    return function(){
        return ++counter; 
    }
})();
console.log(add())//1
console.log(add())//2 

2.4 注意事项

IIFE称为立即执行函数,这个立即执行函数有多立即呢?

立即执行函数再快也得按照代码执行顺序去逐行执行。

var a = 1;
(function(){
    console.log(a);//1
})();

类似的,函数也是如此。

function a(){
    return 1;
}
(function(){
    console.log(a());//1
})();

但,如果是函数表达式就不一样了。执行代码如下,会报错,提示a的值是undefined。

var a = function(){
    return 1;
}
(function(){
    console.log(a());//报错
})();

函数有一个函数声明提前hoisting的过程,函数表达式其实分为先声明后赋值这两步。而,如果后者存在立即执行函数表达式,这个IIFE会快带函数表达式a执行完第一步函数声明后IIFE就会立即执行,此时a未被赋值,是undefined,所以执行a()时会报错。

3、常见的一个循环和闭包错误的理解

关于常见的一个循环和闭包的错误,很多资料都对此有文字解释,但还是难以理解,本文将以执行环境图示的方式来对此进行更直观的解释,以及对此需求进行推衍,得到合适的解决方法。

3.1 犯错

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//2  

以上代码的运行结果是2,而不是预想的0。接下来用执行环境图示的方法,详解到底是哪里出了问题。

犯错执行流图示

执行流首先创建并进入全局执行环境,进行声明提前的过程。执行流执行到第10行,创建并进入foo()函数执行环境,并进行其词法作用域内的声明提前。然后执行第2行,将arr赋值为[]。然后执行到第3行,给arr[0]和arr[1]都赋值一个匿名函数。然后执行到第8行,以arr的值为返回值退出函数。由于此时有闭包的存在,所以foo()的执行环境并不会被销毁,且i的值被保存。

foo()执行环境未被销毁且i值保存

执行流进入全局执行变量,继续执行第10行,将函数的返回值arr赋值给bar。

foo()环境到全局执行环境

执行流执行到第11行,访问bar的第0个元素并执行。此时,执行流创建并进入匿名函数执行环境,匿名函数中存在自由变量i,需要使用其作用域链匿名函数->foo()函数->全局作用域进行查找,最终在foo()函数的作用域找到了i,然后在foo()的执行环境找到了i的值为2,于是给赋值为2。

执行流接着执行第5行,以i的值2作为返回值返回。同时销毁匿名函数的执行环境。执行流进入全局执行环境,接着执行第11行,调用内部对象console,并找到其方法log,将bar0的值2作用参数放入该方法中,最终在控制台显示2。

由此我们看出,犯错的原因主要在循环的过程中,并没有把函数的返回值赋值给数组元素,而仅仅把函数赋值给了数组元素。这就使得在调用匿名函数时,通过作用域找到的执行环境中存储变量的值已经不是循环时的瞬时索引值,二十循环执行完毕之后的索引值。

3.2 IIFE

由此,可以利用IIFE传参和闭包来创建多个执行环境来保存循环时各个状态的索引值。因为函数传参是按值传递的,不同的参数的函数调用时,会创建不同的执行环境。

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = (function fn(j){
            return function test(){
                return j;
            }
        })(i);
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0  
循环+IIFE

3.3 块作用域

使用IIFE还是较为复杂,使用作用域块则更为方便。

由于块作用域可以将索引值i重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值,相当于每一次索引值都创建一个执行环境。

function foo(){
    var arr = [];
    for(let i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0    

在编程中,如果实际和预期结果不符,就按照代码顺序一步一步地把执行环境图示画出来,会发现很多时候就是在想当然!

4、闭包的7中形式

根据闭包的定义,我们知道,无论通过各种手段,只要将内部函数传递到词法作用域以外,它都会持有对原有作用域的引用,无论在何处执行这个函数都会使用闭包,下面会介绍闭包的7种形式。

4.1 返回值

最常用的形式是函数作为函数值被返回:

var F = function(){
    var b = 'local';
    var N = function(){
        return b;
    }
    return N;
}
console.log(F()());

4.2 函数赋值

一种变形的形式是将内部函数赋值给一个外部变量:

var inner;
var F = function(){
    var b = 'local';
    var N = function(){
        return b;
    };
    inner = N;
};
F();
console.log(inner());

4.3 函数参数

闭包可以通过函数参数传递函数的形式来实现:

var Inner = function(fn){
    console.log(fn());
}
var F = function(){
    var b = 'local';
    var N = function(){
        return b;
    }
    Inner(N);
}
F();

4.4 IIFE

前面的代码实例可知,函数F()都是在声明之后立即被调用,因此可以使用IIFE来代替。但是,需要注意的是,这里的Inner()只能用函数声明语句的形式,而不能用函数表达式。

function Inner(fn){
    console.log(fn());
}

(function(){
    var b = 'local';
    var N = function(){
        return b;
    }
    Inner(N);
})();

4.5 循环赋值

在闭包问题上,最常见的一个错误就是循环赋值的错误。其错误的原因在之前闭包环节已经讲述。

错误:

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//2    

正确:

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = (function fn(j){
            return function test(){
                return j;
            }
        })(i);
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0    

4.6 g(s)etter

我们通过提供getter()函数和setter()函数老将要操作的变量保存在函数内部,防止其暴露在外部。

var getValue,setValue;
(function(){
    var secret = 0;
    getValue = function(){
        return secret;
    }
    setValue = function(v){
        if(typeof v === 'number'){
            secret = v;
        }
    }
})();
console.log(getValue());//0
setValue(1);
console.log(getValue());//1

4.7 迭代器

我们经常使用一个闭包来实现一个累加器。

var add = (function(){
    var counter = 0;
    return function(){
        return ++counter; 
    }
})();
console.log(add())//1
console.log(add())//2 

类似地,使用闭包可以很方便的实现一个迭代器。

function setup(x){
    var i = 0;
    return function(){
        return x[i++];
    }
}
var next = setup(['a','b','c']);
console.log(next());//'a'
console.log(next());//'b'
console.log(next());//'c'

全文博文地址:闭包

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容