变量在程序中随处可见。它们是一些始终在相互影响,相互作用的的数据和逻辑。正是这些互动使应用程序活了起来。

在JavaScript中使用变量很重要的一方面就是变量的提升 —— 它决定了一个变量何时可以被你的代码使用。如果你在寻找关于这方面的详细介绍,那你算是来对地方了。让我们一起看看吧。

1. 简介

 

提升是一种将变量和函数的声明移到函数作用域(如果不在任何函数内的话就是全局作用域)最顶部的机制。

提升影响了变量的生命周期,一个变量的生命周期包含3个阶段:

  • 声明 - 创建一个新变量,例如var myValue

  • 初始化 - 用一个值初始化变量 例如myValue = 150

  • 使用 - 使用变量的值 例如alert(myValue)

这个过程通常是像这样执行的:首先声明一个变量,然后用一个值给它初始化,最后就是使用它。让我们看一个例子:

// 声明

var strNumber;

// 初始化

strNumber = '16';

// 使用

parseInt(strNumber); // => 16

在程序中一个函数可以先声明,后使用初始化被忽略掉了。例如:

// 声明

function sum(a, b) {

 return a + b;

}

// 使用

sum(5, 6); // => 11

当这三个步骤按顺序执行的时候,一切看起来都很简单,自然。如果可以话,在使用JavaScript编程的时候你应该遵循这种模式。

JavaScript并没有严格遵循这个顺序,因此提供了更多的灵活性。比如,函数的使用可以在声明之前。下边的例子先调用了函数double(5),然后才声明该函数function double(num) {...}

// 使用

double(5); // => 10

// 声明

function double(num) {

 return num * 2;

}

这是因为JavaScript中的函数声明会被提升到作用域顶部。

变量提升在不同方面的影响也不同:

  • 变量声明: 使用varletconst关键字

  • 函数声明: 使用function () {...}语法

  • 类声明: 使用class关键字

接下来让我们详细看看这些区别。

2. 函数作用域变量: var

变量声明在函数作用域内创建并初始化一个变量,例如var myVar, myVar2 = 'Init'。默认情况下,声明但是未初始化的变量的值是undefined

自从JavaScript的第一版本开始,开发者就在使用这种方式声明变量。

// 声明变量num

var num;

console.log(num); // => undefined

// 声明并初始化变量str

var str = 'Hello World!';

console.log(str); // => 'Hello World!'

提升与var

使用var声明的变量会被提升到所在函数作用域的顶部。如果在声明之前访问该变量,它的值会是undefined

假定myVariable在被var声明前被访问到。在这种情况下,声明操作会被移至double()函数的顶部同时该变量会被赋值undefined

function double(num) {

 console.log(myVariable); // => undefined

 var myVariable;

 return num * 2;

}

double(3); // => 6

JavaScript在执行代码时相当于把var myVariable移动到了double()的顶部,就像下面这样:

function double(num) {

 var myVariable;          // 被移动到顶部

 console.log(myVariable); // => undefined

 return num * 2;

}

double(3); // => 6

 

var语法不仅可以声明变量,还可以在声明的同时赋给变量一个初始值:var str = 'initial value'。当变量被提升时,声明会被移动到作用域顶部,但是初始值的赋值却会留在原地

function sum(a, b) {

 console.log(myString); // => undefined

 var myString = 'Hello World';

 console.log(myString); // => 'Hello World'

 return a + b;

}

sum(16, 10); // => 26

 

var myString被提升到作用域的顶部,然而初始值的赋值操作myString = 'Hello World'不会受到影响。上边的代码等价于下边的形式:

function sum(a, b) {

 var myString;             // 提升到顶部

 console.log(myString);    // => undefined

 myString = 'Hello World'; // 赋值不受影响

 console.log(myString);    // => 'Hello World'

 return a + b;

}

sum(16, 10); // => 26

3. 块级作用域变量: let

let声明在块级作用域内创建并初始化一个变量:let myVar, myVar2 = 'Init'。默认情况下,声明但是未初始化的变量的值是undefined

 

letECMAScript 6引入的一个巨大改进,它允许代码在代码块的级别上保持模块性和封装性:

