js闭包详解

1.什么是闭包?

要了解什么是闭包,首先你要了解作用域。

js的作用域分两种,全局作用域和局部作用域。

我们知道在js作用域环境中访问变量的顺序是由内向外的,内部作用域可以获得当前作用域下的变量和当前作用域的外层作用域下的变量,反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的。
那么我们想在一个函数内部也有限权访问另一个函数内部的变量该怎么办呢?

闭包就是用来解决这一需求的,闭包的本质就是在一个函数内部创建另一个函数。
简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。

2.闭包的特性

  1. 函数嵌套函数
  2. 函数内部可以引用函数外部的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

3.闭包解析

1.简单的闭包案例
function fn1() {
    var name = 'lisi';
    function fn2() {
        console.log(name);
    }
    fn2();
}
fn1();

因为fn2函数执行时,会向外层找变量name,访问到fn1的变量name,fn2本身是个函数,满足’fn2函数访问到fn1函数的变量‘,所有由此产生了闭包

2.为什么闭包函数能够访问其他函数的作用域 ?

从堆栈的角度看待js函数

基本变量的值一般都是存在栈内存中,而对象类型的变量的值存储在堆内存中,栈内存存储对应空间地址。基本的数据类型: Number 、Boolean、Undefined、String、Null。

js栈与堆详情内容请参考另一篇文章(js中的栈内存与堆内存)

 // a 是一个对象
var  a= {b: 10 }  

当我们执行 a={b:20}时,堆内存就会产生新的对象{b:20},栈内存中a的指针会指向新的空间地址( {b:20} ),而堆内存中原来的{b:10}就会被程序引擎垃圾回收掉,节约内存空间。
我们知道js函数也是对象,它也是在堆与栈内存中存储的,我们来看一下转化:

var a = 1;
function fn(){
    var b = 2;
    function fn1(){
        console.log(b);
    }
    fn1();
}
fn();
image

由于栈是一种先进后出的数据结构,所以全局执行环境在最底层,我们由底自上看

  1. 在全局执行环境(浏览器就是window作用域),全局作用域里有个变量a及函数对象fn
  2. 进入fn,此时栈内存就会开辟一个fn的执行环境,这个环境里有变量b和函数对象fn1,这里可以访问自身执行环境和全局执行环境所定义的变量a
  3. 进入fn1,此时栈内存就会开辟 一个fn1的执行环境,这里面没有定义其他变量,但是我们可以访问到fn和全局执行环境里面的变量a,因为程序在访问变量时,是向底层栈一个个找,如果找到全局执行环境里都没有对应变量,则程序抛出underfined的错误。
  4. 随着fn1()执行完毕,fn1的执行环境被杯销毁,接着执行完fn(),fn的执行环境也会被销毁,只剩全局的执行环境下,现在没有b变量,和fn1函数对象了,只有a 和 fn(函数声明作用域是window下)

在函数内访问某个变量是根据函数作用域链来判断变量是否存在的,而函数作用域链是程序根据函数所在的执行环境栈来初始化的,所以上面的例子,我们在fn1里面打印变量b,根据fn1的作用域链的找到对应fn执行环境下的变量b。所以当程序在调用某个函数时,做了一下的工作:准备执行环境,初始函数作用域链和arguments参数对象

3.闭包应用

1.定时器与for循环
for (var i = 1; i <= 10; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

在这段代码中,我们对其的预期是输出1~10,但却输出10次11。这是因为 i 是声明在全局作用中的,定时器中的匿名函数也是执行在全局作用域中,当循环结束时,i 已经是 11 了,所以每次都输出11了。

怎么解决这个问题呢?

// 解决方法1: 使用es6语法,let声明i
for (let i = 1; i <= 10; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}
// 解决方法2:立即执行函数IIFE
// 我们可以让i在每次迭代的时候,都产生一个私有的作用域,在这个私有的作用域中保存当前i的值。
for (var i = 1; i <= 10; i++) {
    (function () {
        var j = i;
        setTimeout(function () {
            console.log(j);
        }, 1000);
    })();
}
2.this指向问题
var object = {
     name: "object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined
// 因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows
3.内存泄露
function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
}

// 改成下面
function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
    el = null    // 主动释放el
}
4.递归调用
function  factorial(num) {
   if(num<= 1) {
       return 1;
   } else {
      return num * factorial(num-1)
   }
}
var anotherFactorial = factorial
factorial = null
anotherFactorial(4)   // 报错 ,因为最好是return num* arguments.callee(num-1),arguments.callee指向当前执行函数,但是在严格模式下不能使用该属性也会报错,所以借助闭包来实现


// 使用闭包实现递归
function newFactorial = (function f(num){
    if(num<1) {return 1}
    else {
       return num* f(num-1)
    }
}) //这样就没有问题了,实际上起作用的是闭包函数f,而不是外面的函数newFactorial

4.闭包的好与坏

好处

  1. 保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
  2. 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
  3. 匿名自执行函数可以减少内存消耗

坏处

  1. 其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
  2. 其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

5.闭包例题检验:

function fun(n,o){
  console.log(o);
  return {
    fun: function(m){
      return fun(m,n);
    }
  };
}

var a = fun(0);  // ?
a.fun(1);        // ?        
a.fun(2);        // ?
a.fun(3);        // ?

var b = fun(0).fun(1).fun(2).fun(3);  // ?

var c = fun(0).fun(1);  // ?
c.fun(2);        // ?
c.fun(3);        // ?

undefined
0
0
0
undefined, 0, 1, 2
undefined, 0
1
1

文章参考: https://www.jianshu.com/p/26c81fde22fb

你可能感兴趣的:(js闭包详解)