Javascript变量

138 阅读8分钟

ECMAScript变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有三个关键字可以声明变量:var、let和const。其中,var在ECMAScript6以及更晚的版本中使用:

Var关键字

要定义变量,可以使用var操作符(注意var是一个关键字),后跟变量名(即标识符,如前所述):

var message;

这行代码定义了一个名为message的变量。可以用它保存任意类型的值。变量保存一个特殊值undefined。下一节讨论数据类型变量的时候会谈到。ECMAScript实现变量初始化,因此可以同属定义变量并设置他的值。

var message="hi"

这里,message被定义为保存一个字符串hi的变量。像这样初始化变量不会被它标识为字符串类型。只是一个简单的赋值而已。随后。不仅可以保存改变的值。也可以改变值的类型:

var message="hi"
message=100

在这个例子中,变量message首先被定义为一个保存字符串值hi的变量,然后又被重写为保存了数值100.虽然不推荐改变变量保存值的类型。但这在ECMAScript中是完全有效的。

var变量声明作用域

关键的问题在于。使用var操作符定义的变量会成为包含它的函数的局部变量。比如,在使用var在一个函数内部定义一个变量,就意味着该变量在函数退出时会被销毁。

function test(){
    var message="hi"
}
test()
console.log(message); //出错

这里,message变量是在函数内部使用var进行定义的。函数叫做test(),调用它会创建这个变量并且给他赋值。调用之后变量随即被销毁。因此示例中的最后一行会导致错误。不过,在函数内定义变量时省略var操作符。可以创建一个全局变量。

function test(){
    message="hi"
}
test();
console.log(message)

去掉之前的var操作符之后,message就变成了全局变量。只要调用一次函数test(),就会定义到这个变量。并且可以在函数外部访问到。

注意,虽然可以通过省略var操作符定义全局变量。但不推荐这么做。在局部作用域中定义的全局变量很难维护。也会造成困惑。这是因为不能一下子断定省略var是不是有意为之。在严格模式下。如果像这样给未声明的变量赋值。则会抛出ReferenceError。

如果需要定义多个变量。可以在一条语句中使用逗号分隔每个变量。

var message="hi"
    found=false,
    age=29

这里定义并且初始化了三个变量。因为ECMAScript是松散类型的。所以使用不同的数据类型去初始化的变量可以用一条语句来说明。插入和换行的空格缩进并不是必须的,但是这样有助于阅读理解。

在严格模式下,不能定义为eval和arguments的变量。否则会导致语法错误。

var声明提升:

使用var时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:

function foo(){
    console.log(age)
    var age=28;
}
foo();

之所以不会报错。是因为ECMAScript运行时吧他看成等价于如下的代码:

function foo(){
    var age;
    console.log(age)
    age=26;
}
foo()

这就是所谓的提升(hoist),也就是吧所有的变量声明都拉到函数作用域的顶部。此外,反复多次使用var声明同一个变量也没有任何问题:

function foo(){
    var age=15;
    var age=14;
    var age=22;
    console.log(age)
}
foo();

let声明

let和var作用差不多。但是有着非常重要的区别。最明显的区别是,let声明的范围是块作用域。var声明的范围是函数作用域。

if(true){
    var name='Matt';
    console.log(name)    //matt
}
console.log(name) //matt
if(true){
    let age=25;
    console.log(age) //ReferenceError
}
console.log(age) //ReferenceError

在这里,age变量之所以不能在if快外部被引用。是因为他的作用域仅仅限于该块内部。块作用域是函数作用域的子集,因此适用于var的作用域限制同样也适用于let。

let也不允许同一个快作用域中出现多的声明。这样会导致报错

var name; 
var name;

let age;
let age; //SyntaxError;

因此,Javascript引擎会记录用于变量声明的标识符以及其所在行的作用顒。因此重复嵌套的使用相同的标识符不会报错。而这是因为同一个快中没有重复的声明:

var name='Nicholas';
console.log(name);
if(true){
    var name='Matt';
    console.log(name)
}
let age=30;
console.log(age)
if(true){
    let age=26;
    console.log(age);
}

对声明重复报错不会因为混用let和var而受到影响。这两个关键字声明的并不是不同类型的变量。他们只是指出变量在相关作用域中如何存在。

var name;
let name; //SyntaxError

let age;
var age; //SyntaxError

暂时性死区:

let和var还有另外一个重要的区别,就是let声明的变量不会在作用域中被提升:

//name会被提升
console.log(name) //undefined
var name='Matt'
//age不会被提升
console.log(age) //ReferenceError :age没有定义
let age=26;

