大家好,又见面了,我是你们的朋友全栈君。
引入
什么是作用域?
一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
全局作用域
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
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/148060.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...