1 作用域是什么
1.1 定义
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是当前执行代码对变量的访问权限。
1.2 类型
作用域有两种:词法作用域(也叫静态作用域)、动态作用域。
词法作用域
特点:函数的作用域在函数定义时决定的
let value = 1;
function foo() {
console.log(value);
}
function bar() {
let value = 2;
foo();
}
bar();
打印的结果是1,因为JS使用的是词法作用域。
动态作用域
特点:动态作用域则是在函数调用的时决定的
// scope.bash
value=1
function foo() {
echo $value;
}
function bar() {
local value=2;
foo;
}
bar
打印的结果是2,因为bash使用的是动态作用域
2 语言实现
从上面的描述可知作用域是一种规范,就像CommonJS规范一样。使用这种规范的语言还需要自己实现。 那JS是如何实现的?
之前学习了JS执行上下文,里边有全局执行上下文、函数执行上下文,对应全局作用域、函数作用域。ES6增加了块级作用域,使用let const声明就行。
defaultName是块级作用域的,外面不可以访问;如果用var,能访问(变量环境)
ES6是如何实现块级作用域的?
我们需要从执行上下文来看。
function greet(nick) {
let str = 'hello ';
if(nick === '') {
let defaultName = 'unknown';
str += defaultName;
}else{
str += nick;
}
console.log(str);
}
greet('');
执行过程中的函数上下文的词法环境变化:
注
- 里边忽略了变量从
uninitialized到undefined的变化,即初始化过程 - 【JS执行上下文】文章中,有用到伪代码来表示结构,此时的词法环境是栈,但不清楚整体是什么结构,所以就用图。
我们可以从VS Code debug模式中观察到块级作用域的变量的一些变化过程:
if执行完毕后就销毁了
3 作用域链
function foo() {
console.log(message);
}
function wrapper() {
let message = 'wrapper函数声明的变量';
foo();
}
let message = '全局变量';
wrapper();
结果:全局变量
为什么foo函数中没有message还能打印出来?
因为JS除了能访问本作用域内的变量,还能访问父级作用域的变量。
为什么foo函数中读取message的结果是全局的而不是wrapper函数中的值?
因为JS采用的是词法作用域,函数的作用域是在声明而不是调用时确定的。
对应到上下文,词法环境中的outer指向的声明函数时的作用域对应的上下文。
4 闭包
JS函数对象的内部状态不仅要包含函数代码,还要包括函数定义时所在作用域的引用。这种函数对象与作用域(及一组变量的绑定)组合起来解析函数变量的机制,在计算机可惜文献中被称为闭包(closure)。
严格来讲,所有JS函数函数都是闭包。但由于多数函数调用与函数定义都在同一个作用域内,所以闭包的存在无关紧要。闭包真正指的关注的时候,是定义函数与调用函数的作用域不同的时候。最常见的情形就是一个函数返回了在它内部定义的嵌套函数
— JavaScript权威指南 第七版
function wrapper() {
let message = 'wrapper函数声明的变量';
function foo() {
console.log(message);
}
return foo;
}
let message = '全局变量';
let foo = wrapper();
foo();
结果:wrapper函数声明的变量 (这个也可以用上面的作用域链解释)