likes
comments
collection
share

ES6之变量声明面试进阶一网打尽

作者站长头像
站长
· 阅读数 53

ES6之变量声明面试进阶一网打尽

概要:"且夫水之积也不厚,其负大舟也无力",基础的重要性不言而喻。在任何编程语言中最基础的语法莫过于变量,所有的存储计算都离不开它,虽然在不同的语言中对于变量的规范有所不同,但本质上它都是作为存储数据值的容器。变量虽然基础,但涉及内容较多且繁杂,今天我们重点讨论变量的声明,在JS中,由于历史原因,变量的声明以ES6为界,前后发生较大的变化和升级,这篇文章将带你拨开历史的云雾,全景立体的将变量声明的每个侧面清晰的呈现在你的面前,带你游历其中,彻底掌握JS的变量声明。

什么是变量

变量是存储数据值的容器,无论是用户的输入,磁盘存储的文件还是计算过程中的产生的数据,我们都需要变量来进行数据的读取、存储,亦或是加工处理。

既然变量用来存储数据,那么存储不同数据的变量是否需要指定类型呢? 答案是有的语言规范要求在编译时期明确声明每个变量的数据类型,如Java,此类编程语言统称为静态类型语言,而有的变量类型不需要明确指定,而是在程序运行时根据赋值自动推断,此类编程语言统称为动态类型语言,而JS便是动态类型语言。

本质上,正是这种存储访问变量值的能力使程序有了状态,而正是状态为程序的灵活性和处理复杂问题的能力提供了必要的支撑和保障。那么变量应该如何赋值,如何存储,如何获取呢?事实上我们需要一套规则来规范关于变量的一切,这套规则便是作用域,下面我们会详细讨论作用域相关的问题。

什么是变量的声明

变量的声明是指在程序中使用关键字显式地指定某个标识符为变量,以便在程序中使用它,通俗的讲就是起个名字用来标识数据。

那么什么是关键字和标识符呢?

  • 关键字:预先定义的标识符,具有特殊的含义和用途,相当于这些标识符已经被语言本身注册,其他人只能使用而无权再进行声明。
  • 标识符:关键字其实就是一种标识符,在JS中,标识符是用来定义变量、函数、类、对象的属性和方法等的名称,可以包含字母、数字、下划线和美元符号,并且必须以字母、下划线或美元符号开头。

ES5和ES6声明变量对比

谈到变量的声明你会想到哪些问题?

在ES5中,变量声明的关键字只有 var,在ES6中,又增加了两个关键字 let和 const,后面我们会逐步讨论它们之间的区别, 现在让我们抛开所有的概念,就单纯的想象一下,我们去声明一个变量,需要考虑哪些方面的问题?

  • 声明的变量和存储数据的数据类型是否有强关联性?
  • 声明的变量在哪可以获取到?(换句话说,声明的变量起作用的范围是哪)
  • 声明的变量赋值之后是否可以再次进行修改?
  • 声明的变量在它起作用的范围内,是否可以重复声明?
  • 声明的全局变量,是否和顶层对象有关联?

接下来我们看下上面我们所能想到的问题在ES5中是如何处理和规定的,又存在哪些问题?然后再对比下ES6中是做了哪些改进。

声明的变量和存储数据的数据类型是否有强关联性?

这个问题其实在上面已经提到了,JS是动态类型语言,在声明变量的时候不需要指定类型,在程序运行的过程当中,根据变量的赋值自动推断变量的类型。在这里我们简单对比下这两种数据类型的语言各自的优缺点:

静态类型语言
  • 静态类型语言在编译时就能发现类型不匹配的错误,并且现代的编辑器通常会提示错误,帮助我们提前避免程序在运行期间有可能发生的一些错误。
  • 如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。
动态类型语言
  • 编写的代码数量更少,更加简洁,程序员可以把精力更多地放在业务逻辑上面。
  • 由于无需进行类型检测,我们可以调用任何对象的任意方法,而无需去考虑它原本是否被设计为拥有该方法。这使得在JS中可以轻松实现面向接口编程,而不是面向实现编程。

声明的变量在哪可以获取到?(换句话说,声明的变量起作用的范围是哪)

我们上面提到,我们需要一套规则来规范变量的声明、存储和访问,这个规则就是作用域,想要彻底掌握作用域,需要理解JS引擎以及语言编译的流程,今天暂且不去做更深入的研究,只从使用的场景上理解作用域,以及在使用的过程中需要注意的地方。

在ES5中,JS的作用域只有两种,全局作用域和函数作用域,即我们只需要区分,用var关键字声明的变量的位置是在函数体外还是体内,理解起来非常简单,但是使用上却会有诸多问题。

  • 内层变量可能会覆盖外层变量。
    // 外部变量
    var outer = "outer";
    
    function f() {
      console.log(outer);
      if (false) {
        // 内部变量
        var outer = 'inner';
      }
    }
    f(); // undefined
    

函数f执行后,输出结果为undefined,由于内部变量outer变量提升,导致内层的变量outer覆盖了外层的变量outer。

var命令声明的变量存在变量提升,提升至当前作用域的顶部,也就是说,变量可以在声明之前使用,值为undefined。所以这也是var命令的问题之一。

  • 用来计数的循环变量泄露为全局变量。
    var temp = 'hello';
    
    for (var i = 0, len = temp.length; i < len; i++) {
      console.log(temp[i]);
    }
    console.log(i); // 5
    

