闭包与作用域

215 阅读4分钟

导读

  1. 了解 var、let、const

作用域

javascript 中大部分情况下,只有两种作用域类型:

  • 全局作用域:全局作用域为程序的最外层作用域,一直存在。
  • 函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。
/* 全局作用域开始 */
var a = 1;

function func () { /* func 函数作用域开始 */
  var a = 2;
  console.log(a);
}                  /* func 函数作用域结束 */

func(); // => 2

console.log(a); // => 1
/* 全局作用域结束 */

作用域链

// 1. 全局作用域
function foo(a) {
  // 2. foo 函数作用域
  var b = a * 2;

  function bar(c) {
    // 3. bar 函数作用域
    console.log(a, b, c);
  }

  bar(b * 3);
}

foo(2); // 2 4 12

当函数执行访问变量时,会先查找本地作用域,如果找到目标变量即刻返回,否则就会去父级作用域继续查找...一直找到全局作用域。我们把这种作用域的嵌套机制成为“作用域链”

作用域的应用场景

作用域的一个常见运用场景之一,就是 模块化,模块化的作用就是处理全局作用域污染和变量名冲突,代码结构臃肿且复用性不高等问题。

function module1 () {
  var a = 1;
  console.log(a);
}
function module2 () {
  var a = 2;
  console.log(a);
}
module1(); // => 1
module2(); // => 2

上面的代码中,构建了 module1module2 两个代表模块的函数,两个函数内分别定义了一个同名变量 a ,由于函数作用域的隔离性质,这两个变量被保存在不同的作用域中(不嵌套),JS 引擎在执行这两个函数时会去不同的作用域中读取,并且外部作用域无法访问到函数内部的 a 变量。这样一来就巧妙地解决了 全局作用域污染变量名冲突 的问题;并且,由于函数的包裹写法,这种方式看起来封装性好多了。

然而上面的函数声明式写法,看起来还是有些冗余,更重要的是,module1 和 module2 的函数名本身就已经对全局作用域造成了污染。

// 自执行函数实现模块化
// module1.js
(function () {
  var a = 1;
  console.log(a); // 1
})();
// module2.js
(function () {
  var a = 2;
  console.log(a); // 2
})();

自执行函数本质上是通过函数作用域解决了命名冲突、污染全局作用域的问题。

闭包

简单来说,闭包就是函数内部定义的函数,被返回了出去并在外部调用, 它绕过了作用域的监管机制,从函数外部也能获取到函数内部作用域的信息

function foo() {
  var a = 1;
  function bar() {
    console.log(a); // 1
  }
  return bar;
}
var baz = foo();
baz(); // 这就形成了一个闭包

我们可以简单剖析一下上面代码的运行流程:

  1. 编译阶段,变量和函数被声明,作用域即被确定。
  2. 运行函数 foo(),此时会创建一个 foo 函数的执行上下文,执行上下文内部存储了 foo 中声明的所有变量函数信息。
  3. 函数 foo 运行完毕,将内部函数 bar 的引用赋值给外部的变量 baz ,此时 baz 指针指向的还是 bar ,因此哪怕它位于 foo 作用域之外,它还是能够获取到 foo 的内部变量。
  4. baz 在外部被执行,baz 的内部可执行代码 console.log 向作用域请求获取 a 变量,本地作用域没有找到,继续请求父级作用域,找到了 foo 中的 a 变量,返回给 console.log,打印出 1

js闭包.png

闭包作用举例

1、累加器:

  • count作为一个全局变量,其他地方都可以对它进行操作,如果其他地方对count重新赋值或者重新定义count,那么这个计时器就被破坏了。
var count = 0; 
function addCount() {
    count++;
}
document.body.addEventListener("click", addCount);
  • 使用闭包实现
function addCount() {
    var count = 0;
    var addCount = function() {
        count++;
   }
    return addCount;
}
document.body.addEventListener("click", addCount);
// 点击一次->输出1
// 点击两次->输出2

2、缓存:

function eater () {
    let food = "apple";
    const obj = {
        eat: function () {
            if (food != "") {
                console.log("i am eating "+ food);
                food = "";
            } else {
                console.log("eat nothing");
            }
        },
        push: function (myFood) {
            food = myFood;
        }
    };
    return obj;
};
const kevin = eater();
kevin.eat(); // i am eating apple
kevin.eat(); // eat nothing
kevin.push('banana');
kevin.eat(); // i am eating banana
function isFirstLoad () {
    const list = [];
    return function (option) {
        if(list.indexOf(option) >= 0) {
            // 检测是否存在于现有数组中,有则说明已存在
            console.log('已存在')
        } else {
            list.push(option);
            console.log('首次传入');
            // 没有则返回true,并把这次的数据录入进去
        }
    }
}
var ifl = isFirstLoad();
ifl("kevin"); // 首次传入
ifl("leslie"); // 首次传入
ifl("kevin"); // 已存在

使用闭包的注意事项

闭包是一把双刃剑,当内部函数被保存到外部时,将会生成闭包。 闭包会导致原有作用域链不释放,造成内存泄漏(内存占用)

解决:能不用闭包就不用,及时释放。fn = null; --> 让内部函数成为垃圾对象 -->回收闭包

小结

希望看完本篇文章能对你有如下帮助:

  • 了解闭包,并合理利用闭包,后面再举个例子体会一下(防抖与节流)。
  • 深入理解作用域与作用域链。

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

往期内容推荐

  1. 深拷贝
  2. 轻松理解JS原型原型链
  3. new 操作符
  4. 手写bind/call/apply