资讯专栏INFORMATION COLUMN

var,let和const深入解析(一)

Hegel_Gu / 1171人阅读

摘要:很显然,局部作用域只能在内部访问。第一次被称为编译状态,这一次的话,代码中定义的变量就会提升。严格来说,我们变量的声明已经被移动了位置,而不是声明的相关代码被移动了位置。

es6有许多特别棒的特性,你可能对该语言的整体非常熟悉,但是你知道它在内部是如何工作的吗?当我们知道它的内部原理以后,我们使用起来也会更加的安心一些。这里我们想逐步的引导你,让你对其有一个更深入,更浅显的认识。让我们就先从es6中的变量开始讲起吧。

let和const

在es6中新引入了两种方式来申明变量,我们仍然可以使用广为传诵的var变量(然而你不应该继续使用它了,继续阅读来了解其中原因),但是现在我们有了两种更牛的工具去使用:let和const。

let

let和var非常的相似,在使用方面,你可以使用完全相同的方式来声明变量,例如:

let myNewVariable = 2;
var myOldVariable = 3;

console.log(myNewVariable); // 2
console.log(myOldVariable); // 3

但是实际上,他们之间有几处明显的不同。他们不仅仅是关键字变了,而且实际上它还让会简化我们的一些工作,防止一些奇怪的bug,其中这些不同点是:

let是块状作用域(我将会在文章后面着重讲一下作用域相关的东西),而var是函数作用域。

let不能在定义之前访问该变量(var是可以的,它确实是js世界中许多bug和困扰的源头)。

let不能被重新定义。

在我们讲解这些不同点之前,首先我们看一个更酷的变量:const

const

const和let非常像(跟var相比来说,他们之间有许多相同点),但是他们有一个主要的不同点:let可以重新赋值,但是const不能。因此const定义的变量只能有一个值,并且这个值在声明的时候就会被赋值。因此我们来看下下面的例子。

const myConstVariable = 2;
let myLetVariable = 3;

console.log(myConstVariable); // 2
myLetVariable = 4;  // ok
myConstVariable = 5;  //wrong - TypeError thrown

但是const是完全不可变的吗?

有一个常见的问题:虽然变量不能被重新赋值,但是也不能让他们真正的变为不可变的状态。如果const变量有一个数组或者对象作为其值的话,你可能会像下面的代码一样改变它的值。

const myConstObject = {mutableProperty: 2};

// myConstObject = {}; - TypeError
myConstObject.mutableProperty = 3; //ok
console.log(myConstObject.mutableProperty); // 3
const myConstArray = [1];

// myConstArray = []; - TypeError
myConstArray.push(2) //ok
console.log(myConstArray); // [1, 2]

当然你不能用原始数据类型的值,比如string,number,boolean等,因为他们本质上是不可变的。

真正的不可变

如果你想让我们的变量真正的不可变的话,可以使用Object.freeze(), 它可以让你的对象保持不可变。不幸的是,他仅仅是浅不可变,如果你对象里嵌套着对象的话,它依然是可变的。

const myConstNestedObject = {
  immutableObject: {
    mutableProperty: 1
  }
};

Object.freeze(myConstNestedObject);

myConstNestedObject.immutableObject = 10; // won"t change
console.log(myConstNestedObject.immutableObject); // {mutableProperty: 1}
myConstNestedObject.immutableObject.mutableProperty = 10; // ok
console.log(myConstNestedObject.immutableObject.mutableProperty); // 10

变量的作用域

在介绍了一些基础知识以后,下面我们要进入一个更高级的话题。现在我们要开始讲解es5和es6变量中的第一个不同-作用域

注意:下面的例子都用的是let,它的规则在const上同样也适用

全局变量和函数作用域变量

在js中,究竟什么是作用域呢?本文不会给出一个关于作用域的完整解释。简单来说,变量的作用域决定了变量的可用位置。从不同的角度来看,可以说作用域是你可以在特定区域内使用的那些变量(或者是函数)的声明。作用域可以是全局的(因此在全局作用域中定义的变量可以在你代码中任何部分访问)或者是局部的。

很显然,局部作用域只能在内部访问。在ES6以前,它仅仅允许一种方式来定义局部作用域 - function,咱们来看一下下面的例子:

