【Js基础】词法作用域和作用域链,this的使用

182 阅读6分钟

前言

  • 词法作用域
  • 作用域链
  • this的使用

刚开始我学这三个概念的每一个概念是分开来看,我以为学懂了。

但是后面做面试题的时候,我发现我总是做错,我就不断回顾这些知识,我发现他们名字都有些相似,特性也相似,而且在学习的时候没有把它们串联起来导致我思路比较混乱。

所以写下这篇文章帮组我更好的理清思路,也帮助再看的各位少走一点弯路。

正文

  • 词法作用域

    • 词法作用域

      词法作用域看起来是一个高大上的说法,其实也就那么一回事。

      也就是我们经常谈到的作用域,以及根据作用域延伸出来的作用域链

      作用域一般分为两种

      • 词法作用域:也称为静态作用域。这是最普遍的一种作用域模型,也是我们学习的重点
      • 动态作用域:相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等

      我们先来看一段代码

      let name = "Barry Song"
      
      function hi() {
          console.log(name);
      }
      
      function hiSuperman() {
          let name = "Clark Kent"
          console.log(name);
      }
      
      function hiBatman() {
          let name = "Bruce Wayen";
          hi();
      }
      
      hi();// Barry Song
      hiSuperman(); //Clark Kent
      hiBatman();//Barry Song
      

      相比有不少小伙伴下意识会认为hiBatman(),会打印出Bruce Wayen,实际上hi()根据词法作用域的特性,它在创建的初始,它的作用域是根据它定义的位置

      词法作用域判定在于它定义的位置

      所以我们如果在hiBatman()里面定义hi()的话,hi的作用域就是hiBatman(),自然能取到Bruce Wayen。

      function hiBatman() {
          let name = "Bruce Wayen";
          function hi() {
              console.log(name);
          }
          hi();
      }
      
    • 作用域链

      我们接着上面的代码来看,稍作修改

      let name = "Barry Song"
      function hiBatman() {
          // let name = "Bruce Wayen";
          function hi() {
              console.log(name);
          }
          hi();// Barry Song
      }
      

      我把hiBatman()里面的name注释掉了。

      这次它再次打印出了Barry Song。

      我们再来结合词法作用域的特性来看作用域链。

      当我们找不到一个变量的时候,往往会向它本身定义的位置向上找

      hi()定义的位置是在hiBatman()里面,hiBatman()里面并没有name这个变量,那么我去找定义hiBatman()的定义域,是全局定义域,里面刚好有一个name。

    • 一个小补充关于函数调用栈

      var foo = 'foo'
      
      function testA() {
        console.log('执行第一个测试函数的逻辑');
        testB();
        console.log('再次执行第一个测试函数的逻辑');
      }
      
      function testB() {
        console.log(foo);
      }
      
      testA();
      

      我们都知道函数的调用时将函数压入栈中,它管理的是执行顺序

      我为什么要提这个,因为我刚开始在学这两个概念的时候,老是把作用域链和函数调用栈搞混搞混,认为栈顶的函数能调用上一个函数的作用域里面的变量。作用域链是在书写的时候就定义好了。

  • this的使用

    回想一下什么时候会用this?

    构造函数里面初始化对象属性的时候,或者在对象字面量的方法里面调用对象的属性的时候。

    所以this要根据对象(滑稽)来看。

    前置小知识,node环境下的顶层对象global浏览器的环境的顶层对象window

    在浏览器的环境下

    • ES5顶层对象的属性等价于全局变量。

    • ES6有所改变:var、function 声明的全局变量,依然是顶层对象的属性;let、const、class 声明的全局变量不属于顶层对象的属性,也就是说 ES6 开始,全局变量和顶层对象的属性开始分离、脱钩。

      https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/413d9323b791491eb9df5f74c44de924~tplv-k3u1fbpfcp-zoom-1.image

    根据上面的情景分为两类

    • 对象字面量

      var a = a
      let Test = {
        a: 1,
        func1: () => {
          console.log("箭头   => a: " + this.a);
        },
      
        func2() {
          console.log("直接命名 => a: " + this.a);
        },
      
        func3: function () {
          console.log("键值对=> a: " + this.a);
        },
      
        func4() {
          return function () {
            console.log("闭包函数 => a: " + this.a);
          };
        },
      };
      

      在对象字面量里面定义函数的方式有很3种,普通键值对,箭头函数键值对,直接名字定义,还有一种最特殊的闭包

      我们分为两种方式来调用函数,一种是直接调用,一种是把函数值传给其他变量,然后调用被赋予的那个变量。

      console.log("\n 直接调用函数 -----------  \n");
      Test.func1();//箭头   => a: 3
      Test.func2();//直接命名=> a: 1
      Test.func3();//键值对 => a: 1
      Test.func4()();//闭包函数 => a: 3
      

      这是因为箭头函数没有this,就算用bind和apply,call也不行,具体看MDN,所以箭头函数的this会绑定父级上下文

      var a = 1;
      let fn = () => {
        console.log(this.a);
      }
      fn();// 1
      

      所以

      Test.func1();//箭头   => a: 3
      Test.func4()();//闭包函数 => a: 3
      

      这里执行的箭头函数会找到外层的this也就是全局对象,获得a,闭包函数也比较特殊和箭头函数一样,会绑定父级this,所以在使用闭包的和箭头函数的时候要特别注意this的使用。

      我们再来看赋值调用

      console.log("\n 赋值调用函数 ----------- \n");
      let func1 = Test.func1;
      let func2 = Test.func2;
      let func3 = Test.func3;
      let func4 = Test.func4();
      func1();//a: 3
      func2();//a: 3
      func3();//a: 3
      func4();//a: 3
      

      这里就比较有趣了,全部都是3。

      我们都知道在对象是一种键值对的散列数据结构,里面的所有都是以**[[key]] : value** 的方式存储的,当我们把value复制给另外一个变量的时候,我们执行这个新的变量是在全局之下执行的,它的前面并没有调用它的对象,所以他的this就变成全局对象。

    • 构造函数

      在创建new这个关键字创建一个对象发生了那些事。

      • 现在内存中创建一个对象
      • 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性
      • 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
      • 执行构造函数内部的代码(给新对象添加属性)
      • 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
      var a = 3;
      function Fn() {
        this.a = 2;
        this.hi = function () {
          console.log(this.a);
        }
      }
      let fn = new Fn();
      
      let hi = fn.hi;
      fn.hi();// 2
      hi();// 3
      

      关于构造函数this使用指南和上面的对象字面量如出一辙,注意new创建对象时的步骤就好了。

  • 改变this

    js能使用三个方法callapplybind三个方法来改变this的指向

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/840704930d254077aecfed66e06dcf14~tplv-k3u1fbpfcp-zoom-1.image

    如何手写着三个方法

    • call / bind 两者比较类似,只不过是参数的不同
    Function.prototype.myCall = function (context, ...args) {
        context.func = this;
        context.func(...args);
        delete context.func;
    }
    
    • bind

      bind比较麻烦一点要使用到闭包

      Function.prototype.myBind = function (context, ...args) {
          let that = this;
          return (
              function () {
                  context.func = that;
                  context.func(args);
                  delete context.func;
              }
          )
      }
      

      这里就用到了,上面所说的this的指向问题,闭包是比较特殊的函数,它取到的this是在返回之后外面那一层的this,所以这里要提前把this存到that里面去。

  • 总结

    • 词法作用域是对变量的访问,跟定义的位置有关系,作用域链则是定义位置的镶套,当当前无法访问到变量的时候,则会根据作用域链往上找

    • 函数调用栈是函数执行的顺序,容易和作用域链搞混。

    • this的使用

      普通的调用函数,this会跟着调用的对象走。闭包和箭头函数则会绑定父级上下文,并且箭头函数不能被改变this的。

      callapplybind可以改变this

      当函数使用构造函数时,this指向new 创建的那个新对象