重点:
- [x] 父级作用域无法访问其子级作用域中的变量和函数,子级作用域可以使用其父级作用域中的变量和函数;
- [x] 当执行一个函数的时候,如果我们需要查找某个变量的值,那么会去这个函数被定义时所在的作用域链中去查找,一旦找到需要的变量,就会停止向后追溯。
一、作用域
简单来说,作用域就是变量和函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。作用域又分为全局作用域、局部作用域以及ES6中的块级作用域。
-
全局作用域
- 全局作用域中的变量和函数在任何地方都可以被访问到;
- 未定义直接赋值的变量为自动变为全局变量;
var a1 = 1;
function say1() {
console.log(a1); // 1,函数内部也可以访问全局变量
a2 = 2;
console.log(a2); // 2,a2自动声明为拥有全局作用域,在严格模式下这种写法会报错
}
say1();
console.log(a2); // 2
// 结果为:1 2 2
-
局部作用域
- 在函数中声明的变量会变为函数的局部变量,只能在函数内部使用,因此函数作用域又称为局部作用域;
- 每个函数都有自己的作用域;
function say2(a) {
console.log(a); // 2
var b = 1;
console.log(b); // 1
}
say2(2);
console.log(a); // ReferenceError: a is not defined,函数的参数也是该函数的局部变量
console.log(b); // ReferenceError: b is not defined,局部变量不能被外部访问
-
块级作用域
-
ES5中没有块级作用域,这导致很多场景都不合理:
- 内层变量可能会覆盖外层变量
var a = 1; // 函数fn的作用是if代码块内部使用内层的变量a,外部使用外层的变量a。 // 但是函数fn执行后,输出结果为undefined,原因在于变量提升导致内层的变量a覆盖了外层的变量a。 function fn() { console.log(a); if (false) { var a = 2; } } fn(); // undefined
- 用来计数的循环变量泄漏为全局变量
// 变量i用于控制循环,但是循环结束后,变量i没有被销毁,而是变成了全局变量 for (var i = 0; i < 5; i++) { console.log(i); // 0 1 2 3 4 } console.log(i); // 5
-
ES6新增了块级作用域
- 使用let和const命令声明变量会形成块级作用域
function fn() { let a = 1; if (true) { let a = 2; } console.log(a); } fn(); // 1
for (let i = 0; i < 5; i++) { console.log(i); // 0 1 2 3 4 } console.log(i); // ReferenceError: i is not defined
{ const c = 'C'; { console.log(c); // C } } console.log(c); // ReferenceError: c is not defined
-
二、作用域链
作用域链,顾名思义,可以想象为是由许多个作用域串在一起形成的一个链。那么问题来了,这许多个作用域是从何而来呢?又是如何串在一起形成链的呢?
-
作用域链的形成
- 程序执行时,会先创建全局上下文,并将全局上下文的变量对象挂载在作用域链的后端端;
- 如果全局上下文中调用了一个函数,则创建该函数的执行上下文,并将该函数上下文的变量对象挂载在全局上下文变量对象之上;
- 如果该函数的内部又调用了一个函数,同样创建该函数的执行上下文,并将其变量对象挂载在外层函数上下文变量对象之上……
- 以此类推,形成了一个作用域链。
-
作用域链的特点
- 作用域链的前端是当前正在执行的代码所处环境的变量对象;
- 全局上下文的变量对象位于作用域链的后端
-
查找标识符
当在某个环境中为了读取或者写入而引入一个标识符时,必须通过搜索来确定该标识符实际代表什么。
搜索过程从作用域链的前端开始,向后逐级查询与给定名字匹配的标识符,如果找到了,则停止搜索,变量就绪;如果追溯到了全局环境的变量对象还是没有找到,则说明该变量尚未声明。
三、练习时刻
var a = 1;
function fn() {
console.log('1:' + a);
function bar() {
console.log('2:' + a)
}
var a = 2;
bar();
console.log('3:' + a);
}
fn();
// 输出结果为:
// 1:undefined => 使用var定义的变量会在当前作用域提升,且只提升声明,不提升赋值
// 2:2 => 函数bar定义在函数fn中,因此其作用域为bar->fn->global,bar中没有定义a,则去fn中查找,在fn中找到后返回
// 3:2 => 这行代码所在的作用域为fn->global,所以会在先在fn中查找a,找到后返回a的值
var a = 1;
function fn() {
console.log('1:' + a);
var a = 2;
bar();
console.log('2:' + a);
}
function bar() {
console.log('3:' + a);
}
fn();
// 输出结果为:
// 1:undefined => 使用var定义的变量会在当前作用域提升,且只提升声明,不提升赋值
// 3:1 => 函数bar是定义在全局作用域中的,所以作用域链是bar->global,bar中没有定义a,则去global中查找
// 2:2 => 这行代码所在的作用域为fn->global,所以会在先在fn中查找a,找到后返回a的值
var a = 1;
function fn() {
console.log('1:' + a);
a = 2;
}
a = 3;
function bar() {
console.log('2:' + a);
}
fn();
bar();
// 输出结果为:
// 1:3 => ① fn中的a=2是给变量a赋值,而不是声明变量a,因此执行fn时,查找到的变量a是全局作用域中的a;② JS中的代码是顺序执行的,因此执行fn之前已经执行了a=3,此时全局作用域中的a的值为3
// 2:2 => ① fn中的a=2修改了全局作用域中a的值,因此执行bar时,a的值为2
var a = 1;
function fn(f) {
var a = 2;
return f;
}
function bar() {
console.log(a)
}
var f1 = fn(bar);
f1();
// 输出结果为:
// 1 => 函数中变量的值由函数定义时的所在的作用域链决定
var a = 1;
function fn(f) {
var a = 2;
return function () {
console.log(a)
}
}
var f1 = fn();
f1();
// 输出结果为:
// 2 => 函数中变量的值由函数定义时的所在的作用域链决定