Javascript作用域和变量提升

下面的程序是什么结果?

[javascript] view plain copy

  1. var foo = 1;
  2. function bar() {
  3. if (!foo) {
  4. var foo = 10;
  5. }
  6. alert(foo);
  7. }
  8. bar();

结果是10;

那么下面这个呢?

[javascript] view plain copy

  1. var a = 1;
  2. function b() {
  3. a = 10;
  4. return;
  5. function a() {}
  6. }
  7. b();
  8. alert(a);

结果是1.

吓你一跳吧?发生了什么事情?这可能是陌生的,危险的,迷惑的,同样事实上也是非常有用和印象深刻的javascript语言特性。对于这种表现行
为,我不知道有没有一个标准的称呼,但是我喜欢这个术语:“Hoisting
(变量提升)”。这篇文章将对这种机制做一个抛砖引玉式的讲解,但是,首先让我们对javascript的作用域有一些必要的理解。

Javascript的作用域

对于Javascript初学者来说,一个最迷惑的地方就是作用域;事实上,不光是初学者。我就见过一些有经验的javascript程序员,但他们对scope理解不深。javascript作用域之所以迷惑,是因为它程序语法本身长的像C家族的语言,像下面的C程序:

[cpp] view plain copy

  1. #include <stdio.h>
  2. int main() {
  3. int x = 1;
  4. printf("%d, ", x); // 1
  5. if (1) {
  6. int x = 2;
  7. printf("%d, ", x); // 2
  8. }
  9. printf("%d\n", x); // 1
  10. }

输出结果是1 2 1,这是因为C家族的语言有块作用域,当程序控制走进一个块,比如if块,只作用于该块的变量可以被声明,而不会影响块外面的作用域。但是在Javascript里面,这样不行。看看下面的代码:

[javascript] view plain copy

  1. var x = 1;
  2. console.log(x); // 1
  3. if (true) {
  4. var x = 2;
  5. console.log(x); // 2
  6. }
  7. console.log(x); // 2

结果会是1 2 2。因为javascript是函数作用域。这是和c家族语言最大的不同。该程序里面的if并不会创建新的作用域。

对于很多C,c++,java程序员来说,这不是他们期望和欢迎的。幸运的是,基于javascript函数的灵活性,这里有可变通的地方。如果你必须创建临时的作用域,可以像下面这样:

[javascript] view plain copy

  1. function foo() {
  2. var x = 1;
  3. if (x) {
  4. (function () {
  5. var x = 2;
  6. // some other code
  7. }());
  8. }
  9. // x is still 1.
  10. }

这种方法很灵活,可以用在任何你想创建临时的作用域的地方。不光是块内。但是,我强烈推荐你花点时间理解javascript的作用域。它很有用,是我最喜欢的javascript特性之一。如果你理解了作用域,那么变量提升就对你显得更有意义。

变量声明,命名,和提升

在javascript,变量有4种基本方式进入作用域:

  • 1 语言内置:所有的作用域里都有this和arguments;(译者注:经过测试arguments在全局作用域是不可见的)
  • 2 形式参数:函数的形式参数会作为函数体作用域的一部分;
  • 3 函数声明:像这种形式:function foo(){};
  • 4 变量声明:像这样:var foo;

函数声明和变量声明总是会被解释器悄悄地被“提升”到方法体的最顶部。这个意思是,像下面的代码:

[javascript] view plain copy

  1. function foo() {
  2. bar();
  3. var x = 1;
  4. }

实际上会被解释成:

[javascript] view plain copy

  1. function foo() {
  2. var x;
  3. bar();
  4. x = 1;
  5. }

无论定义该变量的块是否能被执行。下面的两个函数实际上是一回事:

[javascript] view plain copy

  1. function foo() {
  2. if (false) {
  3. var x = 1;
  4. }
  5. return;
  6. var y = 1;
  7. }
  8. function foo() {
  9. var x, y;
  10. if (false) {
  11. x = 1;
  12. }
  13. return;
  14. y = 1;
  15. }

请注意,变量赋值并没有被提升,只是声明被提升了。但是,函数的声明有点不一样,函数体也会一同被提升。但是请注意,函数的声明有两种方式:

[javascript] view plain copy

  1. function test() {
  2. foo(); // TypeError "foo is not a function"
  3. bar(); // "this will run!"
  4. var foo = function () { // 变量指向函数表达式
  5. alert("this won‘t run!");
  6. }
  7. function bar() { // 函数声明 函数名为bar
  8. alert("this will run!");
  9. }
  10. }
  11. test();

这个例子里面,只有函数式的声明才会连同函数体一起被提升。foo的声明会被提升,但是它指向的函数体只会在执行的时候才被赋值。

上面的东西涵盖了提升的一些基本知识,它们看起来也没有那么迷惑。但是,在一些特殊场景,还是有一定的复杂度的。

