以小白的视角浅谈一下作用域和闭包
作用域是什么?
简单来说作用域就是在代码运行时某些特定部分中变量,函数和对象的可访问性。也就是说,作用域决定了哪些变量在哪些位置可以被访问到。是一套访问变量的规则
在JavaScript中,作用域有两类
- 全局作用域:全局作用域是最外层的作用域,一直存在。
- 函数作用域:函数作用域只有被创建的才会存在,是被包含在父级作用域之间。
变量的创建
在一段源码的执行之前会经历三个步骤,统称为编译
- 词法分析
- 语法分析
- 代码生成
编译过程
当我们执行 var a = 1;这段代码的时候,编译器会先查询是否已经声明变量(在当前作用域中), 如果有就为这个变量进行赋值,反之,就会执行 var a 在当前作用域下变量声明a。
然后,引擎开始执行代码,进行赋值操作,首先在作用域中查询是否有变量a,如果有就会进行赋值,如果没有就会继续查询(向父级作用域)
当在父级作用域内查询到a变量,就会进行赋值操作。否则会进行报错。
词法作用域
在上面说过,作用域可以看成一套规则,而作用域有两种主要的工作模型,词法作用域就是作用域的一种工作模型这也是被大多数编程语言所采用的模型, 另一种叫做动态作用域
编译的第一个阶段词法分析,就是对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。
词法作用域就是定义在词法阶段的作用域,词法作用域是由你在写代码时将变量和块作用域写在哪里所决定的。
看下面这段代码:
function foo(a) {
var b = a * 2
function bar(c) {
console.log(a, b, c)
}
bar(b * 3)
}
foo(2) // 2, 4, 12
上述代码中有三个逐级嵌套的作用域:
- 全局作用域: 标识符: foo
- foo的作用域: 标识符: a, bar, b
- bar的作用域: 标识符: c
作用域由其对应的代码写在哪里决定的,它们是逐级包含的。 没有任何函数可以部分地同时出现在两个父级函数中
无论函数在哪里被调用,也无论它是如何被调用,它的词法作用域都只由函数被声明时所处的位置所决定
块作用域
在ES6之前JavaScript并没有块级作用域的概念,我们来看一段代码:
for( var i = 0; i < 3; i++) {
console.log(i) // 0 1 2
}
console.log(window.i) // 3
我们在for循环里面定义了变量i,我们的本意是只想在for循环内部的上下文中使用i,但是事实是i会被绑定到外部作用域。
ES6改变了现状,ES6引入新的let,const关键字,提供新的变量声明方式。可以将变量绑定到所在的任意作用域中。
for( let j = 0; j < 3; j++) {
console.log(j) // 0 1 2
}
console.log(window.j) // undefined
作用域链
上面提到了向父级作用域查询,什么是父级作用域呢?
其实,在实际情况中,作用域往往是一个嵌套着一个的,当一个块或者函数嵌套在另一个块或函数里面时,就发生了作用域的嵌套。 因此当引擎在当前作用域查找不到时,就会向外层作用域进行查找,直到找到这个变量或到达最外层作用域。
function add(a){
console.log(a + b)
}
var b = 1
add(1) // 2
上述代码中,参数b是存在于全局作用域下,也就是add函数作用域的父级作用域,add函数就是通过作用域链来获取到变量。
垃圾回收机制
JavaScript具有垃圾收集器,垃圾收集器会按照固定的时间间隔周期性的执行。 当变量不再使用就会被垃圾回收机制所回收。
最常见的垃圾回收方式有两种:
- 标记清除
- 引用计算
标记清除
原理:是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
引用计数
原理:跟踪记录每个值被引用的次数。
闭包
闭包是什么
外部函数返回出来的内部函数,当外部函数被调用时,即使内部函数已经执行完成,但是内部函数对外部函数的变量引用依然保存在内存中,我们把这些变量的集合称为闭包。 只有被内部函数应用的变量,才会被保存在执行上下文当中。
见如下代码:
function foo() {
var n = 0
function bar() {
console.log(n)
n++
}
return bar
}
let bb = foo()
bb() // 0
bb() // 1
上述代码中,函数bar的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。
实际上bb函数的调用,就是在调用bar函数,它在自己定义的词法作用域之外的地方执行。
在foo执行完之后,垃圾回收机制通常会将foo的作用域给销毁,而变量n作为这个作用域内的一员,按理来说也应该会被销毁,但实际上n还是能被访问,这就是闭包的效果。
在这段代码中,bar()声明在foo()的内部,因此bar()可以访问foo()的作用域,并且使用了foo()函数的n,因此这个内部作用域就不会被销毁。
bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
总结
作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。