likes
comments
collection
share

深入JavaScript闭包(二):作用域,作用域链

作者站长头像
站长
· 阅读数 50

作用域

作用域是什么?

作用域字面意义上就是某某东西能发挥作用的区域,起作用的范围,在JavaScript中就是指变量、函数能够有效使用的区域,作用域也可以用来隔绝变量

看下面一段代码 深入JavaScript闭包(二):作用域,作用域链

变量color可以在全局范围内使用,变量anotherColor只能在函数内部使用,它们作用于不同的范围。 在不同的作用域内可以声明同名的变量,同名变量间不会产生冲突,作用域将其隔离开了。 同一个作用域定义同名变量会产生冲突。 有一点需要特别注意的是:作用域是在代码、函数定义时就确定的,而不是执行时确定的。这点要和执行上下文区分开。

如果对执行上下文不太了解推荐看下上一篇:深入JavaScript闭包(一):执行上下文,执行上下文栈 - 掘金 (juejin.cn)

小结一下:作用域就是一个范围、地盘。变量在里面可以发挥作用,不会泄露出去。作用域最主要的作用就是隔离变量。

作用域的类别

根据工作模式,作用域可分为:静态作用域,动态作用域。 根据代码内容,作用域可分为:全局作用域,函数作用域,块作用域。 ES6之前作用域只有全局作用域和函数作用域,ES6后作用域新增了块级作用域。

全局作用域

顾名思义,全局作用域就是最大的作用域,作用范围包含全局。比如window对象上的属性拥有全局作用域,可以在全局任一地方发挥作用。 一般而言,以下一些情形拥有全局作用域

1 定义在最外层的变量或函数,或者说全局上下文中存储的变量或函数拥有全局作用域。 深入JavaScript闭包(二):作用域,作用域链 上述代码中的 colorscopechangeColor拥有全局作用域。

2 所有未定义直接赋值的变量会自动声明为全局作用域的变量。 深入JavaScript闭包(二):作用域,作用域链 即使是在函数内部直接赋值的变量也会声明到全局上。

3 window对象上的属性拥有全局作用域,在代码的任何地方都可以使用window。

函数作用域

函数作用域就是在函数内部发挥作用的区域,每个函数都会创建自己的函数作用域。在函数内部定义的变量自动拥有函数作用域。使用var关键字声明的变量具有函数作用域。 深入JavaScript闭包(二):作用域,作用域链

块级作用域

块是指代码中使用{}包裹起来的区域,如ifforwhile、匿名块等等。ES6后新增了letconst两个关键字,使用它们声明的变量具有块作用域。 看一个例子吧。 深入JavaScript闭包(二):作用域,作用域链 上面函数有两个代码块,都声明了变量n,最终的结果输出为5,块作用域里let声明的变量是不会影响到外面作用域的。 如果把let换成var呢?

function f1() {
  var n = 5;
  if (true) { 
    var n = 10;  
  }  
  console.log(n);  //10
}

最终结果会输出10,因为var声明的变量是函数作用域,有变量提升的效果,在块作用域内声明的变量会提升到函数作用域顶部。其等价于:

function f1() {
  var n;
  n = 5;
  if (true) { 
     n = 10;  
  }  
  console.log(n);  
}

再看一段经典的代码。

for(var i=0;i<5;i++){
   setTimeout(()=>{
       console.log(i);
   },0)
}
// 5 5 5 5 5

for(let j=0;j<5;j++){
   setTimeout(()=>{
      console.log(j);
   },0)
}
// 1 2 3 4 5

for循环里面使用var声明的i变量会提升到for外部,变成全局变量,由于事件循环机制,会在同步任务(for循环)执行完毕后再执行异步任务(setTimeout任务),此时i变量的值已经变成5了,所以结果全部都输出5。

使用let声明的变量只会在for循环内部起作用。正常情况下for循环执行完毕后,内部let声明的j变量会被销毁掉,但是在代码块内部通过setTimeout方法引用了这个变量,setTimeout是在主程序执行完毕后执行的,即使for循环执行完毕后其每一层的作用域也被setTimeout所维持,不会被销毁,所以setTimeout可以访问到for循环中每一层作用域的j变量的值。

补充知识点:暂时性死区

其实不仅是var声明的变量可以进行变量提升,let声明的变量也可以进行变量提升。 区别在于var声明的变量会提升到函数作用域顶部,而使用let声明的变量只会提升到块作用域顶部。这个过程是在执行上下文中完成的。

深入JavaScript闭包(二):作用域,作用域链

这个时候有人可能会有疑问:如果使用let声明的变量也可以提升的话,那么在使用let声明变量前使用的话也应该和var一样值为undefined,而不是报错。

console.log(m);  //undefined
var m = 10;

console.log(n);  //Uncaught ReferenceError: Cannot access 'n' before initialization
let n = 100;

这是因为ES6在新增letconst 关键字后提出的一个概念:暂时性死区。 ES6明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前使用这些变量,就会报错。这在语法上称为“暂时性死区”。(temporal dead zone,简称 TDZ)

var tmp = 123;

if (true) {
  tmp = 'abc'; // Uncaught ReferenceError: Cannot access 'tmp' before initialization
  let tmp;
}

