11-作用域
晴空闲云 568 1

本节目标

  1. 掌握js中作用域链(scope chain)。
  2. 掌握js中三类作用域:全局作用域、局部作用域、块作用域。
  3. 掌握js中匿名函数自执行的应用。

内容摘要

本篇介绍了作用域的概念,js中的作用域链(scope chain),从作用域链衍生出全局作用域、函数作用域的概念,最后讲解了块作用域的内容。

阅读时间30~40分钟。

作用域(scope)介绍

作用域在一门编程语言中是逃不开的话题,作用域表明了变量声明了之后,在哪里可以调用,哪里不能调用,以及什么时候销毁等等。

在js中要搞清楚作用域(scopre),就需要先搞清楚作用域链(scope chain)这样一个东西,知道这个了,那就容易了。

作用域链(scope chain)

前面我们知道函数是一个类型,也是一个对象,既然是对象,就有属性和方法。在函数内有一个属性 [[Scope] 存储了可以访问的数据对象

在函数内访问某个变量的时候,在 [[Scope]] 依次寻找,直到找到为止。

从函数内外层结构来说,就是依次从内向外查找,最外层就是全局数据对象。

这种方式就叫做链(chain)。

示例1,声明一个变量i,然后声明一个函数f。

let i = 1;
function f() {
}
console.log([f]); // 打印函数f

其中:

这边有一个技巧,如果直接打印f,在chrome浏览器会打印出函数的定义,如果包在数组内就可以看到函数对象了。

运行查看:

11-作用域

我们可以看到 [[Scope]] 这个属性内部的结构了,其中:

下标为0 有一个i: 1就是声明的变量 let i = 1;
下标为1 就是全局的数据对象了。

思考:

根据上面的描述,请问在函数f内可以访问到i这个变量吗?

解答:

可以的,因为 i 这个变量在函数的属性 [[Scope]] 内。

let i = 1;
function f() {
    console.log(i); // 1
}
f();

示例2,上例是2层的,我们再来一层看看,我们在函数f内再声明一个函数b,然后在控制台查看b。

let i = 1;
function f() {
    let j = 2;
    function b() {
        let k = 3;
        console.log(i, j, k); // 1 2 3
    }
    console.log([b]); // 打印b这个函数
}
f();

运行查看结果:

11-作用域

作用域分类

根据作用域链的特性,结合其他语言的习惯,我们可以把js中的作用域分为如下三种:

1. 全局作用域。
2. 函数作用域。

但是因为上面两个作用域可能会造成变量干扰,所以es6推出了另外一个作用域:

3. 块作用域。

下面依次对每个作用域进行介绍。

全局作用域

从作用域链的例子来看,变量i 在f函数和b函数内都可以访问到,这个i变量是声明在js最外层的,可以在全局内使用,这个就是全局作用域了

例如:

let a = 10;
console.log(a); // 10
function f(){
    console.log(a); // 10
    a = 11;
}
f();
console.log(a); // 11

其中:

变量 a 定义在最js最外围,那么 a 的作用域就是在全局。
在函数外可以访问到 变量a。
函数f 内也可以访问到 变量a。

函数作用域

从作用域链的例子来看,变量j 声明在函数f内,只能在f函数和b函数内访问到,在最外层就访问不到了。

这种定义在函数内的变量,作用域在函数内部,这个可以称作是函数作用域,也可以称作局部作用域。

例如:

function f(){
    let a = 10;
    console.log(a); // 函数f内访问变量a,打印10
    function b() {
        console.log(a); // 函数b内访问变量a,打印10
        a = 11; // 函数b内修改变量a
        console.log(a); // 函数b内访问变量a,打印11
    }
    b();
}
f();
console.log(a); // Uncaught ReferenceError: a is not defined

其中:

变量 a 定义在函数内部,那么 a 的作用域就是在函数内。
f函数 内可以访问到。
b函数 内也可以访问到。
最外层访问不到,在最外层打印 变量a,就会报 变量a 未定义的错误。

块作用域

上面的两类作用域在某些业务场景下会导致变量被干扰,es6就推出了块级作用域来解决这个问题。下面我们分成3个步骤来介绍这个问题:

1. 为什么要有块作用域?
2. 块作用域是什么?
3. 块作用域应用。

1)为什么要有块作用域?

