Javascript中的作用域到底是什么

300 阅读9分钟

Lexical Scope in JavaScript – What Exactly Is Scope in JS?

词法作用域( lexical scope ) 的概念不好理解,也很难掌握。但如果我们可以搞清楚概念里每一个词的意思,对于理解词法作用域将大有帮助。

因此本文将分别解释“词法(lexical)”和“作用域(scope)”这两个概念,然后在解释什么是词法作用域。

让我们开始吧~~

什么是作用域?

作用域 表示一个区间,在这个区间内声明的所有内容(比如方法或变量)都可以被该区间内的代码访问到。

注意:

  • 作用域(Scope) 指一个范围,区域或空间
  • 全局作用域(Global Scope) 指全局空间或一个公共空间
  • 局部作用域(Local Scope) 指一个局部空间或一个受限制的空间

举个例子:

// 定义一个全局变量:
const fullName = "Oluwatobi Sofela";

// 定义多层嵌套函数:
function profile() {
  function sayName() {
    function writeName() {
      return fullName;
    }
    return writeName();
  }
  return sayName();
}

// 打印结果:
console.log(profile()) // 'Oluwatobi Sofela'

点击查看源代码

在上述示例中,我们定义了一个fullName全局变量,这就意味着在脚本内所有代码都可以访问fullName变量。

我们在sayName()函数内定义了writeName()函数,所以writeName()sayName()的局部作用域包裹着。

换言之,writeName()只能被sayName()函数内部的代码访问。

请记住,无论writeName()函数何时被调用,编译器都不会直接访问全局作用域下的fullName变量,而是通过作用域链依次查找。

什么是作用域链

作用域链是一个独特空间。当一个变量被调用,那么变量在 被调用 时所在的局部作用域和全局作用域之间,就形成了一个作用域链。

示例

// 定义一个全局作用域变量:
const fullName = "Oluwatobi Sofela";

// 定义多层嵌套函数:
function profile() {
  function sayName() {
    function writeName() {
      return fullName;
    }
    return writeName();
  }
  return sayName();
}

console.log(profile()) // 'Oluwatobi Sofela'

在上述示例中,fullName 变量在 writeName() 函数作用域中被调用。

因此,从变量的执行作用域到全局作用域之间存在如下作用域链:

writeName() scope ---> sayName() scope ---> profile() scope ---> global scope

换言之,从fullName变量的执行作用域到它的词法作用域(此处指全局作用域)之间有4层作用域。

注意:JavaScript作用域链中,全局作用域是整个作用域链的终点。

作用域链是如何工作的?

Javascript的作用域链规定了编译器在查找 被调用变量 的词法作用域时所遵循的查找规则。

考虑如下示例代码:

// 定义一个全局作用域变量:
const fullName = "Oluwatobi Sofela";

// 定义多层嵌套函数:
function profile() {
  function sayName() {
    function writeName() {
      return fullName;
    }
    return writeName();
  }
  return sayName();
}

console.log(profile()) // 'Oluwatobi Sofela'

在上述示例中,无论何时调用profile()函数,编译器都会首先调用sayName()函数(profile()函数体内只有一个sayName()函数)。

然后,编译器会调用writeName()函数(sayName()函数体内只有一个writeName()函数)。

最终,writeName()函数会调用并返回fullName变量的值,所以编译器将返回fullName的值。但编译器不会直接从全局作用域中调用fullName

相反的,编译器必须通过作用域链一步一步的查找fullName的词法作用域。

下面是编译器查找fullName词法作用域的步骤:

  1. 首先,编译器会检查writeName()的函数作用域是否定义了fullName变量。未找到,继续向上查找下一个作用域,即sayName()作用域。
  2. 第二步,编译器在sayName()的函数作用域中查找,依然没找到,继续向上查找下一个作用域,即profile()作用域。
  3. 第三步,编译器在profile()的函数作用域中查找,依然没找到,继续向上查找下一个作用域,即全局作用域。
  4. 第四步,编译器查找到了全局作用域。幸运的是,在全局作用域下找到了fullName变量,然后将其值("Oluwatobi Sofela")作为返回值返回。

实践一下🤸‍♂️🏋️‍♀️🏊‍

考虑以下代码,并思考:编译器会调用哪一个fullName

// 在全局作用域中定义fullName:
const fullName = "Oluwatobi Sofela";

// 嵌套函数包含2个fullName变量
function profile() {
  const fullName = "Tobi Sho";
  function sayName() {
    const fullName = "Oluwa Sofe";
    function writeName() {
      return fullName;
    }
    return writeName();
  }
  return sayName();
}

console.log(profile())

编译器将调用第一个,第二个,还是第三个fullName呢?

注意: 如果你多多练习,就能从这个教程中收获多多。

如果你卡住了,不要气馁。重新温习以上知识,然后在试一次。

如果你已经用尽了全力(不要自欺欺人)但还是不会,那就继续往下看正确答案吧。

你做对了吗?

在上面脚本中定义的三个fullName中,编译器将调用并返回定义在sayName()函数中的fullName变量。

sayName()中定义的fullName之所以会被调用,是因为编译器查找作用域链时最先在sayName()的作用域中找到fullName

因此,当profile()被调用时,返回值是"Oluwa Sofe"

