闭包没有那么难

716 阅读5分钟

闭包到底是什么呢

闭包来源于functional progra,mming 函数式编程,闭包是一个持久的作用域,即使代码执行已经移出了当前执行上下文环境(当前作用域),这个作用域中的局部变量也不会销毁。作用域对象和作用域里的全部局部变量,都与函数绑定在一起,只要函数持久存在,就会持久存在。

前置知识点

上面提到三个重要知识点,是学明白闭包的充分条件,让我们一起来看一下。

变量作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。。

通俗点讲所有变量各司其职,不脱离所在的范围。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域函数作用域以及不常用的eval。ES6 的到来,为我们提供了“块级作用域 {}”,使用letconst声明块状作用域变量。

通常情况下,子级作用域中可以利用父级作用域中的变量,反之则不行。

执行上下文

执行上下文是当前代码的执行环境的内部对象。

执行上下文的生命周期:创建,执行,销毁

image.png

函数每次执行,都会生成一个会创建一个称为执行上下文的内部对象(AO 对象,可理解为函数作用域),这个 AO 对象会保存这个函数中所有的变量值和该函数内部定义的函数的引用。函数每次执行时对应的执行上下文都是独一无二的,正常情况下函数执行完毕执行上下文就会被销毁。

举个简单的例子来了解一下

function getName(name) {
    var b = 2;
    function foo() {};
    var bar = function() {};
}
getName('xiaoming')

当函数执行时此时的样例代码 AO 大致如下

AO = {
    arguments: {
        0: 'xiaoming',
        length: 1
    },
    name: 'xiaoming',
    b: undefined,
    foo: reference to function foo(){},
    bar: undefined
}

垃圾回收(销毁)

现代浏览器中js引擎垃圾回收使用的是标记-清除算法,即对象是否能被再次获得。

垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象

闭包原理

以这个闭包函数为例

function father() {
  let count = 0
  function children(){
    count++
    console.log(count)
  }
  return children
}
const newFunction = father()
newFunction()

闭包的形成与变量的作用域以及变量的生存周期密切相关,让我们以上面案例分析一下

image.png

在执行上下文创建的过程中,js引擎为变量自动分配内存空间,当用 newFunction 接收 father(),创建了新的对象引用指向 children 的内存空间,由于 children 函数的执行上下文中存在对 count 对象的引用,因此当前函数暂不被回收。

把闭包函数赋值给了一个变量,这个变量是一个活对象,这活对象引用了闭包函数,闭包函数又引用了 AO 对象,所以这个时候 AO 对象也是一个活对象。此时闭包函数的作用域链得以保存,不会被垃圾回收机制所回收。

闭包的创造函数需要包含两部分:

  1. 一些闭包函数执行时依赖的变量,每次执行闭包函数时都能访问和修改
  2. 返回的函数,这个函数中必定使用到上面所说的那些变量

闭包应用

闭包在函数式编程中广泛利用如,私有变量,回调函数,函数柯里化,实现节流防抖,以及各种JavaScript设计模式

私有变量

function Person(name){
    this.getName = function(){
        return name;
    };
    this.setName = function (value) {
        name = value;
    };
}

回调函数

function add(num1, num2, callback){
	var sum = num1 + num2;
	callback(sum);
}

function print(num){
	console.log(num);
}

add(1, 2, print);

节流防抖

节流函数

function throttle(fn, delay) {
    var timer;
    return function () {
        var _this = this;
        var args = arguments;
        if (timer) {
            return;
        }
        timer = setTimeout(function () {
            fn.apply(_this, args);
            timer = null; 
        }, delay)
    }
}

防抖函数

function debounce(fn, delay) {
    var timer; // 维护一个 timer
    return function () {
        var _this = this; // 取debounce执行作用域的this
        var args = arguments;
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function () {
            fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
        }, delay);
    };
}

柯里化(Currying)

function curry(f) { // curry(f) 执行柯里化转换
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// 用法
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert( curriedSum(1)(2) ); // 3

组合(Composing)

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

总结

❤️ 感谢大家

如果你觉得这篇内容对你挺有有帮助的话:

点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)

关注公众号咸鱼爱前端,我们一起学习一起进步。

觉得不错的话,也可以阅读其他文章(感谢朋友的鼓励与支持🌹🌹🌹):

三个网站玩转 Grid 布局

Nodejs 实现定时爬虫

React-Query 让你的状态管理更优雅

前端页面布局学习神器

面试必备知识点之深浅拷贝

SPA 前端路由原理与实现