先看一个变量干扰的例子。

var i = 10; //全局变量
for (var i = 0; i < 5; i++) {}
console.log(i); // 5

思考:

为什么上例最后输出i的时候,是5呢?

解答:

因为在 for 循环语句中,又声明了一个 变量i,就把 全局变量i 给替换掉了,最后退出循环的时候 i = 5,所以最后就打印 5 了。

这个例子比较简单,我们容易看的出来,但是在实务中往往都是上千行的代码,这个想想就有点可怕。

所以es6就推出了块级作用域,来解决这个问题,为了向下兼容,不能修改 var,就推出了 let 这个关键词了,es6中 let 声明的变量就是块作用域。

2)块作用域是什么?

先看下块是什么:

{
    // 这里就是块内的代码    
}

这个看着有点抽象,其实就是语法结构里的{}了,比如:if判断、for循环、while循环、try catch内的块。

for(...省略了) {
    // 这里就是一个块
}

while(...省略了) {
    // 这里就是一个块
}

try {
    // 这里就是一个块
} catch(error) {
    // 这里就是一个块
}

再看下var、let声明的变量在块内块外的访问情况。

{
    var a = 1;
    console.log(a); // 1
}
console.log(a); // 1
{
    let a = 1;
    console.log(a); // 1
}
console.log(a); // Uncaught ReferenceError: a is not defined

从运行结果可以看出 let 声明的变量只能在块内访问到,var可以在块外也可以访问到。

3)块作用域应用

改进上面变量干扰的例子,实现不互相干扰。

通过对块作用域的理解,其实思路很简单,把 for 循环内的 var 改成 let 声明就可以了:

var i = 10; //全局变量
for (let i = 0; i < 5; i++) {}
console.log(i); // 5

因为使用 let 声明的 变量i,作用域就在循环的块内,不会影响到外面了。

匿名函数自执行

上面我们用 let 解决了变量干扰的问题,那么在es6之前广泛是用匿名函数自执行解决的。

回到变量干扰的例子:

var i = 10; //全局变量
for (var i = 0; i < 5; i++) {
    console.log(i); // 依次打印0到4
}
console.log(i); // 5

改成匿名函数自执行的方式:

var i = 10; //全局变量
(function() {
    for (var i = 0; i < 5; i++) {
        console.log(i); // 依次打印0到4
    }
})()
console.log(i); // 10

匿名函数自执行:就是将代码封装到匿名函数里,然后直接调用执行即可。

匿名函数自执行,会将变量的作用域和外部进行隔离,常用的框架jquery等都是这么处理的。

本节总结

  1. js中函数的 [[Scopes]] 属性存储了可以访问的数据对象。
  2. 在函数内访问变量的时候,会在 [[Scopes]] 内依次寻找,这种形式称为作用域链(scope chain)。
  3. 根据作用域链的原理,可以将作用域分为:全局作用域、函数作用域。
  4. 为了避免变量干扰,es6推出了块作用域,需要用配合 let 使用。

练习题

  1. let和var有什么区别?说出你的理解。
  2. 阅读如下两个代码,分析输出,并说明为什么。

第一段:

for (var i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(i);
    }, 1000);
}

第二段:

for (let i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(i);
    }, 1000);
}
  1. 阅读如下代码,分析输出,并说明为什么。
if (true) {
    let i = 0;
}
console.log(i); // 输出?
  1. 阅读如下代码,分析输出,并说明为什么。
let i = 6;

function f() {
    let j = 10;
    b();
    c();

    function b() {
        let k = 88;
        console.log(j); // 输出?
        j = 11;
    }

    function c() {
        console.log(typeof k); // 输出?
        i = 7;
        console.log(j); // 输出?
    }
}
f();
console.log(i); // 输出?
console.log(j); // 输出?
评论区

索引目录