深入剖析JavaScript作用域:如何避免常见的作用域陷阱并写出更高效的代码?

引言

  在JavaScript中,作用域是一个非常重要的概念,它定义了变量、函数和对象的可访问范围。作用域规定了代码在哪里可以访问变量、函数和对象,以及在何处应该搜索它们。掌握作用域是编写高效、可读性强的JavaScript代码的关键之一。今天,我们将深入探讨JavaScript作用域的概念、类型和行为,说说我对作用域的理解,帮助各位理解作用域的重要性以及如何避免常见的作用域陷阱。

什么是作用域

  作用域是编程中一个重要的概念,它指的是变量、函数和对象的可访问范围。在一个程序中,不同的变量、函数和对象可能存在于不同的作用域中,这些作用域可以相互嵌套,形成一个作用域链,作用域规定了代码在哪里可以访问变量、函数和对象,以及在何处应该搜索它们。在JavaScript中。
作用域可以分为全局作用域函数作用域块级作用域等多种类型。不同类型的作用域有着不同的规则和行为。

全局作用域
  • 全局作用域是最外层的作用域,它包含了整个程序中的所有变量和函数。在全局作用域中声明的变量和函数可以在程序的任何地方被访问。
函数作用域
  • 函数作用域是指在函数中声明的变量和函数只能在该函数内部访问。函数作用域可以帮助避免变量名冲突和提高代码可维护性
块级作用域
  • 块级作用域是JavaScript较新的特性,它在ES6中引入。块级作用域是指在{}中声明的变量和常量只能在该块中访问。块级作用域可以帮助避免变量污染和提高代码可读性。

现在我们来举例子来了解它们,上代码:

var globalVar = "I am in global scope"; // 全局作用域

function foo() { // 函数作用域
  var functionVar = "I am in function scope"; // 函数作用域

  if (true) { // 块级作用域
    let blockVar = "I am in block scope"; // 块级作用域
    console.log(blockVar); // 输出 "I am in block scope"
  }

  console.log(functionVar); // 输出 "I am in function scope"
  console.log(globalVar); // 输出 "I am in global scope"
  console.log(blockVar); // 报错,无法访问 blockVar
}

foo();
console.log(globalVar); // 输出 "I am in global scope"
console.log(functionVar); // 报错,无法访问 functionVar
console.log(blockVar); // 报错,无法访问 blockVar

  在这个代码示例中,我们定义了三个不同的作用域:全局作用域、函数作用域和块级作用域。其中,全局作用域包含了所有的变量和函数,函数作用域只包含在函数内部定义的变量和函数,而块级作用域只包含在代码块内部定义的变量。
  在代码中,我们定义了一个名为 globalVar 的变量,它是在全局作用域中定义的,因此可以在程序的任何地方访问。我们还定义了一个名为 foo 的函数,并在其内部定义了两个变量 functionVar 和 blockVar,分别在函数作用域和块级作用域内
  在函数中,我们使用了一个if语句来创建一个块级作用域,并在其中定义了变量 blockVar。由于块级作用域只在if语句内部存在,因此 blockVar 只能在if语句内部被访问。
  最后,在函数外部我们尝试访问 globalVar,可以正常输出,但尝试访问 functionVar 和 blockVar 将会报错,因为它们只在函数作用域和块级作用域内部存在。

let、var、const

   既然讲到了块级作用域那么就不得不提提声明变量的3个方法,它们之间的区别以及使用方法,虽然很多伙伴觉得很简单,但是也是很容易犯错的,所以我来说说它们之前的区别。

let和var的区别

1.作用域:let声明的变化量是块级作用域,只有在其他声明的块内有效,而var声明的变化量是函数作用域的,可以在函数内部的任何地方访问。
代码举例:

function example() {
  if (true) {
    let x = 5;
    var y = 10;
  }
  console.log(x); // 报错,无法访问
  console.log(y); // 输出 10
}

2.变量提升:var声明的变量会被提升到函数或全部作用域的顶部,即使用变量的声明在使用之前也是有效的,而声明let的变量不会被提升,如果在声明之前使用会报错。
代码举例:

