提升----你所不知道的JavaScript系列(3)
很多编程语言在执行的时候都是自上而下执行,但实际上这种想法在JavaScript中并不完全正确, 有一种特殊情况会导致这个假设是错误的。来看看下面的代码,
a = 2;
var a;
console.log( a );
console.log(a) 会输出什么呢?
有些人可能会认为是 undefined,因为 var a 声明在 a = 2 之后,他们自然而然地认为变量被重新赋值了,因此会被赋予默认值 undefined。但是,真正的输出结果是 2。
先不急为什么,我们再继续看另外一段代码,
console.log( a );
var a = 2;
鉴于上一个代码片段所表现出来的某种非自上而下的行为特点,你可能会认为这个代码片段也会有同样的行为而输出 2。还有人可能会认为,由于变量 a 在使用前没有先进行声明,因此会抛出 ReferenceError 异常。
其实不然,两种猜测都是不对的。输出来的会是 undefined。
提升
引擎会在解释 JavaScript 代码之前首先对其进行编译,简单地说,任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前,说通常是因为JavaScript 中存在两个机制可以“欺骗” 词法作用域: eval(..) 和 with)。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。这就是我们通常说的“提升”。
注:只有声明本身会被提升, 而赋值或其他运行逻辑会留在原地。
foo();
function foo() {
    console.log( a ); // undefined
    var a = 2;
}
每个作用域都会进行提升操作。所以 foo(..)函数自身也会在内部对 var a 进行提升(显然并不是提升到了整个程序的最上方)。在这里,你或许会发现,为什么代码里面是先调用 foo() ,再声明 foo() 这样的顺序,却不会报错。这是因为除了变量声明会在其作用域内提升之外,函数声明也具有相似的特效。因此这段代码可以暂时理解为下面的形式:
function foo() {
    var a;
    console.log( a ); // undefined
    a = 2;
} 
foo();
可以看到,函数声明会被提升在作用域的顶部。但是有一点需要和变量声明提升做区别的是:变量提升只是提升了变量的声明,而变量赋值并没有被提升。但是,函数的声明有点不一样,函数体也会一同被提升。
所以上面的一段暂时性的代码实际上可以这样理解:
var foo = {
    var a;
    console.log( a ); // undefined
    a = 2;
} 
foo();
foo 函数的声明(这个例子还包括实际函数的隐含值)被提升了,因此第一行中的调用可以正常执行。
然而并不是所有的函数都能提升!函数声明会被提升,但是函数表达式却不会被提升。
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
    // ...
};
上面这段程序中的变量标识符 foo() 被提升并分配给所在作用域,因此 foo() 不会导致 ReferenceError。但是 foo 此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。foo() 由于对 undefined 值进行函数调用而导致非法操作,因此抛出 TypeError 异常。
同时也要记住,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用:
foo(); // TypeError
bar(); // ReferenceError var foo = function bar() {
// ...
};
这个代码片段经过提升后,实际上会被理解为以下形式:
var foo; foo(); // TypeError
bar(); // ReferenceError foo = function() {
var bar = ...self...
// ...
}
这里我们说到具名函数表达式,就顺便插如一点具名函数表达式的知识点。我们看看下面的例子:
function test() {
   var fn = function fn1() {
        log(fn === fn1); // true
        log(fn == fn1); // true
   }
   fn();
   log(fn === fn1); // Uncaught ReferenceError: fn1 is not defined
   log(fn == fn1);  // Uncaught ReferenceError: fn1 is not defined
}
test();
看上面这例子,是不是很疑惑?
具名函数表达式,是带名字的函数赋值给一个变量,这个名字只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效。也就是说,这个函数名只能在此函数内部使用,可以理解为这个函数名成了函数体内部的一个变量。
这里还有一点需要注意的,函数定义了一个非标准的name属性,通过这个属性可以访问到给定函数指定的名字,这个属性的值永远等于跟在function关键字后面的标识符,匿名函数的name属性为空,而具名的函数表达式会修改到这个属性。
var foo = function(){
    //...
};
console.log(foo.name); //foo
var bar = function foobar(){
    //...
};
console.log(bar.name); //foobar  name值被修改
函数优先
函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复” 声明的代码中)是函数会首先被提升,然后才是变量。
看一下下面的代码:
foo(); //
var foo;
function foo() {
    console.log( 1 );
}
foo = function() {
    console.log( 2 );
};
会输出 1 而不是 2 ! 这个代码片段会被引擎理解为如下形式:
function foo() {
    console.log( 1 );
} 
foo(); //
foo = function() {
    console.log( 2 );
};
var foo 尽管出现在 function foo()... 的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。尽管重复的 var 声明会被忽略掉, 但出现在后面的函数声明还是可以覆盖前面的。
foo(); //
function foo() {
    console.log( 1 );
}
var foo = function() {
    console.log( 2 );
};
function foo() {
    console.log( 3 );
}
我们来看看下面这个,
function text1() {
   var a = 1;
   function b() {
       a = 10;
       return;
       function a() {}
    }
    b();
    console.log(a);   // ?
}
text1();
function text2() {
   var a = 1;
   function b() {
      a = 10;
      function a() {}
   }
   b();
   console.log(a);   // ?
}
text2();
想一想,这两段代码输出的结果会是什么?
结果都是1!为啥???
这里需要注意的是,在 function b() 中,function a() 由于存在函数提升,上述代码实际上的运行代码是这样子的,
function text{
    var a = 1;
    function b() {
        var a = function(){};
        a = 10;
        //return;  //这个return对这段代码没有任何影响
    }
    b();
    console.log(a);  1
}
是不是很神奇~~~~所以在写代码的时候,就要特别注意了,不要因为 JavaScript 的提升机制导致很多莫名其妙的bug出来。
最后还有一个要强调一下,由于一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:
foo(); // "b" var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}
function hoistVariable() {
    if (!foo) {
        var foo = 5;
    }
    console.log(foo); //
}
hoistVariable();
小结:
我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a和 a = 2 当作两个单独的声明, 第一个是编译阶段的任务,而第二个则是执行阶段的任务。这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。
声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引起很多危险的问题!
理解变量提升和函数提升可以使我们更了解这门语言,更好地驾驭它,但是在开发中,我们不应该使用这些技巧,而是要规范我们的代码,做到可读性和可维护性。具体的做法是:无论变量还是函数,都必须先声明后使用。
如果对于新的项目,可以使用let替换var,会变得更可靠,可维护性更高。值得一提的是,ES6中的class声明也存在提升,不过它和let、const一样,被约束和限制了,其规定,如果再声明位置之前引用,则是不合法的,会抛出一个异常。
所以,无论是早期的代码,还是ES6中的代码,我们都需要遵循一点,先声明,后使用。
提升----你所不知道的JavaScript系列(3)的更多相关文章
- js值----你所不知道的JavaScript系列(6)
		
