JavaScript Scoping and Hoisting 翻译

你知道下面的JavaScript代码执行后会alert出什么值吗?


代码如下:

var foo = 1;
function bar() {
if (!foo) {
var foo = 10;
}
alert(foo);
}
bar();

如果答案是"10"令你感到惊讶的话,那么下面这个会让你更加困惑:
[/code]
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
alert(a);
[/code]
浏览器会alert“1”。那么,到底是怎么了?尽管这看起来有点奇怪、有点危险又有点令人困惑,但这事实上却是这门语言一个强力的具有表现力的特性。我不知道是不是有个标准来定义这种行为,但是我喜欢用”hoisting”来描述。这篇文章试着去解释这种机制,但是首先,让我们对JavaScript的scoping做一些必要的了解。
Scoping in JavaScript
对于JavaScript新手来说scoping是最令人困惑的部分之一。事实上,不仅仅是新手,我遇到或很多有经验的JavaScript程序员也不能完全理解scoping。JavaScript的scoping如此复杂的原因是它看上去非常像C系语言的成员。请看下面的C程序:


代码如下:

#include <stdio.h>
int main() {
int x = 1;
printf("%d, ", x); // 1
if (1) {
int x = 2;
printf("%d, ", x); // 2
}
printf("%d\n", x); // 1
}

这段程序的输出是1,2,1。这是因为在C系语言有块级作用域(block-level scope),当进入到一个块时,就像if语句,在这个块级作用域中会声明新的变量,这些变量不会影响到外部作用域。但是JavaScript却不是这样。在Firebug中试试下面的代码:


代码如下:

var x = 1;
console.log(x); // 1
if (true) {
var x = 2;
console.log(x); // 2
}
console.log(x);// 2

在这段代码中,Firebug显示1,2,2。这是因为JavaScript是函数级作用域(function-level scope)。这和C系语言是完全不同的。块,就像if语句,并不会创建一个新的作用域。只有函数才会创建新的作用域。
对于大部分熟悉C,C++,C#或是Java的程序员来说,这是意料之外并且不被待见的。幸运的是,因为JavaScript函数的灵活性,对于这个问题我们有一个解决方案。如果你必须在函数中创建一个临时的作用域,请像下面这样做:


代码如下:

function foo() {
var x = 1;
if (x) {
(function () {
var x = 2;
// some other code
}());
}
// x is still 1.
}

这种方面确实非常灵活,它使用在任何需要创建一个临时作用域的地方,不仅仅是某个块中。但是,我强烈建议你花点时间好好理解下JavaScript scoping。它实在是非常强力,而且它也是我最喜欢的语言特性之一。如果你很好的理解了scoping,理解hoisting将会更加容易。
Declarations, Names, and Hoisting
在JavaScript中,一个作用域(scope)中的名称(name)有以下四种:
1. 语言自身定义(Language-defined): 所有的作用域默认都会包含this和arguments。
2. 函数形参(Formal parameters): 函数有名字的形参会进入到函数体的作用域中。
3. 函数声明(Function decalrations): 通过function foo() {}的形式。
4. 变量声明(Variable declarations): 通过var foo;的形式。
函数声明和变量声明总是被JavaScript解释器隐式地提升(hoist)到包含他们的作用域的最顶端。很明显的,语言自身定义和函数形参已经处于作用域顶端。这就像下面的代码:


代码如下:

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;
}

注意到声明的赋值部分并没有被提升(hoist)。只有声明的名称被提升了。这和函数声明不同,函数声明中,整个函数体也都会被提升。但是请记住,声明一个函数一般来说有两种方式。考虑下面的JavaScript代码:


代码如下:

function test() {
foo(); // TypeError "foo is not a function"
bar(); // "this will run!"
var foo = function () { // 函数表达式被赋值给变量'foo'
alert("this won't run!");
}
function bar() { // 名为'bar'的函数声明
alert("this will run!");
}
}
test();