if (true) {

 // 声明块级变量

 let month;

 console.log(month); // => undefined  

 // 声明并初始化块级变量

 let year = 1994;

 console.log(year); // => 1994

}

// 在代码块外部不能访问month和year变量

console.log(year); // ReferenceError: year is not defined

提升与let

使用let定义的变量会被提升到代码块的顶部。但是如果在声明前访问该变量,JavaScript会抛出异常ReferenceError: is not defined

在声明语句一直到代码库的顶部,变量都好像在一个临时死亡区间中一样,不能被访问。

让我们看看以下的例子:

function isTruthy(value) {

 var myVariable = 'Value 1';

 if (value) {

   /**

    * myVariable的临时死亡区间

    */

   // Throws ReferenceError: myVariable is not defined

   console.log(myVariable);

   let myVariable = 'Value 2';

   // myVariable的临时死亡区间至此结束

   console.log(myVariable); // => 'Value 2'

   return true;

 }

 return false;

}

isTruthy(1); // => true

let myVariable一行一直到代码块开始的if (valaue) {...}都是myVariable变量的临时死亡区间。如果在此区间内访问该变量,JavaScript会抛出ReferenceError异常。

一个有趣的问题出现了:myVariable真的被提升到代码块顶部了吗?还是在临时死亡区间内未定义呢?当一个变量未被定义时,JavaScript也会抛出ReferenceError

如果你观察一下该函数的开始部分就会发现,var myVariable = 'Value 1'在整个函数作用域内定义了一个名为myVariable的变量。在if (value) {...}块内,如果let定义的变量没有被提升,那么在临时死亡区间内myVariable的值就会是'Value1'了。由此我们可以确认块级变量确实被提升了。

 

let在块级作用域内的提升保护了变量不受外层作用域影响。在临时死亡区间内访问let定义的变量时抛出异常会促使开发者遵循更好的编码实践:先声明,后使用。

这两个限制是促使在封装性和代码流程方面编写更好的JavaScript的有效途径。这是基于var用法教训的结果 —— 允许在声明之前访问变量很容易造成误解。

4. 常量: const

常量声明在块级作用域内创建并初始化一个常量:const MY_CONST = 'Value', MY_CONST2 = 'Value 2'。看看下边的示例:

const COLOR = 'red';

console.log(COLOR); // => 'red'

const ONE = 1, HALF = 0.5;

console.log(ONE);   // => 1

console.log(HALF);  // => 0.5

当声明一个变量时,必须在同一条语句中对该变量进行初始化。在声明与初始化之后,变量的值不能被修改。

const PI = 3.14;

console.log(PI); // => 3.14

PI = 2.14; // TypeError: Assignment to constant variable

提升与const

使用const定义的常量会被提升到代码块的顶部。

由于存在临时死亡区间,常量在声明之前不能被访问。如果在声明之前访问常量,JavaScript会抛出异常:ReferenceError: is not defined

 

const声明常量的提升的效果与使用let声明变量的提升效果相同。

让我们在double()函数内声明一个常量:

function double(number) {

  // 常量TWO的临时死亡区间

  console.log(TWO); // ReferenceError: TWO is not defined

  const TWO = 2;

  // 常量TWO的临时死亡区间至此结束

  return number * TWO;

}

double(5); // => 10

由于在声明之前使用常量会导致JavaScript抛出异常。因此使用常量时应该始终先声明,初始化,然后再使用。

5. 函数声明

函数声明使用提供的名称和参数创建一个函数。

一个函数声明的例子:

function isOdd(number) {

  return number % 2 === 1;

}

isOdd(5); // => true

 

function isOdd(number) {...}就是一段定义函数的声明。isOdd()用来验证一个数字是否是奇数。

提升与函数声明

函数声明的提升允许你在所属作用域内任何地方使用该函数,即使是在声明之前也可以。换句话说,函数可以在当前作用域或子作用域内的任何地方访问(不会是undefined值,没有临时死亡区间,不会抛出异常)。

这种提升的行为非常灵活。不管是先使用,后声明,还是先声明,后使用都可以如你所愿。

下边的例子在开始的地方调用了一个函数,然后才定义它:

// 调用被提升的函数

equal(1, '1'); // => false

// 函数声明

function equal(value1, value2) {

  return value1 === value2;

}

