作用域和闭包

470 阅读6分钟

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

作用域、闭包这两个词大家应该都不陌生,今天请跟随我的脚步来进一步了解他们。

什么是作用域

将变量引入程序会引起几个很有意思的问题:

  • 这些变量住在哪里?
  • 他们储存在哪里?
  • 程序需要时如何找到他们? 这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量。这套规则被称为作用域。 几乎所有的编程语言最基本的功能之一,就是能够储存变量中的值,并且能在之后对这个值进行访问和修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和声明周期。

作用域工作模型

作用域有两种工作模型,分别是:词法作用域动态作用域 词法作用域:函数的作用域在函数定义的时候决定,无论函数在哪里被调用,也无论他如何被调用,他的词法作用域都只由函数声明时所处的位置决定; 动态作用域:函数的作用域在函数调用的时候决定;

var value = 1;
function fun() {
  console.log(value);
}
function bar() {
  var value = 2;
  fun();
}
bar();
// 结果是 1
// 【1】如果处于词法作用域,变量value首先在fun()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,找到并赋值为1。
// 【2】如果处于动态作用域,同样地,变量value首先在fun()中查找,没有找到。这里会顺着调用栈在调用fun()函数的地方,也就是bar()函数中查找,找到并赋值为2。

作用域分类

全局作用域:全局代表了整个文档document,变量或函数在函数外面声明;

局部作用域:变量在函数内声明,变量为局部作用域;

块级作用域:是一个语句,将多个操作封装在一起,通常是放在一个大括号里,没有返回值。

闭包的出现

声明式编程:不关注对象的实现细节,只关注在执行过程中计算机应该做什么;

// 声明式编程 告知机器做什么,隐藏具体的实现细节
// HTML
<div>
  <p>Declarative Programming</p>
</div>
// sql
select * from studens where firstName = 'declarative';

命令式编程:关注实现的细节,主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。

//命令式编程,关注实现过程
let arr2 = [1, 2, 3, 4, 5];
var result2 = [];
for(let i=0, len = arr2.length; i < len; i++) {
  let cacheItem = arr2[i];
  if( cacheItem  % 2 == 0 ) {
    result2.push(cacheItem); 
  }    
}

函数式编程:是声明式编程的一部分,函数式编程最重要的特点是“函数第一位”,即函数可以出现在任何地方。

//函数式编程,纯函数实现
var arr = [1, 2, 3, 4]
function findOdd(arr) {
  return findOddHelper(arr, [])
}
function findOddHelper(arr, result) {
  if (isNull(arr) || isNull(car(arr))) {
    return result
  } else if (car(arr) % 2 === 0) {
    return findOddHelper(cdr(arr), cons(car(arr), result))
  } else {
    return findOddHelper(cdr(arr), result)
  }
}
function isNull(obj) {
    return obj === undefined
}
function car(arr) {
  return arr[0]
}
function cdr(arr) {
  return arr.splice(1)
}
function cons(item, arr) {
  return [item, ...arr]
}
console.log(findOdd(arr));

闭包最早出现在函数式编程中,后来被很多编程语言借鉴。

为什么出现闭包

Q:如果想在全局环境中访问函数内部的变量? A:可以使用闭包。

Q:通过对象将想要访问的变量输出就可以做到? A:其实,闭包的另一个目的是让闭包中引用的变量始终保存在内存中。

来看看这个例子吧

// 通过返回对象访问函数内部的变量
function f1() {
    var n = 10;
    var add = function() {
        n++;
    }
    return {
        n: n,
        add: add
    }
}
var result = f1();
console.log(result);

// 使用闭包
function f2() {
    var n = 10;
    var add = function() {
        n++;
    }
    return function() {
        return {
            n: n,
            add: add
        }
    }
}
var result4 = f2();
console.log(result4())
result4().add()
console.log(result4());

什么是闭包

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。

function f3(){
    var a = 999;
    function f2(){
        console.log(a);
    }
    return f2; // f1返回了f2的引用
}
var result5 = f3(); // result就是f2函数了
result5();  // 执行result,全局作用域下没有a的定义,
        //但是函数闭包,能够把定义函数的时候的作用域一起记住,输出999 

在 JavaScript 中,所有函数都是天生闭包的。(只有一个例外,new Function())

// 所有函数都是闭包?
function outer(x){
    function inner(y){
        console.log(x+y);
    }
    return inner;
  }
  var inn=outer(3);//数字3传入outer函数后,inner函数中x便会记住这个值
  inn(5);//当inner函数再传入5的时候,只会对y赋值,所以最后弹出8
  
 // 使用 new Function 创建一个函数,
// 用Function()构造函数创建一个函数时并不遵循典型的作用域,不能获取局部变量;获取的是全局环境下的
function getFunc() {
    let value = "test";
    let func = new Function('alert(value)');
    return func;
}
getFunc()();

本质上,闭包是将函数内部和函数外部连接起来的桥梁

闭包使用场景

将函数本身当作值类型进行传递

// 将函数本身当作值类型进行传递
function foo() {
    var a = 2;
    function bar() {
        console.log(a)
    }
    return bar;
}
var baz = foo();
baz();


function wait(message) {
    setTimeout(function timer() {
        console.log(message);
    }, 1000)
}
wait('hello')

延迟函数中的回调-循环和闭包

for(var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000);
}

for(var i = 1; i <= 5; i++) {
  (function() {
    var j = i;
    setTimeout(function timer() {
        console.log(j)
    }, j * 1000);
  })();
}

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000)
}

模块用闭包模拟私有方法

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 */

闭包的优劣

优点: 1:变量长期驻扎在内存中; 2:避免全局变量的污染; 3:私有成员的存在 ;

缺点: 常驻内存,会增大内存的使用量,使用不当会造成内存泄露。

总结

你是否恍然大悟:原来在我的代码中已经到处是闭包了,现在我终于能理解他们了。

其实,无论何时何地,如果将函数当作第一级的值类型并到处传递,就会看到闭包在这些函数中的应用。

定时器/事件监听器/Ajax请求/跨窗口通信等,只要使用了回调函数,实际上就是在使用闭包。