青女王001.作用域、作用域链

18 阅读6分钟

在解读本文章之前,你需要理解let、const和var的区别,还有this的指向问题。

一、作用域的概念

什么是作用域: 简单来说就是我在我家玩,你在你家玩,当你没来我家的时候我是没办法和你认识的,同样我也没办法向你借钱,我都不认识你咋跟你借钱,对吧?你的钱就只能你用,或者你家人用,而我的钱也同样如此。可能用自己的朋友圈举例子会更为恰当,只有你的圈子里才知道你叫啥名字,同样也只有我的圈子才知道我的名字,除非我们一起玩了,你进入了我的圈子,我们才能相互认识。

  • 在 Web 浏览器中,全局作用域被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。
  • 在 Node环境中,全局作用域是 global 对象。

要是没理解,别担心,后面讲到具体例子还会讲一遍。

二、作用域的分类

  • 全局作用域
  • 函数作用域
  • 块作用域
  • 词法作用域
  • 动态作用域
  • 作用域链

1.全局作用域:

image.png

静态作用域: 静态作用域是指声明时就已经确定作用域


var a = 1;
function fn() {
	var b = 2;
	console.log(a + b);
}

fn(); // 3

2. 动态作用域:

动态作用域是在运行时确定的,词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用,静态作用域是指声明时就已经确定作用域,动态作用域是指哪里调用的这个函数才确定这个作用域。

var name = "ww";
function say() {
  //这里仅仅只是定义了一个函数,还未调用,也就是这里的代码还未运行,
  //所以还无法确定this指向的作用域
  console.log('我的家乡:' + this.name)
}
var china = {
  name: '中国',
  say,
  beijing :{
  	name: '北京',
  	say
  }
}

//该代码表示1秒后运行这行代码,当随机数是大于0.5时运行china.say(),
//小于等于0.5就运行china.beijing.say();
setInterval(() => Math.random() > 0.5 ? china.say() : china.beijing.say() , 1000)

//china.say()表示是china调用的say方法,那么此时this指向
//china这个对象,然后china中存在name这个变量,
//那么china.name就是"中国",那么say中的this.name就是china.name,也就是"我的家乡:中国";
//而china.beijing.say()则是say是定义在china中的变量beijing这个对象里面,
//那么它的this指向现在变成了beijing这个对象,那么say中的this.name
//就是beijing.name,也就是"我的家乡:北京";

console.log('我的名字:' + this.name);//这里打印'我的名字:ww',这里this就是window,window就是指向全局

3. 函数作用域:

指在函数内声明的所有变量在函数体内始终是可见的,可以在整个函数的范围内使用及复用。

var a = 'a';//老祖宗
function f1() {
    var b = 'b';//爷爷
    function f2() {
        var c = 'c';//爸爸
        function f3() {
          if(true) {
          	var d = 'd';//儿子,注意这里是var声明的
          }
          console.log(a, b, c, d);//孙子继承
        }
      	f3()
    }
  	f2()
}
f1()

//最终会打印"a b c d",因为他是在最里面的函数里面,所以他能访问到所有的变量,
//就跟传家宝一样,爷爷传给爸爸,爸爸传给儿子,儿子传给孙子,
//最后孙子辈能得到爷爷爸爸儿子的所有东西。

4. 作用域链

相信聪明的你已经从上面的例子中大概了解了什么是作用域链了, 作用域链就是从自己一级一级往上查找,直到找到window还没有就结束查找。

再举个例子

var a = "3";
function f1() {
    var a = '4';
    function f2() {
        function f3() {
          console.log(a);//实际运行时他先会在f3这个函数内部找,看下有没有定义变量a,
          //然后再往上查找到f2函数内部,也没有,继续往上查找到f1,发现存在变量a为4,
          //那么返回“4”,结束查找。要是f1里面也没有就继续向上查找,直到查找到window里
          //也没有,那么就会报错:a is not defined;说是没有定义变量a。
        }
      	f3()
    }
  	f2()
}
f1()

image.png

通过这个例子我们知道,作用域链就是从自己开始一级一级往上查找,直到找到window还没有就结束查找。 举个例子:你作为一个士兵,勇猛杀敌,从小兵到伍长再到校尉再到将军再到兵马大元帅,再到皇帝。直到封无可封了。

5. 块级作用域

块级作用域:ES6推出了let、const实现块级作用域

var a = 'a'
function f1() {
    var b = 'b'
    function f2() {
        var c = 'c'
        function f3() {
          if(true) {
          	let d = 'd'; // var 改为 let
                console.log("ww", d);//这里可正常访问
          }
          console.log(a, b, c, d);//这里报错
        }
      	f3()
    }
  	f2()
}
f1()

//这段代码和上面的例子中唯一的区别就是定义变量d时,这里使用了let定义,
//当我们使用let定义变量时,我们就要注意这个大括号{}了,
//只要使用了let去定义变量,那么这个变量就只能在{}里才能访问到了,
//也就是{}就是它的块级作用域,那你在这个{}外的任何地方访问它都会报错:d is not defined

var存在变量提升,let不存在变量提升

这句话其实对也不对,实际上,let 和 const 也会有变量提升:

  1. 他们会在创建执行上下文时,记录到词法环境中,但是并不会将 undefined 赋给他们(也就是说,他会提升到块级作用域的顶部,但是是 uninitialized 状态)
  2. var 可以重复声明的原因是,变量提升时会复用执行上下文中变量环境已有的标识符,但是 let 和 const 在提升到块级作用域顶部时, 不会复用词法环境已有的标识符,所以会产生冲突,甚至还会和 var 产生冲突(因为同一个作用域内部,是不允许重复标识符的出现的)
  3. 在块级作用域顶部和他们声明语句所处的地方之间,如果这个 let 或 const 声明的变量被访问,就会引发一个 ReferenceError,这个就是暂时性死区
console.log(a);
let a = 2;

image.png

这个报错意思是不能接受在初始化变量前去访问变量a;

var可以重复定义多次,let重复定义会报错

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

image.png

这个报错意思是变量a已经声明过了;

var定义的变量会挂载在window对象上,它会创建一个新的全局变量作为全局对象的属性

var value1 = "张三";
let value2 = "李四"; 
const value3 = "王五"; 
console.log(window.value1) // 张三 
console.log(window.value2) // undefined 
console.log(window.value3) // undefined

参考文章:

  1. juejin.cn/post/709681…
  2. juejin.cn/post/705308…