作用域
闭包和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,最后产生了闭包,形式变了,本质没有改变。