在这里,只有函数声明的方式会连函数体一起提升,而函数表达式中只会提升名称,函数体只有在执行到赋值语句时才会被赋值。
以上就包括了所有关于提升(hoisting)的基础,看起来并没有那么复杂或是令人困惑对吧。但是,这是JavaScript,在某些特殊情况下,总会有那么一点复杂。
Name Resolution Order
需要记住的最最重要的特例就是名称解析顺序(name resolution order)。记住一个名称进入一个作用域一共有四种方式。我上面列出的顺序就是他们解析的顺序。总的来说,如果一个名称已经被定义了,他绝不会被另一个拥有不用属性的同名名称覆盖。这就意味着,函数声明比变量声明具有更高的优先级。但是这却不意味着对这个名称的赋值无效,仅仅是声明的部分会被忽略而已。但是有下面几个例外:
内置的名称arguments的行为有些怪异。他似乎是在形参之后,函数声明之前被声明。这就意味着名为arguments的形参会比内置的arguments具有更高的优先级,即使这个形参是undefined。这是一个不好的特性,不要使用arguments作为形参。
任何地方试图使用this作为一个标识都会引起语法错误,这是一个好的特性。
如果有多个同名形参,那位于列表最后的形参拥有最高的优先级,即使它是undefined。
Name Function Expressions
你可以在函数表达式中给函数定义名称,就像函数声明的语句一样。但这并不会使它成为一个函数声明,并且这个名称也不会被引入到作用域中,而且,函数体也不会被提升(hoist)。这里有一些代码可以说明我说的是什么意思:


代码如下:

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 () {}; // 匿名函数表达式('foo'被提升)
function bar() {}; // 函数声明('bar'和函数体被提升)
var baz = function spam() {}; // 命名函数表达式(只有'baz'被提升)
foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"

How to Code With This Knowledge
现在你明白了作用域和提升,那么这对编写JavaScript代码意味着什么呢?最重要的一条是声明变量时总是使用var语句。我强烈的建议你在每个作用域中都只在最顶端使用一个var。如果你强制自己这么做,你永远也不会被提升相关的问题困扰。尽管这么做会使的跟踪当前作用域实际声明了哪些变量变得更加困难。我建议在JSLint使用onevar选项。如果你做了所有前面的建议,你的代码看起来会是下面这样:


代码如下:

/*jslint onevar: true [...] */
function foo(a, b, c) {
var x = 1,
bar,
baz = "something";
}

