su37josephxia/frontend-interview

Day17 - let是否会造成变量提升

su37josephxia opened this issue · 28 comments

var x = 'parent value';
(function() {
  // let x 此时暂时性死区开始
  console.log(x); // Uncaught ReferenceError: x is not defined
  //暂时性死区结束
  let x = 'child value' 
}())

如何解释上述代码

总结:
let不会造成变量提升。
解释:
1.代码中,在定义之前使用变量x,输出的是错误,而我们知道,var定义变量导致的变量提升此时会输出‘x is undefined’.
2.let定义的变量只是在局部作用域形成暂时性死区,若在该局部作用域定义了y变量,局部作用域中使用访问都是该作用域中定义的这个变量y.

let 不会造成变量提升
javascrpit 变量作用域一般都是词法作用域,也就是在函数写好之后,在编译器的第一个阶段词法阶段就已经确定好了作用域。let写在了函数内部,也就是说在代码写好后,函数内部的作用域就已经被确定了。变量在函数内部被定义,所以变量的作用域是在函数内部的。根据JS作用域链的原则,函数在执行的时候会现在当前作用域下寻找变量,其实这个变量是在作用域下被定义的,所以默认会认为当前作用域下的变量是函数访问的变量。但是函数的定义又在使用之后,所以导致了找不到该变量,这也是造成暂时性死区的原因。总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”

首先上述代码不会输出 Uncaught ReferenceError: x is not defined而是Uncaught ReferenceError: Cannot access 'x' before initialization。正常情况下,我们使用一个未被声明的变量,会报错提示该变量未定义,而且我们通常在代码中也是通过typeof(x)检测一个未被声明的变量,如果其未被声明,则结果为undefind,并不会报错,但是如果这个变量用let或者const声明并且在其声明其使用,则会引发暂时性死区问题。

暂时性死区
在词法分析阶段,就已经知道未来存在某某变量,比如说这里的x,而在x变量定义之前去使用它就不允许,就报错:Uncaught ReferenceError: Cannot access 'x' before initialization。这是因为本质上浏览器去解析这些JS代码时,这些代码都是文本(字符串),最开始是声明了JS的格式是【Content-Type:application/javascript】,浏览器会按照这个格式解析代码。

  • “词法解析”阶段:目标生成“AST词法解析树”。在这个阶段基于let或const等声明的变量,已经明确了,未来此上下文中,必然会存在这些变量,但是并没有声明;
  • 代码执行阶段,如果出现在具体声明之前的代码使用这些变量,就会报出错误。

结论

不会

原因

变量的声明需要经过binding和initialize两步,完成后才能正常使用。

对于let声明,binding结束并没有赋予默认值。 所以binding完成了,并不代表initial完成,initial出错并不代表,binding会回滚。

但对于var声明,默认是赋予undefine 的值

对于变量表达式,规范有要求变量必须初始化才能使用。

结论,会变量提升,只是和var的表现不一样,因为TDZ的作用,只是抛出了ReferenceError错误
当程序的控制流程在新的作用域(module, function或block作用域)进行实例化时,在此作用域中的用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,也就是对声明语句进行求值运算,所以是不能被访问的,访问就会抛出错误。所以在这运行流程一进入作用域创建变量,到变量开始可被访问之间的一段时间,就称之为TDZ(暂时死区)。

以上面解说来看,以let/const声明的变量,的确也是有提升(hoist)的作用。这个是很容易被误解的地方,实际上以let/const声明的变量也是会有提升(hoist)的作用。提升是JS语言中对于变量声明的基本特性,只是因为TDZ的作用,并不会像使用var来声明变量,只是会得到undefined而已,现在则是会直接抛出ReferenceError错误,而且很明显的这是一个在运行期间才会出现的错误。

