前端常见知识点(三)

前端常见知识点(三)

三月 07, 2019

作用域

前面讲到闭包的时候,我们说闭包是「是指有权访问另一个函数作用域中的变量的函数」,那么所谓的「作用域」又是什么呢?

什么是作用域?

先来看一个例子:

1
2
3
4
5
6
var a = 'Hello!'

function greet() {
console.log(a)
}
greet() // Hello!

在这段代码中,我们在greet函数外部定义了一个变量a,函数greet首先在自己的作用域中查找变量a,没有找到后再在外部的全局作用域中查找,找到之后将其打印输出。

接下来是另外一个例子:

1
2
3
4
5
6
7
8
9
var a = 'Hello!'

function greet () {
var b = 'world!'
console.log(b)
}
greet() // world!
console.log(a) // Hello!
console.log(b) // ReferenceError: b is not defined

在这段代码中,我们在greet函数内部定义了一个变量b,调用greet函数能够正常将其打印输出,但是当我们在外部直接输出变量b的时候却报错了,也就是说,在全局作用域里,变量b是未定义的。

于是我们知道,JavaScript 的作用域是通过函数来实现的,在函数内部定义的变量,在函数的外部是不可以访问的。

在 ES6 的letconst关键字提出来之前,JavaScript 的作用域只有两种:「全局作用域」和「函数(局部)作用域」。JavaScript 是没有「块作用域」的。

作用域链

依旧是第一个例子:

1
2
3
4
5
6
var a = 'Hello!'

function greet() {
console.log(a)
}
greet() // Hello!

我们查找变量a的过程是:首先在函数greet的作用域中查找,之后到函数的上一层(这里是全局)作用域中查找,这样逐步向外查找的过程形成一个链条,就称作「作用域链」。

下面来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var a = 'window'
function outter () {
var a = 'outter'
function inner () {
var a = 'inner'
console.log(a)
}
inner() // inner
console.log(a)
}
outter() // outter
console.log(a) // window

如果把inner函数中的a的定义注释掉,那么ineer()的输出就是outter;如果再把outter函数中的a定义注释掉,那么三个输出就全都是window;这里的作用域链就是inner->outter->window(全局)。

延长作用域链

尽管在 JavaScript 中,作用域只有全局和局部(函数)两种,但是作用域链可能会在某些情况下得到加长。

  1. try-catch语句的catch块;
  2. with语句。

在「红宝书」中有一个 with语句的例子:

1
2
3
4
5
6
7
8
function buildUrl () {
var qs = "?debyg=true"
with (location) {
var url = href + qs
}
return url
}
console.log(buildUrl())

在这个例子中,with语句接受到的是location对象,因此其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被临时添加到了作用域链的前端,作用域链就变成了:location对象->buildUrl的变量对象->windows全局对象,于是当在with语句中引用变量href时,实际引用的就是location.href

至于try-catch语句:

1
2
3
4
5
try {
doSomething()
} catch (error) {
handleError(error)
}

对于catch语句来说,会创建一个包含着被抛出的错误对象的声明的新的变量对象,并将其置于作用域链的头部,当catch语句块的代码执行完,会移除该变量对象,销毁作用域。