作用域与闭包
先编译后执行
JavaScript虽然是动态语言,但是在执行的时候,也仍然是按照先编译后执行的顺序来解析代码的。codeExample_1
就能证明这种结果。
1 | // codeExample_1 |
用node去执行段代码,会发现,不会打印hello
再报错,而是直接报错。因为编译器在遇到错误代码的时候就报错了,从而导致整个程序都无法执行。
词法作用域
在编译时,变量的作用域就已经确定,这样的作用域称为词法作用域。而从一个作用域拿到另一个作用域中的变量的方法,则称为闭包。在代码执行的时候,每当遇到一个函数或者包含有let、const声明的变量的{...}
就会生成一个块级作用域。所以,作用域最后会形成一个作用域里面嵌套多个子作用域。子作用域可以使用父作用域中的变量,但是父作用域不能使用子作用域中的变量。
在作用域中查找变量有以下几个规则:
look up
当一个作用域中使用了一个变量的时候,会优先在当前作用域去查找这个变量的值,如果没有找到这个变量,就会一直往父作用域中查找,知道找到为止或者到顶级作用域为止。当然,这个规则其实是一种抽象,其实当前作用域能否拿到这个作用域中的变量的值在编译时期就已经知道了,不会在运行的时候再去look up。
Shadowing
当一个作用域声明了一个和父作用域中相同的变量名时候,由于look up规则,会优先使用当前作用域中的变量声明的值,从而造成了
shadow
的效果。
变量提升
函数声明和变量声明在编译器编译的时候的行为是不一样的。编译器在遇到函数声明时,会连带着变量声明和变量赋值,所以函数声明可以后声明先使用。
1 | const a = getNumber(1, 2); |
但是变量声明,会将变量的声明语句和赋值语句分开,声明语句会先执行,赋值语句后执行,而var
声明的变量会默认给一个初始值undefined
,let
和const
声明的语句则不会。所以var
声明的变量,可以先使用声明,但是在声明语句之前的值是undefined
,而let
和const
在声明之前使用则会报错,因为他们没有给默认的初始值。
1 | console.log(a); // undefined |
let、var、const的区别
let和const会生成新的作用域,var不会
var、let声明的变量可以改变,const声明的变量必须一开始就赋初始值,且不会改变
var不适合用于for…loop中,因为没有生成新的作用域,会导致所有迭代变最后都是一个值
const不适用于有变量for(;;i++)的循环
1
2
3
4
5
6
7
8
9
10
11for (const i = 0; i < 5; i++) { .... }
// 相当于
{
const $i = 0;
for (; $i < 5; $i++) {
const i = $i;
...
$i++; // const不能重新赋值
}
}
闭包
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
比如:
1 | function foo() { |
上面这段代码很好的展示了什么是闭包。按照作用域的规则,正常情况下,我们是拿不到foo作用域中的a
的。当foo()
执行完之后,垃圾回收机制会将foo
作用域回收,那么其内部声明的变量以及函数都会被回收,但是由于bar被return
了,导致仍然有baz
指向了这段函数,所以bar和它的词法作用域才没有被垃圾回收,因此baz
才能正常执行,也能拿到a
。
1 | function foo () { |
按照作用域的规则,bar是无法拿到foo
中的变量a的。但是,当baz作为函数传递给bar时,由于baz能拿到foo中的a。此时bar也可取到a了,这就是闭包。闭包在JS中随处可见,但是闭包也有一个问题就是,无法及时释放内存,如果存在大量闭包,会导致大量内存无法释放,占用资源,甚至导致内存泄漏。所以,在使用闭包结束之后,应该将闭包清理掉,如:
1 | function foo() { |
原文作者: Billy & Barney
原文链接: https://liangbilin.github.io/2020/01/15/Barney--作用域与闭包/
版权声明: 转载请注明出处(必须保留作者署名及链接)