上面的一段代码按照正常的理解,我已经在全局声明了tmp变量,之后在if块中使用应该是没有问题的。但是它在if块中声明了同名的tmp变量,由于暂时性死区的缘故,不能在声明前使用变量,所以报错了。

作用域链

直白点来讲,作用域链就是作用域的集合,其中子集可以访问父集,父集不能访问子集。 看一段代码 深入JavaScript闭包(二):作用域,作用域链

上面这段代码总共有三个作用域,全局作用域fn函数作用域bar函数作用域。它们之间是包含关系,全局作用域中包含fn函数作用域,fn函数作用域中包含bar函数作用域。这种包含关系组成了一种链式结构,即作用域链

深入JavaScript闭包(二):作用域,作用域链

在bar函数体中使用了变量ab,但在bar函数作用域中并没有找到这些变量,于是它就会到其父级fn函数作用域中继续查找,找到了变量b,变量a还没有找到。继续查找,到fn函数作用域的父级全局作用域中查找,找到变量a,至此查找结束。 查找方向是不可逆的,子级可以到父级中查找,但父级不能到子级中查找。

静态作用域

静态作用域又称词法作用域,是指函数的作用域是在定义时确定的,即根据函数定义的位置确定它的作用域链。JavaScript采用的是静态作用域。

还是看一段代码,通过代码更好理解

//采用静态作用域
let a = 10;
let b = 100;
function fn(){
    let b = 20;
    function bar(){
        console.log(a+b)
    }
    return bar;
}
let x = fn();    
x();  //30

上述代码中定义了两个函数:fn函数和bar函数。并且bar函数是在fn函数内部定义的,fn函数是在全局定义的。根据函数定义时的位置,可以得到这段代码的作用域链如下图:

深入JavaScript闭包(二):作用域,作用域链

再分析下这段代码的执行过程:

代码执行第11行,调用fn函数,并将fn函数的返回值赋值给x。fn函数的返回值是bar函数,x就是bar函数。

代码执行第12行,调用bar函数,在控制台打印a+b的值。但bar函数作用域中没有变量ab,必须去其他作用域中查找。此时bar函数是在全局作用域中被调用的,全局作用域和fn函数作用域中都有变量b的值,该取哪个作用域中的值呢?

这个时候就要看我们刚才生成的作用域链了,bar函数作用域中没有查找到变量,所以要去它的父级作用域中查找,即在fn函数作用域中查找,得到结果b的值为20,而不是在全局作用域中查找,即使它是在全局作用域中被调用的。

小结一下:当查找函数中未定义的变量时,要到函数被创建的作用域中查找,而不是到函数被调用的作用域中查找。

动态作用域

动态作用域是指函数的作用域是在调用时被确定的。也就是说我们不能在初始时就确定作用域链,必须在函数执行时确定

还是这段代码

//采用动态态作用域
let a = 10;
let b = 100;
function fn(){
    let b = 20;
    function bar(){
        console.log(a+b)
    }
    return bar;
}
let x = fn();    
x();  //110

再分析下这段代码的执行过程:

代码执行第11行,调用fn函数,并将fn函数的返回值赋值给x。fn函数的返回值是bar函数,x就相当于是bar函数。

代码执行第12行,调用bar函数,在控制台打印a+b的值。但bar函数作用域中没有变量a、b,必须去其他作用域中查找。此时bar函数是在全局作用域被调用的,此时生成的动态作用域链如下: 深入JavaScript闭包(二):作用域,作用域链

所以bar函数会直接到全局作用域中寻找变量a、b。最终得出结果110。

现在绝大多数的语言都是使用的词法作用域(静态作用域),JavaScript也是使用的是静态作用域,但是它的eval()withthis的机制很像动态作用域。

小结一下:静态作用域是在代码或函数定义时确定的,而动态作用域是在代码或函数运行时确定的。静态作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

作用域与执行上下文

很多人容易将作用域和执行上下文弄混,它们是完全不同的两个概念。

各自的定义

作用域:作用域本质上类似于一个地盘,范围,主要起隔绝变量的作用。

执行上下文:执行上下文又称执行环境,其实就是代码执行前的准备工作,最终会生成一个活动对象,用来存储变量和函数。查找作用域中的变量都是从作用域对应的上下文的活动对象中查找的

创建时间

作用域:JavaScript采用的是静态作用域工作模式,所以作用域是在代码或函数定义时就确定的。

执行上下文:执行上下文是在代码块执行前生成的。

变化过程

作用域:作用域是静态的,代码或函数定义时作用域就确定了,不会更改。

执行上下文:执行上下文是动态的,在代码块执行前生成,同一个作用域中,不同的调用会生成不同的上下文

参考文章: JavaScript深入之作用域链 - 掘金 (juejin.cn) 深入理解javascript原型和闭包(12)——简介【作用域】 - 王福朋 - 博客园 (cnblogs.com) 深入理解JavaScript作用域和作用域链 - 掘金 (juejin.cn) let 和 const 命令 - ECMAScript 6入门 (ruanyifeng.com)

转载自:https://juejin.cn/post/7256721204300382266
评论
请登录