给大家推荐一个实用面试题库
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只有全局作用域和函数作用域
解决以下场景问题:
- 内层变量可能会覆盖外层变量。
- 用来计数的循环变量泄露为全局变量。
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
- var
- function
ES6
- let
- const
- import
- class
4、顶层对象的属性
顶层对象,在浏览器环境指的是window
对象,在 Node 指的是global
对象。
ES5 之中,顶层对象的属性与全局变量是等价的。
顶层对象的属性与全局变量挂钩带来的问题:
- 首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);
- 其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);
- 最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。
- 另一方面,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声明的一个特征,尽管方式不同。通俗地说,有下列行为之一,可视为变量提升:
- 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*
- 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
命令的变量提升
- 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)。但是暂时性死区可能会导致其范围内发生其他可观察到的变化,这表明是存在着变量提升的。
实例如下:
【原文】If the
const x = 2
declaration is not hoisted at all (as in, it only comes into effect when it's executed), then theconsole.log(x)
statement should be able to read the x value from the upper scope. However, because theconst
declaration still "taints" the entire scope it's defined in, theconsole.log(x)
statement reads thex
from theconst 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
是存在变量提升的,但通常不会刻意去这样描述,因为从功利的角度来看,这些变量提升并不会带来任何有意义的特征。
给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库