let是会造成变量提升的

  1. 不定义x
    (function() {
    console.log(x);//ReferenceError: x is not defined
    })()
    报错提示:x压根没被定义。

  2. 定义x但不赋值
    (function() {
    let x;
    console.log(x);//undefined
    })()
    运行正常,但x的值被赋undefined

  3. 定义x但放在使用后边
    (function() {
    console.log(x);//ReferenceError: Cannot access 'x' before initialization
    let x = 3;
    })()
    报错提示:x在初始化前无法访问,这里注意与1中报错的区别不是x没有被定义,这里只是说在x被初始化赋值之前是无法使用的,这就说明console.log(x)时是可以访问到x的,只不过我们没有权利访问而已,所以let依然是将变量提升了,只不过在let定义之前一切访问该变量的行为是不被允许的。

回答

不会造成变量提升

展开

let 声明会在当前作用域中创建一个暂时性死区,如果在当前作用域中提前访问变量,会报错

暂时性死区

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

参考

https://es6.ruanyifeng.com/#docs/let#%E6%9A%82%E6%97%B6%E6%80%A7%E6%AD%BB%E5%8C%BA

答:看定义的角度

从变量能否使用的角度:不存在。

从语言解析的角度:存在。

由于TDZ,我们常规使用let定义变量不能在定义前使用,输出报错是

VM539:1 Uncaught ReferenceError: Cannot access 'a' before initialization

不能在定义前使用变量

而直接输出不定义,则提示

VM552:1 Uncaught ReferenceError: a is not defined

a未定义,因此实际上,在编译的时候,代码其实是知道已经存在a这个变量的,因此是会提升。

会造成变量提升。变量提升是 JavaScript 的设计造成的,是语言的特性。let 和 const 只是增加了暂时性死区,并没有改变变量提升的本质。这里变量提升理解为在代码执行到声明语句 let ,const ,var 之前,这个变量就已经存在了。变量分为创建,初始化,赋值三个阶段,变量会被收集到词法环境里,var 会在创建的时候就初始化为undefined,而 let 在创建的时候没有进行初始化undefined,而是执行到赋值的时候同时进行初始化和赋值。而没有初始化的变量读取的时候会报错 "无法在初始化之前使用" , 这个阶段就是 let 的暂时性死区。

var k = 1
;(function () {
  k = 2 // ReferenceError: Cannot access 'k' before initialization
  console.log(k)
  let k
  console.log(k)
})()
console.log(k)
  • let不会造成变量提升
  • 不能在let声明前去使用变量

let 的创建过程被提升了,但是初始化没有提升
生命周期中的声明阶段和我们通常所说的变量声明是不同的。简单来说,引擎处理变量声明需要经历三个步骤:注册,初始化,赋值
而let的生命周期如下
注册阶段
引用变量跑抛出错误 未初始化状态
let a 初始化阶段
a===undefined。 初始化状态
a===‘value’ 赋值状态

所以,声明提升是变量在作用域顶层处注册和初始化耦合的结果。然而let的注册和初始化阶段是解耦的,这就使得声明提升对let无效。这种解耦造成了变量无法被引用的时间死区

  • 其实,let和const是会被提升的,准确的说是创建被提升了,但是初始化没有被提升

  • let声明变量分为三部分:1.创建,2.初始化,3.赋值。

  • 所以没有初始化时,赋值就会报错

if (true) {
    a = 2; // ReferenceError;  
    let a = 3; 
}

为了理解方便,可以将上述代码拆分成如下几步:

if (true) {
       // 此时a的创建已经被提升到了if代码块内的顶部(TDZ的开始)
       a = 2; // 此时对a进行赋值,由于a仅仅被创建,还没有初始化,所以会报错ReferenceError
       let a; // 完成a的初始化(TDZ的结束)
       a = 3; // 完成对a的赋值操作。
  }

而let从创建被提升到初始化这中间的部分,就是我们平常所说的暂时性死区(TDZ),即在暂时性死区中该变量都是不可用的。

不会被提升,let声明的变量会存在暂时性死区,在声明之前调用会抛出错误,如果外层作用域中有同名变量,也不会去访问,因为js 使用的是词法作用域,在词法分析阶段会绑定访问的变量

