阅读 838

前端面试之JavaScript基础(四)—— 词法作用域

看完 《前端面试之JavaScript基础(三)—— 作用域》 之后,我们了解了作用域的概念,那词法作用域是什么呢?

是什么?

词法作用域 其实是作用域工作模型的一种,JavaScript 遵循的正是这种工作模型。它是定义在词法阶段的作用域,我们了解编译过程中在词法分析的阶段会形成抽象语法树(AST),在同一时间还会根据相应的分析生成对应的作用域。根据这种工作机制我们不难知道,某个标识符属于哪个作用域、作用域的嵌套关系(作用域链)其实在书写时已经决定了。

接下来我们来看一个简单的例子,来感受下词法作用域:

var a = 'global'

function foo() {
  console.log(a);
}

function bar() {
  var a = 'bar'
  foo()
}

bar()  // global
复制代码

这个简单的例子有些人可能会觉得打印的结果为 bar,但事实上并不是。需要我们注意的是 JavaScript 应用的是词法作用域模型,所以 函数的作用域取决于它声明的位置与实际调用位置无关 ,上述例子的作用域嵌套关系如下图所示:

作用域嵌套关系

按照上图所示的嵌套关系,我们知道在 foo 函数执行的时候需要 a 变量,但是在当前作用域当中查找不到,引擎就会顺着作用域链向外层寻找,此时全局作用域当中正好有名为 a 的变量所以打印的结果是 global。

欺骗词法作用域

因为 JavaScript 使用的是词法作用域模型,所以作用域在书写时已经确定,那我们有没有能够在运行时动态改变作用域的方法呢?

当然有,接下来我们就来看看 eval 和 with 它们是如何动态改变作用域的。

eval

eval(...) 函数会接受一个字符串作为参数,并且将这句字符串视为在书写时就存在的代码一样去执行,通过这个函数就可以实现欺骗词法作用域的效果,请看以下例子:

var a = 'global'

function foo(str) {
  eval(str)
  console.log(a)
}

foo('var a = "eval"') // eval
复制代码

如果我们遵照词法作用域模型来分析上述的代码,在词法阶段应该只有全局作用域当中存在值为 global 的变量 a。但是在 foo 函数运行时 eval(...) 函数会将 var a = "eval" 字符串当作本身就书写在这里的代码一样执行。这时 foo 函数作用域内部就生成了一个值为 eval 的变量 a ,因为同名变量的遮蔽作用所以最终打印出了 eval 。

with

with 关键字可能大家都很陌生,我们先来看一段代码再来解释 with 的工作机制。

function foo(obj) {
  with (obj) {
    a = 2
  }
}

var o = {
  a: 3
}

foo(o)

console.log(o.a) // 2
复制代码

上述代码中,with 使得 o 对象上的属性 a 被重新赋值了,其实 with 会利用传入的对象开辟一个新的作用域,并且将对象上的属性作为当前作用域内的变量,这个作用域在词法阶段其实并不存在。

with 能够创建一个新的作用域,在这个作用域当中查找变量仍然遵循一般的规则,所以我们需要避免因为 with 导致的误生成全局变量的行为,将上面的例子修改一下:

function foo(obj) {
  with (obj) {
    a = 2
  }
}

var o = {
  b: 3
}

foo(o)

console.log(o.a, a) // undefined 2
复制代码

with 当中需要对 a 变量进行赋值操作,所以引擎会使用 LHS 查询方式查找变量 a。从 with 生成的作用域出发直到查询到全局作用域都无法找到变量 a ,此时引擎就会在全局作用域当中创建新变量并执行对它的赋值操作。

小结

今天我们在了解了词法作用域是什么,还介绍了两种动态改变词法作用域的方法,我们对今天的内容做个简单的小结:

  1. 词法作用域是一种作用域工作模型,函数、块作用域在代码书写时就已经确定。
  2. eval 和 with 可以在代码运行时动态修改作用域,但是在实际编码中请不要使用这两个方法,它们会大大影响代码的执行效率。
文章分类
前端
文章标签