作用域与作用域链
前言
继续来了解一下ES6,顺便把前段时间留下作用域的坑也填上。
JavaScript中,有一个被称之为作用域(scope)的特性,在之前闭包的文章中提到过,现在来梳理一下。
作用域
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
在ES6之前,JavaScript中有两种作用域,分别叫做全局作用域和函数作用域。
- 全局作用域:全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期,页面被关闭才会被销毁。
- 函数作用域:函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
作用域给我的最大感觉就是隔离变量,在不同作用域的同名变量是不会冲突的。
在ES6中,提出了块级作用域的概念。
- 块级作用域的特点:在代码块内部定义的变量在外部是访问不到的,并且变量会在该代码块执行完后被销毁掉。
块级作用域形式:就是使用大括号包裹的代码块
{}
,比如函数,判断语句,循环语句,甚至单独的一个{}
,包裹的也可以看作是一个块级作用域。// if块 if(){} // while 块 while(){} // 函数块 function fn(){} // for循环块 for(let i = 0; i < 10; i++){} // 单独的{} {}
如何让块级作用域生效,ES6给我们提供了两个关键字:let和const
来看看例子吧
for(var i = 0; i < 100; i++){ } console.log(i); // 100
for(let i = 0; i < 100; i++){ } console.log(i); // not defined
我们来思考一道题:每隔一秒钟,打印出来一个自然数,自然数递增。
是不是用一个for循环,里面设置好一个定时器?
是这样?
for(let i = 0; i<10; i++){ setTimeout(function(){ console.log(i) },1000*i) }
还是这样?
for(var i = 0; i<10; i++){ setTimeout(function(){ console.log(i) },1000*i) }
let、const关键字解决var变量提升的问题
之前有说过变量提升的问题,现在来看一下let与const是怎么解决问题的。
首先举一下都会产生什么问题:
变量提升会导致变量值被覆盖
var a = "张三" function showA(){ console.log(a); if(0){ var a = "李四" } console.log(a); } showA() // undefined undefined
该销毁的变量销毁不掉
for(var i = 0; i<10; i++){ } console.log(i)
那么let、const是如何解决问题的呢?
先来看一下这段代码
function fun(){
var a = 11;
let b = 22;
{
let b = 33;
var c = 44;
let d = 55;
console.log(a); // 11
console.log(b); // 33
}
console.log(b); // 22
console.log(c); // 44
console.log(d); // not defined
}
fun()
- let与const关键字创建的变量是存储在词法环境的,而var创建的变量是存储在变量环境中的,访问变量的时候,先从执行上下文的词法环境中查找,再到变量环境中进行查找。
- 当块级内部代码执行结束后,内部let与const创建的变量会被销毁。
let跟const创建的变量,初始化不提升,创建提升,所以会造成暂时性死区
来看一下什么是暂时性死区吧
console.log(a); let a = 1;
这个时候,我们的浏览器报错与之前的not defined就不一样了,它会报
Cannot access 'a' before initialization
这个错误,告诉我们在初始化前不能访问a这个变量,这是因为let与const只有创建提升,没有初始化提升,所以造成了这个问题。
再次说明一下变量提升的问题
- var的创建和初始化被提升,赋值不会被提升
- let的创建被提升,初始化和赋值不会被提升,所以会有暂时性死区的问题(就是访问不到变量)
- function的创建、初始化、赋值都会被提升
作用域的特点:是代码编译阶段就决定好的,和函数是如何调用的没有关系
看下面这段代码:
function biqibao() {
var myName = "海绵宝宝";
let test1 = 100;
if (1) {
let myName = "派大星";
console.log(test);
}
}
function fn() {
var myName = "章鱼哥";
let test = 2;
{
let test = 3;
biqibao();
}
};
var myName = "珊迪";
let myAge = 10;
let test =1;
fn(); // 1
解释一下流程,biqibao函数在fn函数中被调用,要打印test变量,首先查找biqibao函数的内部作用域,没有找到,于是去到全局作用域进行查找,找到了let test = 1这句话,于是,打印1。与fn函数中的let test = 2这句话,一点关系都没有。
这就是在诠释上面作用域特点的那句话。
作用域链
作用域链:当一个函数中使用了某个变量,首先会在自己内部作用域查找,然后再向外部一层一层查找,直到全局作用域,这个链式查找就是作用域链
let num = 1;
function fn1(){
function fn2(){
function fn3(){
console.log(num);
}
fn3();
}
fn2();
}
fn1(); // 1
fn3要打印num变量,于是他从fn3的函数作用域,找到fn2的函数作用域,再找到fn1的函数作用域,再找到全局作用域,终于找到了num变量,成功将其打印。