在解析代码时,Javascript引擎会注意出现在块后面的let声明,只不过在此之前不能以任何方式来引用未声明的变量。在let声明之前执行瞬间被称为“暂时性死区”。在此阶段引用任何后面才声明的变量都会抛出ReferenceError

全局声明:

与var关键字不同,在使用let在全局作用域中声明的变量不会成为window对象的属性。

var name='Matt'
console.log(window.name) //'Matt'
let age=26;
console.log(window.age) //undefined

不过,let声明任然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了避免SynataxError。必须确保页面不会重复的声明同一个变量。

条件声明:

在使用var声明变量时,由于声明会被提升,Javascript引擎会自动将多余的声明在作用域顶部合并为一个声明。因为let的作用域是快。所以不可能在检查前面是否已经使用let声明过同名变量。同时也就不可能在没有声明的情况下使用他。

<script>
    var name='Nicholes';
    let age=25;
</script>
<script>
    //假设脚本不确定页面是否声明了同名变量
    //那他可以假设还没有声明过
    var name='Matt'
    //这里没有任何问题,因为可以被视作一个提升变量来处理。
    let age=36;
    //如果age之前被声明过,这里会报错。
</script>

使用try/catch语句或者typeof操作符也不能解决。因为条件快中let的声明的作用域仅限于该块。

<script>
    let name='Nicholas'
    let age=36;
</script>
<script>
    //假设脚本不确定页面是否已经声明了同名变量
    //那么还可以假设没有声明过
    if(typeof name==='undefined'){
        let name;
    }
    //name被限制在if{}快的作用域内。
    //因此这个赋值就相当于全局赋值
    name='Matt'
    try(age){
        //如果age没有被声明过。则会报错。
    }
    catch(error){
        let age;
    }
    age=25;
</script>

为此,这个let为ES6声明的新的关键字。不能依赖条件声明模式。

注意,不能使用let进行声明反而是一件好事,因为条件声明是一种反模式。他让程序变得更难理解,如果你发现自己在使用这个模式,那一定有更好的替代方案。

for循环中的let声明

在let出现之前,for循环定义的迭代变量会渗透到循环体外部:

for(var i=0;i<5;i++){
    //循环逻辑
}
console.log(i)

改成使用let之后,这个问题就解决了。因为迭代变量的作用域仅仅限于for循环内部:

for(let i=0;i<5;i++){
    //循环逻辑
}
console.log(i); //ReferenceError I没有定义

在使用var的时候,最常见的问题就是对迭代变量的奇特声明和修饰:

for(var i=0;i<5;i++){
    setTimeout(()=>console.log(i),0)
}
//你可能会绝对会输出0,1,2,3,4
//但是实际上会输出5,5,5,5,5

之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值。5,在之后执行超时逻辑时。所有的I都是同一个变量。因而输出的都是同一个最终值。

而在使用let声明迭代变量时。Javascript引擎在后台为每个迭代循环声明一个新的迭代变量。每个setTimeout引用的都是不同的变量实例。所以console.log输出的是我们期待的值。也就是循环执行过程中每个迭代变量的值。

for(let i=0;i<5;i++){
    setTimeout(()=>console.log(i),0)
}
//会输出1,2,3,4,5

这种每次迭代声明一个独立变量的实例行为适用于所有风格的for循环,包括for-in和for-of循环。

const声明

const的行为与let基本相同。唯一一个重要的区别在于适用他声明变量时必须同时初始化变量。并且尝试修改const的声明的变量会导致运行时错误。

const age=35;
age=23 //TypeError:给常量赋值
//const也不允许重复声明
const name = 'Matt'
const name = 'Nicles'
//const声明的作用域也是块。
const name='Matt'
if(true){
    const name='Nicles'
} 
console.log(name) //Matt

const声明的限制只适用于他指向的变量的引用。换句话说,如果const变量引用的是一个对象。那么修改这个对象内部的值并不违反const的限制。

const person={}
person.name='Matt'

JavaScript引擎会为for循环中的let声明分别创建独立的变量实例。虽然const变量和let变量很相似。但是不能使用const来声明迭代变量。

for(const i=0;i<19;i++){} //TypeError给常量赋值

不过,如果你只想用const声明一个不会被修改的for循环变量。那也是可以的。也就是说,每次迭代都只是创建一个新变量。这对for-of和for-in循环非常的有帮助

let i=0;
for(const j=6;i<5;i++){
    console.log(j)
}
for(const key in {a:1,b:4}){
    console.log(key)
}
for(const value of [1,3,4,5,6]){
    console.log(value)
}