这段代码可以正常执行是因为equal()被提升到了作用域顶部。

需要注意的函数声明function () {...}函数表达式 var = function() {...}之间的区别。两者都用于创建函数,但是却有着不同的提升机制。

下边的例子演示了该区别:

// 调用被提升的函数

addition(4, 7); // => 11

// 变量被提升了,但值是undefined

substraction(10, 7); // TypeError: substraction is not a function

// 函数声明

function addition(num1, num2) {

  return num1 + num2;

}

// 函数表达式

var substraction = function (num1, num2) {

 return num1 - num2;

};

 

addition被彻底的提升并且可以在声明之前被调用。

然而substraction是使用变量声明语句声明的,虽然也被提升了,但被调用时值是undefined。因此会抛出异常TypeError: substraction is not a function

6. 类声明

类声明使用提供的名称和参数创建一个构造函数。类是ECMAScript 6中引入的一项巨大改进。

类建立在JavaScript的原型继承之上并提供了诸如super(访问父类),static(定义静态方法),extends(定义子类)之类的额外功能。

一起看看如何声明一个类并创建一个实例:

class Point {

  constructor(x, y) {

    this.x = x;

    this.y = y;

  }

  move(dX, dY) {

    this.x += dX;

    this.y += dY;

  }

}

// 创建实例

var origin = new Point(0, 0);

// 调用实例的方法

origin.move(50, 100);

提升与class

类声明会被提升到块级作用域的顶部。但是如果你在声明前使用类,JavaScript会抛出异常ReferenceError: is not defined。所以正确的方法是先声明类,然后再使用它创建实例。

类声明的提升与let定义变量的提升效果类似。

让我们看看在声明之前创建类实例会怎样:

// 使用Company类

// 抛出ReferenceError: Company is not defined

var apple = new Company('Apple');

// 类声明

class Company {

 constructor(name) {

   this.name = name;

 }

}

// 在声明之后使用Company类

var microsoft = new Company('Microsoft');

和预期的一样,在类定义之前执行new Company('Apple')会抛出ReferenceError异常。这很不错,因为JavaScript鼓励先声明后使用的方式。

还可以用使用了变量声明语句(varletconst)的类表达式创建类。让我们看看下面的场景:

// 使用Sqaure类

console.log(typeof Square);   // => 'undefined'

//Throws TypeError: Square is not a constructor

var mySquare = new Square(10);

// 使用变量声明语句声明类

var Square = class {

 constructor(sideLength) {

   this.sideLength = sideLength;

 }

 getArea() {

   return Math.pow(this.sideLength, 2);

 }

};

// 在声明之后使用Square类

var otherSquare = new Square(5);

这个类使用变量声明语句var Square = class {...}定义。Square变量被提升到了作用域的顶部,但是直到声明该变量的这行代码之前其值都是undefined。因此var mySquare = new Square(10)语句相当于把undefined当作构造函数使用,这导致JavaScript抛出TypeError: Square is not a constructor异常。

7. 最后的想法

像在本文阐述的那样,JavaScript中的提升有多种形式。即使你知道它的工作方式,一般还是建议按照声明 > 初始化 > 使用的顺序使用变量。通过letconstclass提升的方式可以看出ECMAScript 6强烈支持这种使用方式。这将使你免于遇见不期望的变量,undefined以及ReferenceError的困扰。

有一种函数可以在定义之前执行的例外情况。这就是当开发者需要快速了解函数如何被调用的时候,因为这样就不必再滚动到底部阅读函数的具体实现方式了。