// global scope
var globalVariable = 10;

function functionWithVariable() {
  // local scope
  var localVariable = 5;
  console.log(globalVariable);  // 10
  console.log(localVariable);   // 5
}

functionWithVariable();

//global scope again
console.log(globalVariable);  // 10
console.log(localVariable);   // undefined

上面的例子中,变量globalVariable是全局变量,所以它可以在我们代码中的函数内或者是其他区域内被访问到,但是变量localVariable定义在函数内,所以它只在函数内可访问。

因此,所有在函数内创建的内容都可以在函数内被访问到,包括函数内部里所有的嵌套函数(可能会被嵌套多层)。在这里可能要感谢闭包了,但是在文章里我们并不打算介绍它。不过请继续关注,因为我们在未来的博文中,会更多的介绍它。

提升

简单来说,提升是一种吧所有的变量和函数声明“移动”到作用域的最前面的机制。让我们看一下下面的例子。

function func() {
  console.log(localVariable);   // undefined
  var localVariable = 5;

  console.log(localVariable);   // 5
}

func();

它为什么依然会正常工作呢?我们还没有定义这个变量,但是它依然通过console.log()打印出了undefined。为什么不会报出一个变量未定义的错误呢?让我们再仔细看一下。

编译变量

Javascript解析器要遍历这个代码两次。第一次被称为编译状态,这一次的话,代码中定义的变量就会提升。在他之后,我们的代码就变成类似于下面的这样子的(我已经做了一些简化,只展示出相关的部分)。

function func() {
  var localVariable = undefined;

  console.log(localVariable); // undefined
  localVariable = 5;

  console.log(localVariable); // 5
}

func();

我们看到的结果是,我们的变量localVariable已经被移动到func函数的作用域的最前面。严格来说,我们变量的声明已经被移动了位置,而不是声明的相关代码被移动了位置。我们使用这个变量并打印出来。它是undefined是因为我们还没有定义它的值,它默认使用的undefined。

提升的例子 - 会出什么问题

来让我们看一个令人讨厌的例子,我们的作用域范围对于我们来说,是弊大于利的。也不是说函数作用域是不好的。而是说我们必须要警惕一些由于提升而引起的一些陷进。我们来看看下面的代码:

var callbacks = [];
for (var i = 0; i < 4; i++) {
  callbacks.push(() => console.log(i));
}

callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();

你认为输出的值是多少呢?你猜可能是0 1 2 3,是吗?如果是的话,对于你来说,可能会有一些惊喜。实际上,他真实的结果是4 4 4 4。等等,它到底发生了什么?我们来“编译”一下代码,代码现在看起来就像这样:

var callbacks;
var i;

callbacks = [];
for (i = 0; i < 4; i++) {
  callbacks.push(() => console.log(i));
}

callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();

你看出问题所在了吗?变量i在整个作用域下都是可以被访问到的,它不会被重新定义。它的值只会在每次的迭代中不断地被改变。然后呢,当我们随后想通过函数调用打印它的值得时候,他实际上只有一个值 - 就是在最后一次循环赋给的那个值。

我们只能这样了吗?不是的

Let和Const的拯救

除了定义变量的新方式以外,还引入了一种新的作用域:块级作用域。块就是由花括号括起来的所有的内容。所以它可以是if,while或者是for声明中的花括号,也可以是多带带的一个花括号甚至是一个函数(对,函数作用域是块状作用域)。let和const是块作用域。意味着无论你在块中无论定义了什么变量,什么时候定义的,它都不会跑到块作用域外面去。我们来看一下下面的例子:

