理解es6中的暂时性死区

理解es6中的暂时性死区作用域什么是作用域?一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。全局作用域JS中没有明确的全局作用域的概念,只有局部作用域以及全局执行环境的概念,全局执行环境被认为是window对象,是最外围的一个执行环境。因为作用域的概念只是给后续声明语句做一个铺垫,所以这里就不赘述了。局部作用域在外部无法访问局部作用域中的变量1、函数…

大家好,又见面了,我是你们的朋友全栈君。

引入

什么是作用域?

一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

全局作用域

JS中没有明确的全局作用域的概念,只有局部作用域以及全局执行环境的概念,全局执行环境被认为是window对象,是最外围的一个执行环境。因为作用域的概念只是给后续声明语句的阐述做一个铺垫,所以这里就不赘述了。

局部作用域

在外部无法访问局部作用域中的变量

1、函数作用域

变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。在函数中声明的变量只能在函数内部访问。

function fn(){
	var  a = 1;
}
fn()
console.log(a)//ReferenceError: a is not defined
function fn(){
	let  a = 1;
}
fn()
console.log(a)//ReferenceError: a is not defined

在函数作用域中有一个特殊情况

function fn(){
	a = 1;
}
fn()
console.log(a)//1

在函数中没有声明,直接赋值一个变量时,这个变量会在函数执行之后成为一个全局变量。

2、块级作用域(ES6)

{}内部就是一个块级作用域,ES5中没有块级作用域的概念,块级作用域的概念是在ES6中出现的。
块级作用域的概念只和let/const所声明的变量有关,与var声明的变量无关。

{
    let a = 1;
    var b = 2;
}
console.log(a);//a is not defined
console.log(b);//2

声明变量的方式

1. var

在函数作用域或全局作用域中通过关键字var声明的变量,无论在哪里声明的,都会被当成在当前作用域顶部声明的变量。这就是我们常说的变量提升

function fn(){
	if(false){
		var a = 1;
	}else{
		console.log(a)//undeined
	}
}
fn();

等价于

function fn(){
	var a;
	if(false){
		a = 1;
	}else{
		console.log(a)//undeined
	}
}
fn();

var在全局执行环境下声明的变量会成为window对象的属性

var  i =  1;
console.log(window.i);//1

2. let

ES6 新增了let命令,用来声明变量。它的用法类似于var
官方说法是,let没有变量提升。还有一个说法是,let存在变量提升,变量的声明的过程为,1.创建2.初始化(undefined)3.赋值,用let声明的变量,它的创建提升了,但是它的初始化没有提升。而用var声明的变量,它的创建和初始化都进行了提升,这个点在后面我们会提到。

function fn(){
	if(false){
	    let a = 1;
	}else{
		console.log(a)//undeined
	}
}
fn();//ReferenceError: a is not defined

let所声明的变量,只在let命令所在的代码块内有效。外界访问不到块级作用域中用let/const所声明的变量。

{
    let a = 1
}
console.log(a)//a is not defined

{
    const  a = 1;
}
console.log(a)//a is not defined

与此同时,let/const所声明的变量也会“绑定”这个块级作用域,不再受外部的影响。

let a  = 2
{
    console.log(a);//报错
    let a = 1;
}

这里就符合了之前说的,用let声明的变量,它的创建提升了,因此console.log(a)才会知道,我这个块级作用域里有一个被声明的变量a,但是它的初始化没有提升,因此它会报错,因为要等到执行let a时,a变量才会被初始化。

并且let不允许在相同作用域内,重复声明同一个变量。

// 报错
function func() {
  let a = 10;
  var a = 1;
}

// 报错
function func() {
  let a = 10;
  let a = 1;
}

因此,不能在函数内部重新声明参数。

function func(arg) {
  let arg;
}
func() // 报错

function func(arg) {
  {
    let arg;
  }
}
func() // 不报错

但是,可以在for循环内部重新声明参数,因为for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

接下来大家来看看这两段代码,猜测一下结果

let a = 2;
{
    console.log(a);
    var a = 1;
}


let a = 2;
{
    console.log(a);
    let a = 1;
}

结果揭晓:

let a = 2;
{
    console.log(a);
    var a = 1;
}
//Identifier 'a' has already been declared

let a = 2;
{
    console.log(a);
    let a = 1;
}
// a is not defined

第一段代码报错是因为,对于var声明的变量,是不存在块级作用域的,因此我们用let和var在全局执行环境中声明了a变量两次,从而报错。

第二段代码报错是因为let声明的变量a绑定了{},使{}成为块级作用域,块级作用域内部的变量不再受外部的影响,又因为变量a的调用在变量a的声明之前,所以产生了暂时性死区的问题,这个问题我们等下会讨论,这里就不仔细讲了。
上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。

在全局执行环境中,用let声明的变量不会成为window的属性

let i = 1;
console.log(window.i)//undefined

for循环的let,具有闭包的性质

var arr = [];
for (let i = 0; i < 10; i++) {
	  arr[i] = function(){
	    console.log(i)
	  }
}

//等价于
var arr= [];
for (var i = 0; i < 10; i++) {
	~function(i){//这个i是自执行函数的i
		  arr[i] = function(){
	    	console.log(i)
	  }
	}(i)//这个i是传进去的i
}

如果上面的let用var代替,那么每一个li被点击之后,输出的肯定是10。因为函数绑定肯定在函数点击之前被执行完毕,在那个时候,i的值已经变成了10。
但是由于let却有一丝丝的不同,循环体内部(子作用域)在每一次循环执行的时候都会生成一个新的作用域。不同的子作用域内部接受传进来的不同的i值。
那么我们可以思考一下,每一次循环之后,父作用域内部会不会生成新的与子作用域一一对应的作用域呢?

我们可以用以下代码验证

