引入

什么是作用域?

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

全局作用域

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 declaredlet 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是自执行函数的iarr[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.1415PI = 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'; // ReferenceErrorlet 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 declaredfunction 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

理解es6中的暂时性死区相关推荐

  1. 理解ES6中的暂时死区(TDZ)

    Temporal Dead Zone(TDZ)是ES6(ES2015)中对作用域新的专用语义.TDZ名词并没有明确地写在ES6的标准文件中,一开始是出现在ES Discussion讨论区中,是对于某些 ...

  2. 理解ES6中的TDZ(暂时性死区)

    什么是TDZ Temporal Dead Zone(TDZ)是ES6(ES2015)中对作用域新的专用语义.TDZ名词并没有明确地写在ES6的标准文件中,一开始是出现在ES Discussion讨论区 ...

  3. 理解ES6中暂时性死区TDZ

    什么是暂时性死区 ES6中,在代码块内,使用let/const命令声明变量之前,该变量都是不可用的,在变量声明之前属于该变量的"死区".这在语法上,称为"暂时性死区&qu ...

  4. 这篇看完我得理解ES6中中常见语法

    目录 前言 1let篇 1.1作用域 1.2变量提升 1.3相同作用域赋值 2const篇 3模板字符串篇 3.1传统 3.2模板字符串复制 4扩展运算符篇 4.1传统赋值 4.2扩展字符串复制 前言 ...

  5. 撤底理解es6中的箭头函数

    本质上 是一个函数,是function 是一个被编译层加工过的函数 用 babel 编译一下箭头函数看看,如下 //es6 const a = ()=>{ console.log(this) } ...

  6. 暂时性死区(TDZ)

    常见js面试题之一: 提示:又是一道常见的前端面试题,什么是暂时性死区? 问题描述: console.log(a);var a = 1; 日志输出:undefined console.log(a);l ...

  7. JS-变量提升与暂时性死区概念

    变量提升(Hoisting) 变量提升(Hoisting)被认为是, Javascript中执行上下文 (特别是创建和执行阶段)工作方式的一种认识.在 ECMAScript® 2015 Languag ...

  8. 什么是暂时性死区(TDZ)?

    常见js面试题之一: 提示:又是一道常见的前端面试题,什么是暂时性死区? 问题描述: console.log(a);var a = 1; 日志输出:undefined console.log(a);l ...

  9. 深入解析ES6中let和闭包

    2019独角兽企业重金招聘Python工程师标准>>> 本篇文章主要介绍了深入理解ES6中let和闭包,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下.如有不 ...

最新文章

  1. Oracle 导出CSV、导出大数据
  2. 数学老师必备工具,你的最爱!
  3. 无法分配更多的internet句柄怎么回事_一文精通Java NIO(内容较多,无耐心者勿点)...
  4. 46什么是session
  5. CDH集群异常处理ERROR  Failure due to stall on seeded torrent.、重装时hdfs提示目录已存在、CDH重启不正常
  6. Google搜索图片时只显示第一页
  7. 十八、PHP框架Laravel学习笔记——模型的增删改
  8. python全排列速度最快_python 写的两种打印全排列的方法速度对比
  9. 电商页面设计吸睛大法|C4D背景素材模板,视觉之旅开启!
  10. Internet Explorer 无法显示网页
  11. linux中pip安装步骤与使用详解
  12. 美国DHS向国会提交政府《移动设备安全研究》报告
  13. 打印机驱动下载后只能打印单面(设置双面打印)解决方法
  14. 对接微信公众(开放)平台,获取微信用户信息,实现第三方登录
  15. python实现非常有趣的数学问题
  16. 深度学习之迁移学习介绍与使用
  17. 基于机器视觉的移动消防机器人(一)--功能设计
  18. linux查看指定目录下各个文件大小以及总体大小
  19. GitHub 里的笔记
  20. 薄冰实用英语语法详解A

热门文章

  1. 米哈游108薪年终奖,假的
  2. 第二十二篇玩转【斗鱼直播APP】系列之弹出房间界面
  3. 骑士CMS模版注入+文件包含getshell复现
  4. 盯上年轻人的今日头条,重新以内容出发还有多少可能?
  5. 电影动漫视频推荐网站
  6. 基于Android的sina微博分享功能
  7. [css] 怎么自定义鼠标指针的图案?
  8. 锦标赛选择算法及 matlab 实现
  9. PHP,$this-{$xxx} 是什么意思?
  10. 计算机逻辑函数,计算机智能化简多变量逻辑函数的算法分析