1.数组 在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串.数字.对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的) .----<你所不知道的JavaS ...
 - js类型----你所不知道的JavaScript系列(5)
		
ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型.也有其他的叫法,比如原始类型和对象类型等. 1.内置类型 JavaScript 有七种内置类型: • 空值(null) • 未定义( ...
 - 闭包----你所不知道的JavaScript系列(4)
		
一.闭包是什么? · 闭包就是可以使得函数外部的对象能够获取函数内部的信息. · 闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. · 闭包就 ...
 - let和const----你所不知道的JavaScript系列(2)
		
let 众所周知,在ES6之前,声明变量的关键字就只有var.var 声明变量要么是全局的,要么是函数级的,而无法是块级的. var a=1; console.log(a); console.log( ...
 - LHS 和 RHS----你所不知道的JavaScript系列(1)
		
变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值.----<你所不知道的Ja ...
 - 你所不知道的JavaScript数组
		
相信每一个 javascript 学习者,都会去了解 JS 的各种基本数据类型,数组就是数据的组合,这是一个很基本也十分简单的概念,他的内容没多少,学好它也不是件难事情.但是本文着重要介绍的并不是我们 ...
 - 你所不知道的javascript数组特性
		
工作中,我们经常使用js的数组,但是,下面的东西你见过吗? 1,文本下标: var a=[]; a[-1]=1; 你想过数组的下标为负数的情况吗?我们对数组的下标规定从0开始.但是上面那么写也还是可以 ...
 - JavaScript中你所不知道的Object(二)--Function篇
		
上一篇(JavaScript中你所不知道的Object(一))说到,Object对象有大量的内部属性,而其中多数和外部属性的操作有关.最后留了个悬念,就是Boolean.Date.Number.Str ...
 - 你所不知道的html5与html中的那些事第三篇
		
文章简介: 关于html5相信大家早已经耳熟能详,但是他真正的意义在具体的开发中会有什么作用呢?相对于html,他又有怎样的新的定义与新理念在里面呢?为什么一些专家认为html5完全完成后,所有的工作 ...
 
随机推荐
- 如何创建和还原SQL Server 2005数据库?
			
在还原SQL Server 2005数据库文件之前,建议先把要还原的数据库文件复制粘贴到某个盘的根目录下,这样便于一会儿找到相关的文件,比如C盘. 先打开SQL Server 2005的Microso ...
 - 多浏览器播放wav格式的音频文件
			
html5的audio标签只在火狐下支持wav格式的音频播放,无法兼容IE和google , 使用audioplayer.js 基本上能支持大部分浏览器播放wav音频文件,经测试IE.火狐.googl ...
 - 上下文管理器——with语句的实现
			
前言 with语句的使用给我们带来了很多的便利,最常用的可能就是关闭一个文件,释放一把锁. 既然with语句这么好用,那我也想让我自己写的代码也能够使用with语句,该怎么实现? 下面具体介绍怎样实现 ...
 - eclipse版本和jdk对应关系
			
jdk最新版历史版本下载 http://www.oracle.com/technetwork/java/javase/downloads/index.html http://www.oracle.co ...
 - Ubuntu + python pip遇到的问题
			
今天在做Flask跨源资源共享(CORS)的时候在安装flask-cors时遇到了两个问题. 首先我是在Ubuntu环境下安装的,整了好一会才弄得出来,现在整理一下. 安装flask-cors pip ...
 - windows系统安装python3.6.3和python3.7.0
			
一.装备好从官网下载的python软件包(3.6.3和3.7.0) 二.先安装python3.6.3 1.运行python3.6.3文件 2.选择默认 3.下一步,等待安装 4.检查是否安装成功 ,安 ...
 - SAP ABAP 如何查找SMOD增强
			
1.查找程序名 T-CODE:SE93 2.查找开发类 T-code:se38 3.查找SMOD增强 T-CODE:SE16N.表:TADIR 4.查看增强具有哪些功能 T-CODE:SE16N.表: ...
 - 实用的php清除html,php去除空格与换行,php清除空白行和换行,提取页面纯文本
			
实用的php清除html,换行,空格类,php去除空格与换行,php清除空白行和换行,提取页面纯文本内容 方法一: function DeleteHtml($str) { $str = trim($s ...
 - 基于tomcat插件的maven多模块工程热部署(附插件源码)
			
内容属原创,转载请注明出处 写在前面的话 最近一直比较纠结,归根结底在于工程的模块化拆分.以前也干过这事,但是一直对以前的结果不满意,这会重操旧业,希望搞出个自己满意的结果. 之前有什么不满意的呢? ...
 - CRM项目之stark组件(2)
			
那么从今天开始呢,我们就要开始设计属于我们自己的admin组件,起个名字就叫stark吧(当然你愿意叫什么都可以). stark组件之四步走 仿照admin组件实现流程,stark组件要实现四件事情: ...