什么是作用域,作用域链,闭包,以及闭包的使用场景?

273 阅读8分钟

什么是作用域和作用域链

先上一段代码:

function fn (name) {
    return function (object1, object2) {
        var val1 = object1[name];
        var val2 = object2[name];
        return val1 > val2;
    }
}

var f = fn('www');
f();
// 解除对匿名函数的引用(以便释放内存)
f = null;
  • 每个函数都有自己的执行环境

  • 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。如果这个环境是函数,则将其活动对象作为变量对象

  • 全局执行环境是最外围的一个执行环境。

  • 当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

  • 当代码在一个环境中执行时,会创建变量对象的一个作用域链

  • 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问

  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象。作用域链中的下一个变量对象来自外部环境,而再下一个变量对象来自外部的外部环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象。

  • 当某个函数被调用时,会创建一个执行环境及相应的作用域链

  • 作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

  • 一般情况下,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但闭包情况不太一致。

  • 上边代码的fn函数执行完成后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中,直到匿名函数被销毁后,fn的活动对象才会被销毁。

什么是闭包

JS高级程序设计中给出闭包的概念:闭包是指有权访问另一个函数作用域中的变量的函数。

var local = '变量'
function foo () {
    console.log(local);
}

「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。

function foo(){
  var local = 1
  function bar(){
    local++
    return local
  }
  return bar
}

var func = foo()
func()

为什么要函数套函数呢?

是因为需要局部变量,所以才把 local 放在一个函数里,如果不把 local 放在一个函数里,local 就是一个全局变量了,达不到使用闭包的目的——隐藏变量

所以函数套函数只是为了造出一个局部变量,跟闭包无关。

为什么要 return bar 呢?

因为如果不 return,你就无法使用这个闭包。把 return bar 改成 window.bar = bar 也是一样的,只要让外面可以访问到这个 bar 函数就行了。

所以 return bar 只是为了 bar 能被使用,也跟闭包无关。

闭包的作用

闭包常常用来「间接访问一个变量」。换句话说,「隐藏一个变量」

通过局部变量的方式,暴露一个访问器(函数),让其他函数可以间接访问到。

!function(){

  var lives = 50

  function fn (){
    lives += 1;
  }

  return fn

}()

到底什么是闭包?

闭包是 JS 函数作用域的副产品。

换句话说,正是由于 JS 的函数内部可以使用函数外部的变量,所以这段代码正好符合了闭包的定义。而不是 JS 故意要使用闭包。

闭包是否会引发内存泄漏?

