作者: 她不美却常驻我心 博客地址: https://blog.csdn.net/qq_39506551 微信公众号:老王的前端分享 每篇文章纯属个人经验观点,如有错误疏漏欢迎指正。转载请附带作者信息及出处。
我们在上一节介绍了 JS 的作用域,推荐将上篇文章和这一篇连在一起看。 JS 将作用域分为了全局作用域和局部函数作用域,这表明了在函数的外部是不能访问函数内部的变量的,而内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回了之后。当函数运行结束之后,函数内部定义的变量也会被回收机制回收。而闭包的作用就是通过作用域链,使函数对象相互链接,使内部变量不被回收,保存在函数的作用域内,让可以我们可以在外部访问它。 闭包是什么样子呢?简单的理解,就是一个函数中嵌套着另一个函数,就是一个闭包,例:
var a = 1; function fn1(){ var a = 2; function fn2(){ console.log(a); } return fn2(); } fn1() // 2fn1 声明了一个局部变量 a,并定义了一个返回变量 a 的值的函数 fn2 ,此时,按照函数作用域链的规则,我们可以知道,当调用 fn1 之后,返回的结果是局部作用域中定义的变量 a 的值。
首先来我们更改一下上方的函数:
var a = 1; function fn1(){ var a = 2; function fn2(){ console.log(a); } return fn2; } fn1()() // ?我们将代码稍作更改,返回内部嵌套函数,而不是直接返回函数运行结果,此时在定义函数的作用域外调用 fn2,再看一下它的返回结果。 fn2的返回值依然是 2 ,并不是全局变量 1。这是因为按照定义域链的规则:当前作用域没有找到变量时,会逐步向外层查找。而 fn1 是 fn2 的父函数,fn2 会首先去它的父函数作用域中查找变量 a ,不管在任何地方调用,都会按照定义 fn2 时创建的作用域链来查找变量。由此我们可以得出闭包的第一个特性:
函数外部可以引用函数内部的变量,且避免了全局变量对函数内部的污染; function add(x){ return function(y){ return x + y; }; } var fn1 = add(1); var fn2 = add(2); fn2(4); // 6 fn1(3); // 4首先我们定义一个函数 add,它接受一个参数 x ,返回一个匿名函数,接受一个参数 y,返回 x+y 的计算结果。 fn1 和 fn2 分别传入不同的值来接收这个匿名函数,它们使用相同的函数进行定义,但传入的值不同,且 fn2 传入的值并没有覆盖掉 fn1 传入的值。两个函数的词法环境不同,所以计算出的结果也不同。
由于 fn2 并没有覆盖掉 fn1 ,这就说明每次外部函数执行的时候,都会重新创建一个新的对象,也就是说外部函数的引用地址不同。闭包可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。这就说明当操作不当时,由于存储的内容过多,会有发生内存泄漏的可能。
词法环境指函数创建时可访问的所有变量。
闭包里面的环境都是独立的,互不干扰,内部变量会常驻内存中,不会被垃圾回收机制回收,有发生内存泄漏的可能。上面的例子可能不能很好的说明 内部变量会常驻在内存中,所以我们来看下面的代码:
function fn1() { var a = 1; return function fn2() { var b = 0; console.log(++a); console.log(++b); } } var fn = fn1(); fn(); // a = 2 b = 1 fn(); // a = 3 b = 1一般情况下,当函数执行完毕之后,会被垃圾回收机制所注销,但由于 fn2 被赋值给了 fn ,所以这时候 fn 相当于:
var fn = function(){ var b = 0; console.log(++a); console.log(++b); }而因为函数内部中又引用着外部环境变量 a ,所以变量 a 并没有被销毁。而变量 b 随着 fn 的每次调用被重新创建,调用完毕之后又会被回收机制所销毁。 至于为什么外部环境变量 a 为什么没有被销毁呢?上一篇文章说过:当函数执行完毕后,如果不存在嵌套函数或其他引用指向该函数的对象,它就会被当做垃圾被删除掉。
我们来总结一下闭包的特点和优劣: 特点:
一个函数嵌套另一个函数。函数外部可以引用函数内部的变量。内部变量常驻内存,不会被垃圾回收机制回收。优点:
可以使一个变量长期存储在内存中;可以避免全局变量对函数内部的污染;可以给局部声明私有变量;缺点:
变量常驻内存,增加内存使用量,使用不当会很容易造成内存泄露;闭包可以在父函数外部,改变父函数内部变量的值,容易造成疏忽;下方代码的打印结果:
function fn1() { var a = 0; function fn2() { console.log(++a); } return { a: a, fn2: fn2 } } var test1 = fn1(); var test2 = fn1(); test1.fn2(); // 1 函数第一次调用,沿作用域链查找变量 a 并将其存储于内存中, a 初始值为 0 ,打印结果 1; test1.fn2(); // 2 因为内部变量常驻内存,不会被回收,所以此时调用函数的时候 a 初始值为 1,打印结果 2; test1.a; // test.a 是函数返回对象中的变量 a,它指向的是函数 fn1 的局部变量 a ,而不是函数 fn2 执行环境中的私有变量 a; test2.fn2(); // 1首先,fn1 在自己的函数作用域内声明了一个变量 a,以及一个函数 fn2,这时 fn2 就作为一个闭包可以在外部访问到 fn1 内部的变量 a,并对其在使用前进行一次自增。fn1 返回一个对象,这个对象有一个 a 变量以及一个 fn2 函数,变量 a 是 fn1 内部的变量 a 的一个缓存,而 fn2 则是 fn1 内部函数 fn2 的一个引用。前面我们提到,闭包里面的环境都是独立的,互不干扰。所以所 test1 和 test2 分别是两个独立的执行环境,结果互不影响。
下方代码的打印结果:
function fun(n, o) { console.log(o); return { fun: function (m) { return fun(m, n); } } }; var a = fun(0); // undefined a.fun(1); // 0 a.fun(2); // 0 var b = fun(0).fun(1).fun(2).fun(3); // undefined , 0, 1 , 2 var c = fun(0).fun(1); // undefined , 0 c.fun(2); // 1 c.fun(3); // 1首先我们先分析函数,最外层函数 fun 接受两个参数,并打印第二个参数 o,返回一个函数名同样为 fun 的函数。第二层函数 fun 接收一个参数 m,而且返回的是一个 对象 ,这个对象中有一个叫做 fun 的匿名函数,这里都使用相同的名字就是为了混淆视线,增加理解难度。第二层的匿名函数实际上是在调用最外层的 fun 函数,将自身接受到的变量 m 和父函数接受的变量 n传递给最外层,此时,对于最外层的 fun 而言,接受的参数 n 为 内层传递的变量 m ,参数 o 为之前自身接受的变量 n。 理解了代码的含义之后,我们可以将代码改成方便理解的形式:
// 改写函数名字,便于区分理解。 function fn1(n, o) { console.log(o); return { fn2: function (m) { return fn1(m, n); } } }; // 调用最外层函数实际执行代码: function fn1(n, o){ console.log(o); return fn2; } // 调用内层函数实际执行代码: function fn2(m){ // 当前作用域无变量 n ,沿作用域链向上查找变量 n ,可知这里传入 fn1 的变量 n 与父函数接收到的 n 为同一个变量。 // fn1 接受的参数 `n` 为 fn2 传递的变量 `m` ,参数 `o` 为之前自身接受的变量 `n`。 fn1(m, n); }var a = fun(0) => var a = fn1(0) :此时 fn1 接受参数 ( n = 0 ), 变量 o 不存在,打印 undefined ; a.fun(1); => a.fn2(1):此时 fn1 接受参数( n = m = 1 , o = 第一次传入的 n = 0 ),打印 0; a.fun(2); => a.fn2(2):此时 fn1 接受参数( n = m = 2 , o = 第一次传入的 n = 0 ),打印 0;
var b = fun(0).fun(1).fun(2).fun(3);将其拆分为:
var b1 = fn1(0); // fn1(n = 0) 打印 undefined var b2 = b1.fn2(1); // fn1(n = 1 , o = 0) 打印 0 var b3 = b2.fn2(2); // fn1(n = 2 , o = 1) 打印 1 var b = b3.fn2(3); // fn1(n = 3 , o = 2) 打印 2看懂了上面的解释,c 的输出就一目了然了。
var c1 = fn1(0); // fn1(n = 0) 打印 undefined var c = c1.fn2(1,0) // fn1(n = 1 , o = 0) 打印 0 c.fun(2) // => c.fn2(2) => c.fn1(n = 2, n = 1) 打印 1 c.fun(3) // => c.fn2(3) => c.fn1(n = 3, n = 1) 打印 1下方代码的打印结果:
for (var i = 0; i < 4; i++) { setTimeout(function() { console.log(i); }, 100); }这是一道考验对线程的基础面试题,这里举这个例子是想要通过闭包来解决这个问题。上边打印的结果都是 5,可能部分人会认为打印的是 0、1、2、3、4。
原因: JS 分为同步任务和异步任务,同步任务都在主线程上执行,当主线程执行完毕之后再执行异步队列。而定时器操作属于异步任务,JS 在执行的时候首先会先执行主线程,异步相关的会存到异步队列里,当主线程执行完毕之后再执行异步队列,主线程执行完毕后,此时 i 的值为 4,所以在执行异步队列的时候,打印出来的都是 4。 这里需要大家对 event loop (js 的事件循环机制) 有所了解,推荐一个GitHub上的有关事件循环的面试题和讲解,有基础和的经验同学可以 戳这里查看事件循环面试题。 解决方案:
for (var i = 0; i < 5; i++) { setTimeout((function (i) { return function () { console.log(i); }; })(i), 100); }利用闭包给所有的 button 添加点击事件:
var aBtn = document.querySelectorAll("button"); for (var i = 0; i < aBtn.length; i++) { aBtn[i].onclick = (function (i) { return function () { console.log(i); }; })(i) }种一棵树,最好的时间是十年前,其次是现在。人的一生,总的来说就是不断学习的一生。 蚕吐丝,蜂酿蜜。人不学,不如物。与其纠结学不学,学了有没有用,不如学了再说。
每篇文章纯属个人经验观点,如有错误疏漏欢迎指正。转载请附带作者信息及出处。您的评论和关注是我更新的动力! 请大家关注我的微信公众号,我会定期更新前端的相关技术文章,欢迎大家前来讨论学习。 都看到这里了,三连一下呗~~~。点个关注,少个 Bug 。