【深度剖析】JavaScript中块级作用域与函数作用域

简介: 【深度剖析】JavaScript中块级作用域与函数作用域

面试官必问系列:深入理解JavaScript块和函数作用域


  • • 在 JavaScript 中,究竟是什么会生成一个新的作用域,只有函数才会生成新的作用域吗?那 JavaScript 其他结构能生成新的作用域吗?


3.1 函数中的作用域


  • • 在之前的词法作用域中可见 JavaScript 具有基于函数的作用域,这也就意味着一个函数都会创建一个新的作用域。但其实并不完全正确,看以下例子:
function foo(a) {
    var b = 2;
    function bar() {
        // ...
    }
    var c = 3;
}
  • • 以上代码片段中,foo() 的作用域中包含了标识符 a, b, c 和 bar。无论表示声明出现在作用域中的何处,这个标识符所代表的变量和函数都附属于所处作用域的作用域中。
  • • bar() 中也拥有属于自己的作用域,全局作用域也有属于自己的作用域,它只包含了一个标识符: foo()
  • • 由于标识符 a, b, c 和 bar 都附属于 foo() 的作用域内,因此无法从 foo() 的外部对它们进行访问。也就是说,这些标识符在全局作用域中是无法被访问到的,因此如下代码会抛出 ReferenceError:
bar(); // ReferenceError: bar is not defined
console.log(a, b, c); // 全都抛出 ReferenceError
  • • 但标识符 a, b, c 和 bar 可在 foo() 的内部被访问的。
  • 函数作用域的含义属于这个函数的全部变量都可以在整个函数的范围内使用及复用(在嵌套的作用域中也可以使用)。这种设计方案可根据需要改变值类型的 "动态" 特性。


3.2 隐藏内部实现


  • • 我们对函数的传统认知就是先声明一个函数,然后再向里面添加代码,但反过来可带来一些启示:从所写的代码中挑选出一个任意片段,然后就用函数声明的方式对它进行包装,实际上就是把这些代码 "隐藏" 起来了。
  • 实际的结果就是在这个代码片段的周围创建了一个新的作用域,也就是说这段代码中的任何声明(变量或函数)都将绑定在这个新创建的函数作用域中,而不是先前所在的作用域中。换句话说,可把变量和函数包裹在一个函数的作用域中,然后用这个作用域来 "隐藏" 他们。
  • • 为什么 "隐藏" 变量和函数是一个有用的技术?