变量解析顺序

最需要牢记在心的是变量解析顺序。记得我前面给出的命名进入作用域的4种方式吗?变量解析的顺序就是我列出来的顺序。一般来说,如果一个名称已经被定义,则不会被其他相同名称的属性覆盖。这是说,(译者没理解这句,所以先做删除样式) 函数的声明比变量的声明具有高的优先级这并不是说给那个变量赋值不管用,而是声明不会被忽略了。
(译者注: 关于函数的声明比变量的声明具有高的优先级,下面的程序能帮助你理解)

[javascript] view plain copy

  1. <script>
  2. function a(){
  3. }
  4. var a;
  5. alert(a);//打印出a的函数体
  6. </script>
  7. <script>
  8. var a;
  9. function a(){
  10. }
  11. alert(a);//打印出a的函数体
  12. </script>
  13. //但是要注意区分和下面两个写法的区别:
  14. <script>
  15. var a=1;
  16. function a(){
  17. }
  18. alert(a);//打印出1
  19. </script>
  20. <script>
  21. function a(){
  22. }
  23. var a=1;
  24. alert(a);//打印出1
  25. </script>

这里有3个例外:
1 内置的名称arguments表现得很奇怪,他看起来应该是声明在函数形式参数之后,但是却在函数声明之前。这是说,如果形参里面有arguments,它会比内置的那个有优先级。这是很不好的特性,所以要杜绝在形参里面使用arguments;
2 在任何地方定义this变量都会出语法错误,这是个好特性;
3 如果多个形式参数拥有相同的名称,最后的那个具有优先级,即便实际运行的时候它的值是undefined;

命名函数

你可以给一个函数一个名字。如果这样的话,它就不是一个函数声明,同时,函数体定义里面的指定的函数名(
如果有的话,如下面的spam, 译者注)将不会被提升, 而是被忽略。这里一些代码帮助你理解:

[javascript] view plain copy

  1. foo(); // TypeError "foo is not a function"
  2. bar(); // valid
  3. baz(); // TypeError "baz is not a function"
  4. spam(); // ReferenceError "spam is not defined"
  5. var foo = function () {}; // foo指向匿名函数
  6. function bar() {}; // 函数声明
  7. var baz = function spam() {}; // 命名函数,只有baz被提升,spam不会被提升。
  8. foo(); // valid
  9. bar(); // valid
  10. baz(); // valid
  11. spam(); // ReferenceError "spam is not defined"

怎么写代码

现在你理解了作用域和变量提升,那么这对于javascript编码意味着什么?最重要的一点是,总是用var定义你的变量。而且我强烈推荐,对于一个名称,在一个作用域里面永远只有一次var声明。如果你这么做,你就不会遇到作用域和变量提升问题。

语言规范怎么说

我发现ECMAScript参考文档总是很有用。下面是我找到的关于作用域和变量提升的部分:
如果变量在函数体类声明,则它是函数作用域。否则,它是全局作用域(作为global的属性)。变量将会在执行进入作用域的时候被创建。块不会定义新的作用域,只有函数声明和程序(译者以为,就是全局性质的代码执行)才会创造新的作用域。变量在创建的时候会被初始化为undefined。如果变量声明语句里面带有赋值操作,则赋值操作只有被执行到的时候才会发生,而不是创建的时候。

我期待这篇文章会对那些对javascript比较迷惑的程序员带来一丝光明。我自己也尽最大的可能去避免带来更多的迷惑。如果我说错了什么,或者忽略了什么,请告知。

译者补充

xu281828044提醒了我发现了IE下全局作用域下命名函数的提升问题:
我翻译文章的时候是这么测试的:

[html] view plain copy

  1. <script>
  2. functiont(){
  3. spam();
  4. var baz = function spam() {alert(‘this is spam‘)};
  5. }
  6. t();
  7. </script>

这种写法, 即非全局作用域下的命名函数的提升,在ie和ff下表现是一致的. 我改成:

[html] view plain copy

  1. <script>
  2. spam();
  3. var baz = function spam() {alert(‘this is spam‘)};
  4. </script>

则ie下是可以执行spam的,ff下不可以. 说明不同浏览器在处理这个细节上是有差别的.

这个问题还引导我思考了另2个问题,1:对于全局作用于范围的变量,var与不var是有区别的. 没有var的写法,其变量不会被提升。比如下面两个程序,第二个会报错:

[html] view plain copy

  1. <script>
  2. alert(a);
  3. var a=1;
  4. </script>

[html] view plain copy

  1. <script>
  2. alert(a);
  3. a=1;
  4. </script>

2: eval中创建的局部变量是不会被提升的(它也没办法做到).

[html] view plain copy

  1. <script>
  2. var a = 1;
  3. function t(){
  4. alert(a);
  5. eval(‘var a = 2‘);
  6. alert(a);
  7. }
  8. t();
  9. alert(a);
  10. </script>


