很多编程语言在执行的时候都是自上而下执行,但实际上这种想法在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)的更多相关文章

  1. js值----你所不知道的JavaScript系列(6)

    1.数组 在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串.数字.对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的) .----<你所不知道的JavaS ...

  2. js类型----你所不知道的JavaScript系列(5)

    ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型.也有其他的叫法,比如原始类型和对象类型等. 1.内置类型 JavaScript 有七种内置类型: • 空值(null) • 未定义( ...

  3. 闭包----你所不知道的JavaScript系列(4)

    一.闭包是什么? · 闭包就是可以使得函数外部的对象能够获取函数内部的信息. · 闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. · 闭包就 ...

  4. let和const----你所不知道的JavaScript系列(2)

    let 众所周知,在ES6之前,声明变量的关键字就只有var.var 声明变量要么是全局的,要么是函数级的,而无法是块级的. var a=1; console.log(a); console.log( ...

  5. LHS 和 RHS----你所不知道的JavaScript系列(1)

      变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值.----<你所不知道的Ja ...

  6. 你所不知道的JavaScript数组

    相信每一个 javascript 学习者,都会去了解 JS 的各种基本数据类型,数组就是数据的组合,这是一个很基本也十分简单的概念,他的内容没多少,学好它也不是件难事情.但是本文着重要介绍的并不是我们 ...

  7. 你所不知道的javascript数组特性

    工作中,我们经常使用js的数组,但是,下面的东西你见过吗? 1,文本下标: var a=[]; a[-1]=1; 你想过数组的下标为负数的情况吗?我们对数组的下标规定从0开始.但是上面那么写也还是可以 ...

  8. JavaScript中你所不知道的Object(二)--Function篇

    上一篇(JavaScript中你所不知道的Object(一))说到,Object对象有大量的内部属性,而其中多数和外部属性的操作有关.最后留了个悬念,就是Boolean.Date.Number.Str ...

  9. 你所不知道的html5与html中的那些事第三篇

    文章简介: 关于html5相信大家早已经耳熟能详,但是他真正的意义在具体的开发中会有什么作用呢?相对于html,他又有怎样的新的定义与新理念在里面呢?为什么一些专家认为html5完全完成后,所有的工作 ...

随机推荐

  1. weblogic系列漏洞整理 -- 2. weblogic弱口令

    目录 二. weblogic弱口令 0. 思路 1. python爆破脚本 2. 技巧 一.weblogic安装 http://www.cnblogs.com/0x4D75/p/8916428.htm ...

  2. python第二十三天-----作业中

    #!usr/bin/env python #-*-coding:utf-8-*- # Author calmyan import os ,sys,time from core import trans ...

  3. WebSocket简单尝试

    System.Net.WebSockets.WebSocket 需要.NET 4.5,IIS8以上,Windows Server2008R2自带的IIS不支持,Windows8及Server2012以 ...

  4. nslookup debug

    Try adding forwarders to some public DNS servers leave the box ticked which says use root hints if f ...

  5. Vim和Vi的常用命令

    Vim 文本编辑器 1.Vim 和 Vi: 两者都是多模式编辑器: Vim 是 Vi 升级版,再兼容 Vi 所有指令的同时增加了一些新功能支持: 特点: 语法加亮:使用不同的颜色加亮代码: 多级撤销: ...

  6. mac系统如何在当前目录下打开终端

    给大家推荐一个好用的终端工具 Go2Shell:https://itunes.apple.com/cn/app/go2shell/id445770608?mt=12 在没有这个工具之前 找了好多在当前 ...

  7. Alpha冲刺! Day3 - 砍柴

    Alpha冲刺! Day3 - 砍柴 今日已完成 晨瑶:补充安卓技能树: review接口文档:看了点七牛云安卓API. 昭锡:没有团队项目相关贡献. 永盛: API 文档基本完成:根据 API 文档 ...

  8. Sring容器技术内幕之InstantiationStrategy类介绍

    引言 org.springframework.beans.factory.support.InstantiationStrategy负责根据BeanDefinition对象创建一个Bean实例.Spr ...

  9. Go学习笔记03-结构控制

    目录 条件语句 循环语句 条件语句 条件语句用 if 关键字来判定条件,如: func bounded(v int) int { if v > 100 { return 100 } else i ...

  10. webpack打包去掉console.log打印与debugger调试

    如图,找到build/webpack.prod.conf.js 在 UglifyJsPlugin 插件下添加下列代码 drop_debugger: true, drop_console: true