给大家推荐一个实用面试题库

1、前端面试题库 (面试必备)            

地址:​​前端面试题库​

1、let命令

基本用法

用法类似于​​var​​​,但是所声明的变量,只在​​let​​命令所在的代码块内有效。

​let​​只能出现在当前作用域的顶层。

在​​for​​​循环中,使用​​let​​​声明循环变量​​i​​​,当前的​​i​​​只在本轮循环有效,每一次循环的​​i​​都是一个新的变量。

​for​​循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

不存在变量提升现象

变量提升:变量可以在声明之前使用,使用​​var​​声明的变量会自动提升到函数作用域顶部。

==》具体争议转看下文“​​关于是否存在变量提升的争议问题​​”

暂时性死区

只要块级作用域内存在​​let​​命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
tmp = 'abc'; // ReferenceError 受到let约束
let tmp;
}
复制代码

ES6 明确规定,如果区块中存在​​let​​​和​​const​​命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

在代码块内,使用​​let​​命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,TDZ)

在没有引入​​let​​​前​​typeof​​​是绝对安全,不会出错的 ​​typeof​​​对于没有声明的变量会显示​​undefined​​​不会报错,但是对于未声明的变量会报错​​ReferenceError​

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

不允许重复声明

注意区分重复冗余声明和嵌套声明

2、块级作用域

存在意义

ES5只有全局作用域和函数作用域

解决以下场景问题:

  1. 内层变量可能会覆盖外层变量。
  2. 用来计数的循环变量泄露为全局变量。

ES6的块级作用域

​let​​​为JavaScript新增了块级作用域。
ES6允许块级作用域的任意嵌套。
内层作用域可以定义外层作用域的同名变量。

块级作用域的出现,实际上使得获得广泛应用的**匿名立即执行函数表达式(匿名 IIFE)**不再必要了。

块级作用域与函数声明

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于​​let​​​,在块级作用域之外不可引用。
为了避免块级作用域内声明的函数的处理规则对老代码产生很大的影响,减轻不兼容问题,ES6允许浏览器有自己的行为方式,规则如下:

  • 允许在块级作用域内声明函数。
  • 函数声明类似于​​var​​,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作​​let​​处理。

根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于​​var​​​声明的变量。
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'secret';
function f() {
return a;
}
}

// 块级作用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
复制代码

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

3、const命令

基本用法

​const​​声明一个只读的常量。一旦声明,常量的值就不能改变。

​const​​一旦声明变量,就必须立即初始化,不能留到以后赋值。

​const​​​的作用域与​​let​​命令相同:只在声明所在的块级作用域内有效。

​const​​命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

​const​​​声明的常量,也与​​let​​一样不可重复声明。

本质

​const​​实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

对于简单数据类型,值就是保存变量的内存地址【常量】;复合数据类型,变量指向的内存地址保存的只是一个指向实际数据的指针

​const​​只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

因此使用​​const​​声明一个对象,对象的属性是可变的,但是不能改变对象为另一个对象(不能改变指针的指向)。

冻结对象,无法改变对象属性应该使用​​Object.freeze​​方法。

var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
复制代码

ES6声明变量的六种方法

ES5

  1. var
  2. function

ES6

  1. let
  2. const
  3. import
  4. class

4、顶层对象的属性

顶层对象,在浏览器环境指的是​​window​​​对象,在 Node 指的是​​global​​对象。

ES5 之中,顶层对象的属性与全局变量是等价的。

顶层对象的属性与全局变量挂钩带来的问题:

  1. 首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);
  2. 其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);
  3. 最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。
  4. 另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

  • ​var​​​命令和​​function​​命令声明的全局变量,依旧是顶层对象的属性;
  • ​let​​​命令、​​const​​​命令、​​class​​命令声明的全局变量,不属于顶层对象的属性。

5、globalThis对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。

但是,顶层对象在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是​​window​​​,但 Node 和 Web Worker 没有​​window​​。
  • 浏览器和 Web Worker 里面,​​self​​​也指向顶层对象,但是 Node 没有​​self​​。
  • Node 里面,顶层对象是​​global​​,但其他环境都不支持。

很难找到一种方法在所有情况下都取到顶层对象,因此ES2020引入了​​globalThis​​作为顶层对象。

关于是否存在变量提升的争议问题

在学习该部分知识时,依据的是ES6入门教程以及MDN中文文档的内容,没有过多参看其他官方文档,便断定​​let​​​和​​const​​不存在变量提升。但经过大佬指点后拜读了mdn英文文档,文档中描述了let​const​​class​来说是存在变量提升的,以下做详细解释。