如果变量提升意味着预解析(pre parser), let 是有变量提升的,不然不会存在暂时性死区。
如果变量提升意味着把变量提升到作用域顶部,let 是没有变量提升的,因为我们无法在 let 声明前使用该变量。

let关键字跟var很相似,但他的作用域是块级的,严格来讲,let在JavaScript运行时中也会被提升,但由于暂时性死区的缘故,实际上不能在声明之前使用let变量,因此,let的提升跟var是不一样的

要搞清楚提升的本质,JS 变量的「创建create、初始化initialize 和赋值assign」
let 声明的 创建、初始化 和赋值 过程
{ let s = 1 s = 2 }
找到所有用 let 声明的变量,在环境中「创建」这些变量
开始执行代码此时没有初始化
执行 s = 1,将 s 「初始化」为 1 如果代码是 let s,就将 s 初始化为 undefined
执行 s = 2,对 s 进行「赋值」
在let声明前执行 log 时 s 还没「初始化」,所以不能使用(也就是所谓的暂时死区)
let 的「创建」过程被提升了,但是初始化没有提升。

let会造成变量提升。
在ECMA262文档里面对Let 和 Const 的介绍中明确的指出,let 会有变量的提升,只是没有进行initial赋值,直到运行到 声明语句的地方才会进行赋值操作。所以在赋值语句之前虽然被声明却不能进行访问,强行访问会报错,这也就形成了我们常说的暂时性死区。

  • let,const若定义为变量提升,则表明他预解析提升,但并未赋值,而var赋值了undefined,所以let在定义之前使用会报错,var则会输出undefined
  • 若定义为狭义的是否在作用域顶端可用,则未提升,在顶部未定义let的地方使用会报错

let会造成变量提升。
一个比较从分的理由是,从块顶部到该变量的初始化语句,这块区域叫做 TDZ(临时死区),如果你在 TDZ 内使用该变量,JS 就会报 ReferenceError的错误;

let造成的暂时性死区的原因是因为:

  1. let 的「创建」过程被提升了,但是初始化没有提升。
  2. var 的「创建」和「初始化」都被提升了。
  3. function 的「创建」「初始化」和「赋值」都被提升了。
  4. const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没有「赋值」过程。
  5. 暂时性死区,就是不能在初始化之前,使用变量。

然后就导致了上面那个问题

var 与 let 的 提升是不一样的。严格来讲 let 的提升只是提升了创建阶段,此时还不能被访问。如果冒然的访问会抛出错误 ReferenceError。而 var 的提升由于var的创建阶段与初始化阶段合二为一了直接进入赋值阶段可是访问的。

对于js引擎预编译来说,let声明会提升声明,用来开辟内存空间,用来判断是否重名或者是否未初始化调用该变量, 还有用来实现暂时性死区
对于我们来使用来说,let不会造成变量提升,正因为js引擎实现了暂时性死区,所以不会被提升
是否提升是对提升在哪里阶段的不同理解

是有提升的,准确来说,
let 的注册过程被提升了,但是初始化没有提升,所以在声明前使用let 的变量会报错 is not defined。这也就形成了暂时性死区。
我们声明函数s

function s( ) {
  let a=1;
  let a=2;
}

声明函数s的时候,即使没有执行函数内部表达式,依旧会报错重复声明。
说明js引擎在let a执行时就已经注意到了let ,不然不会在没执行的情况下报出错误,这也是注册过程提升的一种表现。
但虽然提升,实际是不能在声明前使用该变量的。

