欢迎大家来到”深入本质”系列,Blue在这个系列中将带领大家一起挖掘代码背后的本质,一窥潜藏在代码背后作者的思想与意图,体会本质之下的精巧构思与设计之美
想理解事物为什么是这样,要站在创造者的角度思考问题
——Blue
临时死区(Temporal Dead Zone,或TDZ)也许大家听过,Blue不知道别人什么感觉,我第一次看到的感觉就是——”这么中二的破名字谁取的啊?”,这也不能怪我,这名字在我眼里跟”大幻梦森罗万象狂气断罪眼“差不多是一个类型的,那么本文将帮你彻底理解临时死区,并探究它背后的原理,欢迎点赞、收藏、评论、转发
本文中我们将涉及以下内容
- 认识JavaScript的变量声明与作用域
- 何为临时死区
- 不同变量的临时死区有何不同
- 深入本质:JS为何设计TDZ机制、它是如何工作的
相关文章
-
本文用到部分前文所述undefined的知识,如有需要欢迎查看
变量
变量是所有语言最基础、也是最核心的特性,甚至可以说,其他所有语法都是为了操作变量而存在的,毕竟数据就是一切,一切都是数据,而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中使用
复制代码
const基本跟let一样,除了它是常量以外
let a=12;
a=5; //可以再次赋值
const b=12;
b=5; //报错:常量,不可修改
const c; //报错:常量不可修改,所以必须拥有初始值
复制代码
作用域
作用域代表一个东西(变量、函数、类型、模块…)能被使用的范围,在这里咱们仅限于讨论变量作用域就好,毕竟其他几个也差不多
三种作用域
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;
})();
复制代码
好吧。。。我错了
变量禁止被使用的区域
通过上面例子我们知道了,变量在声明之前,是禁止被使用的,如果使用了程序就会死掉(报错),这就是临时死区,但是真的这么简单吗,来看几个问题
问题1-var有没有tdz
前面咱们就提过,var其实是没有tdz的,那么我提前使用var会怎样?
(function (){
//作用域开始
console.log(a); //不会死,只是undefined
//真正声明变量
var a=12;
})();
复制代码
也就是说,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临时死区的本质
而let
和const
则稍有不同,他们也会被提升(其实所谓提升,就是记录你的变量而已),并且,他们不会被初始化为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;
})();
复制代码
所以大家也看出来了,TDZ高于typeof,这个也算正常,一般新标准的东西都优先级高于老的标准,不然被屏蔽了
看本质:TDZ到底是什么,为什么存在
首先,这个事情跟js本身有关,因为js是一种解释型语言,也就是说,它没有硬性的编译阶段,而在其他语言(比如Java)就根本没这个问题
public class Test{
static void main(String args[]){
System.out.println(a); //"妄图"在声明前使用变量时
int a=28;
}
}
复制代码
(哥,我错了.jpg)
一种帮助
JavaScript预编译时其实完全可以将let
、const
和var
等同起来,都初始化为undefined
,这对于语言自身反而简单,它为什么要这样做
- 健壮性的考虑:首先,我们在变量声明前就是用它,是对的吗?绝对不对,所以
undefined
显得过于温柔了,要用更强硬的办法,帮我们找出这个问题 - 便于调试:就像上面说的,js其实是在帮我们,帮我们找到这种低级错误,从而便于我们调试代码,这也是为什么连typeof都干不过TDZ的原因——typeof也是一种使用,从语法角度这样用也是错的
一种工具
如前所述,JS是解释型语言,那么它就必须在预编译阶段进行变量提升(否则无法检查重复声明等错误),所以TDZ并不完全是为我们开发人员服务的,它在编译期间,也可以帮助编译器寻找可能的错误(变量重复声明)
var
允许重复声明这一错误,在TDZ的帮助下得以修复,也是它的贡献之一
什么时候用到TDZ?
作为语言的用户,你永远不会主动用到它,它的价值是帮你找问题(声明前就用了);对语言自身的开发人员,它可以帮助确定重复声明等语法问题,但也仅此而已,没有更多应用了
总结
是时候梳理一遍Blue讲过的东西了,那么首先
- 从作用域顶部,到变量被声明的这一段空间,是变量的死区,因为它是临时的,所以叫临时死区
- 变量会被提升到作用域顶端
var
变量提升的同时赋值undefined
let
和const
则标记为uninitialization(TDZ状态)- TDZ的变量无论如何不能碰,碰了就死(连typeof都干不过它)
有bug?想补充?
感谢大家观看这篇教程,有任何问题或想和我交流,请直接留言,发现文章有任何不妥之处,也请指出,提前感谢