循环结束后,用来控制循环的变量i并没有消失,泄露成了全局变量。

上面我们主要发现了两个问题:

  • 变量提升

  • 缺少块级作用域

    变量提升会造成逻辑上的混乱,缺少块级作用域会没有办法更精细的控制代码逻辑。

    ES6引入了let和const关键字来解决这两个问题:

  • 使用let和const声明的变量不允许变量提升

  • 使用let和const声明的变量能够绑定到当前所处的任意作用域中,把变量的作用域封闭再所处的代码块。

    针对上面两个例子,我们将var替换成let,看下会有什么变化

    // 外部变量
    let outer = "outer";
    
    function f() {
      // 获取外部的变量
      console.log(outer); 
      if (false) {
        // 内部变量 ,没有变量提升,作用域封闭再{}中
        let outer = 'inner';
      }
    }
    f(); // outer
    

    f函数执行,输出outer,因为内部变量用let声明,没有变量提升,并且将其作用域封闭if的大括弧中,所以,外部的console在获取outer的时候,获取的就是外部的outer变量,这样内层的变量就不会覆盖外层的变量了。

    如果外部没有outer,那么console肯定会报错,因为你访问了没有定义的变量,但是如果在if里面let声明的上面访问outer可以吗?答案也是不可以的,因为,用let声明的变量不发生变量的提升,在let所在的作用域开始到声明的这句话中间的区域被称为“暂时性死区”(TDZ),在此区间引用任何后面才声明的变量都会报错ReferenceError。

let temp = 'hello';

for (let i = 0, len = temp.length; i < len; i++) {
  console.log(temp[i]);
}
console.log(i); // ReferenceError: i is not defined

for循环中,计数器 i只在 for循环体内有效,在循环体外引用就会报错。用let声明i,i是在块级作用内有效,所以每次循环的i其实都是一个新的变量,JS引擎会记住上一轮循环的值,用上一次循环的值计算本轮循环的值。

上面可以看出,ES6通过引入新的let和const关键字,以及块级作用域的概念,完美的解决了var所遗留的部分问题,当然var的问题远不止于此,我们继续往下看。

声明的变量赋值之后是否可以再次进行修改?

用var声明的变量肯定是可以重复赋值的,这大家都知道。但是在ES5中,我们是无法定义常量的,我们都是通过命名规范(大写字母,下划线连接)定义的,虽然用起来很方便,但是没有语法层面的约束,全凭程序员自觉,ES6为了解决这个问题,引入了const,从而彻底的从语法层面解决了这一问题。我总结了一下几点,供大家参考:

  • const声明一个只读的常量。一旦声明,常量的值就不能改变。
  • const声明变量,就必须立即初始化,因为常量不可更改。
  • const声明的常量和let一样,不提升,也同样是在块级作用域内有效。
  • const实际限制的是变量和内存地址之间的绑定,即const让变量无法更改所对应的内存地址。 大家可参考以下示例:
        -----------------------------------------------------
        const str = "hello";
        PI // hello
    
        PI = "world"; 
        // TypeError: Assignment to constant variable.
        
        //const让变量无法更改所对应的内存地址,但可以改变内存地址指向对象的属性
        const obj = {}
        obj.a = "a"
        console.log(obj.a); // "a"
        ----------------------------------------------------
        if (true) {
           // 同let一样,此处是TDZ
           console.log(MAX); // ReferenceError
           const MAX = 5;
        }
        ----------------------------------------------------
        
    

声明的变量在它起作用的范围内,是否可以重复声明?

在ES5中,变量的声明是可以重复声明的,这样很容易造成代码逻辑的混乱和降低代码的可读性,ES6也解决了这个问题,let和const声明的变量在相同的作用域中 是不允许重复声明的,这样极大的保证了代码的可读性,特别是多人协作的代码编写,在ES6之前很容易出现这样的问题。如下面例子所示:

    {
       let a = 5;
       var a = 6;  
       // Identifier 'a' has already been declared
    }

声明的全局变量,是否和顶层对象有关联?

浏览器中的顶层对象指的是 window,在 Node 指的是 global对象。ES5 中,顶层对象的属性与全局变量是等价的。这种设计带来了几个很大的问题:

  1. 全局变量可能是顶层对象的属性创建的,而属性的创建是动态的,所以没法在编译时就报出变量未声明的错误,只有运行时才能知道;
  2. 顶层对象的属性是到处可以读写的,不利于模块化编程。
  3. window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,再逻辑上容易引起混淆。

ES6 在为了保持兼容性的同时又使顶层对象和全局变量脱钩,做了两点规定:

  1. 为了保持兼容性,var命令和 function命令声明的全局变量,依旧是顶层对象的属性。
  2. let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

本节重点讨论了变量的声明,详细比较了ES6较于ES5关于变量声明的改进之处,小小的变量声明看似简单,其实背后涉及JS引擎的运行机理和编译流程,如果小伙伴们不满足于仅仅了解其使用场景,还要继续深挖背后的深层次的原理,后面有时间我们再一起探讨。

转载自:https://juejin.cn/post/7244075827357204537
评论
请登录