闭包,看这一篇就够了

引言

变量的作用域:全局变量、局部变量

js 复制代码
function f1() {
  n = 999;
}
f1();
alert(n); // 999 直接读取全局变量

function f1() {
  var n = 999;
}
alert(n); // error 函数外部无法读取函数内的局部变量

function f1() {
  n = 999; // 未使用 var 命令,实际上声明了一个全局变量
}
f1();
alert(n); // 999

那如果想从外部读取局部变量,该如何实现?

在某些情况下,我们可能会需要得到函数内部的局部变量,"链式作用域"结构(chain scope):子对象会一级一级地向上寻找所有父对象的变量(反之则不成立)。

那么,我们可以在外部函数内定义并返回一个内部函数,并在内部函数中返回该局部变量,这就是最常见的闭包

js 复制代码
function outerFun(){
  let name = 'june';
  function innerFun(){
    return name;
  }
  return innerFun;
}
const getName = outerFun();
console.log(getName()); // june

闭包的定义

MDN:一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合

阮一峰:闭包就是能够读取其他函数内部变量的函数(简单理解为"定义在一个函数内部的函数")

词法作用域

它是一种作用域决定机制。词法作用域根据源代码中 声明变量的位置 来确定该变量在何处可用,而非函数的调用位置。嵌套函数可访问声明于它们外部作用域的变量。

为了更好地说明词法作用域,我们来看下面例子:

js 复制代码
var x = 'global';
function outer() {
  var x = 'outer';
  function inner() {
    var x = 'inner';
    console.log('1', x); // inner
  }
  console.log('2', x); // outer
  inner();
}
console.log('3',x); // global
outer();

⚠️ 与动态作用域区分

动态作用域是基于函数的调用栈来决定变量作用域的(即变量的作用域是在运行时确定的),而不是基于代码的物理结构

闭包实例

js 复制代码
const counter = function () {
  let count = 1;
  function acc() {
    count++;
    console.log(count);
  };
  return acc;
};
const add = counter();
add(); // 2
add(); // 3

在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦函数执行完毕,变量将不能再被访问。但这里显然不是。

词法环境包含了这个闭包创建时作用域内的任何局部变量。

在本例子中,add 是执行 counter 时创建的 acc 函数实例的引用。acc 的实例维持了一个对它的词法环境(变量 count 存在于其中)的引用。因此,当 add 被调用时,变量 count 仍然可用。

使用闭包定义一个函数工厂

js 复制代码
function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

// add5和add10共享相同的函数定义,但是保存了不同的词法环境
const add5 = makeAdder(5);
const add10 = makeAdder(10);
add5(5); // 10
add10(10); // 20

用闭包模拟私有变量

JavaScript 没有原生支持声明私有变量,可以使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式称为模块模式(module pattern)

js 复制代码
const makeCounter = function () {
  // 一个词法作用域
  let privateCounter = 0;
  function changeCount(val) {
    privateCounter += val;
  }
  // 三个公共函数共享同一个环境的闭包
  return {
    increase: function () {
      changeCount(1);
    },
    decrease: function () {
      changeCount(-1);
    },
    value: function () {
      return privateCounter;
    },
  };
};
const count1 = makeCounter();
const count2 = makeCounter();
count1.increase();
count2.decrease();
console.log(count1.value(), count2.value()); // 1  -1

计数器 Counter1Counter2相互独立,互不影响,各自维护自己词法作用域内的变量 privateCounter

常见的错误:在循环中创建闭包

可以将内部函数本身当做一个值类型进行传递,提供了一个入口可以更改函数内部的私有变量(在词法作用域之外执行)

js 复制代码
for (var i = 1; i <= 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000 * i);
}

尽管循环的五个函数在各自迭代中分别定义但它们都被封闭在一个共享作用域中,故实际上就只有一个 i。

而循环终止的条件是 i 不再<=5,故首次满足条件的 i 为 6,延迟函数的回调会在循环结束之后才执行,故执行后的打印结果都是 6。

如果想按照预期输出结果呢?

a. IIFE

