JavaScript 作用域和变量提升
本文是这篇文章的简单翻译。
如果按照下面的代码按照JavaScript程序的执行方式执行,alert函数会显示什么?
var foo = 1;
function bar() {
if (!foo) {
var foo = 10;
}
alert(foo);
}
bar();
你可能会吃惊于答案是10,而下面这个很可能让你迷糊:
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
alert(a);
在这,浏览器会显示1。那么,这里到底发生了什么呢?当然,它看起来有点奇怪、危险同时让人迷糊。但事实上它是JavaScript这门语言一个强大的富有表现力的特性(powerful and expressive feature)。我不知道这个具体的行为的标准名字,但我倾向于”提升(hoisting)“这一术语(term)。这篇文章试图阐明这个机制,但首先我们要先绕一个必要的弯路,了解JavaScript作用域(scoping)。
JavaScript的作用域
对于JavaScript初学者来说,最混乱的来源之一就是作用域。事实上,许多我遇到的有经验的JavaScript程序员都不能完全理解作用域(scoping)。JavaScript作用域之所以让人困惑,是因为它看起来像C-family语言。考虑如下C程序:
#include <stdio.h>
int main() {
int x = ;
printf("%d, ", x); //
if () {
int x = ;
printf("%d, ", x); //
}
printf("%d\n", x); //
}
程序的输出是1,2,1。这是因为C以及其他C family语言拥有块级作用域(block-level scope)。当控制流进入块中,例如if语句,在这个作用域中可以声明新的变量而不影响外面的作用域。但这并不在JavaScript中适用。在Firebug中试试下面的代码:
var x = 1;
console.log(x); //
if (true) {
var x = 2;
console.log(x); //
}
console.log(x); //
在这种情况下,Firebug会显示1,2,2。这是因为JavaScript有函数级作用域(functions-level scope)。这从根本上不同于C-family语言。代码块,例如if语句,不会产生新的作用域。只有函数可以产生新的作用域。
对于大多数使用C、C++、C# 或者Java的程序员来说,这很让人意外而不受欢迎。幸运的是,由于JavaScript的灵活性,我们有变通的办法。如果你需要在一个函数产生临时的作用域,可以按照下面的做法做:
function foo() {
var x = 1;
if (x) {
(function () {
var x = 2;
// some other code
}());
}
// x is still 1.
}
这个方法很灵活(原作者在if语句中写了一个立即执行函数),你可以在任何需要临时作用域的地方使用这个方法,而不仅仅是块语句。但是,我强烈建议你花时间好好理解和欣赏JavaScript 作用域。它十分强大,是我最喜欢的特性之一。如果你理解了作用域,你就可以比较容易的理解提升(hoisting)了。
声明(declarations),名字(names)和提升(hoisting)
在JavaScript中一个名字进入作用域有四个方法(a name enters a scope in one of four basic ways):
1).Language -defined:所用的作用域默认含有this和arguments;
2).Formal parameters:函数可以具名的形参,它的作用域在这个函数中;
3).Function declaration:形式类似于function foo() {};
4).Variable declaration:形式类似于var foo;
函数和变量的声明总是被JavaScript解释器隐式的提升到他们所在的作用域的顶部。函数形参和语言默认定义的名字也在顶部。这意味着如下代码:
function foo() {
bar();
var x = 1;
}
实际上被解释成这样:
function foo() {
var x;
bar();
x = 1;
}
这证明了无论包含了变量声明的语句是否被执行,变量总是会存在(可能没有初始化)。下面的两段代码是相等的:
function foo() {
if (false) {
var x = 1;
}
return;
var y = 1;
}
function foo() {
var x, y;
if (false) {
x = 1;
}
return;
y = 1;
}
需要注意的是,声明与剧中关于赋值的那一部分没有配提升,只有名字被提升了。不同的是,函数声明则有另一套规则--整个函数体均被提升。但是,请记住,我们有两种声明函数的方式。考虑如下JavaScript代码:
function test() {
foo(); // TypeError "foo is not a function"
bar(); // "this will run!"
var foo = function () { // function expression assigned to local variable 'foo'
alert("this won't run!");
}
function bar() { // function declaration, given the name 'bar'
alert("this will run!");
}
}
test();
在这个情况下,只有包含了函数体的函数声明被提升到了顶部(指的是bar),名字foo虽然被提升了,剩下的赋值要等到执行时才会执(其实foo这种叫做赋值式函数声明,类似于变量声明,而bar这种声明叫做声明式函数声明)。
这就是提升的基本概念,似乎不是那么复杂和让人迷惑。当然,在有些特殊的情况下还有一些复杂。
名称解析顺序(Name Resolution Order)
我们需要谨记的最重要的特殊情况是名称解析顺序。记住,让一个名字进入一个给定的作用域有四种方法。上面列出来的顺序就是他们被解析的顺序。大体上,如果一个名字被定义,它永远不会被拥有相同名字的属性覆盖掉。这意味着函数声明的优先级高于变量声明。这不意味这无法给那个名字赋值,只是声明部分会被忽略。这也解释了第二个例子中为什么没有最后的a值为什么还是1。因为函数声明优先级高于变量声明,所以函数经过提升之后相当于声明了一个名字为a的函数,然后又重新赋值10给a,这时a就有函数变成了数值,这样相当于a成为了函数b内的局部变量了。所以并没有改变外部全局变量a的值。
这有几个例外:
1) 内建的名字 arguments 行为很奇怪。它看起来似乎在形参之后声明,但却在函数声明之前。这就意味着如果形参中有名为arguments的参数,它将优先于内建的,即使它是undefined。这是一个不好的特性。不要使用arguments作为形参的名字。
2)试图使用this作为标识符会导致语法错误(syntaxError)。
3)多个形参拥有同一个名字,最后一个有最高的优先级即使它是undefined。
命名函数表达式
你可以给在函数表达式定义的函数一个名字,语法类似于函数声明。这样不会产生一个函数声明,同时名字没有被带入作用域,而且函数体不会被提升。这些代码将解释我的意思:
foo(); // TypeError "foo is not a function"
bar(); // valid
baz(); // TypeError "baz is not a function"
spam(); // ReferenceError "spam is not defined" var foo = function () {}; // anonymous function expression ('foo' gets hoisted)
function bar() {}; // function declaration ('bar' and the function body get hoisted)
var baz = function spam() {}; // named function expression (only 'baz' gets hoisted) foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"
如何利用这个知识编程
现在你已经理解了作用域和提升,那在编写JavaScript程序时,这些意味着什么?最重要的是,总是使用var关键字声明变量。我强烈建议你在每个作用域中只含有一个var语句,同时它要在顶部。如果你强迫自己这样子,你就不会面临提升方面的问题了。然而,这样很难在当前的作用域中追踪哪个变量实实在在的被声明了。我建议使用JSLint并开启onevar选项来确保可以这样做。如果你按照上面做了,你的代码将会类似于:
/*jslint onevar: true [...] */
function foo(a, b, c) {
var x = 1,
bar,
baz = "something";
}
标准怎么说
查看标准是理解事情如何工作的最好办法。在12.2.2中谈到了关于变量声明和作用域的事:
If the variable statement occurs inside a FunctionDeclaration, the variables are defined with function-local scope in that function, as described in section 10.1.3. Otherwise, they are defined with global scope (that is, they are created as members of the global object, as described in section 10.1.3) using property attributes { DontDelete }. Variables are created when the execution scope is entered. A Block does not define a new execution scope. Only Program and FunctionDeclaration produce a new scope. Variables are initialised to undefined when created. A variable with an Initialiser is assigned the value of its AssignmentExpression when the VariableStatement is executed, not when the variable is created.
后话:这里给出另外一个关于JavaScript执行顺序的链接:javascript运行机制之执行顺序详解
JavaScript 作用域和变量提升的更多相关文章
- Javascript作用域和变量提升
下面的程序是什么结果? var foo = 1; function bar() { if (!foo) { var foo = 10; } alert(foo); } bar(); 结果是10: 那么 ...
- javascript中的变量作用域以及变量提升
在javascript中, 理解变量的作用域以及变量提升是非常有必要的.这个看起来是否很简单,但其实并不是你想的那样,还要一些重要的细节你需要理解. 变量作用域 “一个变量的作用域表示这个变量存在的上 ...
- javascript中的变量作用域以及变量提升详细介绍
在javascript中, 理解变量的作用域以及变量提升是非常有必要的.这个看起来是否很简单,但其实并不是你想的那样,还要一些重要的细节你需要理解变量作用域 “一个变量的作用域表示这个变量存在的上下文 ...
- JS 函数作用域及变量提升那些事!
虽然看了多次js函数作用域及变量提升的理论知识,但小编也是一知半解~ 这几天做了几道js小题,对这部分进行了从新的理解,还是有所收获的~ 主要参考书籍: <你不知道的JavaScript(上卷) ...
- JS _函数作用域及变量提升
虽然看了多次js函数作用域及变量提升的理论知识,但也是一知半解~ 这几天做了几道js小题,对这部分进行了从新的理解,还是有所收获的~ 主要参考书籍: <你不知道的JavaScript(上卷)&g ...
- JS 作用域与变量提升---JS 学习笔记(三)
你知道下面的JavaScript代码执行时会输出什么吗? var foo = 1; function bar() { if (!foo) { var foo = 10; } console.log(f ...
- 关于JavaScript的词法作用域及变量提升的个人理解
关于JavaScript的作用域,最近听到一个名词:“词法作用域”:以前没有听说过(读书少),记录一下对此的理解,加深印象. 词法作用域:在JavaScript中,一个函数的作用域,在这个函数定义好的 ...
- JavaScript基础03——函数的作用域及变量提升
1.作用域 作用域,变量在函数内部作用的范围/区域.有函数的地方就有作用域. 2.局部作用域和全局作用域 function fn(){ var a = 1; } console.log(a); / ...
- js 作用域,变量提升
先看下面一段代码: 代码执行的结果是: 1st alert : a = 0 2nd alert : a = undefined 5th alert : a = 0 3rd alert : a = 3 ...
随机推荐
- i++与++i的区别
i++与++i的意思都是i自身加1,不过这个两个语句却有很大的区别. ++i,就是直接在i上再加1,这个无需多解释. i++会稍微特殊些,他会在下次执行语句,再遇到i时,才会在i身上加1. 打个比方, ...
- 查询制定行数的数据(2)对了,mysql不能用top关键字
采用嵌套查询的方式,倒序之后前10条 倒序之后前9条 采用嵌套查询的方式,倒序之后前10条 排正序之后从第一条开始弄十条数据 排正序之后从第一条开始弄九条数据 排正序之后从第十条开始弄十条数据 排正序 ...
- 20160330javaweb之session 小练习
练习一:session 实现登录注销 package com.dzq.session.logout; import java.util.*; public class UserDao { /** * ...
- ios隐藏导航栏底线条和导航、状态栏浙变色
方法一遍历法: 在你需要隐藏的地方调用如下代码 [self findlineviw:self.navigationBar].hidden = YES; -(UIImageView*)findlinev ...
- java Email发送及中文乱码处理。
public class mail { private String pop3Server=""; private String smtpServer=""; ...
- 暑假集训(2)第二弹 ----- The Suspects(POJ1611)
B - The Suspects Crawling in process... Crawling failed Time Limit:1000MS Memory Limit:20000KB ...
- Nginx中让 重写后的路径 自动增加斜线 /
http://www.111cn.net/sys/nginx/56067.htm(参考文章) 现在有个这样的需求,在重写的url地址后,自动加斜线 / 例如 xx.com/abc/1-2 (默认ur ...
- c#面向对象小结
特点: 1:将复杂的事情简单化. 2:面向对象将以前的过程中的执行者,变成了指挥者. 3:面向对象这种思想是符合现在人们思考习惯的一种思想. 过程和对象在我们的程序中是如何体现的呢?过程其实就是函数: ...
- shell中case的用法学习笔记
这篇文章主要为大家介绍shell中的case语句:可以把变量的内容与多个模板进行匹配,再根据成功匹配的模板去决定应该执行哪部分代码. 本文转自:http://www.jbxue.com/article ...
- python【第十七篇】jQuery
1.jQuery是什么? jQuery是一个 JavaScript/Dom/Bom 库. jQuery 极大地简化了 JavaScript 编程. jQuery 很容易学习. 2.jQuery对象与D ...