提升----你所不知道的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完全完成后,所有的工作 ...
随机推荐
- Python:GUI之tkinter学习笔记2界面布局显示
相关内容: pack 介绍 常用参数 使用情况 常用函数 grid 介绍 常用参数 使用情况 常用函数 place 介绍 常用参数 使用情况 常用函数 首发时间:2018-03-04 14:20 pa ...
- weblogic系列漏洞整理 -- 1. weblogic安装
目录 0. 概述 1. 下载安装Java环境 2. 下载安装weblogic 安装 部署domain域 进入weblogic 3. 排错 如果出现如下错误 0. 概述 WebLogic是美国Oracl ...
- Luncene介绍
1.Luncene介绍 案例:实现一个文件的搜索功能,通过关键字搜索文件,凡是文件名或文件内容包括关键字文件都需要找出来.还可以根据中文词语进行查询,并且需要支持多个条件查询.Lucene可以解决 数 ...
- 03-openldap服务端安装配置
openldap服务端安装配置 阅读目录 基础环境准备 安装openldap服务端 初始化openldap配置 启动OpenLDAP 重新生成配置文件信息 规划OpenLDAP目录树组织架构 使用GU ...
- AIX mount nfs 文件系统失败
报 mount: 1831-008 的错,配置系统参数后恢复. 操作系统版本为: # oslevel 6.1.0.0 LOG如下: # mount 192.168.240.69:/xyz/xvdh2/ ...
- [MapReduce_8] MapReduce 中的自定义分区实现
0. 说明 设置分区数量 && 编写自定义分区代码 1. 设置分区数量 分区(Partition) 分区决定了指定的 Key 进入到哪个 Reduce 中 分区目的:把相同的 Key ...
- spring IOC中三种依赖注入方式
Spring的核心思想是IOC和AOP,IOC-控制反转,是一个重要的面向对象编程的法则,用来消减计算机程序之间的耦合问题,控制反转一般分为两种类型,依赖注入和依赖查找,依赖什么?为什么需要依赖?注入 ...
- (下一篇博客)提示5G信道
原本注册这个博客是要不定期更新一些产品的测试内容的 但由于一些个人原因并没有坚持去做到, 每次有点子的时候却没能来得及记下来导致很内容的缺失 接下来将关键点以图片形式 和一些摘要形式先发上来, 已做备 ...
- (转)Spring boot(一):入门篇
https://www.cnblogs.com/ityouknow/p/5662753.html#!comments 构建微服务:Spring boot 入门篇 什么是Spring Boot Spri ...
- [NOI2003],[AHOI2006]文本编辑器
嘟嘟嘟 [NOI2003]的其实就是一个板子--所以我就不说啥了. 唯一需要注意的是读入字符(哎--):题中说"中间可能有空格,请忽略"的意思是要在程序里特判掉,不是不管他-- 输 ...