闭包

92 阅读6分钟

作用域

闭包和js作用域息息相关,所以我们先来看作用域概念。对于所有编程语言来说,核心都是在处理变量定义,分配空间,变量访问,变量值修改。那么怎么找到变量呢? 此时就涉及到作用域的概念,作用域就是 js访问变量的一套规则。

js引擎解析

js是解释性语言,边解释边执行,执行前会有很短的时间进行解析,解析也叫预编译,预编译完再执行。那么两个阶段分别做了什么呢? 以一段代码为例:

var name= 'lucy'

var name // 编译处理
name = lucy // 执行处理

编译阶段

查询声明的变量,分配空间,变量提升,赋初始值 编译器会在当前作用域查询,是不是存在name的变量,如果有就忽略 var name,继续往下编译;如果没有,编译器就会在当前作用域创建一个name变量,并生成js引擎需要的代码,程序进入执行阶段。

执行阶段

js引擎执行代码的过程中,也会在作用域中查找,有没有name变量,找到,就做赋值操作。如果找不到,就会到当前作用域的商上级作用域查找name,逐级向上。如果一直没找到,js引擎就会抛出一个异常。

js引擎查找变量的过程就叫做作用域链。作用域指变量能够被访问到的范围。

js中作用域有几种,最开始只有函数/全局作用域,后来出现块级作用域。

作用域类别

全局作用域

变量分为局部变量和全局变量,不在函数内声明的都是全局变量,全局变量在代码的任何地方都能访问,相当于挂在window对象下。

var globalName = 'global';
function getName() { 
  console.log(globalName) // global
  var name = 'innerName'
  console.log(name) // inner
} 
getName();
console.log(name); 
console.log(globalName); //global

globalName在全局下声明,在代码片段任何位置都能访问。innerName在函数内部声明,只有函数内部能访问。

不带var的变量

function sayName(){
    name = 'lucy'
}
sayName()
console.log(name) // lucy
console.log(window.name) //lucy

在 JavaScript 中,所有没有经过定义而直接被赋值的变量默认就是一个全局变量

sayName()执行时,会先进行编译,编译器发现name声明没有var,就会在全局作用域查找有没有name,没有就增加一个name。执行阶段,js引擎会在当前作用域找name,没找到,到上一级全局作用域查找,找到就赋值。函数执行完之后,console的作用域为全局,在当前作用域下,依然能找到name.

可以发现,全局变量是拥有全局的作用域,无论在何处都可以使用它,在浏览器控制台输入 window.name 时,就可以访问到 window 上的全局变量。当然全局作用域有相应的缺点,当定义很多全局变量时,可能会引起变量命名的冲突,所以在定义变量时应该注意作用域的问题。

函数作用域

函数内声明的变量,只有函数内能访问,这个变量的作用域就是函数作用域。

function getName () {
  var name = 'inner';
  console.log(name); //inner
}
getName();
console.log(name); // 报错

name在函数内声明,是函数变量,作用域只有函数内。函数执行完毕,函数作用域内的变量会被销毁,其他地方访问不到。

块级作用域

js发展到ES6之后,新增了块级作用域,只要用let const声明的变量,就只有块级作用域,并且不进行变量提升。同时还要注意,函数开始到let声明之间,暂时性死区问题。

暂时性死区

function foo() { 
  console.log(bar) 
  let bar = 3 
} 
foo() // Uncaught ReferenceError: bar is not defined。
  • 对于块级作用域,但凡有{}就有新的词法作用域。
  • 对于var声明的变量,只要进入函数作用域,不关心任何 {},除了对象,所有var变量都会提升。
console.log(a) //a is not defined
if(true){
  let a = '123'console.log(a); // 123
}
console.log(a) //a is not defined

变量 a 是在 if 语句{} 中由 let 关键词进行定义的变量,所以它的作用域是 if 语句括号中的那部分,而在外面进行访问 a 变量是会报错的,因为这里不是它的作用域。所以在 if 代码块的前后输出 a 这个变量的结果,控制台会显示 a 并没有定义。

闭包

概念

  • 一个可以访问其他函数内部变量的函数 or
  • 一个嵌套在其他函数内部,并访问了外层函数变量的函数

通常情况下,函数内部的变量在外部无法访问,但是通过闭包就能解决这个问题。

function sayName () {
    var name = 'lucy'
    return function(){
        console.log(name)
    }
}
var getName = sayName()
getName() // lucy

能输出lucy,按理说name是sayName函数内部的变量,外层函数getName不能访问到才对,但是通过闭包能访问。

从直观上来看,闭包这个概念为 JavaScript 中访问函数内变量提供了途径和便利。这样做的好处很多,比如,可以利用闭包实现缓存等。

产生原因

  • 编译阶段形成了作用域链 or
  • 编译阶段,就生成了每个变量的查找作用域链
  • 且 每个函数都有其上级作用域的引用

当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。

需要注意,每一个子函数都会拷贝上级的作用域,形成一个作用域链:

var a = 1;
function fun1() {
  var a = 2
  function fun2() {
    var a = 3;
    console.log(a);//3
       function fun3() {
        var a = 4;
        console.log(a);//4
       }
    }           
}
  • fun1作用域链:fun1->window
  • fun2作用域链:fun2->fun1->window
  • fun3作用域链:fun3->fun2->fun1->window

作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。这就很形象地说明了什么是作用域链,即当前函数一般都会存在上层函数的作用域的引用,那么他们就形成了一条作用域链。

闭包本质 当前函数中存在指向上级作用域的引用

function fun1() {
  var a = 2
  function fun2() {
    console.log(a);  //2
  }
  return fun2;
}
var result = fun1();
result();

result就是fun2函数,而fun2中有上层函数作用域的引用,因此不管fun2被放在什么地方执行,都好保留其作用域链,因此在他的作用域链上能找到a=2.

并不是必须嵌套函数才形成闭包

var fun3;
function fun1() {
  var a = 2
  fun3 = function() {
    console.log(a);
  }
}
fun1();
fun3();

可以看到,其中实现的结果和前一段代码的效果其实是一样的,就是在给 fun3 函数赋值后,fun3 函数就拥有了 window、fun1 和 fun3 本身这几个作用域的访问权限;然后还是从下往上查找,直到找到 fun1 的作用域中存在 a 这个变量;因此输出的结果还是 2,最后产生了闭包,形式变了,本质没有改变。

闭包应用场景