前端基础(三十一):你不知道的JavaScript - 作用域和闭包

作用域是什么

变量储存在哪里?

作用域中

程序需要时如何找到变量?

依据作用域的规则去调用变量

js实际上也是编译类型语言,但不是提前编译的

编译语言的编译过程(三步)

  • 分词/词法分析

    • 此过程会将字符组成的字符串分解成对编程语言来说有意义的代码块,这些代码块被称为词法单元

      • 例如 var a = 2; 会被分解为 vara=2; 这几个词法单元,空格是否会被当做词法单元取决于空格在语言中是否具有实际意义。
    • 分词词法分析 的区别在于词法单元的识别是通过有状态还是无状态的方式进行的。

      • 例如词法单元判断 a 是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,这个过程就是词法分析
  • 解析/语法分析

    • 词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法的结构的树(抽象语法树)

      • 例如 var a = 2; 的抽象语法树中可能有一个叫作 VariableDeclaration 的顶级节点,顶级节点下有一个叫作 Identifier 值为 a 和一个叫作 AssignmentExpression子节点AssignmentExpression 下有一个叫作 NumericLiteral 值为 2子节点

        在这里插入图片描述

  • 代码生成

    • 将抽象语法树转换成可执行代码的过程(与语言、目标平台等相关)

      • 例如平台通过某种方式将 var a = 2;抽象语法树转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值存储在a中

js与一般编译语言的区别

  • js引擎不会拥有像其他语言编译器那么多的时间来进行优化,因为js的编译过程不是发生在构建之前的
  • 对于js来说,大部分情况下编译是发生在代码执行前几微妙的时间内(甚至更短),例如js编辑器会对var a = 2;这段程序进行编译,然后做好执行他的准备,并且通常马上就会执行它。

作用域

引擎 :负责js程序的编译及执行过程

编译器 :负责在js代码执行前的编译

作用域 :负责收集并维护由所有生命的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

编译过程

var a = 2;

  1. 遇到 var a ,编译器会在当前作用域中声明一个变量(如果之前没有声明过)。
  2. 在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值 a = 2 ,否则就会抛出异常。

引擎查找变量的两种类型: LHSRHS

  • LHS: 赋值操作的目标是谁
    • 给查询到的变量进行赋值,如把2赋值给a变量,首先要查询a是否存在,这个时候用的就是LHS查询
  • RHS: 谁是赋值操作的源头
    • 要获取到某个变量的值,如打印变量a,console.log(a); js引擎要去查询这个变量是否存在,得到变量的值,这个时候用的就是RHS查询
  • 示例:
    function foo(a) {
          
          
        console.log(a); // 2 
    }
    foo(2);
    
    // 引擎:   我说作用域,我需要为 foo 进行 RHS 引用。你见过它吗? 
    // 作用域: 别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。 
    // 引擎:   哥们太够意思了!好吧,我来执行一下 foo。 
    // 引擎:   作用域,还有个事儿。我需要为 a 进行 LHS 引用,这个你见过吗? 
    // 作用域: 这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。 
    // 引擎:   大恩不言谢,你总是这么棒。现在我要把 2 赋值给 a。 
    // 引擎:   哥们,不好意思又来打扰你。我要为 console 进行 RHS 引用,你见过它吗? 
    // 作用域: 咱俩谁跟谁啊,再说我就是干这个。这个我也有,console 是个内置对象。 给你。 
    // 引擎:   么么哒。我得看看这里面是不是有 log(..)。太好了,找到了,是一个函数。 
    // 引擎:   哥们,能帮我再找一下对 a 的 RHS 引用吗?虽然我记得它,但想再确认一次。 
    // 作用域: 放心吧,这个变量没有变动过,拿走,不谢。 
    // 引擎:   真棒。我来把 a 的值,也就是 2,传递进 log(..)。
    

作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。

引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止

异常

function foo(a) {
    
    
    console.log(a + b); // a + undefined
    var b = a;
}
foo(2); // NaN
// 对 b 进行RHS时未找到,说明b是个“未声明”的变量,如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。
function foo(a) {
    
    
    console.log(a + b);
    b = a;
}
foo(2); // b is not defined

测试

