原文:JavaScript Variable Scope and Hoisting Explained
这篇文章,我们要学习的是javascript的变量作用域和变量的提升以及它们的特性。
理解javascript的变量作用域和变量提升对于学习javascript来说,是非常重要的。这些概念看起来似乎很直白,但是还是有些很重要的细微之处需要我们理解的。
变量的作用域
变量的作用域就是变量的生存环境。简单的说,变量的作用域就是你在什么地方可以访问到这个变量。
javascript中的变量作用域只用两种:局部作用域和全局作用域。
局部作用域(函数级作用域)
跟其他大多数编程语言不同的是,javascript没有块级作用域(比如java中的if语句块中声明的变量,作用域就是这个if语句块,但是javascript中,if语句块中的变量却是全局变量)。不过,javascript有函数级作用域。在一个函数内定义的变量,叫做局部变量。它只能在这个函数里或者这个函数的内部函数里被访问到。更多关于内部函数访问外部函数中定义的变量的内容,请参考Closures(闭包)这篇文章。
函数级作用域演示:
var name = "Richard";
function showName () {
var name = "Jack"; // 局部变量; 只能在 showName 函数中被访问
console.log (name); // Jack
}
console.log (name); // Richard: 全局变量
javascript中没有块级作用域,这是跟其他大部分编程语言都不一样的地方,需要特别注意
var name = "Richard";
// 这个if语句块没有生成一个新的环境
if (name) {
name = "Jack"; // 这个name是全局变量name,它的值被改成了"Jack"
console.log (name); // Jack: 这个还是全局变量
}
// 这里的name,还是全局变量,但是它在if语句块中被改变了值。
console.log (name); // Jack
- 如果你不声明局部变量的话,很容易出错
在使用局部变量之前,一定要记得先声明。你可以用JSHint 来检查你的语法。下面这个例子是不声明本地变量导致的问题://即使是在函数里面,声明一个变量时,没有使用var这个关键字,那这个变量依然是全局变量。 var name = "Michael Jackson"; function showCelebrityName () { console.log (name); } function showOrdinaryPersonName () { name = "Johnny Evers"; console.log (name); } showCelebrityName (); // Michael Jackson //因为在函数里面没有对name进行声明,所以它用到的name是全局变量里面的name,只是把它的值改变成了"Johnny Evers" showOrdinaryPersonName (); // Johnny Evers // 全局变量name的值现在是Johnny Evers了,而不再是原来的"Michael Jackson"了。 showCelebrityName (); // Johnny Evers // 下面是正确的在函数里声明本地变量的方法 function showOrdinaryPersonName () { var name = "Johnny Evers"; // 现在这个name是本地变量了,不会覆盖掉全局变量里面的name. console.log (name); }
- 在函数里局部变量的优先权高于全局变量
如果你同时声明了一个全局变量,一个局部变量,两者的名字一样的话,在函数里面使用这个变量的时候,将优先使用本地声明的这个变量。在一个函数里面使用一个变量的时候,它会首先在本地环境里面查找这个变量,找到了就直接使用,只有在本地找不到该变量时,它才会到全局环境里去找全局变量。var name = "Paul"; function users () { // 这里声明了一个局部变量,跟全局变量的变量名都是name,但是在函数里,它的优先级比全局变量高 var name = "Jack"; // 首先在这个函数里面查找name变量,找不到的话,才会去查找函数外的全局变量。 console.log (name); } users (); // Jack
全局变量
所有在函数之外声明的变量都是全局变量。在浏览器里面,全局环境或者说全局作用域的作用范围是window对象(或者是整个html文档)
- 所有在函数外面声明或者初始化的变量都是全局变量,全局变量在整个应用中任何地方都可以被使用。比如:
// 下面几种都是声明全局变量的方式 var myName = "Richard"; // 或者 firstName = "Richard"; // 或者 var name; name; //需要注意的是,所有全局变量都跟window对象是关联上的。所有我们声明的全局变量可以像这样访问: console.log(window.myName); // Richard; // 或者 console.log("myName" in window); // true console.log("firstName" in window); // true
- 如果变量初始化(赋值)之前,没有使用var 关键字进行声明,那这个变量自动被加到全局环境里面,变成全局变量了。
function showAge () { // 这个age是全局变量 age = 90; console.log(age);// } showAge (); // 90 // 因为age是全局变量,所以在函数外也能被访问到。 console.log(age); // 90
另一个例子:
// 尽管第二个firstName被包含在{}中,这两个都是全局变量。因为javascript中没有块级作用域这一说。 var firstName = "Richard"; { var firstName = "Bob"; } // 第二个firstName只是又声明了一次并且覆盖了第一个firstName的值 console.log (firstName); // Bob
另一个例子:
for (var i = 1; i <= 10; i++) { console.log (i); // 输出 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; }; // i是全局变量,所以下面的函数里面可以访问到。 function aNumber () { console.log(i); } // aNumber函数里面的i是全局变量,它的值在上面的for循环里面被改变了,最后的值是for循环结束前的11. aNumber (); // 11
- setTimeout 的变量会被放在全局域里执行
注意所有在setTimeout里面的函数都会在全局域里面执行。 这是比较棘手的一点,看下面这个:// 在setTimeout里面的 "this" 对象表示的是 window 对象,而不是 myObj var highValue = 200; var constantVal = 2; var myObj = { highValue: 20, constantVal: 5, calculateIt: function () { setTimeout (function () { console.log(this.constantVal * this.highValue); }, 2000); } } //setTimeout 函数里面的 "this" 对象使用全局的变量 highValue 和 constantVal,因为setTimeout函数里面的 "this" 的引用时全局的window对象,不是myObj myObj.calculateIt(); // 400 // This is an important point to remember.
- 不要过度使用全局变量
如果你想成为一个javascript高手的话,这个应该是毫无疑问的(否则你现在应该在看选美小姐什么的而不是在读这篇文章了),你一定要尽量避免在全局域中创建过多的变量,像下面这样:// 这两个变量现在在全局域里面,但是更好的方法是把它们放到函数里面 var firstName, lastName; function fullName () { console.log ("Full Name: " + firstName + " " + lastName ); }
下面这个是改良之后的版本
// 在函数里面声明变量,它们就成了局部变量了。 function fullName () { var firstName = "Michael", lastName = "Jackson"; console.log ("Full Name: " + firstName + " " + lastName ); }
这上面这个例子中,函数fullName依然是在全局域中。
变量提升
在javascript中,有个变量声明提升的概念。在一个函数里声明的变量,它的声明会被提到函数的顶部。在函数外定义的变量,它的声明则会被提到全局环境的顶部。
需要强调的是,只有函数的声明会被提升,变量的初始化或者赋值都不会被提升。
变量提升的例子:
function showName () {
console.log ("First Name: " + name);
var name = "Ford";
console.log ("Last Name: " + name);
}
showName ();
// First Name: undefined
// Last Name: Ford
// 第一行打印出来的是undefined的原因就是局部变量name被提升到了函数的顶部。
// 第一行的name调用的就是这个name
// 下面的代码是javascript引擎实际的执行过程:
function showName () {
var name; // name的声明被提升到函数的顶部,由于赋值不会被提升,所以此时name的值为undefined
console.log ("First Name: " + name); // First Name: undefined
name = "Ford"; // name在原来这个地方被赋值
// 现在name的值变为了Ford
console.log ("Last Name: " + name); // Last Name: Ford
}
在提升的时候函数声明覆盖变量声明
函数的声明和变量的声明都会被提升到所在作用域的顶部。如果函数的名字和变量的名字相同的话,函数的声明会覆盖掉变量的声明(但是不能覆盖变量的赋值)。像上面提到的,变量的赋值是不会被提升的,函数的赋值也不会被提升。附:函数的赋值的格式 var myFunction = function () {}。
下面是一个基本的演示例子:
// 函数和变量的名字都是 myName
var myName;
function myName () {
console.log ("Rich");
}
//函数的声明覆盖了变量的名字
console.log(typeof myName); // function
// 但是在这个例子中,变量的赋值又覆盖了函数的声明
var myName = "Richard"; // 变量赋值(初始化)覆盖函数的声明
function myName () {
console.log ("Rich");
}
console.log(typeof myName); // string
值得注意的是,像下面这样的函数表达式,是不会被提升的。
var myName = function () {
console.log ("Rich");
}
在strict 模式下,如果你给一个没有被声明的变量赋值,会产生错误。所以一定在变量赋值之前要先声明变量。