let会造成变量提升
let
if(true) { console.log(name); // Uncaught ReferenceError: Cannot access 'name' before initialization; let name = 'gsk'; console.log(name); // gsk } console.log(name); // undefined;
不加let
if(true) { console.log(name); // undefined; if块中没有会向上找到全局,默认未声明的的变量属于window,所以是undefined;

  1. Javascript引擎在执行预编的时候也会注意到在if块中的let声明,但是在声明之前不能以任何方式来引用该变量,这个不能引用的瞬间,即console.log到let name这个执行瞬间被称为暂时性死区;

  2. 虽然有暂时性死区的存在导致我们访问不了let声明之前的变量,但是我们比较let和不加let两块,let块中未声明之前报的错误是不能得到一个未初始化的值,那说明已经有了变量声明的提升,只是没有初始化而已;

在let声明之前的执行瞬间都被称为暂时性死区,在此阶段引用的任何后面才声明的变量都会抛出ReferenceError没有定义的错误。
image

而从结果来看,对于let定义的变量,虽然不能访问,但是确实是存在提升的,只是由于暂时性死区的存在使let 不能在声明之前被使用,这与var的变量提升不太一样。
所以,说let是存在变量提升,只是说由于暂时性死区的存在使得我们无法直观感受到变量提升的效果

let会造成变量提升

首先明确一下变量提升的概念:变量提升是指当我们在执行到定义变量的这行代码之前,该变量就已经在内存中存在了

如果访问一个未定义的变量时,所报的错是 变量未定义,说明在内存中找不到该变量

function a () {
    console.log(c)
}  // Uncaught ReferenceError: c is not defined
          at a (<anonymous>:1:28)
          at <anonymous>:1:1

如果在 let 定义变量之前访问该变量,所报的错是 不能在初始化前访问该变量,说明此时该变量已经存在内存中了

function b () {
    console.log(c); 
    let c = 1
}  // Uncaught ReferenceError: Cannot access 'c' before initialization
    at b (<anonymous>:1:28)
    at <anonymous>:1:1

所以,从结果来看,在使用 let 定义变量确实存在变量提升,只是由于暂时性死区的原因,我们无法访问到当前的变量

let不会造成变量提升,但是js引擎在let声明语句之前是能感知到它的存在的,只是无法正常访问而已。

首先我们得明确变量提升的含义。变量提升是指,变量在声明之前可以访问并且不报错。对于var申明的变量来说,这个过程是因为执行上下文在创建的过程中会扫描变量,var创建的变量放到变量环境并给一个初始化值undefined,所以我们可以在申明语句之前访问这个变量,并且值为undefined。

console.log(a); undefined
var a = 0;

那么对于let和const来说,在执行上下文创建阶段,也会被扫描到,但是会被放入词法环境,并且不会初始化。而且这时候很重的一点,从块级作用域的开始到这条申明语句的区域,会成为一个“暂时性死区”(temporal dead zone),如果在这个区域内访问该变量,会报referrenceError。这也就导致我们不能在申明语句之前去正常访问一个let和const申明的变量。

console.log(a); // referrenceError: can't access xxx before initialization
let a = 0;

但是这里很有趣的一点就是,这里报的错是referrenceError: can't access xxx before initialization而不是 referrenceError: xx is not defined。这说明此时js引擎是能直到这个变量的存在的,但由于该变量未初始化,导致这里是暂时性死区,所以报了这个不一样的错。

通常意义上,我们所说的提升指的是这个变量是否被初始化了;基于这种理解,var 定义的变量存在提升,而let 定义的变量不存在提升,所以在let 给变量赋值之前,变量都是不能够被使用的,这个位置也被称为let定义变量的暂时性死区;

但是在从变量的创建到赋值再到被引用的过程中,var 定义的变量的创建和赋值都会被提升到其被引用位置的前面,所以在未执行到其被创建的代码行的位置时,打印的变量的值是undefined;而let定义的变量只有创建过程被提升到其被引用的位置的前面,而赋值过程没有同时被提升,因此在未执行到其被创建的代码行的位置时,打印变量的值会报语法错误,并表示其在被初始化之前是不能够被引用的,这也就是暂时性死区的由来;但是let 定义的变量的创建过程的提升,只存在于块级作用域之内,当其在被定义的块级作用域之外引用的话,就会报未定义;

es6 文档中所提到的 let 不存在变量提升,也只是基于变量的初始化,而不是变量的创建。