看啥推荐读物
专栏名称: 程序媛兔子
目录
相关文章推荐
今天看啥  ›  专栏  ›  程序媛兔子

Javascript进阶1--作用域和闭包

程序媛兔子  · 掘金  ·  · 2019-09-26 15:48
阅读 35

Javascript进阶1--作用域和闭包

从今天开始,我打算将我学到的js知识进行分享,欢迎大家的讨论和补充,有任何不足之处,尽情地提出来吧~😁

作用域介绍

作用域是什么?

本质上是一套规则,用于确定在何处以及如何查找变量(标识符)。

何处?

作用域是可以嵌套的,引擎从当前作用域开始查找,如果找不到,就会向上一级继续查找,当抵达到最外层的全局作用域查找后,无论找到还是没找到,都会停止。

如何查找变量?

有以下两种方式:

  • LHS:赋值操作的目标是谁;结果不成功的话,有两种情况:
    • 严格模式下:抛出 Reference 异常。
    • 非严格模式下,自动隐式地创建一个全局变量。
  • RHS:谁是赋值操作的源头;结果不成功会报 Reference 异常。

⚠️注意:只会查找一级标识符,比 如foo.bar.baz,只会试图找到 foo 标识符,找到后,对象属性访问规则后分别接管对 bar、baz 的属性访问。

举🌰:

function foo(a) {
    console.log(a + b);
}
var b = 2;
foo(3);

引擎:作用域,我需要为 b 进行 LHS引用,这个你见过吗?
全局作用域:见过见过!刚才编译器声明它来着,给你。
引擎:谢谢大哥,现在我要把2赋值给 b
引擎:作用域啊,还有个事,我要对 foo 进行 RHS 引用,你见过没啊?
全局作用域:见过呀,它是个函数,给你。
引擎:好的,我现在执行一下 foo 
引擎:哥啊,我需要对 a 进行 LHS 引用,这个你见过没?
全局作用域:这个也见过,是编译器把它声明成 foo 的一个形参了,拿去吧。
引擎:太棒了,现在我把3赋值给 a 了
引擎:foo 作用域啊,我要对 console 进行 RHS 引用,你见过没啊?
foo作用域:这我也有,是个内置对象,给你
引擎:你总是那么给力,现在我要看看这里有没有 log(),找到了,是个函数。
引擎:哥,我要对 a 进行 RHS 引用,虽然我记得好像有这个值,但是想让你帮我确认以下。
foo作用域:好,这个值没变过,你拿走吧。
引擎: 哥,我还要对 b 进行 RHS 引用,你找找呗
foo作用域:我没听过啊,你问问我的上级吧:
引擎:foo 的上级作用域兄弟,你见过 b 没啊?
全局作用域:见过 b 啊,等于2,拿走不谢!
引擎:真棒,我现在把 a + b ,也就是5,传递进 log(...)
复制代码

作用域的工作模型

主要有两种:

  • 词法作用域:由你在书写代码时将变量和块作用域写在哪里来决定的。有时候可能会有在代码运行时“修改”词法作用域的需求,可以通过以下机制:
    • eval():可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。

      function foo(str, a) {
          eval(str);
          console.log(a, b)
      }
      var b = 3;
      foo("var b = 4", 2); // 2, 4  
      复制代码
    • with:通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域。

      function foo(obj) {
          with(obj) {
              a = 2;
          }
      }
      var o1 = {
          a: 3
      };
      var o2 = {
          b: 3
      };
      foo(o1);
      console.log(o1.a)  //2
      foo(o2)
      console.loh(o2.a)  //undefined;
      console.log(a)  
      //2 在o2中,对a进行LHS引用,没有找到,
      //在o2中不会创造a这个属性
      //因为是非严格模式,所以会在全局作用域中创建一个变量 a,并赋值给2
      复制代码

      ⚠️注意:这两个机制只在非严格模式下有效,严格模式下会抛出 Reference 错误。还会导致性能下降,引擎无法在编译时对其进行优化,所以会变慢。

  • 动态作用域:

作用域的种类

