• 作用域和作用域链


    概述

    本文将讲解作用域的形成和应用,并且在这基础上简单讲解for循环中的let创建的块级作用域原理。

    一,作用域

    1.1,作用域的概念

    作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。

    function testFn(){
      var a=1
    }
    testFn()
    console.log(a)//a is not defined
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如上代码,之所以a is not defined就是因为a定义在函数作用域中,全局无法获取。

    通俗理解,作用域就是变量与函数的可访问范围,它的最大作用就是隔离变量

    它有这样的特性:作用域在代码书写时被确定(静态作用域),代码执行时使用(作用域链)。

    在ES6出来之前,js中存在两种作用域:【全局作用域】和【函数作用域】。而在es6中又引入了const和let的【块级作用域】。

    1.2,全局作用域

    全局作用域的特点是:任何地方的代码都能够访问到。主要的创建方式有以下几种:

    1.2.1,最外层定义的函数和变量
    var a="最外层变量"
    function testFn(){
      var a="函数内变量"
      console.log(a)//函数内变量
    }
    testFn()
    console.log(a)//最外层变量
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1.1.2,未定义直接赋值的变量自动声明为全局作用域
    function testFn(){
      a="未定义直接赋值的变量,自动声明为全局变量"
     var b="函数内定义的变量,外层无法访问"
     
    }
    testFn()
    console.log(a)//未定义直接赋值的变量,自动声明为全局变量
    console.log(b)//b is not defined
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1.1.3,window对象的属性拥有全局作用域

    window对象上的属性也拥有全局作用域,比如我们常用的window.location,window.href,window.scrollTo等,就算不使用window.而是直接访问也是可以的。

    1.2,函数作用域

    全局作用域看起来很方便,只要一定义,任何地方都能访问到,但是存在一个很大的坑:污染全局空间。

    如果变量命名重复,后面的将会覆盖前面的。要想写完备可靠的代码,就只能不断地想新的变量/函数名。这无疑是种痛苦。

    所以js中除了全局作用域外,还有函数作用域。

    简单点理解就是js编译解析的时候,发现这是个函数,则在全局作用域中单独划拉出来个空间,用来做函数作用域。

    在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁
    
    • 1
    function testFn(){
      var a="函数内定义的变量"
      console.log(a)//这里能访问到,控制台打印:函数内定义的变量
    }
    testFn()
    console.log(a)//a is not defined
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    就是因为函数作用域内部的变量能够被隔离,不会污染全局作用域,所以JQuery等库都是利用函数自调用的形式,来进行变量隔离:

    var b=10;
    (function() {
      var b = 20;
    })();
    console.log(b)//10
    
    • 1
    • 2
    • 3
    • 4
    • 5

    值得注意的是,js中没有块级作用域(使用{}包裹),而只有函数作用域(function中使用{}包裹)。

    也就是if等语句并不会创建新的作用域:

    if (true) {
      var name = 'test'; // name 依然在全局作用域中
    }
    console.log(name); //test
    
    • 1
    • 2
    • 3
    • 4
    1.3,ES6的块级作用域

    在es6中,可以通过let和const创建块级作用域。块级作用域和函数作用域一样,可以变量隔离,不能被外层访问。

    1.3.1,花括号({})中,如果使用了let或者const,那么这个{}会生成一个作用域,并且该作用域只针对该变量生效。
    if (true) {
      var a="全局变量"
      let name = 'test'; // name创建了个块级作用域,这个块级作用域只针对name生效
    }
    console.log(a); 
    console.log(name); //name is not defined
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到,这个话括号中使用let生成的块级作用域,只对name起作用,而对于变量a,它依旧是全局变量,可以在外层直接访问。

    1.3.2,const/let修饰的变量不会提升

    在es6之前的js,我们使用var来创建新的变量,是会将变量自动提升到当前作用域的顶部。(关于变量提升,可以看我另一篇文章)。

    而const/let修饰的变量不会提升,这就意味着,如果我们要声明并使用const/let修饰的变量,需要将它们放在作用域的顶部(最不济也要放在调用位置的前面。)

    if (true) {
      let a="第一个变量"
      console.log(a); //正常执行
      console.log(b); //Cannot access 'b' before initialization
      let b="第二个变量"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1.3.3,同个作用域内禁止重复声明

    如果一个标识符在该作用域已经被声明,则不能再使用let/const声明了。

    if (true) {
      var a="第一个变量"
      let a="第二个变量"//Identifier 'a' has already been declared
    }
    
    • 1
    • 2
    • 3
    • 4

    而不同作用域内,则可以重复声明,且归属于不同作用域:

    var a=1
    if (true) {
      let a=2
      console.log("内部",a)//内部 2
    }
    console.log("外部",a)//内部 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1.3.4,for循环中的变量提升和块级作用域

    在for循环中,新手常常遇到的是如下代码产生的问题:

    DOCTYPE html>
    <html>
    	<head>
    		<meta charset="utf-8">
    		<title>title>
    		<style type="text/css">
    			li{
    				list-style: none;
    				width: 100px;
    				height: 30px;
    				background-color: blue;
    				text-align: center;
    				line-height: 30px;
    				margin-bottom: 10px;
    			}
    		style>
    	head>
    	<body>
    		<ul id="list">
    			<li>0li>
    			<li>1li>
    			<li>2li>
    			<li>3li>
    			<li>4li>
    			<li>5li>
    			<li>6li>
    			<li>7li>
    		ul>
    		<script type="text/javascript">
    			var lisarr=document.getElementsByTagName("li");
    			for(var i=0;i<lisarr.length;i++){
    				lisarr[i].onclick=function(i){
    					console.log(i)
    				};
    			}
    		script>
    	body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    这时候,无论点击哪个li标签,都会打印8,而不是打印对应的次序。这是因为for中使用的是var来创建变量,由于变量提升,它是一个全局作用域中的变量。那么在所有的li标签绑定点击事件后,全局的i已经变成了8,这时候我们点击任一个li标签执行如下方法:

    function(i){
    	console.log(i)
    };
    
    • 1
    • 2
    • 3

    取到的自然是全局的i=8。

    而当我们使用let的时候:

    var lisarr=document.getElementsByTagName("li");
    for(let i=0;i<lisarr.length;i++){
        lisarr[i].onclick=function(i){
        	console.log(i)
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    每次遍历,生成的i是一个新的变量,有自己对应的作用域。(因为同于作用域,let不能重复声明的),这样每个点击回调函数中的i就是对应的序号了。(这里的具体解释,见下文作用域链中描述。)

    二,作用域链

    2.1,自由变量和约束变量的区别

    既不是形参也不是函数内部定义的局部变量的变量即自由变量。形参或函数内部定义的局部变量即约束变量

    let a=1
    function testFn(c){
      let b=2
      console.log(a)//a:自由变量
      console.log(b)//b约束变量
      console.log(c)//c约束变量
    }
    testFn(3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2.2,自由变量的取值流程-顺着作用域链向上查找

    上文已经讲了作用域的概念,如下图所示,不同的颜色是一层作用域(值得注意的是,函数形参是归属于函数作用域的),作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行

    1.PNG

    对于约束变量而言,自己的作用域中已经定义了,所以必然能够直接读取值。

    而对于自由变量而言,当前作用域并没有对应变量,于是就需要顺着作用域向外层查找,直到找到为止。若是到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。

    let a=1
    function testFn(c){
      let b=2
      function testFn2(){
        let c=3
        console.log(a)//顺着作用域链查找,在全局作用域找到
        console.log(b)//顺着作用域链查找,在testFn函数的作用域找到
        console.log(c)//在本作用域找到
      }
    }
    testFn(3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    2.3,作用域链基于静态作用域

    看如下代码:

    function testFn1(){
        let a=1
        function testFn2(){
            console.log(a)
        }
        return testFn2
    }
    function testFn3(fn){
        let a=2
        fn()
    }
    testFn3(testFn1())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结果打印的是1,而不是2,这是因为js作用域链是基于静态作用域(或者说词法作用域),函数的作用域在函数定义的时候就决定了

    在上面的代码中,静态作用域只看函数定义时代码是怎么写的,和最后怎么调用的没关系。于是testFn2函数作用域中没有a变量,就会向外层查找,在testFn1中找到了,于是就会取这个1。

    2.4,静态作用域的创建时机

    那为啥说静态作用域只和变量定义时有关系,而和调用没关系呢?

    这是因为JavaScript属于解释型语言,JavaScript的执行分为:解释执行两个阶段,这两个阶段所做的事并不一样:

    解释阶段做的事情:

    词法分析
    语法分析
    生成可执行代码
    
    • 1
    • 2
    • 3

    执行阶段做的事情:

    创建执行上下文
    执行函数代码
    垃圾回收
    
    • 1
    • 2
    • 3

    作用域的规则是在解释阶段确定的。在识别变量和函数声明时,JavaScript引擎会确定它们的作用域。而在执行阶段,JavaScript引擎将根据作用域规则来查找变量和函数。

    也就是这个阶段,作用域被确定。所以才说作用域只是静态作用域,和什么时候调用执行没关系。

    2.5,代码中作用域链的实现过程

    Javascript中一切皆对象,这些对象有一个[[Scope]]属性,该属性包含了函数被创建时的作用域中对象的集合,这个集合被称为函数的作用域链(Scope Chain),它决定了哪些数据能被函数访问。当函数创建的时候,它的[[scope]]属性自动添加好全局作用域。之所以要强调创建是因为JavaScript采用词法作用域(lexical scoping),也就是静态作用域.

    作用链其实就是通过每个执行上下文的outer链接起来形成的。实际上,每个执行上下文在创建的时候,都会生成一个名为[[scoped]]的属性。

    它是一个数组,在创建执行上下文的时候,就会顺着outer连接的链查找自由变量(既不是形参也不是函数内部定义的局部变量的变量即自由变量),outer变量每连接上一个执行上下文(或者闭包),它就会往数组添加一条。

    var a=1
    function foo(){
      var b=3
      var d=4
      function test1(){
        var c=5
        console.log(a,b,c)
      }
      console.dir(test1)
      test1()
    }
    foo()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    console.dir(test1)打印出来的结果就是:

    ƒ test1()
    arguments: null
    caller: null
    length: 0
    name: "test1"
    prototype: {constructor: ƒ}
    	[[FunctionLocation]]: index.js:5
    	[[Prototype]]: ƒ ()
    	[[Scopes]]: Scopes[2]
    		0: Closure (foo) {b: 3}
    		1: Global {window: Window, a: 1,...}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看到在test1生成的执行上下文,它内部的自由变量是a和b,于是顺着outer连接的执行上下文查找,第一个找到了foo执行上下文中的b,第二个找到了全局执行上下文的a,所以最后生成了Scopes[2]是两个值的数组。

    2.6,for循环中的作用域解析

    上文1.3.4节中提到的代码,在了解了作用域链之后,就可以继续深入讲解了。

    其实,for循环中()是个独立的作用域,我们可以验证这一点:之前说过同一个作用域下,let不允许重复声明(上文1.3.3)。

    于是可以写如下代码:

    let i =10
    for(let i =0;i<5;i++){
        let i=20
        console.log(i)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结果是打印了五次20而没有报错,说明这里有三层作用域:全局作用域,for的括号中一层作用域,for的花括号中一层作用域。

    另外,每次遍历for的()中都会生成一个新的作用域给对应的i。所以说这段代码又有了更深一层的解释:

    var lisarr=document.getElementsByTagName("li");
    for(let i=0;i<lisarr.length;i++){
        lisarr[i].onclick=function(i){
        	console.log(i)
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如下图所示,每次遍历都会生成一个新的作用域,并创建一个新的变量i,而{}内的代码的作用域中因为没有i,则会取外层这个i值,从而取到对应的序号。

    2.png

    2.7,延长作用域

    除了let、const之外,其实还有try……catch、with、eval来创建块作用域,其他用的相对少,暂且不谈。主要讲讲try……catch对作用域链的延长作用。

    catch可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。

    如下所示:

    let k=0
    try {
      let i = 1;
      throw new Error('error occurred');
    } catch (e) {
      var j = 2;
      console.log(k); // 0
      console.log(j); // 2
      console.log(i); // i is not defined
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    从这里看,catch会临时创建一个内层的作用域。也就是上面代码中存在三个作用域:全局作用域、try作用域、catch作用域,其中try作用域和catch作用域同级。

    具体来说,变量会被放置到 catch 块的作用域中,其作用域链的顶端指向当前作用域中的变量,然后再向上查找,直到全局作用域。

    延长表现为:

    1. catch 块内部可以访问和操作 catch 块外层作用域的变量;
    2. catch 块内部声明的变量,在块外不能被访问;
    3. catch 块内部声明的变量在 catch 块内部与外部是可以同时存在的,并且名字可以相同,不会发生冲突;
    4. var 声明变量的提升仍然存在,但是定义未赋初值的声明不会被提升,否则会被赋值为 undefined;
    
    • 1
    • 2
    • 3
    • 4

    按照这样的理解,这样延长作用域,也只是在cacth中新增了一节内层的作用域给catch中的代码使用。(红宝书里的说法是在作用域的前端生成一个临时的作用域,所以说是延长)

    这和新建个函数作用域有啥区别?

    与函数中新创建一个函数不同的是,catch块语句中的作用域只在catch块语句内部有效,在catch块语句执行完毕后该作用域就会被销毁。而函数作用域则是在函数被调用时创建,在函数执行完后才被销毁。

    本文相关系列文章

    相关系列文章

    js从编译到执行过程 - 掘金 (juejin.cn)

    从异步到promise - 掘金 (juejin.cn)

    从promise到await - 掘金 (juejin.cn)

    浅谈异步编程中错误的捕获 - 掘金 (juejin.cn)

    作用域和作用域链 - 掘金 (juejin.cn)

    原型链和原型对象 - 掘金 (juejin.cn)

    this的指向原理浅谈 - 掘金 (juejin.cn)

    js的函数传参之值传递 - 掘金 (juejin.cn)

    js的事件循环机制 - 掘金 (juejin.cn)

    从作用域链和内存角度重新理解闭包 - 掘金 (juejin.cn)

    js的垃圾回收机制 - 掘金 (juejin.cn)

    js的模块化 - 掘金 (juejin.cn)

    js设计模式-创建型 - 掘金 (juejin.cn)

    js设计模式-结构型 - 掘金 (juejin.cn)

  • 相关阅读:
    FastJson
    什么是找出芯片bug的最好办法
    【CVPR2022 点云3D检测SOTA】SoftGroup for 3D Instance Segmentation on Point Clouds
    因mapjoin加载内存溢出而导致return code 3
    Acer W700废物利用- 第一章 - 安装Linux系统Debian 11.5
    Java注解,看完就会用
    人脸检测之PCN(一)——论文阅读
    Git 的基本概念和使用方式
    Python ML实战-工业蒸汽量预测02-数据探索
    Postfix别名邮件与SASL验证
  • 原文地址:https://blog.csdn.net/weixin_42349568/article/details/133907894