点击查看源代码

特别强调3点:

  • 假如编译器没有在任何作用域中找到fullName,那么编译器将返回一个引用错误Uncaught ReferenceError: fullName is not defined
  • 全局作用域是Javascript作用域链的最后一个作用域,即全局作用域是查找的终点。
  • 一个内部作用域(子作用域)可以访问它的外部作用域(父作用域),但是外部作用域不能访问它的子作用域。 举个例子,在上面代码中,writeName()可以访问它的任何父级作用域(比如sayName()的局部作用域,profile()的局部作用域,或者全局作用域)。 但是,无论是sayName()profile()的局部作用域,还是全局作用域都不能访问writeName()的作用域。

作用域小结

Javascript中所有的作用域都是一个可被访问的区间。

因此,如果你女(男)朋友打电话让你去他的私人作用域时,切记他们是在邀请你去他们的私人空间 😜!

你一旦去了,就要问他们最好的词法游戏是什么...

但是词法(lexical)究竟是什么意思,让我们一起往下看。

什么是词法(Lexical)?

词法(Lexical) 指的是定义某个事物。

任何创建文字,表达式,或变量的声明都叫词法。

比如,拼字游戏就是一种词法活动,因为这个游戏在创造文字。

语言学家的工作也是一种词法事业。

注意: 字典(dictionary)又叫词典(lexicon),也就是说,词典(lexicon)就是一部罗列并定义文字的字典(dictionary)。

现在我们知道了作用域和词法的意思,那就可以讨论 词法作用域 了。

什么是词法作用域(Lexical Scope)?

词法作用域(Lexical Scope) 是定义表达式并能被访问的区间。

换言之,一个声明(定义变量,函数等)的词法作用域就是它被定义时所在的作用域。

注意:

  • 词法作用域又叫静态作用域。
  • 一个声明 被调用时的作用域 不一定是它的词法作用域。相反的,定义时的作用域 才是词法作用域

一个词法作用域的小示例

考虑如下代码:

// 定义一个全局作用域变量:
const myName = "Oluwatobi";

// 在函数体内调用myName变量
function getName() {
  return myName;
}

console.log(getName()) // 'Oluwatobi'

在上述示例中,我们在全局作用域定义了myName变量,并在getName()函数作用域内调用了该变量。

问题: myName变量的词法作用域是什么? 全局作用域还是 getName()的局部作用域?

答案: 切记 词法作用域 意味着 定义时的作用域,并不是调用时的作用域 。因此myName变量的词法作用域是全局作用域,因为我们在全局环境下定义了myName变量。

再来一个小例子

function getName() {
  const myName = "Oluwatobi";
  return myName;
}

console.log(getName())  // 'Oluwatobi'

问题: myName变量的词法作用域是什么?

答案: 我们在getName()函数内定义并调用了myName变量。因此,myName的词法作用域是getName()的局部作用域,因为getName()myName定义时所在的作用域。

词法作用域如何工作?

Javascript表达式的词法作用域决定了哪些代码可以访问它。

换言之,只有词法作用域内的代码才可以访问该作用域内部的代码。

考虑如下代码:

// 定义一个函数:
function showLastName() {
  const lastName = "Sofela";
  return lastName;
}

// 再定义一个函数:
function displayFullName() {
  const fullName = "Oluwatobi " + lastName;
  return fullName;
}

// 调用 displayFullName():
console.log(displayFullName());

// 返回值:
Uncaught ReferenceError: lastName is not defined

上述代码中,调用displayFullName()时报错Uncaught ReferenceError。这是因为只有词法作用域内的代码才能访问该作用域的代码。

因此,不管是displayFullName()函数,还是它内部的代码都不能访问lastName变量,因为lastName定义在不同的作用域中。

换言之,lastName的词法作用域和displayFullName()不相同。

lastName的词法作用域是showLastName()函数作用域, 而displayFullName()的词法作用域是全局作用域。

现在,考虑另一段代码:

// 定义一个函数:
function showLastName() {
  const lastName = "Sofela";
  return lastName;
}

// 再定义一个函数:
function displayFullName() {
  const fullName = "Oluwatobi " + showLastName();
  return fullName;
}

// 调用 displayFullName():
console.log(displayFullName());

// 返回值:
"Oluwatobi Sofela"

在上述代码中,displayFullName()成功返回"Oluwatobi Sofela",因为displayFullName()showLastName()在相同的词法作用域中。

换言之,displayFullName()可以调用showLastName(),因为这两个函数都定义在全局作用域中。

注意:

  • 上述2个示例中,displayFullName()不能访问showLastName()lastName变量。 但displayFullName()可以调用showLastName(),而showLastName()返回了lastName变量的值。
  • 词法作用域的替换方案是动态作用域,但是除了在极少的开发语言(比如bash脚本)中使用外,很少使用它。

总结

只要你一听到词法,就要联想到是定义某个事物。

因此,一辆车,一个变量,一部手机,一个函数,一件泳衣等,这些事物的词法作用域指的就是定义他们时所在的区间。

写在最后

这篇文章不仅讨论了在Javascript中什么是词法作用域,还探讨了为什么它是一个很重要的编程概念。 感谢你的阅读!