console.log(x); // 报错,无法访问
console.log(y); // 输出 undefined
var x = 10;
let y = 20;
  1. 重复声明:在同一个作用域中使用var重复声明同一个变化量不会报错,并且是会罩盖之前的声明;而在同一个作用域中使用let重复声明同一个变量会报错。`
    代码举例:
var x = 10;
var x = 20; // 不会报错,覆盖之前的声明
console.log(x); // 输出 20

let y = 10;
let y = 20; // 报错,重复声明
console.log(y); // 不会执行

总之,let和var有很多不同之处,但let更适合结合现代JavaScript的开发,因为它可以避免无变化提升和恢复声明等问题,同时使用代码更加清澄和易于维护。

let和const之间的区别
  1. 重新赋值:let允许为变量重新赋值,而const不允许变量在初始化后重新赋值。这意味着const变量的值在整个程序执行过程中是不变的。
    代码举例:
let value1 = 10;
value1 = 20; // Valid with let
console.log(value1); // Output: 20

const value2 = 10;
value2 = 20; // Invalid with const
console.log(value2); // Output: 10
  1. 初始化:let允许变量在没有初始值的情况下声明,而const要求变量在声明时用值初始化。const这意味着在声明时必须知道变量的值。
    代码举例:
let value1; // Valid with let
console.log(value1); // Output: undefined

const value2; // Invalid with const
console.log(value2); // SyntaxError: Missing initializer in const declaration
  1. 作用域: 和let都const具有块作用域,这意味着它们只能在声明它们的块内访问。但是,const变量的作用域也仅限于声明它们的只读块级作用域,这意味着它们无法在该作用域之外访问。
    代码举例:
if (true) {
  let value1 = 10;
  const value2 = 20;
}

console.log(value1); // ReferenceError: value1 is not defined
console.log(value2); // ReferenceError: value2 is not defined

综上所述,let用于声明可以重新赋值的变量,而const用于声明初始化后不能重新赋值的变量。和let都const具有块作用域,但const变量的作用域也仅限于声明它们的只读块级作用域

作用域链

  回到本文重点,讲到作用域的话那么作用域链就自然而然的引出了作用域链,那么什么是作用域链呢?

概念

  作用域链是一种用于解析标识符引用的机制,它是由多个嵌套的作用域对象组成的链式结构。当程序在一个作用域中访问一个变量或函数时,它会首先在该作用域中查找,如果找不到,它会沿着作用域链向上查找,直到找到该变量或函数或者到达全局作用域为止。作用域链的顶端是当前执行上下文的变量对象,底部是全局对象。当一个新的执行上下文被创建时,它会创建一个新的变量对象,并将该变量对象添加到作用域链的顶端。当执行上下文被销毁时,它对应的变量对象也会被销毁,从而将作用域链中的顶端移除。

   概念太长我们以代码来理解,以下是代码举例:

let a = 1;

function outer() {
  let b = 2;

  function inner() {
    let c = 3;

    console.log(a, b, c); // 输出:1 2 3
  }

  inner();
  console.log(a, b); // 输出:1 2
}

outer();
console.log(a); // 输出:1

  在这个例子中,我们首先在全局作用域中声明了一个变化量a,并赋予它价值1。然后我们确定了一个函数outer,在outer函数中声明了一个一个变化量b,并赋予它价值为2。outer函数中还义了另外一个函数inner,在inner函数中声明了一个变化量c,并赋予它价值为3。

inner() -> outer() -> global()

  当我们调使用inner函数时,它会首先在自己的使用范围中查找变化量a、b和c,但只能找到变化量c,因为a并且b都不在inner函数的使用范围中。因此,它会沿着作用域链接上找,找到了b变化量和a变化量的价值分别为2和1,并将它们打印出来。

outer() -> global()

  当inner数执行完成后,程序回到outer数中,并打印出了变化的量a和b的价值,分别为1和2。最后,程序回到全部使用范围中,并打印出来了量a的值,为1。

  这个例子说明了作 用域的工作原理:在 JavaScript 中,每个函数都有自己的作 用域,如果在当时作 用域中找到不是一个变量或函数,程序会沿着作用域链接上找,直达找到该变量或数或到达全局作域为停止。这个过程中,如果找到了同名的变数或数,会优先使用当前作用域中的变量或函数,而不是沿用域链接继续查找。

注意
使用域链的长度可以对程序的性能产生影响
  在 JavaScript 中,当一个新的操作上下文被创建时,都会创建一个新的变化量对象,并将变化量对象添加到作用域链的顶端。因此,如果使用域链接非常长,就需要在每次访问变量时遍历整个操作域链接,这会导致程序的性能下降。

  另外,如果代号中存在大量的封套数,也会导致使用域链接变得非常长。在这种情况下,可以通过减少少量的封套数来缩小作 用域链,或者通过将经常使用的量提升到外部作 用域中来避免访问过长的作 用域链。

function outer() {
  let a = 1;

  function inner1() {
    let b = 2;

    function inner2() {
      let c = 3;

      console.log(a + b + c);
    }

    inner2();
  }

  inner1();
}

outer();

  在此示例中,我们有三个嵌套函数:outer()、inner1()和inner2()。每个函数声明一个新变量并从其父作用域访问变量。当我们调用时outer(),它将运行所有嵌套函数并记录所有三个变量的总和。

  然而,每次我们从更高级别的作用域访问变量时,JavaScript都需要遍历整个作用域链才能找到该变量。在这个例子中,当我们访问afrom时inner2(),JavaScript 需要遍历作用域链inner1()才能outer()找到 的值a。这种遍历需要时间,如果范围链很长,它会减慢程序。
为了说明这一点,让我们修改示例以创建更长的作用域链:

function outer() {
  let a = 1;

  function inner1() {
    let b = 2;

    function inner2() {
      let c = 3;

      console.log(a + b + c);
    }

    function inner3() {
      let d = 4;

      inner2();
    }

    function inner4() {
      let e = 5;

      inner3();
    }

    function inner5() {
      let f = 6;

      inner4();
    }

    function inner6() {
      let g = 7;

      inner5();
    }

    inner6();
  }

  inner1();
}

outer();

  在这个修改后的示例中,我们添加了几个嵌套inner3()在. 这些函数中的每一个都会创建一个新变量并调用下一个嵌套函数。当我们运行时,它会遍历一个更长的作用域链来访问。inner6()inner1()outer()ainner2()

  这个较长的作用域链会降低程序的速度,尤其是在程序运行多次或函数被频繁调用的情况下。因此,避免创建不必要的长作用域链并优化嵌套函数中变量的使用是一种很好的做法。

如何避免常见的作用域陷阱并写出更高效的代码

  1. 避免使用全局变量:全局变量是在全局作用域中定义的变量,它们可以在代码中的任何位置访问。使用全局变量会导致作用域陷阱,因为它们可以被任何函数访问。为了避免这种情况,应该尽可能减少全局变量的使用,可以使用模块化的方法来封装各个模块,减少全局变量的数量。

  2. 避免使用 var 关键字:在 JavaScript 中,使用 var 关键字定义的变量是函数作用域的,它们可以被内部函数访问。如果在函数内部定义了一个变量但没有使用 var 关键字,那么这个变量会成为全局变量,从而导致作用域陷阱。为了避免这种情况,应该使用 let 和 const 关键字,它们定义的变量是块作用域的,只能在定义它们的代码块内访问。

  3. 避免在循环中使用函数嵌套:在循环中使用函数嵌套会导致作用域陷阱,因为在每次迭代中,函数都会重新定义。为了避免这种情况,可以将函数定义在循环外部,在循环中传递参数。

  4. 使用闭包:闭包是指一个函数能够访问它的外部作用域中的变量。使用闭包可以避免作用域陷阱,并使代码更加模块化和可重用。使用闭包时,需要注意不要创建过多的嵌套函数,以免影响程序的性能。

  5. 使用立即调用函数表达式(IIFE):立即调用函数表达式是指定义一个函数并立即调用它。使用 IIFE 可以避免作用域陷阱,并将变量限制在函数作用域内。这种方法特别适用于在全局作用域中定义变量和函数时。

最后

  理解作用域是理解 JavaScript 的关键之一。作用域是描述程序中变量和函数可被访问的区域。了解作用域的概念和原理可以帮助我们编写更优雅、更高效的 JavaScript 代码。同时,避免作用域陷阱也是编写高质量 JavaScript 代码的重要部分之一。通过遵循一些最佳实践,如避免全局变量、避免使用 var 关键字、避免在循环中使用函数嵌套、使用闭包和使用 IIFE 等。
   以上就是我对作用域的理解,有什么不对的地方小伙伴们也可以在评论区里提出,写文章不容易,喜欢的小伙伴可以点点赞,哈哈。
   总之,主打的就是一个认真学习,不断学习,将学会的东西进行总结,不容易忘记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小新-alive

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值