JavaScript变量:变量声明

发布于 大漠

这几天都在折腾JavaScript中有关于变量的基础知识,不折腾不知道,一折腾才知道还有很多未能明白。我把变量相关的知识分为:变量的声明变量命名原则变量值的数据类型变量作用域变量提升几个部分。为了能更好的理清楚相关的知识点,绘制了一张思维导图。感兴趣的可以点击这里查阅。(图太大,可能有不对之处,还希望大婶们指正(^_^))。

在这篇文章主要来理清楚变量声明相关的知识点。

变量声明的方法

在ES6之前,声明变量的方法有使用关键词var和在function中声明,而其中var又是最常见的声明变量方法。但在ES6中,新增了letconstclassimport几种声明变量的方法。在这篇文章,主要来看看常见的关键词varletconst声明变量的方法,以及它们之间使用的细节。

使用var声明变量

在未接触ES6之前,知道var可以声明一个变量,声明变量方法很简单,在关键词var后面紧跟一个变量名(也称之为变量的标识符),比如:

var myVar;

上面定义了一个名为myVar的变量,并且未给这个变量进行初始化(也就是没有赋值给它),这个时候其默认值为undefined。当然,我们在声明变量时,可以给其赋值:

var myVar = "w3cplus";

看上去是不是很简单。简单是简单,但使用关键词var声明变量时,也有很多有意思的东东。下面我们一条一条的看。

使用var可以重复声明变量,而且是合法的,并且是无害

var myVar;
var myVar = "w3cplus";

第一次声明变量myVar并未赋值,但紧接着进行了第二次声明,并且这次赋了一个字符串值"w3cplus"。此时,变量myVar不是undefined而是"w3cplus"

使用var声明的变量,可以被赋值多次

var myVar;
myVar = "w3cplus";
myVar = ["w3cplus",2];

这个时候变量的值既不是undefined也不是字符串"w3cplus",而是一个数组["w3cplus", 2]

使用var声明变量时,在代码块内可以修改在代码块之外声明的变量

var myVar = "w3cplus";

function fnTest(str) {
    return myVar = "W3cplus" + str;
}
fnTest("blog");
console.log(myVar); // => "W3cplusblog"

使用var声明变量的变量在全局范围内都有效

var myVar = [];
for (var i = 0; i < 10; i++) {
    myVar[i] = function () {
        console.log(i);
    }
}
myVar[6](); // => 10

上面的代码中,变量ivar声明的,由于其在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧的值,导致最后输出的是最后一轮的i的值。所以myVar[6]最后得到的值是10

使用var声明变量存在生命提升(即在声明这个变量之前使用这个变量也是有效的)。如:

myVar = "w3cplus";
var myVar;

上面声明的变量可以理解为:

var myVar;
myVar = "w3cplus";

在使用var声明变量时经常会被提到三条规则,然后告诉你什么时候可以打破这些规则。

不要把var语句放在代码块中

以常规的看法来说,下面的写法是不太好的(非常规则的一种写法):

function foo (x, y) {
    if (x > y) {
        var tmp = x;
        x = y;
        y = tmp;
    }
    ...
}

看到这样的代码,也许有人会认为变量tmp只存在于if语句块中,而事实上,变量tmp的声明会被提升,即变量tmp的声明操作实际上是在函数的最开始处,而赋值操作还留在原来语句存在的地方。因此,上面的代码修改成下面的样子,更适合常规写法:

function foo (x, y) {
    var tmp;
    if (x > y) {
        tmp = x;
        x = y;
        y = tmp;
    }
    ...
}

不要把var语句放在循环体中

常规看法告诉我们,我们不应该像下面这样书写我们的JavaScript代码:

for (var i = 0; i < 10000; i++) {
    var foo = 1;
}

说下面的写法会更快一点:

var foo;
for (var i = 0; i < 10000; i++) {
    foo = 1;
}

每个函数都使用单一的var语句

常规写法告诉我们:应该把所有的变量声明都放在函数体开始的地方,而且仅能使用一条var语句来声明。比如:

var foo = 1,
    bar = 2,
    baz = 3;

这样的写法可以防止变量生命提升带来的诡异问题和避免使用重复的var关键字。不过这样写有时候自己的疏忽会产生负面的效应,比如说漏掉一个逗号,可能意外声明一些全局变量。比如:

var foo = 1
    bar = 2,
    baz = 3;

有关于在使用var声明变量时经常会被提到三条规则更详细的介绍,可以阅读@Dr. Axel Rauschmayer的《Variable declarations: three rules you can break》一文。

另外,使用var声明变量时,有时候在使用的过程中会产生一些奇怪的现象。比如下面的这个例子:

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

其打印出来的结果如下:

10
10
10
10
10
10
10
10
10
10

事实上,期望JavaScript输出的应该是这样的:

0
1
2
3
4
5
6
7
8
9

对于熟悉JavaScript的程序员,对于代码输出的都是10这种行为并不会感到陌生,但对于我这们的生手,还是不了解其中原因(后面有一天我也会懂的)。

setTimeout会在若干毫秒的延时后执行一个函数,并且是在for循环结束后。for循环结束后i的值为10,这就造成了,我们看到的效果,它打印出来全部是10!

以前碰到这样的现象,一个通常的解决方法是使用立即执行的函数表达式(IIFE)来获取for循环中每次迭代时i的值:

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

IIFE是Immediately-Invoked Function Expression的简写,称为立即执行函数表达式。有关于其详细的介绍可以点击这里这里进行了解。

