架构师分享:弄懂 JavaScript闭包

前言:闭包(Closure)是JavaScript 的一个难点,在面试中经常被问到,而且很多高级应用都要依靠它,所以我想整理一篇关于”闭包“的学习笔记,希望对掘友们起到帮助。

函数的执行环境和变量的作用域

要理解闭包,首先要理解JavaScript函数的执行环境和变量的作用域

执行环境(Execution Context,也称为"执行上下文")是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其它数据,决定了各自的行为。当JavaScript代码执行的时候,会进入不同的执行环境,这些不同的执行环境就构成了执行环境栈。

let name = "qianshu";
function foo() {
  let year = 2020;
  function bar() {
    let month = 12;
    console.log(month);  //这里能读取到外部变量name、year
  }
  bar();
}
foo();
复制代码

变量的作用域分为两种:全局变量和局部变量。

let name = 'qianshu';  //全局变量
function foo(){
    let year =2020;    //局部变量
}
Foo();
console.log(year)  //ReferenceError: year is not defined. 外部函数不能读取到内部变量 
复制代码

什么是闭包

在红宝书中,闭包是这样定义的。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

首先我们先来看一段代码。

function outer() {
   let year =2020

    function insider() {
        console.log(`欢迎来到${year}年`);
    };
    return insider;
};
let a = outer()
a()  //欢迎来到2020年
复制代码

上面这段代码inside( )函数就是一个简单的闭包函数。为了获取到内部数据(变量/函数),inside( )函数作为一个返回值,让outer( )函数读取到它内部变量。

那么,我的理解是,闭包就是能够读取其他函数内部变量的函数。A( insider )函数嵌套在B( outer )函数内,B函数使用了A函数的内部变量,且A函数返回B函数,这就是闭包。

要使用闭包,只需要简单地将一个函数定义在另一个函数内部,并将它暴露出来。要暴露一个函数,可以将它返回或者传给其他函数。

内部函数将能够访问到外部函数作用域中的变量,即使外部函数已经执行完毕。

闭包的作用

闭包的作用

  • 使用函数内部的变量在函数执行完后,仍然存活在内存中,延伸了函数环境生命周期

    ​ 父级变量被子函数调用,则父级作用域中的变量都会被保存在内存中

  • 让函数外部可以操作到函数内部的数据

//延伸函数环境的生命周期例子
function foo() {
  let n = 1;
  return function sum() {
    console.log(++n);
  };
}
let a = foo();
a();  // 2
a();  //3

//没有闭包的例子
function hd() {
  let n = 1;
  return function sum() {
    console.log(++n);
  };
}
hd()();  //2
hd()(); //2
复制代码
//让函数外部可以操作到函数内部的数据的例子 
 function f1(){

    var n=999;

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999
复制代码

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗

闭包常见应用场景

获取区间数值

let arr = [1, 2, 3, 15, 54, 31, 65, 10];
function between(a, b) {
  return function (v) {
    return v >= a && v <= b;
  };
}
let data = arr.filter(between(2, 20));
console.log(data);  //[2,3,15,10]

复制代码

防抖

事件相应函数在一段时间后才执行,如果在这段时间内再次调用,则重新计算执行时间;当预定时间内没有再次调用,则执行doSomeThing。(现实生活中的例子:公交车司机开门让乘客上车,要等所有乘客都上车后才关门,而不是每上一位乘客执行一次开关门的操作)

function debounce(func, wait, immedicate) {
  let timeoutm,result;
  return function () {
    //改变执行函数内部的this的指向
    let context = this;
    //event 指向问题
    let args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immedicate) {
      let callNow = !timeout;
      timeout = setTimeout(() => {
        timeout = null;
      }, wait);
      //立即执行
      if (callNow) result = func.apply(context, args);
    } else {
      timeout = setTimeout(function () {
        func.apply(context, args);
      }, wait);
    }
    return result;
  };
}
//测试代码
let count = 0;
let container = document.querySelector("#container");
function doSomeThing(e) {
  container.innerHTML = count++;
}
container.onmousemove = debounce(doSomeThing, 300, true);
复制代码

节流

节流,就是指连续触发事件但是在 n 秒中只执行一次函数。

function throttle(func, wait) {
  let context, args, timeout;
  let old = 0; //时间戳
  let later = function () {
    old = new Date().valueOf();
    timeout = null;
    func.apply(context, args);
  };
  return function () {
    context = this;
    args = arguments;
    //获取当前时间戳
    let now = new Date().valueOf();
    if (now - old > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      func.apply(context, args);
      old = now;
    } else if (!timeout) {
      timeout = setTimeout(later, wait);
    }
  };
}
//测试代码
let count = 0;
let container = document.querySelector("#container");
function doSomeThing(e) {
  container.innerHTML = count++;
}
container.onmousemove = throttle(doSomeThing, 300, true);
复制代码

使用闭包的注意点

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

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


   
123
345


如果大家想学习前端方面的技术,我把我多年的经验分享给大家,还有一些学习资料,分享Q群:1046097531

架构师分享:弄懂 JavaScript闭包_第1张图片

 

架构师分享:弄懂 JavaScript闭包_第2张图片

 

你可能感兴趣的:(web开发,前端开发)