为了账号安全,请及时绑定邮箱和手机立即绑定

理解JavaScript的作用域

标签:
JavaScript

大纲

  • 作用域
  • 作用域嵌套与作用域链
  • 查询异常
  • 词法作用域(静态作用域)
  • 函数作用域
  • 全局作用域
  • 块级作用域
  • 变量提升机制
  • 函数优先

作用域

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

作用域嵌套与作用域链

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

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

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

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

foo(2)

查询异常

  • 如果获取未声明的变量的值会导致 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是未定义的变量,无法使用

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

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

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

词法作用域的经典例子

请思考以下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被当前全局变量泄露到全局作用域上了

为什么最后能输出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

块级作用域

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

使用关键字 letconst 定义块级作用域的变量。

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 aa = 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)

函数优先

  • 函数声明和变量声明都会被提升,但是出现在有多个“重复”声明的代码中,函数会首先被提升,然后才是变量。
  • 函数没有方法重载,存在两个相同的函数名,后面的函数会覆盖前面的函数。
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]

参考链接

点击查看更多内容
44人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
4318
获赞与收藏
1744

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消