?「深入本质」临时死区?哇哦,好中二的名字,啥玩意?

欢迎大家来到”深入本质”系列,Blue在这个系列中将带领大家一起挖掘代码背后的本质,一窥潜藏在代码背后作者的思想与意图,体会本质之下的精巧构思与设计之美

想理解事物为什么是这样,要站在创造者的角度思考问题

​ ——Blue

临时死区(Temporal Dead Zone,或TDZ)也许大家听过,Blue不知道别人什么感觉,我第一次看到的感觉就是——”这么中二的破名字谁取的啊?”,这也不能怪我,这名字在我眼里跟”大幻梦森罗万象狂气断罪眼“差不多是一个类型的,那么本文将帮你彻底理解临时死区,并探究它背后的原理,欢迎点赞、收藏、评论、转发

本文中我们将涉及以下内容

  • 认识JavaScript的变量声明与作用域
  • 何为临时死区
  • 不同变量的临时死区有何不同
  • 深入本质:JS为何设计TDZ机制、它是如何工作的

相关文章

变量

变量是所有语言最基础、也是最核心的特性,甚至可以说,其他所有语法都是为了操作变量而存在的,毕竟数据就是一切,一切都是数据,而js中,我们有三种声明变量的方法

//最古老的变量声明方式
var name='blue';

//ES6添加的两种声明方式
let url='www.zhinengshe.com';
const age=18;
复制代码

三种声明方式

为了更好的理解TDZ,我来带大家简单的回忆一下他们之间的区别:

注意,这3种都可以通过babel轻松兼容所有浏览器,所以兼容性不是我们的考量维度

  • var:最古老传统的变量声明方式,没啥特点(但它的机制最特殊,下面会说)
  • let:ES6新增,防止重名、带有块级作用域、带有DTZ临时死区
  • const:ES6新增,防止重名、带有块级作用域、带有DTZ临时死区,常量不可修改

简单试一下

var最古老、最普通

var a=12;
var a=5;  //不会报错,var允许重复声明


if(true){
  var b=44;
}
alert(b);  //44,能出来,var没有块级作用域(是函数级的)
复制代码

let不允许重复声明、具有块级作用域

let a=12;
let a=5; //报错,不允许重复声明


if(true){
  let b=44;
}
alert(b); //报错,b是块级变量,只能在if中使用
复制代码

1

const基本跟let一样,除了它是常量以外

let a=12;
a=5; //可以再次赋值


const b=12;
b=5; //报错:常量,不可修改

const c; //报错:常量不可修改,所以必须拥有初始值
复制代码

2

作用域

作用域代表一个东西(变量、函数、类型、模块…)能被使用的范围,在这里咱们仅限于讨论变量作用域就好,毕竟其他几个也差不多

三种作用域

js中典型的有三种作用域:

  • 全局作用域:声明在最外层,可以被任何代码使用
  • 局部作用域:在函数里声明,只能在本函数(和其子函数)中使用
  • 块级作用域:在语法块中声明(if、for、while等),只能在本语法块内使用

简单试一下

//全局变量——声明在所有函数之外
var g1=12; //非严格模式下,全局var其实是window的属性
let g2=55;
const g3=23;


//局部变量——声明在函数当中
function show(){
  var l1='blue';
  let l2='zhangsan';
  const l3='lisi';
}
show();


//块级变量
if(true){
  var s1=18; //还是全局的,var不具备块级作用域
  let s2=29;
  const s3=30;
}
console.log(s1); //18
console.log(s2); //报错,找不到s2
console.log(s3); //报错,找不到s3
复制代码

何为临时死区、本质是什么

临时死区(???????? ???? ????)是一种。。。配上这个字体是不是就有内味了?

好了不闹了,那么前面说了变量、作用域,这跟临时死区有啥关系?简单来说,变量要先声明再用这个天经地义吧,那么如果我偏不,Blue我今天就是要在声明之前就用了,怎么滴吧,这时候就会触发TDZ出来救场了

  • 作用域顶部,到变量真正声明之前的这一段,被称为死区
  • 因为死区是临时的,变量声明了就解除了,所以叫做临时死区

直接看个例子吧

(()=>{
  //作用域开始
  
  
  /* 这一段是a的死区,也就是说用了a就会死 */
  
  
  //a真正声明
  let a=12;
})();
复制代码

那么如果我用了,会怎么样?

(()=>{
  //作用域开始
  
  
  console.log(a);  //我今天就用了,怎么滴吧
  
  
  //a真正声明
  let a=12;
})();
复制代码

3

好吧。。。我错了

变量禁止被使用的区域

通过上面例子我们知道了,变量在声明之前,是禁止被使用的,如果使用了程序就会死掉(报错),这就是临时死区,但是真的这么简单吗,来看几个问题

问题1-var有没有tdz

前面咱们就提过,var其实是没有tdz的,那么我提前使用var会怎样?

(function (){
  //作用域开始
  
  console.log(a); //不会死,只是undefined
  
  //真正声明变量
  var a=12;
})();
复制代码

4

也就是说,ES6之前的js(也就是var变量)如果提前使用,只会得到一个undefined;而ES6开始(let和const)提前使用,则会直接触发一个ReferenceError的运行时错误,但是,为什么?到底发生了什么?