作用域有三种:

  • 全局作用域:生命周期存在于整个程序内,能被程序中任何函数或者方法访问,在js中默认是可以被修改的。
  • 局部作用域
    • 函数作用域:函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域。
    • 块级作用域:任何一对花括号中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的。

函数作用域

在javascript中,定义一个函数有四种方式,分别是:

  • 函数声明:function 关键字出现在声明中第一个词,它的调用可以先于声明

  • 函数表达式:在执行到达时创建,并只有从那时起才可以用。

    • 匿名函数表达式:省略了函数名
    • 具名函数表达式
  • ES6 中的箭头函数

  • new Function()

    ⚠️注意:函数声明和函数表达式最大的区别就是他们的名称标识符将会被绑在何处。

    var a = 2;
    // 函数声明,被绑定在所在作用域中,可以直接通过 foo() 来调用它
    function foo() {
        var a = 3;
        console.log(a); //3
    }
    foo();
    // 函数表达式,foo2被绑定在函数表达式自身的函数中,而不是所在的作用域中
    // 也就是,只能在函数内部里被访问foo2,外部作用域内不能访问
    (function foo2() {
        var a = 3;
        console.log(a)  //3
    }
    )()
    console.log(a)  //2
    复制代码

在函数表达式中的立即执行函数表达式(IIFE)使我们不用主动调用函数,它会自己调用,对于做模块化、处理组件是非常有用的,IIFE一般使用匿名函数表达式。

⚠️注意:调用函数最简单的方法就是加一对小括号,但函数声明不能直接调用的原因是:

  1. 小括号里只能放表达式,不能放语句
  2. function关键字即可以当作语句,也可以当作表达式。但js规定function关键字出现在行首,一律解释成语句

解决办法:不让 function 关键字出现在行首

function fn() {
    console.log(1);
}();    //报错

const fn1 = function() {
    console.log('表达式执行');
}();    //执行函数
复制代码

块作用域

在 ES6 之前,js中也是有块作用域概念的,但只限于个别具体的语法中:

  • with:用 with 从对象创建的出的作用域仅在 with 声明中有效,而非外部作用域中。
  • try/catch:ES3 规定,try/catch 中的 catch 分句会创建一个块级作用域,其中声明的变量仅在 catch 内部有效。

在 ES6 中,引入了新的块作用域

  • let:用来声明变量,不允许声明提升,也不允许重复声明
  • const:用来声明常量,不允许声明提升,也不允许重复声明

⚠️注意:提升是指声明会被视为存在于其所出现的作用域的整个范围内。var允许变量声明提升,但不允许赋值或其他运行逻辑提升。函数声明会被先提升,然后才是变量

var scope = "global";
function scopeTest() {
    console.log(scope);
    var scope = "local" ; 
}
scopeTest(); //undefined
复制代码

闭包

当函数可以记住并且访问所在的词法作用域时,并且函数是在当前词法作用域之外执行,此时该函数和声明该函数的词法环境的组合。

不成功的代码

直接看代码吧,用语言来描述过于空洞。

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
      console.log(i);
  }, i * 1000);
}
复制代码

这是一个高频率会看到的题,我们期望的结果是:分别输出数字1 - 5,每秒一个,每次一个。但实际上,会以每秒一次的频率输出五次6。

那代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?缺陷是我们试图假设循环中每个迭代在运行时,都会为自己"捕获"一个 i 的副本。但是实际上,尽管这五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此只有一个i。

如果想要返回的预期结果,可以通过以下方法。

立即执行函数表达式

在迭代内,使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新作用域封闭在每个迭代内部。

for (var i = 1; i <= 5; i++) {
  (function(j) {
      setTimeout(function timer() {
          console.log(j);
      }, i * 1000)
  })(i);
}
复制代码

let 语法

let语法本质上是将一个块转换成一个可以被关闭的作用域,let声明的变量在每次迭代都会声明。

for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000) }

最后,如果觉得文章还不错,请点个赞吧~👍




原文地址:访问原文地址
快照地址: 访问文章快照