闭包

169 阅读3分钟

作用域、作用域连(scope、scope chain)

作用域

作用域分为:函数作用域全局作用域

注:ES6中的块级作用域,是假的,实际上是函数作用域

函数作用域

js引擎在调用函数时才临时创建的一个作用域对象,里面保存函数的局部变量,而调用完后,函数作用域对象就释放

全局作用域

其实是window的对象。所有全局变量和全局函数都是window对象的成员

作用域链

一个函数把可用的作用域串起来,形成了一个作用域链

程序中任何地方(函数内、全局)去访问一个变量的时候,会沿着作用域链由内往外,如果找到了,立马停止。

变量

JS中变量分为:局部变量全局变量

局部变量

  • 类似var定义的变量
  • 形参也是局部变量 优点:不会有污染

缺点:不可重用

全局变量

在浏览器中是保存在window对象上的变量

优点:可以重用

缺点:会形成污染

注意

  • 作用域和作用域链都是对象结构
  • 只有函数的{}才会形成作用域
  • 不是所有的{}都能形成作用域
  • 不是所有的{}内部都是局部变量 比如:

1.对象的{},就不是作用域

2.对象的属性就不是局部变量

var lilei = {
    sname: "Li Lei"
}

面临的问题

如果有一个函数,希望可以修改某一个变量。我们知道,变量分为局部变量、全局变量

如果是局部变量,那么每次在执行函数的时候,都会临时创建作用域和撤销。那么这个局部变量就没有办法做到持久化。

如果是全局变量,可以做到持久化。但是如果项目中其他地方有意无意中修改了这个全局变量。那么程序就会出现BUG。

这个时候就需要一个函数,提供一个可以供内部函数修改的一个局部变量

创建闭包三步走

  • 创建外层函数
  • 创建函数内的变量和返回的内层函数
  • 执行外层函数,返回内层函数
// 1.创建外层函数mother
function mother() {
    // 外层函数内的变量total
   var total = 1000;
   // 2.内层函数
   return function pay(money) {
       total -= money;
       console.info(`花费了${money},剩余${total}`);
   }
}
// 3.执行外层函数,返回内层函数
var pay = mother();

外层函数返回内层函数的方法(三种)

  • 直接return(顺产)(最常用) 三部走中的例子就是最常用的return方法
  • 全局变量(剖腹产)
function mother() {
    var total = 999;
    nAdd = function() { total++ } // 这里的nAdd就是剖腹产,直接挂到了window全局作用域对象上
}
  • 将函数包裹在对象或者数组中返回
function mother() {
    var total = 0;
    arr = []
    arr.push(function() {
        total++;
        console.info(total)
    })
}

闭包是如何形成的(一句话概括)

外层函数在执行后,外层函数的作用域对象,被返回的内层函数的作用域链引用着。无法释放,形成了闭包

闭包缺点

作用域无法释放,容易导致内存泄露

闭包释放

及时把返回的内层函数赋值为null

pay = null;

练习

练习一

function fun() {
     var i = 999;
     nAdd = function() { i++ } // 注意这是一个内层函数1。剖腹产
     return function() { // 内层函数2。顺产
         console.info(i)
     }
}
var getN = fun();
getN(); // 999
nAdd();
getN(); // 1000

总结:一次生多个孩子,共享一个红包

练习二

function mother() {
    var i = 0;
    return function() {
        i++
        console.info(i);
    }
}
var get1 = mother();
get1(); // 1
var get2 = mother();
get2(); // 1
get1(); // 2
get2(); // 2

// 这里的mother生了两次

总结:多次生孩子,每次都有各自的红包

练习三

function fun(){ // 妈妈
    arr = []; // 不算红包,因为内存函数中没有用到
    for (var i = 0; i <3;i++) { // i 是红包。在for循环结束后,i = 3
        // 妈妈生成了三个孩子
       arr[i] = function (){
           console.info(i);
       }
    }
}
fun();
arr[0](); // 3
arr[1](); // 3
arr[2](); // 3

总结:妈妈(fun函数)一次剖腹产生了3个孩子(arr中保存的三个方法)。包了一个公用的红包,红包里面是3块钱(for循环后,i = 3)

练习四

var name = "window";
var p = {
    name: "peter",
    getName: function() { // 闭包外层函数
        var self = this; // 外层函数内的局部变量。这里p.getName执行时,this指向p
        return function(){ // 内层函数
            return self.name;
        }
    }
}
var getName = p.getName();
var _name = getName();
console.info(_name); // peter

练习五

// 每次都会输出第二个参数值
function fun(n, o) {
    console.info(o);
    return {
        fun: function(m) {
            return fun(m, n); // 这里执行的是最外层的fun
        }
    }
}

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

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

练习六(实现bind)(重要)

题目:用原生的call()模拟实现bind()

// 应该在所有函数的原型对象中添加自定义bind函数
Function.prototype.bind = function(obj, ...rest) {
   console.info("调用自定义的bind()");
   // 先获取将来获取bind()的.前的原函数,保存在一个局部变量中
   var fun = this; // this-> 将来的function jisuan(){ ... }
   return function(...values) { // 调用返回的内层函数,等效于调用原函数
       fun.call(obj, ...rest, ...values);
   }
}

function jisuan(value1, value2) {
    console.info(`${this.ename}, ${value1},${value2}`)
}
var lilei = {
    ename: "Li Lei"
}
var jisuan2 = jisuan.bind(lilei, "value1");
jisuan2(2)

使用场景

return一个函数

循环赋值

回调函数就是闭包

防抖节流

匿名函数自调

// 没有自调的情况,下面的arr可以被很多地方访问修改
var arr = [];
function add(){
    // 操作arr
}

// 使用匿名函数自调
(function() {
    // 这里的代码就是上面的代码
    // 这里的arr就不会被外面影响到
})();

柯里化

可以连续给一个函数反复传参,函数传入的参数,还可以累积

比如:实现一个add函数,可以接受任何多的参数,返回和值(add(1)(2)(3))

var add = function(){
    var sum = 0;
    function temp(num){
       sum += num;
       return temp
    }
    temp.toString = function() {
        return sum.toString();
    }
    return temp
}
// 注意第一个参数1是add函数的形参,后面的参数才是temp函数的参数
console.info(`${add(1)(2)(3)}`) // 5

其他

当遇到互不干扰的问题时,要想到闭包