JS三座大山之作用域和”闭包“

196 阅读6分钟

1. 闭包的概念

  • MDN:
    • 函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
    • 闭包是由函数以及声明该函数的词法环境组合而成的
  • 阮一峰老师:
    • 闭包就是能够读取其他函数内部变量的函数。可以把闭包简单理解成***"定义在一个函数内部的函数"***

总结下来就是闭包是定义在一个函数内部的函数,可以从内部函数访问外部函数作用域,它是由函数以及声明该函数的词法环境组合而成的,包含被引用变量 or 函数的对象

2. 变量的作用域

首先明确JavaScript中变量的作用域。

分为局部变量和全局变量

//Javascript的函数,在内部可以直接读取全局变量。

  var n=999;
  function f1(){
    alert(n);
  }
  f1(); // 999

//另一方面,在函数外部自然无法读取函数内的局部变量。

  function f1(){
    var n=999;
  }
  alert(n); // error

//这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

  function f1(){
    n=999;
  }
  f1();
  alert(n); // 999
  
  /*
  于是,为了在外部读取局部变量,我们在函数中再返回一个函数
  result 是执行 f1 时创建的 f2 函数实例的引用。f2 的实例维持了一个对它的词法环境(变量 n 存在于其中)的引用。
  通过调用这个实例,实现了对函数内部变量的访问
  */
 function f1(){
    var n=999;
    function f2(){
      alert(n); 
    }
    return f2;
  }
  var result=f1();
  result(); // 999

作用域链

作用域链:内部函数访问外部函数的变量,采用的是链式查找的方式来决定取哪个值,这种结构称之为作用域链。查找时,采用的是就近原则

var num = 10;

function fn() {
    // 外部函数
    var num = 20;

    function fun() {
        // 内部函数
        console.log(num);
    }
    fun();
}
fn();
//结果为20

3. this

this指的是,调用函数的那个对象。this永远指向函数运行时所在的对象。

  1. 以函数的形式调用时,this永远都是window。比如fun();相当于window.fun();

  2. 以方法的形式调用时,this是调用方法的那个对象

  3. 以构造函数的形式调用时,this是新创建的那个对象

  4. 使用call和apply调用时,this是指定的那个对象

一般的定义函数是运行的时候决定this的指向。箭头函数中的this是在定义函数的时候绑定,而不是在执行函数的时候绑定。箭头函数没有自己的this,箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。当对箭头函数使用call()和apply()方法时对函数内的this没有影响。箭头函数会从自己的作用域链的上一层继承this

4. 闭包的用法

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。

/*
可以利用闭包,将具有不同参数的同一功能分别用一个全局变量引用
add5和add10其实就是闭包function(y)
原因就在于makeAdder是function(y)的父函数,而function(y)被赋给了一个全局变量,这导致function(y)始终在内存中,而function(y)的存在依赖于makeAdder,因此makeAdder也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
*/
function makeAdder(x) {
return function(y) {
  return x + y;
};
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12



/*
编程语言中,比如 Java,是支持将方法声明为共有或者私有(public、private)的,即它们只能被同一个类中的其它方法所调用。
而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。这种方式可称为模块模式
*/
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
  privateCounter += val;
}
return {
  increment: function() {
    changeBy(1);
  },
  decrement: function() {
    changeBy(-1);
  },
  value: function() {
    return privateCounter;
  }
}   
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
//或者不声明为自调用函数
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
  privateCounter += val;
}
return {
  increment: function() {
    changeBy(1);
  },
  decrement: function() {
    changeBy(-1);
  },
  value: function() {
    return privateCounter;
  }
}  
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

应用举例

(1)myModule.js:(定义一个模块,向外暴露多个函数,供外界调用)

function myModule() {
    //私有数据
    var msg = 'Smyhvae Haha'

    //操作私有数据的函数
    function doSomething() {
        console.log('doSomething() ' + msg.toUpperCase()); //字符串大写
    }

    function doOtherthing() {
        console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写
    }

    //通过【对象字面量】的形式进行包裹,向外暴露多个函数
    return {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
}

上方代码中,外界可以通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到。

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>05_闭包的应用_自定义JS模块</title>
</head>
<body>
<!--
闭包的应用 : 定义JS模块
  * 具有特定功能的js文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  * 【重要】只向外暴露一个包含n个方法的对象或函数
  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
    var module = myModule();
    module.doSomething1();
    module.doOtherthing2();
</script>
</body>
</html>

方式二

同样是实现方式一种的功能,这里我们采取另外一种方式。

1)myModule2.js:(是一个立即执行的匿名函数)

(function () {
    //私有数据
    var msg = 'Smyhvae Haha'

    //操作私有数据的函数
    function doSomething() {
        console.log('doSomething() ' + msg.toUpperCase())
    }

    function doOtherthing() {
        console.log('doOtherthing() ' + msg.toLowerCase())
    }

    //外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象
    window.myModule = {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
})()
(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>05_闭包的应用_自定义JS模块2</title>
</head>
<body>
<!--
闭包的应用2 : 定义JS模块
  * 具有特定功能的js文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  * 只向外暴露一个包信n个方法的对象或函数
  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->

<!--引入myModule文件-->
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
    myModule.doSomething1()
    myModule.doOtherthing2()
</script>
</body>
</html>

5. 闭包的作用

由上可见,闭包的作用主要有两个:

  • 作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)

  • 作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

隐藏局部变量,暴露操作函数

  function fn1() {
      var a = 2

      function fn2() {
        a++
        console.log(a)
      }
      return fn2;
    }

    var f = fn1();   //执行外部函数fn1,返回的是内部函数fn2
    f() // 3       //执行fn2
    f() // 4       //再次执行fn2


const createAdd = ()=>{
    let n = 0
    return ()=>{
        n += 1
        console.log(n)
    }
}

const add = createAdd()
add() // 1
add() // 2

6. 闭包的注意点

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

  2. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。


参考自阮一峰JavaScript闭包mdn 闭包qianguyihao github