作用域和闭包

作用域

作用域的定义

作用域[[scope]]指的是执行上下文中变量和声明的作用范围。
在JavaScript中,作用域为可访问变量,对象和函数的集合。

作用域的分类

作用域可以分为全局作用域,局部作用域(函数作用域)和块级作用域(es6中新增)。

全局作用域

代码在程序中任意位置都可以被访问到,window对象的所有属性都拥有全局作用域。

局部作用域

代码中的变量只能在固定代码片段中被访问到。
局部作用域中可以访问到全局作用域的变量,但是反过来全局作用域中访问不到局部作用域中的变量。当局部作用域操作一个变量,会先在局部作用域中寻找,如果有就直接使用,否则就向上一级的作用域(可能是全局作用域,也可能是局部作用域)中查到。(可以通过直接使用window.在全局作用域中查找。)
在es6到来之前,所有的变量都用var来声明,而作为局部作用域执行的作用域,var定义的变量可以穿透if、for这些语句,我们来看一个例子:

if(true){
    var a = 'a1';
}
console.log(a);

这段代码的打印结果为a1,我们看到test这个变量穿透了if内部这个作用域来到了console所在的位置。我们怎么来约束这个test的作用范围呢?
在es6到来之前,我们有一种方法,叫做快速执行函数表达式(IIFE),通过创建一个函数病立即执行来构造一个新的域,从而控制这个test变量的范围。

var a = 'a1';
(function(){
    var a = 'a2';
    console.log(a)
}());
console.log(a)

通过在函数后面增加括号的方式来让函数直接运行,但是由于JavaScript规定由关键字function开头的是一个函声明,所以我们还得再加个括号,就像上面。(由于JavaScript对分号的不敏感,在一些不写分号的情况下,括号会被当作上一行最后的函数调用,所以我们可以在括号面前加一个分号(void关键字也可以)来避免这种情况。)
上面那段代码,依次可以打印出a2a1
这里有一种特殊的情况需要注意:

var test = 'a1';
(function test(){
    test = 'a2';
    console.log(test)
}());
console.log(test)

这里依次打印出来的是Function testa1,因为函数名在函数内为只读状态,所以不能被赋值。

块级作用域

es6为我们引入了letconst两个新的变量声明模式,他可以在if和for等语句下产生作用域。
我们把上面第一段代码的var改为let,代码就会报错:

if(true){
    let a = 'a1';
}
console.log(a);

作用域链

作用域链可以让我们在执行上下文中访问到父级直到全局的变量,他可以理解为包含了父级和自身的变量对象,我们通过作用域链可以访问到父级的声明变量对象。

闭包

什么是闭包

闭包(closure)表示一种函数。
能够读取其他函数内部变量的函数就是闭包。
或者说,定义在一个函数内部的函数就是闭包,因为内部函数可以访问外部函数的变量。
在父函数被销毁的情况下,在返回的子函数的作用域中仍在保留着父级的变量对象和作用域链,因此可以继续访问到父级的变量对象。

使用闭包的坑

使用闭包会产生一个问题:
因为多个子函数的作用域都是同时指向父级,因此当父级的变量对象被修改时,所有子函数都会受到影响。
我们来看一个典型的例子:

function test() {
    var result = [];
    for (var i = 0; i<10; i++){
      result[i] = function () {
          console.log(i)
      }
    }
    return result
}
test().map(fun=> fun());

我们执行这段代码,会发现这个地方返回的结果居然是10个一模一样的10。而要解决这个问题也非常的简单:可以通过函数参数形式传入变量,避免作用域链向上查找,我们对他稍微改造一下:

function test() {
    var result = [];
    for (var i = 0; i<10; i++){
      result[i] = function (num) {
            return function() {
                console.log(num);
            }
      }(i)
    }
    return result
}
test().map(item => item());

这样返回的结果就是我们希望得到的从0到9。

闭包的作用

  1. 我们可以通过闭包间接调用函数内部的局部变量。
  2. 通过闭包我们可以将函数内部的变量始终保存在内存中。(闭包的问题也在这里,滥用闭包可能会导致系统的崩溃)
    我们可以通过闭包来实现缓存(亦或者是单例模式)。
var Cache=(function(){
    var cache={};
    return {
        getData:function(id){
            if(id in cache){
                return cache[id];
            }
            var temp=new Object(id);
            cache[id]=temp;
            return temp;
        }
    }
})()
  1. 通过执行自执行函数可以避免污染全局变量,如下:
var count = 100;
var add = (function () {
    var count = 0;
    return function () {count += 1; return count}
})();
console.log(add()) // 1
console.log(count) // 100

闭包的使用场景

这可能是很多同学在面试中都遇到过的一个问题,什么情况下必须使用闭包,或者说必须使用闭包的场景。
1.回调
闭包最为经典的使用场景,就是循环给多个DOM节点增加绑定事件

var list = document.getElementsByTagName('button')

// 不使用闭包的情况
for(var i = 0; i < list.length; i++) {
    list[i].addEventListener('click', function(){
        alert(i)
      })
}

// 使用闭包
for(var i = 0; i < list.length; i++) {
  list[i].addEventListener('click', function(i){
    return function() {
      alert(i)
    }
  }(i))
}

我们比较上下两段代码的结果,会发现如果使用上面一段代码,所有的按钮点击返回都是一样的,因为当我们点击按钮的时候,for循环早已结束,此时的变量i正是按钮的个数。
所以我们需要使用闭包,用立即执行的方式将i的值传入,并且保存起来,就是下面这段代码,这个时候我们就能得到我们所要得到的结果。

  1. 函数防抖和节流
    我们可以提供一个函数
/*
* fn [function] 需要进行防抖处理的函数
* delay [number] 防抖时限
*/
function debounce(fn,delay){
    let timer = null
    return function() {
        if(timer){
            clearTimeout(timer)
            timer = setTimeout(fn,delay) 
        }else{
            timer = setTimeout(fn,delay)
        }
    }
}

然后我们可以这样来对函数进行防抖处理:

function test(){
    console.log('test');
}
let fun = debounce(test,1000);

这边得到的这个fun就是一个实现了防抖的函数。
节流同样的处理:

/*
* fn [function] 需要进行节流处理的函数
* delay [number] 节流时限
*/
function throttle(fn, delay) {
    let canRun = true;
    return function () {
        if (!canRun) return;
        canRun = false;
        setTimeout(() => { 
            fn.apply(this, arguments);
            canRun = true;
        }, delay);
    };
}

你可能感兴趣的:(作用域和闭包)