MDN英文文档说明存在以下三种认为是变量提升(Hositing)的行为:

【原文】Hoisting is not a term normatively defined in the ECMAScript specification. The spec does define a group of declarations as ​​HoistableDeclaration​​, but this only includes ​​function​​, ​​function*​​, ​​async function​​, and ​​async function*​​ declarations. Hoisting is often considered a feature of ​​var​​ declarations as well, although in a different way. In colloquial terms, any of the following behaviors may be regarded as hoisting:

【翻译】Hosting(变量提升)不是 ECMAScript 规范中规范定义的术语。该规范确实将一组声明定义为​​HoistableDeclaration​​,但这仅包括函数,函数*,​​异步​​函数和​​异步函数*​​声明。变量提升通常也被认为是​​var​​声明的一个特征,尽管方式不同。通俗地说,有下列行为之一,可视为变量提升:

  1. Being able to use a variable's value in its scope before the line it is declared. ("Value hoisting")

【翻译】能够在声明变量的行之前在其范围内使用变量的值。(“值变量提升”)

【实例】ECMAScript规范说明中写到作为​​HoistableDeclaration​​的四种​​function​​​声明:​​function​​​, ​​function*​​​, ​​async function​​​和​​async function*​

  1. Being able to reference a variable in its scope before the line it is declared, without throwing a ​​ReferenceError​​, but the value is always ​​undefined​​. ("Declaration hoisting")

【翻译】能够在声明该变量的行之前在其作用域中引用该变量,而不会引发​​ReferenceError​​,但该值始终未​​定义​​。(“声明变量提升”)

【实例】​​var​​命令的变量提升

  1. The declaration of the variable causes behavior changes in its scope before the line in which it is declared.

【翻译】变量的声明会导致在声明它的行之前在其作用域中发生行为更改。

【实例】​​let​​​、​​const​​​和​​class​​声明命令(也统称为词汇声明lexical declarations)

看到这里有个问题:既然存在变量提升,为何ES6入门和MDN中文文档中都没有注明呢?

MDN英文文档也给出了解释:

因为暂时性死区的存在,严格禁止了在变量声明前进行使用,所以很多地方都认为​​let​​​、​​const​​​和​​class​​声明命令不存在变量提升。这种异议也合理的,因为变量提升并不是一个被定义到ECMAScript中的普遍认可的术语(universally-agreed term)。但是暂时性死区可能会导致其范围内发生其他可观察到的变化,这表明是存在着变量提升的。

实例如下:

【面试题】【ES6】let和const命令 (面试必看)_变量提升

【面试题】【ES6】let和const命令 (面试必看)_javascript_02

【面试题】【ES6】let和const命令 (面试必看)_块级作用域_03

【面试题】【ES6】let和const命令 (面试必看)_块级作用域_04

【原文】If the​​ const x = 2 ​​​declaration is not hoisted at all (as in, it only comes into effect when it's executed), then the ​​console.log(x)​​​ statement should be able to read the x value from the upper scope. However, because the ​​const​​​ declaration still "taints" the entire scope it's defined in, the ​​console.log(x)​​​ statement reads the ​​x​​​ from the ​​const x = 2​​ declaration instead, which is not yet initialized, and throws a ​​ReferenceError​​. Still, it may be more useful to characterize lexical declarations as non-hoisting, because from a utilitarian perspective, the hoisting of these declarations doesn't bring any meaningful features.

【翻译】如果说 ​​const x = 2​​​的声明完全没有被变量提升(那么它只在执行时生效),那么​​console.log(x)​​​应该是能够从上层作用域中读取到​​x​​​的值的。但此处因为​​const​​​的声明依然“污染”到了它定义的整个作用域,所以​​console.log(x)​​​语句实际上读取到的是​​const x = 2​​​ 声明的​​x​​,并抛出​​ReferenceError​​。尽管如此,将词汇声明(lexical declarations) 描述为非变量提升可能更有用,因为从功利的角度来看,这些变量提升不会带来任何有意义的特征。

【注意】以下这种情况不属于变量提升:

{
var x = 1;
}
console.log(x); // 1
复制代码

因为这里没有“先访问后声明”,这里只是因为​​var​​声明没有在块范围内。

结论

综上所述,实际上​​let​​​、​​const​​是存在变量提升的,但通常不会刻意去这样描述,因为从功利的角度来看,这些变量提升并不会带来任何有意义的特征。