What the Standard Says
我发现直接参考ECMAScript Standard (pdf)来理解这些东西是如何运作的总是很有用。下面是关于变量声明和作用域的一段摘录(section 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程序员最容易困惑的部分一些启示。我尽力写的全面,以免引起更多的困惑。如果我写错了或是漏掉了某些重要的东西,请一定让我知道。

(0)

相关推荐

  • JavaScript中变量提升 Hoisting

    因为我在写这文章的时候,百度里找资料,找到了园友的一篇文章,写的很好,可是我写了又不想放弃,所以就在里面拿了很多东西过来!~~ [翻译]JavaScript Scoping and Hoisting希望得到大家谅解. 一.案发现场 我们先看一段很简单的代码: 复制代码 代码如下: var v='Hello World'; alert(v); 这个没有疑问吧,弹出"Hello World".OK,我们继续. 我们在看一段Code: 复制代码 代码如下: var v='Hello Worl

  • 深入解读JavaScript中的Hoisting机制

    hoisting机制 javascript的变量声明具有hoisting机制,JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面. 先看一段代码 var v = "hello"; (function(){ console.log(v); var v = "world"; })(); 这段代码运行的结果是什么呢? 答案是:undefined 这段代码说明了两个问题, 第一,function作用域里的变量v遮盖了上层作用域变量v.代码做少

  • JS中作用域和变量提升(hoisting)的深入理解

    作用域(Scoping) 对于Javascript初学者来说,一个最迷惑的地方就是作用域:事实上,不光是初学者.我就见过一些有经验的javascript程序员,但他们对scope理解不深.javascript作用域之所以迷惑,是因为它程序语法本身长的像C家族的语言.我对作用域的理解是只会对某个范围产生作用,而不会对外产生影响的封闭空间.在这样的一些空间里,外部不能访问内部变量,但内部可以访问外部变量. c语言的变量分为全局变量和局部变量,全局变量的作用范围是任何文件和函数访问(当然,对于非变量定

  • 理解 JavaScript Scoping & Hoisting(二)

    Scoping & Hoisting var a = 1; function foo() { if (!a) { var a = 2; } alert(a); }; foo(); 上面这段代码在运行时会产生什么结果? 尽管对于有经验的程序员来说这只是小菜一碟,不过我还是顺着初学者常见的思路做一番描述: 1.创建了全局变量 a,定义其值为 1 2.创建了函数 foo 3.在 foo 的函数体内,if 语句将不会执行,因为 !a 会将变量 a 转变成布尔的假值,也就是 false 4.跳过条件分支,

  • JavaScript的变量声明提升问题浅析(Hoisting)

    一.变量声明提升 hoisting 英['hɔɪstɪŋ] 美['hɔɪstɪŋ] n. 起重,提升 v. 把-吊起,升起( hoist的现在分词 ) 先来看一个栗子 var cc = 'hello'; function foo(){ console.log(cc); var cc = 'world'; console.log(cc); } foo(); console.log(cc); 这里将会输出 undefined.'world' .'hello' 此处主要有两个知识点: 1.作用域 2.

  • JavaScript中Hoisting详解 (变量提升与函数声明提升)

    本文主要给大家介绍了关于JavaScript中Hoisting(变量提升与函数声明提升)的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 如何将 函数声明 / 变量 "移动" 到作用域的顶部. 术语 Hoisting(提升) 在很多 JavaScript 博文中被用来解释标识符的解析.其实 Hoisting(提升) 这个词是用来解释 变量 和 函数声明 是如何被提升到 函数或全局 作用域顶部的.你在任何的 JavaScript 文档中找不到这个术语,我们说的

  • JavaScript Scoping and Hoisting 翻译

    你知道下面的JavaScript代码执行后会alert出什么值吗? 复制代码 代码如下: var foo = 1; function bar() { if (!foo) { var foo = 10; } alert(foo); } bar(); 如果答案是"10"令你感到惊讶的话,那么下面这个会让你更加困惑: [/code] var a = 1; function b() { a = 10; return; function a() {} } b(); alert(a); [/cod

  • JavaScript提升机制Hoisting详解

    前言 刚接触到JavaScript的时候,便知道JavaScript是按顺序执行的,是如浏览器的解析DOM树一样的流程,解析DOM结构的时候,如果遇到JS脚本或者外联脚本便会停止解析,继续下载脚本之后,执行脚本,然后再解析DOM. 然而,却因此常常碰到问题. 看如下代码以及输出: var name; console.log(name); // undefined name = 'tom'; age = 10; var age; console.log(age); // 10 上面的代码让我们产生

  • PPK 谈 JavaScript 的 this 关键字 [翻译]

    下面先讲如何在event handling(事件处理)中用它,再接着是讲 this 的其他用法. 自己本身 先来看看函数 doSomething() 里的 this 到底是指向(refer to)了什么? function doSomething() { this.style.color = '#cc0000'; } JavaScript的 this 总指向所运行的函数"自己本身".也就是说,它是一种指向函数对象的方法.在页面中定义 doSomething() 函数,自己本身是指页面.

  • 关于javascript中this关键字(翻译+自我理解)

    下文有大概70%的内容出自http://www.quirksmode.org/js/this.html,另外30%是我自己对它的理解和感想.希望能对有需要的人一点帮助... 首先,先看一个很典型的关于this关键字题目: 复制代码 代码如下: var name = 'hong' var obj = { name: 'ru', getName: function(){ return function(){ return this.name; }; } } alert(obj.getName()()

  • 每个 JavaScript 工程师都应懂的33个概念

    简介 这个项目是为了帮助开发者掌握 JavaScript 概念而创立的.它不是必备,但在未来学习(JavaScript)中,可以作为一篇指南. 本篇文章是参照 @leonardomso 创立,英文版项目地址在这里. 由于原版资源都要翻墙,所以本人创立一个中文版,附上关于这些概念在国内的一些文章和视频. 若有觉得更好的文章或者视频,可以贡献出来,觉得有误的,请联系我删除. 若有觉得更好的文章或者视频,可以贡献出来,觉得有误的,请联系我删除. 文章的排序优化,前面的文章是介绍概念,后面的文章是深入解

  • javascript实现的基于金山词霸网络翻译的代码

    上图: 注意下面的代码,最好保存为utf-8格式的,要不容易出现乱码. 复制代码 代码如下: <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>javascript 金山词霸在线网络翻译 </title> </head> <body> <scrip

  • 现代 JavaScript 参考

    简介 初心 本文档是一份 JavaScript 速查表,你在现代项目中会经常遇到,以及最新的代码示例. 本指南不是为了教你从头开始学习 JavaScript ,而是为了帮助那些可能不熟悉当前代码库(例如 React)所用到 JavaScript 概念的开发人员. 此外,我有时还会提供一些个人的建议,这些建议可能是有争议的,但我也会注意到,当我这么做的时候,这是我个人的建议. 注意: 这里介绍的大多数概念来自于最新的 JavaScript 语言( ES2015,通常称为 ES6).你可以在 这里

随机推荐