变量的作用域是程序源代码中定义这个变量的区域.全局变量拥有全局作用域,在JavaScript代码中的任何地方都有定义.

在一些类似C语言的编程语言中,大括号内的每一段代码都有各自的作用域,变量只在作用域之内有效,在其外部不可见,这种作用域叫做块级作用域,但JavaScript没有块级作用域的概念,只有函数作用域,即在函数内部声明的变量只在函数体内有定义,是局部变量,函数参数也是局部变量,只在函数体内有定义.在其他大括号内的变量并不是局部变量.

代码+解析:

1
2
3
4
var a = 1;
window.a; // 1
a; // 1
window.a === a // true

var a = 1;在全局环境下(此时为浏览器)声明了一个全局变量a,所以a是浏览器全局对象window的一个属性[注2],故window.a和a的值相等.

1
2
3
4
5
function foo(){
var b = 2;
}
foo();
console.log(b); // b is not defined

foo函数内部相当于子作用域,变量b只在foo函数内的作用域存在,在foo函数外部无法获取b的值,故返回not defined.

1
2
3
4
5
6
var a = 1;
function foo(){
var b = 2;
console.log(a);
}
foo(); // 1

函数内部可以获取外部的变量值.[注1]故输出a的值为1.

1
2
3
4
5
6
7
var a = 1;
function foo(){
var a = 2;
console.log(a);
}
foo(); // 2
console.log(a); // 1

上述代码出现了命名冲突,在全局环境和函数内都声明了a变量,这时在函数内执行console.log(a);不会到外部寻找a,因为内部已经声明了a.

如果想要获得外部变量a,可以在foo内执行console.log(window.a);这时返回值就是外部的a变量值.因此作用域的存在可以解决命名冲突问题.

而如果在函数foo外部(此时是全局环境)中输出a的值,输出的就是在全局环境中定义的a值,即为1.

1
2
3
4
5
6
var a = 1;
function foo(){
a = 2;
}
foo();
console.log(a); // 2

函数内部没有对a进行声明,所以调用foo();会跑到函数外部寻找var a=,此时函数外部声明了a变量,而函数内部的a=2因为缺少var关键字会修改外部函数中a的值.因为不对变量加关键词var相当于创建了一个全局变量,会污染全局环境.程序中的每个部分都能获取这个值.(用”use strict”能防止全局环境被污染,返回a is not defined.)

1
2
3
4
5
function foo(){
a = 2;
};
foo();
console.log(a); // 2

这时调用foo,函数外部并没有声明a变量,如果第一层外部函数没有声明a,还会再跑到外部直至全局环境,如果多层函数均没有对a进行声明,那么内部函数就会自己创建一个a变量(全局变量)并将值设为函数内部未声明的a的值,上述情况就是2.

1
2
3
4
function foo(){
a = 2;
}
console.log(a); // a is not defined

函数内的变量在函数被调用之后才存在,在函数被调用之前,a变量是不存在的,所以返回a is not defined.

1
2
3
4
5
function foo(){
a = 2;
}
foo();
console.log(a); // 2

函数在首次调用时,a变量会被创建,因为内部外部都没有对其进行声明,a就变成了全局变量

参数和作用域

代码+解析:

1
2
3
4
5
6
7
var a = 1;
function foo(a){
console.log(a);
a = 2;
}
foo(a); // 1
console.log(a); // 1

foo(a)相当于foo(var = a), 调用foo(a),执行其内部的console.log(a)时,会在函数内部console.log语句之前寻找a的值,在这个情况下无法找到,因为此时还未对a进行赋值,但会跑到函数外部寻找a的赋值语句,于是找到了a = 1,因此foo(a)输出1.

而在foo函数外部的console.log(a);会直接在外部寻找a的值,尽管函数内部为a = 2没有var关键字而且被调用过,但是在foo(a)内传入参数a相当于声明了变量,所以不会污染全局环境,因此第二次弹出的也为1.

1
2
3
4
5
6
7
var a = 1;
function foo(a){
console.log(a);
a = 2;
}
foo(); // undefined
console.log(a); // 1

这段代码与第一段代码唯一的不同是调用foo()时没有传入参数,而在对函数进行定义时设定了参数a,这时JavaScript引擎会自动将a设定为undefined[注3].第二句的console.log(a);中a就是指全局变量a的值,即为1.

1
2
3
4
5
6
7
var a = 1;
function foo(){
console.log(a);
a = 2;
}
foo(); // 1
console.log(a); // 2

foo函数在定义和调用时均没有声明a,但函数内部需要执行console.log(a),所以会跑到外部寻找a的值,正好外部声明了a其值为1,所以输出的就是1,接下来执行的a=2;污染了全局环境,将a值修改为2.因此第二句输出的值为2.

1
2
3
4
5
6
7
var a = 1;
function foo(a){
console.log(a);
a = 2;
}
foo(a); //1
console.log(a); //1

调用foo(a),在函数内部声明了var a;,但内部的console.log(a);之前没有赋值语句,所以会跑到外部获取a的值,因此输出1.第二句console.log(a);弹出的值就是全局变量a的值,因为函数内部声明了a,所以没有污染全局环境.

变量声明提前

JavaScript函数内部声明的所有变量(不涉及赋值)都会被”提前”至函数体的顶部.

代码+解析:

1
2
3
4
5
6
7
8
9
10
var a = 123;

function f() {
console.log(a);
var a = 1;
console.log(a);
}
f();
// undefined
// 1

由于声明提前,可以将上述代码转换为:

1
2
3
4
5
6
7
8
9
10
11
var a = 123;

function f() {
var a;
console.log(a);
a = 1;
console.log(a);
}
f();
// undefined
// 1

那么第一次输出undefined就是因为声明了a但未对其赋值,第二次赋了值1之后自然就会弹出a为1.

外部声明的a值对输出值没有影响是因为在函数体内局部变量的优先级高于同名的局部变量.

由于变量提升,最好是在函数体的顶部声明函数中可能用到的所有变量.


参考

  • 《JavaScript编程精解2》:函数的参数如同一个普通变量,但其初始值是由函数的调用者提供的,而不是函数自身的内部代码.函数有一个重要属性,就是其内部创建的变量以及参数,都属于函数的局部变量.但只有参数和在函数体内使用关键字var声明的变量才属于函数的局部变量.由于在函数外部生命的任何变量在整个程序当中都是可见的,因此我们把这些变量称为全局变量.在函数内部,我们同样可以访问这些变量,只要确保函数中没有声明过同名的局部变量即可.

  • 《JavaScript权威指南》:当声明一个JavaScript全局变量时,实际上是定义了全局对象的一个属性.对局部变量没有如此规定.

  • 《JavaScript面向对象编程指南2》:定义一个函数时,可以指定该函数调用时所需的参数,当然也可以不指定.但如果指定参数之后,却在调用时忘了传递相关参数值,JavaScript引擎就会将参数自动设定为undefined.