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 ...
随机推荐
- R-大数据分析挖掘(4-R爬虫实现)
library("XML") #获取全部的链接 url <- 'http://www.csdn.net/tag/' i_url_parse<-htmlParse(ur ...
- JS实现跳转到页面任何地方
要实现两个内容: 1.从A页面跳转到B页面任何地方 方法:用id对要跳转的地方进行标记. 首先,在A页面可以设一个链接 <a href = "b.html#pos" targ ...
- 用 C# 如何判断数据库中是否存在一个值
选定一个列,比如用户编号列 //欲插入的用户编号string ll_userID="xxxxxxxx"; //查询此编号是否存在SqlCommand mycmd = new Sql ...
- VS 2013 Chrome PPAPI 开发环境
当前系统版本为 Windows 8.1 x64, Chrome 版本为 50.0 1. 准备工作 下载并安装 Python https://www.python.org/download/ * 必须使 ...
- C#获取时间戳的问题
最近在做一个接口,需要用到时间戳,在请求接口时,返回超时,接口方的技术称是时间戳的不对(超出一定范围[比如1分钟]就返回超时)导致的. 首先,看代码: public static double Get ...
- Cogs 1844. [JSOI2008]最大数maxnumber
[JSOI2008]最大数maxnumber ★★ 输入文件:bzoj_1012.in 输出文件:bzoj_1012.out 简单对比 时间限制:3 s 内存限制:162 MB [题目描述] 现在请求 ...
- (转) UIALertView的基本用法与UIAlertViewDelegate对对话框的事件处理方法
首先,视图控制器必须得实现协议UIAlertViewDelegate中的方法,并指定delegate为self,才能使弹出的Alert窗口响应点击事件. 具体代码如下: #import <UIK ...
- Sql server 浅谈用户定义表类型
1.1 简介 SQL Server 中,用户定义表类型是指用户所定义的表示表结构定义的类型.您可以使用用户定义表类型为存储过程或函数声明表值参数,或者声明您要在批处理中或在存储过程或函数的主体中使用的 ...
- 今天收到报警邮件,提示网站502 bad gateway,
今天收到报警邮件,提示网站502 bad gateway, 输入网站url后果然无法打开: 登录服务器查看nginx进程正常: 查看fastcGI进程已经停止运行了: 问题找到后就该查找是什么原因产生 ...
- 网站开发常用jQuery插件总结(12)固定元素插件scrolltofixed
这个插件在前段时间用过一次,当时是改一个网站.要求顶部的菜单栏随着滚动条的滚动而固定.也大体写了一下,不过在文章中也只是提了一下,文章地址:jQuery插件固定元素位置. 在这篇文章中,再进行总结一下 ...