var arr = [];
for (const i = 0; i < 10; i++) {
	  arr[i] = function(){
	    console.log(i)
	  }
}
//Assignment to constant variable
//常量变量赋值

假如是在十个分别独立的父作用域里分别执行
const i = 0;const i = 1;…肯定是不会报错的。因此我们可以推断,父作用域是同一个,在每一次循环之后修改了i的值,并将它传入十个独立的子作用域中。

而for in 循环却不太一样

var arr = [];
var number = [1,2,3,4,5];
for(const i in number){
	arr[i] = function(){
		console.log(number[i]);
	}
}
arr[0]();//1
arr[1]();//2

所以我们可以推测出,for in循环的父作用域,在每次i++的时候,都创建了一个新的作用域,并在作用域中用const声明并赋值了i,父作用域和子作用域是一一对应的关系。

3. const

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

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

上面代码表明改变常量的值会报错。
因此,每个通过const声明的变量必须进行初始化

const foo;
// SyntaxError: Missing initializer in const declaration

上面代码表示,对于const来说,只声明不赋值,就会报错。

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

if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined

暂时性死区

暂时性死区就是由于,let/const声明变量时没有变量提升所导致的。或者我们可以理解为,在变量仅创建,还没有初始化之时就使用了变量

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

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

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

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

有些“死区”比较隐蔽,不太容易发现。

function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 报错

上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。
这说明默认赋值有可能导致暂时性死区
我看到网上有一个说法说,上面的代码出现暂时性死区的原因是因为,函数参数的默认赋值,其实是用let声明的
即等价于下面的代码

function bar(let x = y, let y = 2) {
  return [x, y];
}

bar(); // 报错

经过我的探究,这个说法是不正确的,首先我们来做一个小测试

function fn(x = 1){
    let x = 2;
    console.log(x);
}
fn()//Identifier 'x' has already been declared

function fn(x = 1){
    var x = 2;
    console.log(x);
}
fn()//2

第一个测试可以说明,函数参数的默认赋值和函数内部是同一作用域,这样函数才会因为变量x的重复声明而报错
第二个测试可以说明,函数参数的默认赋值不是用let声明的,这样函数内部用var重复声明变量x的时候才不会报错。

今天在小组讨论的时候,有一个说法可以解释这个现象。
函数创建是有一个过程的

  • 构建A0(默认赋值就是在这一步)(x=y,y=2)
  • 给变量形参赋值undefined
  • 形参实参统一
  • 函数声明
    这一切完成之后,才会有所谓的变量提升。
    所以暂时性死区的现象,其实是在构建AO时,找y给x赋值,因为找不到y,所以出错了。

本文参考
《深入理解es6》
《ECMAScript 6 入门》http://es6.ruanyifeng.com/#docs/object
https://blog.csdn.net/nicexibeidage/article/details/78144138
https://www.zhihu.com/people/zhihusucks/activities

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/148060.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • html中bgsound背景音乐标签在浏览器里无法播放[通俗易懂]

    html中bgsound背景音乐标签在浏览器里无法播放[通俗易懂]1.原代码:问题:经过尝试,发现仅仅只有IE浏览器可以支持自动播放,但是需要先进行添加控件(自动弹出)。其他浏览器不支持自动播放。查找W3C后发现是bgsound的兼容性

    2022年7月25日
    30
  • 网络编程API-下 (I/O复用函数)[通俗易懂]

    网络编程API-下 (I/O复用函数)

    2022年1月29日
    46
  • googleearth离线地图_谷歌插件离线安装

    googleearth离线地图_谷歌插件离线安装Google离线地图API概要解析发布时间:2018-01-17版权: 1.说明离线地图发布有多种方式均可以实现,可以利用ArcGisServer、GeoServer等构建地图Web服务器,还可以使用谷歌地图、百度地图等API进行地图发布服务。本篇主要简单介绍如何调用Google离线地图API实现地图标注、获取坐标、及其他参数的设置。【如何发布Google离线地图】2.实…

    2022年9月2日
    3
  • ShuffleNet算法详解[通俗易懂]

    ShuffleNet算法详解[通俗易懂]论文:ShuffleNet:AnExtremelyEfficientConvolutionalNeuralNetworkforMobileDevices论文链接:https://arxiv.org/abs/1707.01083算法详解:ShuffleNet是Face++的一篇关于降低深度网络计算量的论文,号称是可以在移动设备上运行的深度网络。这篇文章可以和MobileNet

    2022年9月10日
    0
  • Java课程设计_java课设「建议收藏」

    Java课程设计_java课设「建议收藏」1.代码截图:2.设计思路建立GUI界面,系统产生一个随机数(对用户不可见),然后用户输入猜测数,系统根据用户每次输入的数据给出评语(偏大,偏小,猜测成功)。当用户最终猜测成功后,就把当次的随机数和猜测次数放到文件夹内。3.遇到的问题:(1).Guess里面每次产生的随机数m和最终猜测次数n一直不知道怎么传到sava里并保存输出到文件。(2).怎么在生成的guessgame文件里追加内容,而不是每…

    2022年7月12日
    16
  • eNSP静态路由配置_ensp多条静态路由互联

    eNSP静态路由配置_ensp多条静态路由互联ensp静态路由配置(详细)一、首先了解一下数据转发过程中路由器的工作原理路由器的工作原理:(1)解封装:此处解封装的前提是目的mac地址是自己才能解封装(2)根据目的ip查路由表转发数据。查看路由表的命令:[Huawei]displayiprouting-table此处分两种情况:情况1:如果目的ip在路由表中,则会把数据转发到相应的出接口情况2:如果目的ip不在路由表中,则把数据丢了就可以了二、搭建好拓扑图拓扑图如下:图中我已经标好了每个接

    2022年9月25日
    0

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号