变量提升的本质

上面的问题,跟js的工作原理有关,js虽然是解释型语言,但也存在一个预编译的阶段,在这个阶段,js源码会被编译为js字节码(是的,跟java的class字节码的机制很像),那么预编译阶段它到底做了什么?

js在预编译阶段,会扫描所有的变量,并预先为其分配内存空间,这一过程被称为变量提升(Variable Hoisting),用最通俗的话来说——就像把变量全拿到作用域顶部的效果一样

//你眼中的代码
function show(){
  console.log(a);
  
  var a=12;
  var b=5;
  
  console.log(b);
}



//经过预编译后的代码
function show(){
  var a,b; //所有变量都被提升到作用域顶部,当然,这时仅有声明而无赋值,这也是为什么他们是undefined
  
  console.log(a); //undefined
  
  a=12;
  b=5;
  
  console.log(b); //undefined
}
复制代码

TDZ临时死区的本质

letconst则稍有不同,他们也会被提升(其实所谓提升,就是记录你的变量而已),并且,他们不会被初始化为undefined,而是标记为uninitialization状态,而这一状态是临时的,在运行到声明的那句代码时,这种状态会被解除(临时死区)

//你眼中的代码
function show(){
  console.log(a);
  
  let a=12; //跟上面的例子不同,我们用了let
  let b=5;
  
  console.log(b);
}



//预编译后的代码
function show(){
  //注意,这里仅为便于理解,uninitialization并不是js能使用的,只在引擎内部存在
  let a=uninitialization,
      b=uninitialization;
  
  console.log(a); //报错,因为uninitialization不可使用,碰了就死
  
  a=12;
  b=5;
  
  console.log(b); //这里没事,因为b从uninitialization变成5了
}
复制代码

通过这个例子,我们明白了,临时死区本质上是一种被标记为”不可使用”的特殊状态

那有同学可能会说了”老师,那我要是不给他赋值,它是不是还是保持uninitialization?”,不会

//你眼中的代码
function show(){
  console.log(a);
  
  let a;
  
  console.log(a);
}


//预编译后的代码
function show(){
  let a=uninitialization;
  
  console.log(a);
  
  a=undefined; //任何未被初始化的变量,js会自动的赋值为undefined
  
  console.log(a);
}
复制代码

问题2-用你的矛刺你的盾,会怎样?

我咋觉得自己写这篇文章时也变得有点中二?

  • 在前一篇文章中,我们提到过一个很猛的东西——typeof,它可以对未定义的变量使用而不会报错
  • 而这一篇文章中,我们说到tdz碰了就死

那么问题来了,如果我们用typeof去处理tdz会怎样?

(()=>{
  //TDZ开始
  
  
  //你猜怎么着?
  console.log(typeof a); //处于tdz的变量
  console.log(typeof b); //真没声明的变量
  
  
  //TDZ结束
  let a=12;
})();
复制代码

5

所以大家也看出来了,TDZ高于typeof,这个也算正常,一般新标准的东西都优先级高于老的标准,不然被屏蔽了

看本质:TDZ到底是什么,为什么存在

首先,这个事情跟js本身有关,因为js是一种解释型语言,也就是说,它没有硬性的编译阶段,而在其他语言(比如Java)就根本没这个问题

public class Test{
  static void main(String args[]){
    System.out.println(a);  //"妄图"在声明前使用变量时

    int a=28;
  }
}
复制代码

6

(哥,我错了.jpg)

一种帮助

JavaScript预编译时其实完全可以将letconstvar等同起来,都初始化为undefined,这对于语言自身反而简单,它为什么要这样做

  • 健壮性的考虑:首先,我们在变量声明前就是用它,是对的吗?绝对不对,所以undefined显得过于温柔了,要用更强硬的办法,帮我们找出这个问题
  • 便于调试:就像上面说的,js其实是在帮我们,帮我们找到这种低级错误,从而便于我们调试代码,这也是为什么连typeof都干不过TDZ的原因——typeof也是一种使用,从语法角度这样用也是错的

一种工具

如前所述,JS是解释型语言,那么它就必须在预编译阶段进行变量提升(否则无法检查重复声明等错误),所以TDZ并不完全是为我们开发人员服务的,它在编译期间,也可以帮助编译器寻找可能的错误(变量重复声明)

var允许重复声明这一错误,在TDZ的帮助下得以修复,也是它的贡献之一

什么时候用到TDZ?

作为语言的用户,你永远不会主动用到它,它的价值是帮你找问题(声明前就用了);对语言自身的开发人员,它可以帮助确定重复声明等语法问题,但也仅此而已,没有更多应用了

总结

是时候梳理一遍Blue讲过的东西了,那么首先

15-三连

  • 从作用域顶部,到变量被声明的这一段空间,是变量的死区,因为它是临时的,所以叫临时死区
  • 变量会被提升到作用域顶端
  • var变量提升的同时赋值undefined
  • letconst则标记为uninitialization(TDZ状态)
  • TDZ的变量无论如何不能碰,碰了就死(连typeof都干不过它)

有bug?想补充?

感谢大家观看这篇教程,有任何问题或想和我交流,请直接留言,发现文章有任何不妥之处,也请指出,提前感谢

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享