function doSomething(a) {
    b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
function doSomethingElse(a) {
    return a - 1;
}
var b;
doSomething( 2 ); // 15
  • • 上述代码片段中,变量 b 和函数 doSomethingElse(..) 应该是 doSomething(..) 内部具体实现的 "私有" 内容。而上述代码将变量 b 和函数 doSomethingElse(..) 的访问权限放在了外部作用域中,这可能是 "危险" 的。更 "合理" 的设计应该是将这些私有内容放在 doSomething(...) 的内部。
  • • 如下:
function doSomething(a) {
    function doSomethingElse(a) {
        return a - 1;
    }
    var b;
    b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
doSomething( 2 ); // 15
  • 规避冲突
  • "隐藏" 作用域中的变量和函数的另一个好处是可避免同名标识符的冲突,两个标识符名字相同但用途不同,无意间可能会造成命名冲突,而冲突会导致变量的值被意外覆盖。
  • • 例如:
function foo() {
    function bar(a) {
        i = 3; // 修改for 循环所属作用域中的i
        console.log( a + i );
    }
    for (var i=0; i<10; i++) {
        bar( i * 2 ); // 糟糕,无限循环了!
    }
}
foo();
  • • bar(...) 内部的赋值表达式 i = 3 意外地覆盖了声明在 foo(..) 内部 for 循环中的 i。在这个例子中将会导致无限循环,因为 i 被固定设置为 3,永远满足小于 10 这个条件。
  • 规则冲突的方式
  1. 1. 全局命名空间:在全局作用域中声明一个足够独特的变量,通常为一个对象,如下:
var MyReallyCoolLibrary = {
    awesome: "stuff",
    doSomething: function() {
        // ...
    },
    doAnotherThing: function() {
        // ...
    }
}
2.  **模块管理**

3.3 函数作用域

  • • 现在知道,在任意代码片段外部添加包装函数,可将内部的变量和函数定义 "隐藏" 起来,外部作用域无法访问包装函数内部的任何内容。
  • • 如下:
var a = 2;
function foo() { // <-- 添加这一行
    var a = 3;
    console.log( a ); // 3
} // <-- 以及这一行
foo(); // <-- 以及这一行
console.log( a ); // 2
  • • 上述代码会导致一些额外的问题,首先,必需先声明一个具名函数 foo(), 这就意味着 foo 这个名称本身 "污染" 了所在作用域(上述代码为全局作用域)。其次,必须显式地通过 foo() 来调用这个函数。
  • • 如果函数不需要函数名(或者至少函数名可以不污染所在作用域),且能够自行运行,这将会更理想。
  • • JavaScript 提供了两种方案来解决:
var a = 2;
(function foo() {
    // <-- 添加这一行
    var a = 3;
    console.log(a); // 3
})(); // <-- 以及这一行
console.log(a); // 2
  • • 在上述代码中,包装函数的声明以 (function... 而不仅是以 function... 开始。函数会被当做函数表达式而不是一个标准的函数声明来处理。
  • 如何区分函数声明和表达式?
  • • 最简单的方式就是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 为声明中的第一个关键字,那它就是一个函数声明,否则就是一个函数表达式。
  • 函数声明和函数表达式之间最重要的区别就是他们的名称标识符将会绑定在何处。
  • • 比较一下前面两个代码片段。第一个片段中 foo 被绑定在所在作用域中,可以直接通过 foo() 来调用它。第二个片段中foo 被绑定在函数表达式自身的函数中而不是所在作用域中。
  • • 换句话说,(function foo(){...}) 作为函数表达式意味着 foo 只能在 ... 所代表的位置中被访问,外部作用域则不行。


3.3.1 匿名和具名


  • • 对于函数表达式最熟悉的就是回调参数了,如下:
setTimeout(function () {
    console.log("I waited 1 second!");
}, 1000);
  • • 这叫作匿名函数表达式,因为 function().. 没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名——在JavaScript 的语法中这是非法的。
  • 匿名函数表达式的缺点
  1. 1. 匿名函数在栈追踪中不会显示出有意义的函数名,这使调试很困难。
  2. 2. 如果没有函数名,当函数需要引用自身时只能通过已经过期arguments.callee 来引用。
  3. 3. 匿名函数对代码可读性不是很友好。
  • • 上述代码的改造结果:
setTimeout(function timeoutHandler() {
    console.log("I waited 1 second!");
}, 1000);


3.3.2 立即执行函数表达式

var a = 2;
(function IIFE() {
    var a = 3;
    console.log(a); // 3
})();
console.log(a); // 2
  • • 由于函数被包含在一对( ) 括号内部,因此成为了一个表达式,通过在末尾加上另外一个( ) 可以立即执行这个函数,比如(function foo(){ .. })()。第一个( ) 将函数变成表达式,第二个( ) 执行了这个函数。
  • • 立即执行函数表达式的术语为:IIFE(Immediately Invoked Function Expression);
  • IIFE 的应用场景
  1. 1. 除了上述传统的 IIFE 方式,还有另一个方式,如下:
var a = 2;
(function IIFE() {
    var a = 3;
    console.log(a); // 3
}());
console.log(a); // 2
  • • 第一种形式中函数表达式被包含在 ( ) 中,然后在后面用另一个 () 括号来调用。第二种形式中用来调用的 () 括号被移进了用来包装的 ( ) 括号中。
  • • 这两种方式的选择全凭个人喜好。
  1. 1. IIFE 还有一种进阶用法,就是把他们当做函数调用并传递参数进去,如下:
var a = 2;
(function IIFE(global) {
    var a = 3;
    console.log(a); // 3
    console.log(global.a); // 2
})(window);
console.log(a); // 2
  1. 1. IIFE 的另一个应用场景是解决 undefined 标识符的默认值被错误覆盖导致的异常
  • • 将一个参数命名为 undefined, 但在对应的位置不传入任何值,这样就可以就保证在代码块中 undefined 标识符的值为 undefined
undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!
(function IIFE(undefined) {
    var a;
    if (a === undefined) {
        console.log("Undefined is safe here!");
    }
})();
  1. 1. IIFE 的另一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当做参数传递进去
var a = 2;
(function IIFE(def) {
    def(window);
})(function def(global) {
    var a = 3;
    console.log(a); // 3
    console.log(global.a); // 2
});
  • • 函数表达式 def 定义在片段的第二部分,然后当做参数(这个参数也叫做 def)被传递 IIFE 函数定义的第一部分中。最后,参数 def(也就是传递进去的函数)被调用,并将 window 传入当做 global 参数的值。
相关文章
|
1月前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
1月前
|
自然语言处理 JavaScript 前端开发
[JS]作用域的“生产者”——词法作用域
本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。
35 2
[JS]作用域的“生产者”——词法作用域
|
1月前
|
JavaScript 前端开发 Java
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
本文介绍了JavaScript中常用的函数和方法,包括通用函数、Global对象函数以及数组相关函数。详细列出了每个函数的参数、返回值及使用说明,并提供了示例代码。文章强调了函数的学习应结合源码和实践,适合JavaScript初学者和进阶开发者参考。
42 2
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
|
1月前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。
|
1月前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域
|
1月前
|
前端开发 JavaScript 开发者
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。
|
2月前
|
JavaScript 前端开发
JavaScript 作用域
JavaScript 作用域是指程序中可访问的变量、对象和函数的集合。它分为函数作用域和局部作用域。函数作用域内的变量仅在函数内部可见,而全局作用域的变量在整个网页中均可访问。局部变量在函数执行完毕后会被销毁,而全局变量则在整个脚本生命周期中都存在。未使用 `var` 关键字声明的变量默认为全局变量。
|
2月前
|
JavaScript 前端开发
JavaScript 函数语法
JavaScript 函数是使用 `function` 关键词定义的代码块,可在调用时执行特定任务。函数可以无参或带参,参数用于传递值并在函数内部使用。函数调用可在事件触发时进行,如用户点击按钮。JavaScript 对大小写敏感,函数名和关键词必须严格匹配。示例中展示了如何通过不同参数调用函数以生成不同的输出。
|
2月前
|
存储 JavaScript 前端开发
JS函数提升 变量提升
【10月更文挑战第6天】函数提升和变量提升是 JavaScript 语言的重要特性,但它们也可能带来一些困惑和潜在的问题。通过深入理解和掌握它们的原理和表现,开发者可以更好地编写和维护 JavaScript 代码,避免因不了解这些机制而导致的错误和不一致。同时,不断提高对执行上下文等相关概念的认识,将有助于提升对 JavaScript 语言的整体理解和运用能力。