js 复制代码
for (var i = 1; i <= 5; i++) {
  (function () {
    var j = i; // 闭包到块作用域(本质是将块作用域转换成可以被关闭的作用域); IIFE函数需要有自己的变量用来在每次迭代的时候存储i的值
    setTimeout(() => {
      console.log(j); // 正常输出:1 2 3 4 5
    }, 1000 * j);
  })();
}

b. 可以不用额外定义变量,直接将变量传递到 IIFE 函数中

js 复制代码
for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j); // 正常输出:1 2 3 4 5
    }, 1000 * j);
  })(i);
}

c. 更简洁的做法,将变量 i 的定义使用 let 取代 var

js 复制代码
for (let i = 1; i <= 5; i++) {
  // let声明的特殊行为:该变量在循环中不止被声明一次而是每次迭代都会声明,随后每个迭代都会使用上一个迭代结束的值来初始化这个变量
  setTimeout(() => {
    console.log(i); // 正常输出:1 2 3 4 5
  }, 1000 * i);
}

换个例子,它的执行结果是什么?

js 复制代码
var result = [];
var a = 3;
var total = 0;
function foo(a) {
  for (var i = 0; i < 3; i++) {
    result[i] = function () {
      total += i * a;
      console.log(total);
    };
  }
}
foo(1);
result[0]();
result[1]();
result[2]();

foo 函数传入变量 a 为 1,for 循环中 i 变量是通过 var 声明的,意味着在整个 foo 函数的作用域内,i 只有一个实例,循环后的值为 3。

第一次执行完 total 为 3,第二次 total 为 6,第三次 total 为 9,打印结果为 3 6 9。同样地,我们可以使用前面提到的 IIFE 或者 let 来获得期望的值。

再来看看这个:

js 复制代码
for (var i = 0; i < 6; i++) {
  (function () {
    console.log(i);
  })();
}

i变量是通过var声明的,它内部引用的i变量是由外部for循环作用域中的同一个i变量。

但由于 IIFE 是"立即执行"的,它会立即打印每次循环迭代时i的当前值,而不是等到变量i循环完成后的最终值。

闭包的应用场景

闭包的用途主要有两个:可以读取函数内部的变量、让变量的值始终保持在内存中

  • 自执行函数
  • 防抖与节流
  • 函数柯里化
  • 订阅发布
  • 迭代器...

思考题

js 复制代码
var name = 'The Window';
var object = {
  name: 'My Object',
  getNameFunc: function () {
    console.log(this); // object
    return function () {
      console.log(this); // window
      return this.name; // 对象属性中,嵌套超过一级及以上的函数,this指向都是window(构造函数中也是这样)
    };
  },
};
alert(object.getNameFunc()()); // The Window
js 复制代码
var name = 'The Window';
var object = {
  name: 'My Object',
  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name;
    };
  },
};
alert(object.getNameFunc()()); // My Object

当一个函数作为函数而不是方法来调用的时候,this 指向的是全局对象。

对象属性中,嵌套超过一级及以上的函数,this 指向都是 window

构造函数中的一级函数,this 指向通过构造函数; 构造函数中的二级(及以上)函数,this 指向的是 window

性能考量

需要思考是否需要使用闭包,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

滥用会导致内存泄漏:

js 复制代码
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function () {
    return this.name;
  };

  this.getMessage = function () {
    return this.message;
  };
}

继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法

js 复制代码
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}

MyObject.prototype.getName = function () {
  return this.name;
};
MyObject.prototype.getMessage = function () {
  return this.message;
};

闭包中变量存储的位置是栈内存还是堆内存?

闭包可以访问外部函数中的变量,对于基本类型在逻辑上属于栈内存,但由于闭包的特性,这些变量实际上会被存储或复制到堆内存中,以便在外部函数执行完毕后仍然可以访问。

因此,可以认为闭包中变量的存储位置主要是在堆内存中。这也是为什么闭包可以持续访问外部函数作用域中的变量,即使那个作用域已经执行结束。

如果文章对你有用,请点赞再收藏,你的鼓励是我创作的动力~

参考文章:

MDN-闭包

阮一峰-学习Javascript闭包(Closure)

相关推荐
喵叔哟25 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django