JS面试点-作用域(全局&局部作用域/ 作用域链)

5,104 阅读6分钟

什么是作用域

  • 作用域是可访问变量的集合。
  • JavaScript 中, 对象和函数同样也是变量。
  • JavaScript 中, 作用域为可访问变量,对象,函数的集合。
  • 分为 全局作用域/局部作用域(函数作用域与块级作用域)

全局作用域:

  • 全局作用域贯穿整个javascript文档,在所有函数声明或者大括号之外定义的变量,都在全局作用域里。一旦你声明了一个全局变量,那么你在任何地方都可以使用它,包括函数内部。事实上,JavaScript默认拥有一个全局对象window,声明一个全局变量,就是为window对象的同名属性赋值。
  • 注:若变量在函数内部没有声明(未使用var关键字),该变量为全局变量。全局变量在页面关闭后销毁。
如下面代码所示。

function zxxFn () {
    zxxs = 2
}
var zxx = 1
console.log(window.zxx) // 1
// console.log(zxxs) 未定义
zxxFn() // 执行后zxxs成为全局变量
console.log(zxxs) // 2

局部作用域(函数作用域和块级作用域)

JavaScript中,任何定义在函数体内的变量或者函数都将处于函数作用域中,这些变量也无法被在函数外部使用。(闭包除外)

  • 变量在函数内声明,变量属于局部作用域。
  • 局部变量:只能在函数内部访问。
  • 局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量。
  • 局部变量在函数开始执行时创建,函数执行完后局部变量会自动销毁。

局部作用域外部无法访问示例:

function zxxFn () {
    var zxx = 'zxx is a great girl'
    console.log(zxx) // zxx is a great girl
}
zxxFn() // zxx is a great girl
console.log(zxx)
// Uncaught ReferenceError: zxx is not defined 函数内部定义的局部变量外部无法访问

当函数体内局部变量和函数体外的变量重名

函数内部优先使用自己的变量。(部分知识涉及变量提升)

    var zxx = 'zxx is a good girl'
    function zxxFn () {
        console.log(zxx) 
        var zxx = 'zxx is a great girl'
        console.log(zxx)
        zxx = 'very good'
        console.log(zxx)
    }
    console.log(zxx)
    zxxFn()
    console.log(zxx)
    // 依次输出 1. zxx is a good girl 2. undefined 3. zxx is a great girl
    // 4. very good 5.zxx is a good girl
    var zxx = 'zxx is a girl'
    var zxxs = 'yes'
    function zxxFn () {
        console.log(zxx) // undefined
        console.log(zxxs) // yes
        zxx = 'very good'
        zxxs = 'not'
        console.log(zxx) // very good
        console.log(zxxs) // not
        var zxx = 'zxx is a great girl'
        console.log(zxx) // zxx is a great girl
        console.log(zxxs) // not
    }
    console.log(zxx) // zxx is a girl
    console.log(zxxs) // yes
    zxxFn()
    console.log(zxx) // zxx is a girl
    console.log(zxxs) // not

作用域链:

  • 遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到, 就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
  • 简单来讲,局部作用域(如函数作用域)可以访问到全局作用域中的变量和方法,而全局作用域不能访问局部作用域的变量和方法。

出现函数嵌套函数,则就会出现作用域链。

zxxAge = 'zxx is 18 years old'
function zxxFn () {
    console.log(zxxAge)
    var zxxAge = 'zxx is 8 years old'
    var zxxSub = 'zxx is 25 years old'
    console.log(zxxAge) // zxx is 8 years old
    // console.log(zxxSub) // zxxSub is not defined
    function zxxFnSub () {
        var zxxSub = 'zxx is 18 years old'
        console.log(zxxSub) // zxx is 18 years old 
        console.log(zxxAge) // zxxSub is not defined
    }
    zxxFnSub()
}
zxxFn()

如图:在执行变量打印的时候,会先在第一作用域寻找该变量是否存在,存在则使用该作用域的变量,不存在则向上寻找第二作用域,以此类推一直找到全局作用域。如果找不到则会抛出异常。

例:

    function zxxFn (a) {
        console.log(a + b)
    }

    var b = 2
    zxxFn(2) // 4

对变量b查询无法在函数zxxFn内部,但可以在上一级作用域中完成(全局作用域)
注:代码中隐式的 a=2 操作可能很容易被你忽略掉。
比较下列两个代码
function zxxFn (a) {
    console.log(a + b) // b变量在任何相关的作用域都找不到,引擎就会抛出ReferenceError异常
    b = a
}
zxxFn(2)

=================

function zxxFn (a) {
    b = a
    console.log(a + b)
}
zxxFn(2) // 4

函数被调用之前作用域链已经存在

zxxAge = 'zxx is 18 years old'
function zxxFn () {
    console.log(zxxAge)
    var zxxAge = 'zxx is 8 years old'
    var zxxSub = 'zxx is 25 years old'
    console.log(zxxAge) // zxx is 8 years old
    // console.log(zxxSub) // zxxSub is not defined
    function zxxFnSub () {
        console.log(zxxSub) // zxx is 25 years old
        console.log(zxxAge) // zxxSub is not defined
    }
    return zxxFnSub;
}
var zxx = zxxFn()
zxx()
上述代码,在函数被调用之前作用域链已经存在:
  • 全局作用域 -> zxxFn函数作用域 -> zxxFnSub函数作用域
  • 当执行【zxx();】时,由于其代指的是zxxFnSub函数,此函数的作用域链在执行之前已经被定义为:全局作用域 -> zxxFn函数作用域 -> zxxFnSub函数作用域,所以,在执行【zxx();】时,会根据已经存在的作用域链去寻找变量。
var zxx = 'zxx is a great girl'
function zxxFn() {
    console.log(zxx)
}
function zxxFnSub() {
    var zxx = 'zxx is 18 years old'
    return zxxFn
}
var myZxx = zxxFnSub()
myZxx() // zxx is a great girl
上述代码,在函数被执行之前已经创建了两条作用域链:
  • 全局作用域 -> zxxFn函数作用域
  • 全局作用域 -> zxxFnSub函数作用域

当执行【myZxx();】时,myZxx代指的zxxFn函数,而zxxFn函数的作用域链已经存在:全局作用域 -> zxxFn函数作用域,所以,执行时会根据已经存在的作用域链去寻找。

最小特权原则:

  • 也叫最小授权或最小暴露原则,这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来。
  • 比如某个模块或对象的 API 设计。这个原则可以延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作用域中,当然可以在所有的内部嵌套作用域中访问到它们。
  • 但这样会破坏前面提到的最小特权原则,因为可能会暴漏过多的变量或函数,而这些变量或函数本应该是私有的,正确的代码应该是可以阻止对这些变量或函数进行访问的。

例:

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(..) 的适用条件。更“合理”的设计会将这些私有的具体内 容隐藏在 doSomething(..) 内部,例如:
function doSomething (a) {
    function doSomethingElse (a) {
        return a - 1
    }
    var b
    b = a + doSomethingElse(a * 2)
    console.log(b * 3)
}
doSomething(2) // 15
现在,b doSomethingElse(..) 都无法从外部被访问,而只能被 doSomething(..) 所控制。 功能性和最终效果都没有受影响,但是设计上将具体内容私有化了,设计良好的软件都会 依此进行实现。​