内存泄露是指你用不到(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

闭包在IE9之前的版本中使用会导致一些特殊问题:

function assignHandler() {
    var element = docment.getElementById('someEle');
    element.onclick = function () {
        alert(element.id);
    }
}

上边这个闭包创建了一个循环引用。由于匿名函数保存了一个对assignHandler的活动对象,因此没法减少对element的引用数。只要匿名函数存在,element的引用数就不会为0,它占用的内存就不会被回收。

可以对代码进行做一下修改:

function assignHandler() {
    var element = docment.getElementById('someEle');
    var id = element.id;
    element.onclick = function () {
        alert(id);
    }
    element = null;
}

以上代码对element对象设置为null,这样能够解除对DOM对象的引用,顺利减少其引用数,确保正常回收其占用的内存。

如何避免闭包引起的内存泄漏?

  • 避免变量的循环赋值和引用

闭包使用场景

  1. 函数防抖
/**
 * @function debounce 函数防抖
 * @param {Function} fn 需要防抖的函数
 * @param {Number} interval 间隔时间
 * @return {Function} 经过防抖处理的函数
 * */
function debounce(fn, interval) {
    let timer = null; // 定时器
    return function() {
        // 清除上一次的定时器
        clearTimeout(timer);
        // 拿到当前的函数作用域
        let _this = this;
        // 拿到当前函数的参数数组
        let args = Array.prototype.slice.call(arguments, 0);
        // 开启倒计时定时器
        timer = setTimeout(function() {
            // 通过apply传递当前函数this,以及参数
            fn.apply(_this, args);
            // 默认300ms执行
        }, interval || 300)
    }
}

概念:就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。 通俗一点:在一段固定的时间内,只能触发一次函数,在多次触发事件时,只执行最后一次。

使用时机:搜索功能,在用户输入结束以后才开始发送搜索请求,可以使用函数防抖来实现。

  1. 函数节流
/**
 * @function throttle 函数节流
 * @param {Function} fn 需要节流的函数
 * @param {Number} interval 间隔时间
 * @return {Function} 经过节流处理的函数
 * */
function throttle(fn, interval) {
    let timer = null; // 定时器
    let firstTime = true; // 判断是否是第一次执行
    // 利用闭包
    return function() {
        // 拿到函数的参数数组
        let args = Array.prototype.slice.call(arguments, 0);
        // 拿到当前的函数作用域
        let _this = this;
        // 如果是第一次执行的话,需要立即执行该函数
        if(firstTime) {
            // 通过apply,绑定当前函数的作用域以及传递参数
            fn.apply(_this, args);
            // 修改标识为null,释放内存
            firstTime = null;
        }
        // 如果当前有正在等待执行的函数则直接返回
        if(timer) return;
        // 开启一个倒计时定时器
        timer = setTimeout(function() {
            // 通过apply,绑定当前函数的作用域以及传递参数
            fn.apply(_this, args);
            // 清除之前的定时器
            timer = null;
            // 默认300ms执行一次
        }, interval || 300)
    }
}

概念:就是限制一个函数在一定时间内只能执行一次。

使用时机:

  • 改变浏览器窗口尺寸,可以使用函数节流,避免函数不断执行;

  • 滚动条scroll事件,通过函数节流,避免函数不断执行。

函数节流与函数防抖的区别:

我们以一个案例来讲一下它们之间的区别: 设定一个间隔时间为一秒,在一分钟内,不断的移动鼠标,让它触发一个函数,打印一些内容。

函数防抖:会打印1次,在鼠标停止移动的一秒后打印。

函数节流:会打印60次,因为在一分钟内有60秒,每秒会触发一次。

总结:节流是为了限制函数的执行次数,而防抖是为了限制函数的执行时机。

  1. 函数柯里化

函数柯里化指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3
  1. 给一组元素循环绑定事件
function bindClick() {
    var allLi = document.getElementsByTagName('li');
    for (var i = 0; i < allLi.length; i++) {
      (function(i) {
        allLi[i].onclick = function () {
          console.log(i)
        }
      })(i);
    }
}
bindClick(); 

点击三个元素分别打印0,1,2。

  1. setTimeout。

    原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果。

function f1(a) {
    function f2() {
        console.log(a);
    }
    return f2;
}
var fun = f1(1);
setTimeout(fun, 1000); //一秒之后打印出1
  1. 封装私有变量
let Counter = (function() {
    let privateCount = 0;
    //私有方法
    function change(val) {
        privateCounter += val;
    }
    return {
        increment: function(val) {
            change(val);
        },
        decrement: function(val) {
            change(-val);
        },
        value: function() {
            return privateCounter;
        }
    }
})();
console.log(Counter.value())
console.log(Counter.increment(1))
console.log(Counter.decrement(1))
console.log(Counter.value())
//打印出0和2

  1. 回调函数
//使用Promise
function getUserId(url) {
    return new Promise(function (resolve) {
        //异步请求
        http.get(url, function (id) {
            resolve(id)
        })
    })
}
getUserId('some_url').then(function (id) {
    //do something
    return getNameById(id);
}).then(function (name) {
    //do something
    return getCourseByName(name);
}).then(function (course) {
    //do something
    return getCourseDetailByCourse(course);
}).then(function (courseDetail) {
    //do something
});
  1. 自执行函数
(function () {
    var a = 0;
    setInterval(function() {
        console.log(a++);
    },1000);
})();

参考文章

本文参考以下文章以及JS高级程序设计(第三版)

JS 中的闭包是什么?

什么是闭包,闭包的优缺点,闭包的应用场景

详解JS函数柯里化