function foo(a) {
    
    
    var b = a;
    return a + b;
}
var c = foo(2);
  1. 找出所有的 LHS 查询(这里有 3 处!)
    • c = ..;
    • a = 2(隐式变量分配)
    • b = ..
  2. 找出所有的 RHS 查询(这里有 4 处!)
    • foo(2..
    • = a;
    • a ..
    • .. b

词法作用域

作用域的两种工作模式

  • 词法作用域(JavaScript)
  • 动态作用域(如Bash脚本、Perl中的一些模式等)

词法作用域

词法作用域就是定义在词法阶段的作用域。
(词法作用域是写的代码将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。)

function foo(a) {
    
    
    var b = a * 2;

    function bar(c) {
    
    
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2); // 2, 4, 12
  • 作用域分析
    1. 全局作用域下,一个标识符:foo
    2. foo作用域下,三个标识符:a, b, bar
    3. bar作用域下,一个标识符:c
  • 查找
    • 作用域查找会在找到第一个匹配的标识符时停止。
    • 在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。
    • 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。

欺骗词法(欺骗词法作用域)

欺骗词法作用域会导致性能下降。

eval (不推荐使用)

  • 非严格模式下

    function foo(str) {
          
          
        eval(str); // 欺骗! 
        console.log(a);
    }
    var a = 2;
    foo("var a = 3;"); // 3
    

    以上代码可以理解为在foo的作用域下创建了一个变量b,并遮蔽了外部(全局)作用域下的同名变量。

    function foo(str) {
          
          
        var a = 3; // eval(str);
        console.log(a);
    }
    var a = 2;
    foo("var a = 3;"); // 3
    
  • 严格模式下(严格模式下,eval在运行是将拥有自己的词法作用域,意味着其中的声明无法修改所在的作用域。)

    function foo(str) {
          
          
        'use strict';
        eval(str);
        console.log(a);
    }
    var a = 2;
    foo("var a = 3;"); // 2
    
  • 与eval相似的方法(不提倡使用

    • setTimeout('var a = 1;console.log(a);', 5000); // 1
    • setInterval('var a = 2;console.log(a);', 1000); // 2
    • new Function('var a = 3; console.log(a)')(); // 3
      • new Function(...) 构建方法比eval安全一些,但也不提倡使用

with (不推荐使用)

function foo(obj) {
    
    
    with(obj) {
    
    
        a = 2;
    }
}

var o1 = {
    
    
    a: 3
};
var o2 = {
    
    
    b: 3
};

foo(o1);
console.log(o1); // {a: 2}              a被修改
console.log(o1.a); // 2                 a被修改
console.log(window.a); // undefined     a没有被暴露到全局

foo(o2);
console.log(o2); // {b: 3}          b未被修改
console.log(o2.a); // undefined     无法访问a
console.log(window.a); // 2         a被暴露到全局

不推荐原因

当浏览器发现 eval(..)with ,它只能假设关于标识符位置的判断是无效的,无法在词法分析阶段明确知道 eval(...) 接收的是什么代码,无法知道 with 创建的新的词法作用域的内容是什么。因此浏览器并不会对内部代码进行优化,如果大量使用 eval(..)with 方法会使浏览器运行很慢,所以不推荐使用。

函数作用域和块作用域

函数中的作用域

属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

function foo(a) {
    
    

    var b = 2;

    function bar() {
    
    
        // ...
        console.log(foo, a, b, c, bar); // success
    }

    var c = 3;

    console.log(foo, a, b, c, bar); // success

}

console.log(foo); // success
console.log(a, b, c, bar); // fail

隐藏内部实现

应遵守 最小授权(最小暴露) 原则,指的是在软件设计中,应该最小限度地暴露必要内容,而将其他内容都 隐藏 起来,比如某个模块或对象的API设计。

  • 不推荐的方式
    function funA(a) {
          
          
        b = a + funB(a);
        console.log(b);
    }
    var b;
    
    function funB(b) {
          
          
        return b;
    }
    funA(1);
    
  • 推荐的方式
    function funA(a) {
          
          
        var b;
    
        function funB(b) {
          
          
            return b;
        }
        b = a + funB(a);
        console.log(b);
    }
    funA(1);
    

规避冲突

规避定义变量、方法或其他属性的冲突,避免错误的赋值和使用。相关如下示例:

  • 比如有一个需求需要打印全局作用域下的a,但是经过foo方法执行后a的值发生了变化。如下:

    var a = 1;
    function foo() {
          
          
        a = 2;
    }
    foo();
    console.log(a); // 2
    
  • 修改结果如下:

    // 方式一(推荐)
    var a = 1;
    function foo() {
          
          
        var a = 2;
    }
    foo();
    console.log(a); // 1
    
    // 方式二
    var a = 1;
    function foo() {
          
          
        // b = 2 但b会暴露到全局作用域上
        var b = 2; 
    }
    foo();
    console.log(a); // 1
    

函数作用域

var a = 1;
function foo() {
    
    
    var a = 2;
    console.log(a); // 2
}
foo();
console.log(a); // 1

以上代码虽然规避了变量冲突,但是全局上还存在foo方法污染着全局作用域,此时如果还想执行foo方法单不想污染到全局作用域,只需要执行一次此方法,还想打印全局作用域下的a,那么可以把foo改写成不具名函数。

var a = 1;
(function foo() {
    
    
    var a = 2;
    console.log(a); // 2
}())
console.log(a); // 1

匿名与具名

  • 匿名
    • 行内函数表达式即使具名也会被处理为匿名表达式
    • 函数内部可使用 arguments.callee 进行调用自身
  • 具名
    • 带有名字的表达式,可以根据名字进行调用

立即执行函数表达式(术语:IIFE)

var a = 2;
(function IIFE(def) {
    
    
    def(window);
})(function def(global) {
    
    
    var a = 3;
    console.log(a); // 3 
    console.log(global.a); // 2 
});

块作用域

var

  • 写在 forif 等中的变量污染到了全局变量,如果在其他方法中用到了同名变量就会严重影响代码的可维护性
  • for
    // for
    for (var i = 1; i <= 10; i++) {
          
          
        console.log(i); // 1 - 10
    }
    console.log(i); // 11
    
  • if
    // if
    var isTrue = false;
    if (isTrue) {
          
          
        var a = 1;
    } else {
          
          
        console.log(a); // undefined 
    }
    console.log(a); // undefined
    
  • {}
    // {}
    {
          
          
        var a = 2;
    }
    console.log(a); // 2
    
  • with
    // with
    var obj = {
          
           a: 1 }
    with (obj) {
          
          
        var a = 2;
        var b = 3;
    }
    console.log(obj, a, b); // {a: 2} undefined 3
    
  • try/catch
    // try/catch
    try {
          
          
        var a = 1;
        console.log(b); // undefined
        undefined();
    } catch (err) {
          
          
        var b = 2;
        console.log(a); // 1
    }
    console.log(a, b); // 1 2
    console.log(err); // err is not defined
    
  • try/catch
    // try/catch
    try {
          
          
        var a = 1;
        console.log(b); // undefined
    } catch (err) {
          
          
        var b = 2;
        console.log(a);
    }
    console.log(a, b); // 1 undefined
    console.log(err); // err is not defined
    

let

当把以上代码中的var换成let后就不会出现污染全局的情况了,需要注意的是let不会在块作用域中进行声明提升。如下:

// for
// console.log(i); // i is not defined
for (let i = 1; i <= 10; i++) {
    
    
    console.log(i); // 1 - 10
}
console.log(i); // i is not defined
  • 当需要定义多个各自执行各自的作用域内的代码时,同时这个机制也与闭包及回收垃圾的回收机制相关,避免不必要的变量污染全局,可以使用如下方式:
    function foo(a){
          
          
        console.log(a);
    }
    
    {
          
          
        let a = 1;
        foo(a); // 1
        console.log(a); // 1
    }
    
    {
          
          
        let a = 2;
        foo(a); // 2
        console.log(a); // 2
    }
    
    foo(3); // 3
    

const

与let类似,但const定义的变量不能进行修改,一般用来创建常量。

var,let,const 区别

  • var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
  • let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
  • const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。

提升

先有鸡还是先有蛋

先有的 声明 还是先有的 赋值,或是先有的 打印 操作?

a = 2;
var a;
console.log(a);
console.log(a);
var a = 2;

提升示例

具体参见: https://blog.csdn.net/weixin_43526371/article/details/107360156

foo(); // 2
var foo = function () {
    
     console.log(1); }
foo(); // 1
function foo() {
    
     console.log(2); }
foo(); // 1

/**
 * 相当于:
 *      var foo;
 *      function foo() { console.log(2); }
 *      foo(); // 2
 *      foo = function () { console.log(1); }
 *      foo(); // 1
 *      foo(); // 1
 */

作用域闭包

参考地址

https://blog.csdn.net/weixin_43526371/article/details/107360445

猜你喜欢

转载自blog.csdn.net/weixin_43526371/article/details/125004838