JavaScript之作用域

1/1/2018 JavaScript

作用域是JavaScript最重要的概念之一,想要进一步学习巩固JavaScript,就必须得理解JavaScript作用域

# 作用域

任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。

# 全局作用域(Global Scope)

在代码中任何地方都能访问到的对象拥有全局变量,一般来说以下三种情形拥有全局作用域

1.最外层函数和在最外层函数外面定义的变量拥有全局作用域,例如:

var global = "global";     // 显式声明一个全局变量
function checkscope() {
    var local = "local";   // 显式声明一个局部变量
    return global;         // 返回全局变量的值
}
console.log(global);       // "global"
console.log(checkscope()); // "global"
console.log(local);        // error: local is not defined.
1
2
3
4
5
6
7
8

上面代码中,global是全局变量,不管是在checkscope()函数内部还是外部,都能访问到全局变量global

2.所有未定义直接赋值的变量自动申明为拥有全局作用域,例如:

function checkscope() {
    var local = "local"; // 显式声明一个局部变量
    global = "global";   // 隐式声明一个全局变量(不好的写法)
}
checkscope();
console.log(global);     // "global"
console.log(local);      // error: local is not defined.
1
2
3
4
5
6
7

上面代码中,变量global未用var关键字定义就直接赋值,所以隐式的创建了全局变量global,但这种写法容易造成误解,应尽量避免这种写法。

3.所有 window 对象的属性拥有全局作用域

一般情况下,window对象的内置属性都拥有全局作用域,例如window.namewindow.locationwindow.top等等。

# 局部作用域(Local Scope)

和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到。最常见的是在函数体内定义的变量,只能在函数体内使用。例如:

function checkscope() {
    var local = "local";   // 显式声明一个局部变量
    return local;         // 返回全局变量的值
}
console.log(checkscope()); // "local"
console.log(local);        // error: local is not defined.
1
2
3
4
5
6

上面代码中,在函数体内定义了变量 local,在函数体内是可以访问了,在函数外访问就报错了。

# 全局和局部作用域的关系

在函数体内,局部变量的优先级高于同名的全局变量。如果在函数内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就被局部变量所遮盖。

var scope = "global";      // 声明一个全局变量
function checkscope() {
    var scope = "local";   // 声明一个同名的局部变量
    return scope;          // 返回局部变量的值,而不是全局变量的值
}
console.log(checkscope()); // "local"
1
2
3
4
5
6

尽管在全局作用域编写代码时可以不写var语句,但声明局部变量时则必须使用var语句。思考一下如果不这样做会怎样:

scope = "global";           // 声明一个全局变量,甚至不用 var 来声明
function checkscope2() {
    scope = "local";        // 完犊子了!我们刚修改了全局变量
    myscope = "local";      // 这里显式地声明了一个新的全局变量
    return [scope, myscope];// 返回两个值
}
console.log(checkscope2()); // ["local", "local"],产生了副作用
console.log(scope);         // "local",全局变量修改了
console.log(myscope);       // "local",全局命名空间搞乱了
1
2
3
4
5
6
7
8
9

函数定义是可以嵌套的。由于每个函数都有它自己的作用域,因此会出现几个局部作用域嵌套的情况,例如:

var scope = "global scope";         // 全局变量
function checkscope() {
    var scope = "local scope";      //局部变量
    function nested() {
        var scope = "nested scope"; // 嵌套作用域内的局部变量
        return scope;               // 返回当前作用域内的值
    }
    return nested();
}
console.log(checkscope());          // "nested scope"
1
2
3
4
5
6
7
8
9
10

# 函数作用域和声明提前

在一些类似 C 语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称为块级作用域(block scope),而 JavaScript 中没有块级作用域。JavaScript 取而代之地使用了函数作用域(function scope),变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

在如下所示的代码中,在不同位置定义了变量 i、j 和 k,它们都在同一个作用域内,这三个变量在函数体内均是有定义的。

function test(o) {
    var i = 0; // i在整个函数体内均是有定义的
    if (typeof o == "object") {
        var j = 0; // j在函数体内是有定义的,不仅仅是在这个代码段内
        for (var k = 0; k < 10; k++) { // k在函数体内是有定义的,不仅仅是在循环内
            console.log(k); // 输出数字0~9
        }
        console.log(k); // k已经定义了,输出10
    }
    console.log(j); // j已经定义了,但可能没有初始化
}
1
2
3
4
5
6
7
8
9
10
11

JavaScript 的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。有意思的是,这意味着变量在声明之前甚至已经可用。JavaScript 的这个特性被非正式地称为声明提前(hoisting),即 JavaScript 函数里声明的所有变量(但不涉及赋值)都被「提前」至函数体的顶部,看一下如下代码:

var scope = "global";
function f() {
    console.log(scope);  // 输出"undefined",而不是"global"
    var scope = "local"; // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
    console.log(scope);  // 输出"local"
}
1
2
3
4
5
6

你可能会误以为函数中的第一行会输出 "global",因为代码还没有执行到 var 语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量遮盖了同名全局变量。尽管如此,只有在程序执行到 var 语句的时候,局部变量才会被真正赋值。因此,上述过程等价于:将函数内的变量声明“提前”至函数体顶部,同时变量初始化留在原来的位置

function f() {
    var scope;          // 在函数顶部声明了局部变量
    console.log(scope); // 变量存在,但其值是"undefined"
    scope = "local";    // 这里将其初始化并赋值
    console.log(scope); // 这里它具有了我们所期望的值
}
1
2
3
4
5
6

在具有块级作用域的编程语言中,在狭小的作用域里让变量声明和使用变量的代码尽可能靠近彼此,通常来讲,这是一个非常不错的编程习惯。由于 JavaScript 没有块级作用域,因此一些程序员特意将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处。这种做法使得他们的源代码非常清晰地反映了真实的变量作用域。