变量的作用域及生存期

2018-02-26大约14分钟

变量的作用域,指的是变量的有效范围——哪部分代码可以访问该变量。

全局作用域及函数内部作用域

在网页上,任何函数之外声明的变量,都是全局的变量,该页面的所有脚本,不管是函数内的,还是函数外的,都是可以访问这个变量的。

const apple = "Apple";   // 全局变量

function printApple() {
    console.log(apple);       // 可以直接访问全局变量
}

printApple();

而函数内声明的变量,则只有函数内可以使用:

function printApple() {
    const apple = "Apple";
    console.log(apple);
}

console.log(apple); // 不可以访问函数内的变量apple,程序报错“apple is not defined”

编码风格探讨——函数内外同名变量

如果这样写代码,调用printApple的结果是什么呢?

const apple = "Apple";   // 全局变量

function printApple() {
    const apple = "Orange";   //函数内部变量
    console.log(apple);
}

printApple();

结果是程序打印出了"Orange",也就是说,函数执行会优先使用函数内的这个局部变量。虽然程序不会报错,但这种命名方式是不是很糟糕呢?

代码块作用域

很多其他的编程语言,一个代码块创建了一个作用域(scope),变量只在这个作用域内有效。

在JavaScript中,一个代码块是括号{}之内的代码,但是代码块却没有创建一个新的作用域!在JavaScript里,只有函数作用域,在函数内定义的变量,在函数内部任何地方都是可用的。

结果呢?我们都知道下面这段代码是会报错的:变量a未定义

function doSomething() {
    a = a + 1;
}

doSomething();

但为什么这样的代码就好使呢?

function doSomething() {
    a = a + 1;
    var a = 1;
    console.log(a);
}

doSomething();

原因就是,JavaScript里只有函数作用域,好使的代码里在第三行定义了变量a,虽然是在第二行使用之后定义的!还没晕的话,再看一个例子:

function doSomething() {
    console.log(a);  // 这里可以访问变量a
    for (var a = 1; a < 5; a++) {
        console.log(a); // 这里也可以访问变量a
    }
    console.log(`a=${a}`);  // 这里还可以访问变量a
}

doSomething();

如果你有其他编程语言经验的话,会不会感觉JavaScript的变量定义很酸爽?

var关键字的这个行为,就是JavaScript语言本身最开始的时候不良设计造成的。

所以,为了纠正上面的不良设计,ES6里面新增的let关键字,增加了代码块作用域,用let的话,上面怪异的代码就变得正常了:

function doSomething() {
    // a = a + 1;  // 这里也不可以访问变量a
    for (let a = 1; a < 5; a++) {
        console.log(a); // 这里也可以访问变量a,变量a只在for语句块内可见
    }
    // console.log(`a=${a}`);  // 这里不可以访问变量a,注释掉
}

doSomething();
只要代码运行环境允许,能用let,就不要用var来定义变量

变量生存期

全局变量由于需要满足任何代码在任何时候都能访问,因此生存期和整个程序的生存期是一样的。函数内变量,则会随着函数调用结束而被全部释放,下次该函数被调用时会重新创建内部变量。

编程新手喜欢把变量都定义成全局变量,这样使用起来最方便。但是因为任何代码都可以在任何时候修改全局变量,如果由于某个编程错误对全局变量做了错误修改的话,那么这样的代码将会很难调试。

最好尽量少用全局变量,除非不得不用——比如需要在函数之间共享数据时。

this关键字

在全局执行(即任何函数的外部)的JavaScript的代码中,this都指代全局对象。在浏览器上,this指的是window对象;在Node.js中,this指的是global对象。

console.log(window === this);


在函数内部,非严格模式下,指的是全局对象;在严格模式下,如果当前函数没有执行上下文(execution context),那么this的值是undefined:

function doSomething() {
    console.log(this);  // 非严格模式
}

function doSomethingInStrictMode() {
    "use strict";     //  严格模式
    console.log(this);
}

doSomething();
doSomethingInStrictMode();

什么时候函数有执行的上下文?常见情况下,就是我们调用某个对象的方法的时候,这些方法就有执行上下文了,即当前对象:

const toy = {
    name: "Teddy",
    getThis: function () {
        return this;   // this即对象toy
    },
    sayName: function () {
        console.log(`My name is ${this.name}`);  // 可以通过this访问对象的属性或方法
    }
};

console.log(toy.getThis() === toy);
toy.sayName();

但是有时候,我们写代码,需要在JavaScript的函数里动态创建并调用另一个函数,或者在一个异步函数里把当前对象的函数作为callback,这时候,this就不一样了:

const toy = {
    name: "Teddy",
    sayName: function () {
        const printName = function () {
            "use strict";   //  这里使用严格模式
            console.log(`My name is ${this.name}`);
        }
        printName();
    }
};

toy.sayName();

这时,this竟然是undefined,也就是说printName这个内部函数没有执行上下文,怎么办?如果你是写过一定量JavaScript代码的人,你一定会发现这个错误太熟悉了,这简直就是JavaScript的一个大坑啊。。。

两个办法可以选择:

  1. Function的bind方法
const toy = {
    name: "Teddy",
    sayName: function () {
        const printName = function () {
            "use strict";
            console.log(`My name is ${this.name}`);
        }.bind(this); // 创建了一个匿名函数,然后调用bind函数再创建一个新的和匿名函数具有相同函数体和作用域的函数,但是这个新函数的执行上下文是this,即toy。
        printName();
    }
};

toy.sayName();
  1. 用箭头函数
const toy = {
    name: "Teddy",
    sayName: function () {
        const printName = () => { // 箭头函数自动绑定当前this作为执行上下文
            "use strict";
            console.log(`My name is ${this.name}`);
        };
        printName();
    }
};

toy.sayName();

一般情况下,建议使用箭头函数,这样就不会当发现代码代码出错之后,回过头来才发现原来函数忘了和this做绑定。