理解JavaScript的作用域

作者: 子瑜说IT | 来源:发表于2019-06-11 21:54 被阅读6次

    作用域

    • 是什么? 作用域是指程序源代码中定义变量的区域。
    • 有什么用? 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

    作用域嵌套与作用域链

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

    • 首先,JS引擎从当前的执行作用域开始查找变量。
    • 然后,如果找不到,引擎会在外层嵌套的作用域中继续查找。
    • 最后,直到找到该变量,或抵达最外层的全局作用域为止。

    这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

    function foo(b) {
     // 变量a在全局作用域下查询得到的
     console.log(a + b) // 4
    }
    var a = 2
    foo(2)
    
    
    image

    查询异常

    • 如果获取未声明的变量的值会导致 ReferenceError 异常。
    • 如果对未声明过的变量进行赋值:
    • 在非严格模式下,JS引擎会为其自动创建一个全局变量且进行赋值。
    • 如在严格模式下,会导致 ReferenceError 异常。
    var a = 10
    function sum() {
     // b 是没有显式声明的,但被隐式创建为全局变量
     b = 20
     return a + b
    }
    console.log(sum()) // 30
    console.log(c) // ReferenceError: c is not defined
    // 因为c是未定义的变量,无法使用
    
    

    词法作用域(静态作用域)

    • 词法作用域就是定义在词法阶段的作用域,简单说就是函数的作用域在函数定义的时候就决定了。
    • 词法作用域查找规则是:作用域查找是从内到外进行查找的,直到找到第一个匹配的标识符时停止。

    而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

    image

    词法作用域的经典例子

    请思考以下2个例子输出的结果

    // 例子1
    var scope = "global scope";
    function checkscope() {
     var scope = "local scope";
     function f() {
     return scope;
     }
     return f();
    }
    checkscope();
    // 例子2
    var scope = "global scope";
    function checkscope() {
     var scope = "local scope";
     function f() {
     return scope;
     }
     return f;
    }
    checkscope()();
    
    

    上面两段代码的结果都输出:local scope,因为 JS 采用的是词法作用域,函数的作用域基于函数创建的位置。

    欺骗词法作用域

    • 欺骗词法作用域意思是(无意地)修改了所在的作用域。
    • 欺骗词法作用域的两个方法:eval() 和 with()。
    • 缺点:欺骗词法作用域会导致性能下降。

    eval() 函数可以接受一个字符串,并执行其中的的 JS 代码。

    function foo(str, a) {
     eval(str) // 欺骗作用域,引擎在此声明了:var b = 2,所以能计算以下结果
    
     console.log(a + b) // 3
    }
    var b = 1
    foo('var b = 2', 1)
    
    

    with() 语句通常被当作重复引用同一个对象中多个属性的快捷方式,可以不需要重复引用对象本身。

    function foo(obj) {
     with (obj) {
     b = 4
     a = 2
     }
    }
    var obj = {
     b: 3
    }
    foo(obj)
    console.log(obj.a) // undefined
    console.log(obj.b) // 4
    console.log(a) // 2,a被当前全局变量泄露到全局作用域上了
    
    
    image

    为什么最后能输出a的值为2?

    原因是把 obj 对象传入函数内,obj 对象没有 a 属性,所以 obj.a 的值是 undefined,却在 with()语句中的 a 被当作全局变量隐式声明了,而且进行了赋值为2。

    函数作用域

    函数作用域内的变量或者内部函数,对外都是封闭的,从外层的作用域无法直接访问函数内部的作用域,否则会报引用错误异常。解决方法:闭包。

    function f1() {
     var a = 1;
     var b = 2;
     var c = 3;
    }
    console.log(a, b, c) // ReferenceError: a, b, c is not defined
    // 原因变量a,b,c是定义在函数内部的变量,外部作用域是无法访问的。
    
    

    全局作用域

    最外层的全局作用域,任何地方都可以访问得到。在最外层作用域下使用 var 关键字会定义全局变量,也就是说会挂载在 window 对象上,或者不使用关键字 var、let、const 直接对变量名字进行赋值,JS也会自动为其创建为全局变量。

    var a = 10;
    function f1() {
     b = 20
     function f2() {
     c = 30
     console.log(a) // 10
     }
     f2()
    }
    f1()
    // b和c变量被隐式声明到全局变量了,所以能访问到,这也叫变量提升机制
    console.log(b) // 20
    console.log(c) // 30
    // 但a,b,c也被挂载在window对象(全局作用域)上面了
    console.log(window.a) // 10
    console.log(window.b) // 20
    console.log(window.c) // 30
    
    
    image

    块级作用域

    块级作用域指在代码块 {} 里面定义的变量,只会在当前代码块有效,如果外层作用域下想访问该变量,会报引用错误异常。

    使用关键字 let 或 const 定义块级作用域的变量。

    for (let i = 0; i < 10; i++) {
    }
    console.log(i) // ReferenceError: i is not defined
    // 因为i只能在for循环内部有效,外部作用域是访问不到的。
    
    

    变量提升机制

    先声明,后赋值

    JS变量的声明和赋值是2个不同的步骤,比如:

    a = 10
    var a
    console.log(a) // 10
    
    

    JS引擎会将 var a 和 a = 10 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务:

    // 编译阶段的任务
    var a
    // 执行阶段的任务
    a = 10
    console.log(a) // 10
    
    

    变量提升

    function getValue(condition) {
     console.log(value) // 不会报错
     if (condition) {
     var value = "blue";
     return value;
     } else {
     return null;
     }
    }
    getValue(undefined == null)
    
    

    实际上以上代码的 value 已经被变量提升了:

    function getValue(condition) {
    
     // 编译阶段的任务
     var value;
    
     console.log(value) // undefined
     if (condition) {
     // 执行阶段的任务
     value = "blue";
     return value;
     } else {
     return null;
     }
    }
    getValue(undefined == null)
    
    
    image

    函数优先

    • 函数声明和变量声明都会被提升,但是出现在有多个“重复”声明的代码中,函数会首先被提升,然后才是变量。
    • 函数没有方法重载,存在两个相同的函数名,后面的函数会覆盖前面的函数。
    foo() // 3
    function foo() {
     console.log(1)
    }
    var foo = function () {
     console.log(2)
    }
    function foo() {
     console.log(3)
    }
    foo() // 2 函数表达式的提升情况
    
    

    为什么呢?原因:

    1. 函数声明和变量声明都会被提升,但是出现在有多个“重复”声明的代码中,函数会首先被提升,然后才是变量。
    2. 相同函数名字,后面函数覆盖前面的函数。

    实际上以上代码可以看成这样:

    function foo() {
     console.log(3)
    }
    var foo // 被忽略了
    foo()
    console.log(foo) // [Function: foo]
    
    
    image
    最后,给大家推荐一个前端学习进阶内推交流群685910553前端资料分享),不管你在地球哪个方位,
    不管你参加工作几年都欢迎你的入驻!(群内会定期免费提供一些群主收藏的免费学习书籍资料以及整理好的面试题和答案文档!)

    如果您对这个文章有任何异议,那么请在文章评论处写上你的评论。

    如果您觉得这个文章有意思,那么请分享并转发,或者也可以关注一下表示您对我们文章的认可与鼓励。

    愿大家都能在编程这条路,越走越远。

    相关文章

      网友评论

        本文标题:理解JavaScript的作用域

        本文链接:https://www.haomeiwen.com/subject/yshyaqtx.html