其实在ES6中,我们只要将var声明的i变量,换成关键词let来声明,就可以不依赖IIFE就能解决:

for (let i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(i);
    }, 100 * i);
}

接下来,我们一起来看看let声明变量的方法和使用细节。

使用let声明变量

通过上面的内容介绍,你已经知道使用关键词var声明变量会存在一些问题,这恰好说明ES6中为什么要提供let关键词来声明变量。

let声明变量的用法与var基本相同,不同之处在于let声明的变量只在其代码块中生效,也就是传说中的块级作用域。接下来看看let声明变量需要注意的一些细节吧。

前面也说了,使用let声明变量时,变量只在let命令所在的代码块内有效。如:

function foo () {
    if (true) {
        let a = 5;
        console.log(a); //5 作用域名为if{}内
    }
    console.log(a); //Uncaught ReferenceError: a is not defined
}

在JavaScript中,{}iffor这些都称之为块级作用域。这也就是说,let可以都可以在这些块级作用域中声明变量,比如上面的示例,就是在if语句块中声明的,所以变量a也只是在if块中声效,而在if语句块外就失效了。

除此之外,使用let声明的变量是定义在块内,但也可以作用在这个块的子块中。比如let在程序或函数的顶层声明变量,比如:

function foo () {
    let x = 31;
    if (true) {
        let x = 71;
        console.log(x); // 71
    }
    console.log(x); // 31
}

使用let声明变量时,不允许在相同的作用域名重复声明相同一个变量。比如:

if (true) {
    let x = 12;
    let x = "w3cplus"; // Uncaught SyntaxError: Identifier 'x' has already been declared
}

如果在同一个作用域下使用let声明同一个变量,程序将会报错。比如上面的代码,就报出下面的错误信息:

Uncaught SyntaxError: Identifier 'x' has already been declared

使用let声明变量,除了不能在同一个作用域内声明同一个变量之外,还不能同时和使用var声明的变量同名,不然也会报错。如下:

if (true) {
    let x = 12;
    var x = "w3cplus"; // Identifier 'x' has already been declared
}

使用let声明变量不像使用var声明变量存在生命提升这一现象。也就是说,在代码作用块中,如果在使用let声明的变量前就使用,代码会报错:

if (true) {
    console.log(x); //x is not defined
    let x = 10;
}

除此之外,就算是外层代码块已经有同名的变量,也会报错:

var x = "w3cplus";
if (true) {
    console.log(x); //x is not defined
    let x = 10;
}
console.log(x);

因为在ES6中,如果代码块中存在let,这个区域从一开始就形成了封闭作用域。凡事在声明前使用变量就会报错。即在代码块内,在let声明之前使用变量都是不可用的。而这种现象在ES6称之为**暂时性死区(temporal dead zone),简称为TDZ**。

在使用let声明变量时,该变量不会受到代码块外的影响,同样它也不会影响代码块之外的同名变量。来看一个letvar对比的示例:

// 使用var,代码块内的会影响代码块外的同名变量
var a = "w3cplus";
if (true) {
    var a = "blog";
}
console.log(a); // blog

// 使用let,代码块内的不会影响代码块外的同名变量

var a = "w3cplus";
if (true) {
    let a = "blog";
    console.log(a);// blog
}
console.log(a);// w3cplus

使用const声明变量

在ES6中除了提供let声明变量之外,还提供了const声明变量,但是其声明的是常量。一旦使用const声明,常量的值就不能改变。

const PI = 3.1415;
PI; // 3.1415

PI = 3;
PI; // 3.1415

const PI = 3.1;
PI; // 3.1415

上面的示例代码说明改变常量的值是不起作用的。需要注意的是,对常量重新赋值是不会报错的,只会默默地失败

使用const声明变量和let一样,只在声明所在的块级作用域内有效

if (true) {
    const MAX = 5;
    console.log(MAX); // 5
}
console.log(MAX); //MAX is not defined

如果在代码块外调用声明的常量,代码会报错MAX is not defined

使用const声明的常量和使用let一样不可以重复声明同名的变量

var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!"; //Identifier 'message' has already been declared
const age = 30; //Identifier 'age' has already been declared

另外,使用const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值

const foo; // SyntaxError: missing = in const declaration

使用const声明的变量和使用let声明变量同样不会发生变量提升:

if(true){
    console.log(a); // a is not defined
    const a = 'ss';
}

const声明的常量同样不会受到外层代码块的干扰,同样也不会感染外部变量/常量:

var a = 'cc';
if(true){
    const a = 'ss';
    console.log(a); // ss
}
console.log(a); // cc

var VS. let

  var let
作用域 函数内 块内
在同一个域内有多个同名声明 没问题 报错
暂时性死区 No Yes
块外使用 Yes No
与函数参数冲突 No Yes
全局作用域 存在全局变量,成为一个全局对象的属性 存在全局变量,但不成为一个全局对象的属性

const VS. let

使用constlet关键词声明的变量还是有很多相同点:

  • 只在块级作用域内有效
  • 存在暂时性死区(即使用变量要提前声明)
  • 不可重复声明

扩展阅读

总结

本文主要介绍了如何使用关键词varletconst声明变量。并且总结了这几种方法声明变量之间的差异以及在实际使用当中需要注意的一些细节。希望对于初学者有些许的帮助,如果文中有不对之处,还希望大婶们多多指正,同时欢迎在下面的评论中与大家一起分享。