原文地址:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

From: http://blog.csdn.net/sunxing007

时间: 2024-12-19 13:00:03

Javascript作用域和变量提升的相关文章

JavaScript作用域与声明提升【翻译】

原文链接:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html JavaScript作用域与声明提升 你知道下面JavaScript执行后alert的值吗? 1 var foo = 1; 2 function bar() { 3 if (!foo) { 4 var foo = 10; 5 } 6 alert(foo); 7 } 8 bar(); 如果因为结果为“10”而吃惊,那么可能你需要好好看看这篇文章: 1 va

作用域与变量提升的面试题方法总结

前言:下面的方法能快速的解面试题,主要针对=>作用域与变量提升的面试题.并且没有this改变指向的情况 (有错或者不足的地方,随时修改补充) 1.没有参数的时候:看有没有var,或者函数申明(也就是说如果有变量提升,函数体内就变成私有变量了,函数体内修改了不会影响父级.) 有,子集是undefined,也不会找父级,下面修改了(简单和复合类型)都不会影响父级. 没有,子集找不到,会找到父级,下面修改了(简单和复合类型)都会影响父级. 2.有参数的时候:(有传参,函数体内就变成私有变量了,函数体内

js 作用域,变量提升

先看下面一段代码: 1 var a = 0; 2 alert("1st alert : a = " + a); 3 function fun(){ 4 alert("2nd alert : a = " + a); 5 var a = 1; 6 setTimeout(function(){ 7 alert("3rd alert : a = " + a); 8 a = 2; 9 },1000); 10 a = 3; 11 setTimeout(fun

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

作用域(Scoping) javascript作用域之所以迷惑,是因为它程序语法本身长的像C家族的语言.我对作用域的理解是只会对某个范围产生作用,而不会对外产生影响的封闭空间.在这样的一些空间里,外部不能访问内部变量,但内部可以访问外部变量. c语言的变量分为全局变量和局部变量,全局变量的作用范围是任何文件和函数访问(当然,对于非变量定义的其他c文件,需要使用extern关键字进行申明,使用static关键字也可以将作用范围限定在当前文件中),局部变量的作用范围就是从申明到最近的大括号涵盖的块级

JavaScript中的变量提升和严格模式

1.什么是变量提升 所谓的变量提升指的是:函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体(作用域)的最顶部. //先声明后使用 var x; console.log(x);// undefined //先试用后声明 console.log(y);// undefined var y; //上面的式子可以写成下面的样子 变量提升:把x,y提升到顶部 var x; var y; console.log(x); console.log(y); ①变量的使用有两种形式,先声明再使用或

初识javascript 闭包和变量提升

先上一小段代码: 1 function outFun(){ 2 var num = 2; 3 function inFun(){ 4 console.log(num); 5 } 6 return inFun; 7 } 8 var out = outFun(); 9 out();//2,这里能够访问,其实是把num这个变量往上面一层提升了一下,out()往里面执行了一层.刚刚好在同一层. 10 console.log(num);//浏览器报错,不能访问outFun函数里面的局部变量num 敲黑板,

作用域、变量提升、函数提升、数据类型

一.作用域分类 (作用域范围内定义的变量,整个作用域都可以访问) 1. 全局作用域 使用var声明(或者不严格模式下没有声明)且在函数外定义的变量,其作用域范围是全局的,称其为全局作用域. 2. 函数作用域 使用var声明且在函数内部定义的变量,其作用域范围是整个函数,称其为函数作用域. 3. 块作用域(ES6) 使用let(或者const 常量)声明且在一个花括号(非函数)里面,其作用域范围就是这个花括号以内,称其为块作用域. 二.变量提升 (变量声明会提升至函数或者语句的最前面,位置还在其作

javas基础03——函数的作用域及变量提升

1.作用域 作用域,变量在函数内部作用的范围/区域.有函数的地方就有作用域. 2.局部作用域和全局作用域 function fn(){ var a = 1; } console.log(a); //报错:a is not defined 用var声明的变量,实际上是有作用域的 在函数内部定义的变量,该变量的作用域是整个函数体,在函数体外不可引用该变量,这是局部作用域 变量的生命周期: 1.永远存在----全局 程序没关,一直占用内存,少用全局 2.朝生暮死----局部 函数的大括号开头到函数的大

javascript闭包、变量提升

一.add(2)(3)求和函数(闭包) 1 function add(x) { 2 var a = x; 3 return function(b) { 4 sum = a + b; 5 console.log(sum); 6 } 7 } 8 add(2)(3); //5 二.变量提升 1 //第一种情况 2 3 var a = 100; //全局变量 4 5 function test() { 6 alert(a); //向上搜索全局变量 -->100 7 a = 10; //没有var修饰,所