160622、详解JavaScript变量提升的更多相关文章

  1. 详解JavaScript变量提升

    变量在程序中随处可见.它们是一些始终在相互影响,相互作用的的数据和逻辑.正是这些互动使应用程序活了起来. 在JavaScript中使用变量很重要的一方面就是变量的提升 —— 它决定了一个变量何时可以被 ...

  2. js对象详解(JavaScript对象深度剖析,深度理解js对象)

    js对象详解(JavaScript对象深度剖析,深度理解js对象) 这算是酝酿很久的一篇文章了. JavaScript作为一个基于对象(没有类的概念)的语言,从入门到精通到放弃一直会被对象这个问题围绕 ...

  3. 详解Javascript的继承实现(二)

    上文<详解Javascript的继承实现>介绍了一个通用的继承库,基于该库,可以快速构建带继承关系和静态成员的javascript类,好使用也好理解,额外的好处是,如果所有类都用这种库来构 ...

  4. 详解js变量、作用域及内存

    详解js变量.作用域及内存 来源:伯乐在线 作者:trigkit4       原文出处: trigkit4    基本类型值有:undefined,NUll,Boolean,Number和Strin ...

  5. 【转】详解JavaScript中的this

    ref:http://blog.jobbole.com/39305/ 来源:foocoder 详解JavaScript中的this JavaScript中的this总是让人迷惑,应该是js众所周知的坑 ...

  6. 详解javascript中的this对象

    详解javascript中的this对象 前言 Javascript是一门基于对象的动态语言,也就是说,所有东西都是对象,一个很典型的例子就是函数也被视为普通的对象.Javascript可以通过一定的 ...

  7. 详解JavaScript调用栈、尾递归和手动优化

    调用栈(Call Stack) 调用栈(Call Stack)是一个基本的计算机概念,这里引入一个概念:栈帧. 栈帧是指为一个函数调用单独分配的那部分栈空间. 当运行的程序从当前函数调用另外一个函数时 ...

  8. 详解javascript的类

    前言 生活有度,人生添寿. 原文地址:详解javascript的类 博主博客地址:Damonare的个人博客 Javascript从当初的一个"弹窗语言",一步步发展成为现在前后端 ...

  9. 回归基础: JavaScript 变量提升

    from me: javascript的变量声明具有hoisting机制,它是JavaScript一个基础的知识点,也是一个比较容易犯错的点,平时在开发中,大大小小的项目都会遇到. 它是JavaScr ...

随机推荐

  1. 记一次.net core调用SOAP接口遇到的问题

    背景        最近需要将一些外部的Web Service及其他SOAP接口的调用移到一个独立的WebAPI项目中,然后供其他.Net Core项目调用.之前的几个Web Service已经成功迁 ...

  2. centos7 crontab 定时执行python任务不执行的原因及解决办法

    1.问题描述 在用crontab设置定时任务时,发现py脚本在crontab中报错,显示import某些包找不到,但是手动直接运行py脚本,完全正常.   01 05 * * * ./get_topi ...

  3. EMQ学习笔记---Clean Session和Retained Message

    MQTT会话(Clean Session)MQTT客户端向服务器发起CONNECT请求时,可以通过’Clean Session’标志设置会话.‘Clean Session’设置为0,表示创建一个持久会 ...

  4. centos7防火墙的关闭

    从centos7开始使用systemctl来管理服务和程序,包括了service和chkconfig. #systemctl list-unit-files|grep firewalld.servic ...

  5. php serialize序列化对象或者数组

    serialize序列化对象或者数组 $str=serialize(array('a'=>1,'b'=>2)); echo $str; 输入出a:2:{s:1:"a"; ...

  6. C#使用技巧之调用JS脚本(转)

    .创建个Winform项目. .在From1上增加一个文本框一个按钮. .在解决方案中创建一个test.js文件. test.js代码如下: function sayHello(str) { retu ...

  7. 运行百度语音识别官方iOS demo报错: load offline engine failed: 4001

    运行官方BDVRClientSample这个demo(ios版的),demo可以安到手机上,但是点“识别UI”那个按钮后“授权验证失败”.如果点“语音识别”那个按钮,控制台输出:2015-10-23 ...

  8. [求助] 关于DDR3的读写操作,看看我的流程对吗?

    [求助] 关于DDR3的读写操作,看看我的流程对吗? 最近简单调了一下KC705开发板上面的DDR3,型号是MT8JTF12864HZ-1G6:有时候加载程序后,发现读出数据不是写进去的,在这将我的操 ...

  9. PHPEXCEL在thinkphp中封装成类使用

    PHPEXCEL在thinkphp中封装成类使用 标签: phpexcel导出导入thinkphp -- : 435人阅读 评论() 收藏 举报 分类: php() 版权声明:本文为博主原创文章,未经 ...

  10. mongodb数据库shell

    mongoexport -d mofangdb -c log_user_access_index --type=csv -f _id,uid,page,date -o log_user_access_ ...