function func() {
  // function scope
  let localVariable = 5;
  var oldLocalVariable = 5;

  if (true) {
    // block scope
    let nestedLocalVariable = 6;
    var oldNestedLocalVariable = 6;

    console.log(nestedLocalVariable); // 6
    console.log(oldNestedLocalVariable); // 6
  }

  // those are stil valid
  console.log(localVariable); // 5
  console.log(oldLocalVariable); // 5
  // and this one as well
  console.log(oldNestedLocalVariable); // 6
  // but this on isn"t
  console.log(nestedLocalVariable); // ReferenceError: nestedLocalVariable is not defined

你能看出来差别吗?你能看出来怎么使用let来解决早些时候提出问题的吗?我们的for循环包含一组花括号,所以它是块作用域。所以如果在定义循环变量的时候,使用的是let或者是const来代替var的话,代码会转为下面的形式。注意:我实际上已经简化了很多,不过我确定你能理解我的意思。

let callbacks = [];
for (; i < 4; i++) {
  let i = 0 //, 1, 2, 3
  callbacks.push(() => console.log(i));
}

callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();

现在的每一次循环都有它自己的变量定义,所以变量不会被重写,我们确信这行代码可以完成让他做的任何事情。

这是这一部分结束的例子,但是我们再看一下下面的例子,我相信你明白打印出来的值的原因,以及对应的表现是什么。

function func() {
  var functionScopedVariable = 10;
  let blockScopedVariable = 10;

  console.log(functionScopedVariable);  // 10
  console.log(blockScopedVariable);  // 10
  if (true) {
    var functionScopedVariable = 5;
    let blockScopedVariable = 5;

    console.log(functionScopedVariable);  // 5
    console.log(blockScopedVariable);  // 5
  }

  console.log(functionScopedVariable);  // 5
  console.log(blockScopedVariable);  // 10
}

func();

本文翻译自:

https://blog.pragmatists.com/...

本文转载自:http://www.lht.ren/article/15/

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101656.html

相关文章

  • Javascript基础之-varletconst深入解析(二)

    摘要:规范对其是这样进行的描述的。声明定义了在正在运行的执行上下文作用域内的变量环境中的变量。在执行时,由带有的定义的变量被赋其设定项的的值。由于变量已经被声明,是可访问的,因此会打印出正确的结果。 你想在在变量声明之前就使用变量?以后再也别这样做了。 新的声明方式(let,const)较之之前的声明方式(var),还有一个区别,就是新的方式不允许在变量声明之前就使用该变量,但是var是可以...

    XiNGRZ 评论0 收藏0
  • 深入理解ES6 () 块级绑定

    摘要:声明声明的语法与的语法一致。总结文章都是以深入理解读书笔记形式,大部分引用书中的定义,加上作者的理解,样例也做了调整,所有样例都可以放到里运行亲自尝试。 1.变量提升 使用 var 关键字声明的变量,无论其实际声明位置在何处,都会被视为声明于所在函数的 顶部(如果声明不在任意函数内,则视为在全局作用域的顶部)。这句话从字面上不难理解。 但是他是怎样一个过程,为什么会这样。当你代...

    KunMinX 评论0 收藏0
  • 深入理解letvar的区别(暂时性死区)!!!

    摘要:会出现这样的情况是因为拥有暂时性死区。规定暂时性死区和语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。 首先我们应该知道js引擎在读取js代码时会进行两个步骤: 第一个步骤是解释。 第二个步骤是执行。 所谓解释就是会先通篇扫描所有的Js代码,然后把所有声明提升到顶端,第二步是执行,执行就是操作一类的。 我们先来看个简单的变量提升...

    tanglijun 评论0 收藏0
  • 深入编译器——第部分:词法解析Scanner(介绍ECMAScript的词法规范TypeScr

    摘要:词法分析对构成源程序的字符流进行扫描然后根据构词规则识别单词也称单词符号或符号。语义分析是编译过程的一个逻辑阶段语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查进行类型审查,审查抽象语法树是否符合该编程语言的规则。 1. 文章的内容和主题 我对编译器的深入了解起源于一条推特中的问题:Angular是如何用Angular预先编译器(AOT)对静态代码进行解析工作的。在进行一些...

    pingan8787 评论0 收藏0
  • 深入了解babel(二)

    摘要:接着上一篇文章深入了解一的处理步骤的三个主要处理步骤分别是解析,转换,生成。模块是的代码生成器,它读取并将其转换为代码和源码映射抽象语法树抽象语法树在以上三个神器中都出现过,所以对于编译器来说至关重要。 接着上一篇文章《深入了解babel(一)》 Babel 的处理步骤 Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。对...

    ChanceWong 评论0 收藏0

发表评论

0条评论

Hegel_Gu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<