资讯专栏INFORMATION COLUMN

JS学习系列 02 - 词法作用域

bladefury / 1833人阅读

摘要:作用域有两种主要工作模型词法作用域和动态作用域。可能会有一些同学认为是,那就是没有搞清楚词法作用域的概念。在严格模式下,在运行时有自己的词法作用域,意味着其中的声明无法修改所在的作用域。

1. 两种作用域

“作用域”我们知道是一套规则,用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。

作用域有两种主要工作模型:词法作用域动态作用域

大多数语言采用的都是词法作用域,少数语言采用动态作用域(例如 Bash 脚本),这里我们主要讨论词法作用域。

2. 词法

大部分标准语言编译器的第一个工作阶段叫作词法化
简单地说,词法作用域是由你在写代码时将变量和函数(块)作用域写在哪里来决定的。当然,也会有一些方法来动态修改作用域,后边我会介绍。

举个例子:

var a = 2;

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

function foo2 () {
   var a = 10;

   foo1();
}

foo2();

这里输出结果是多少呢?

注意,这里结果打印的是 2

可能会有一些同学认为是 10,那就是没有搞清楚词法作用域的概念。
前边介绍了,词法作用域只取决于代码书写时的位置,那么在这个例子中,函数 foo1 定义时的位置决定了它的作用域,通过下图理解:

foo1 和 foo2 都是分别定义在全局作用域中的函数,它们是并列的,所以在 foo1 的作用域链中并不包含 foo2 的作用域,虽然在 foo2 中调用了 foo1,但是 foo1 对变量 a 进行 RHS 查询时,在自己的作用域没有找到,引擎会去 foo1 的上级作用域(也就是全局作用域)中查找,而并不会去 foo2 的作用域中查找,最终在全局作用域中找到 a 的值为 2。

总结来说,无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

3. 欺骗词法

JavaScript 中有 3 种方式可以用来“欺骗词法”,动态改变作用域。

第一种: eval

JavaScript 中 eval(...) 函数可以接受一个字符串作为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。

在执行 eval(...) 之后的代码时,引擎并不知道或在意前面的代码是以动态形式插入进来并对词法作用域环境进行修改的,引擎只会像往常一样正常进行词法作用域的查找。

举个例子:

function foo (str) {
   eval(str);        // "欺骗"词法

   console.log(a);
}

var a = 2;

foo("var a = 10;");

如大家所想,输出结果为 10。
因为 eval("var a = 10;") 在 foo 的作用域中新创建了一个同名变量 a,引擎在 foo 作用域中对 a 进行 RHS 查询,找到了新定义的 a,值为 10,所以不再向上查找全局作用域中的 a,所以导致输出结果为 10,这就是 eval(...) 的作用。

严格模式下,eval(...) 在运行时有自己的词法作用域,意味着其中的声明无法修改所在的作用域。

"use strict;"

function foo (str) {
   eval(str);        // eval() 有自己的作用域,所以并不会修改 foo 的词法作用域

   console.log(a);
}

var a = 2;

foo("var a = 10;");

这里输出结果为 2。

JavaScript 中还有一些功能和 eval(...) 类似的函数,例如 setTimeout(...) 和 setInterval(...) 的第一个参数可以是一个字符串,字符串的内容可以解释为一段动态生成的代码。这些功能已经过时并且不被提倡,最好不要使用它们。new Function(...) 函数的最后一个参数也可以接受代码字符串,并将其转化为动态生成的函数,也尽量避免使用。

在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损失。

第二种: with
with 通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

举个例子:

var obj = {
   a: 2,
   b: 3
};

with (obj) {
   console.log(a);      // 2
   console.log(b);      // 3
   c = 4;         
};

console.log(c);          // 4, c 被泄露到全局作用域上

如上所示,我们对 c 进行 LHS 查询,因为在 with 引入的新作用域中没有找到 c,所以向上一级作用域(这里是全局作用域)查找,也没有找到,在非严格模式下,在全局对象中新建了一个属性 c 并赋值为 4。

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。

尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var 声明并不会限制在这个块作用域中,而是被添加到 with 所处的函数作用域中。

严格模式下,with 被完全禁止使用。

"use strict";

var obj = {
   a: 2,
   b: 3
};

with (obj) {
   console.log(a);     
   console.log(b);      
   c = 4;         
};

console.log(c);       

第三种: try...catch
try...catch 可以测试代码中的错误。try 部分包含需要运行的代码,而 catch 部分包含错误发生时运行的代码。

举个例子:

try {
   foo();
} catch (err) {
   console.log(err);   

   var a = 2; 
// 打印出 "ReferenceError: foo is not defined at :2:4"
}

console.log(a);      // 2

当 try 中的代码出现错误时,就会进入 catch 块,此时会把异常对象添加到作用域链的最前端,类似于 with 一样,catch 中定义的局部变量也都会添加到包含 try...catch 的函数作用域(或全局作用域)中。

4. 性能

JavaScript 引擎会在编译阶段进行数项性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数定义的位置,才能在执行过程中快速找到标识符。

但如果引擎在代码中发现了 eval(...)、with 和 try...catch ,它只能简单的假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道 eval(...) 会接受到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。

最悲观的情况是如果出现了这些动态添加作用域的代码,所有的优化可能都是无意义的,因此最简单的做法就是完全不进行任何优化。

如果代码中大量使用 eval(...) 和 with,那么运行起来一定会变得非常缓慢。

5. 结论

很多时候我们对代码的分析出错,就是源于对词法作用域的忽略,所以让我们重新审视代码,继续努力!

欢迎关注我的公众号

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81599.html

相关文章

  • 再谈闭包-词法作用

    摘要:权威指南第六版关于闭包的说明采用词法作用域,也就是说函数的执行依赖于变量的作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。闭包这个术语的来源指函数变量可以被隐藏于作用域链之内,因此看起来是函数将变量包裹了起来。 最近打算换工作,所以参加了几次面试(国内比较知名的几家互联网公司)。在面试的过程中每当被问起闭包,我都会说闭包是作用域的问题?令人惊讶的是几乎无一例外的当我提到...

    Ku_Andrew 评论0 收藏0
  • JS脚丫系列】重温闭包

    摘要:内部的称为内部函数或闭包函数。过度使用闭包会导致性能下降。,闭包函数分为定义时,和运行时。循环会先运行完毕,此时,闭包函数并没有运行。闭包只能取得外部函数中的最后一个值。事件绑定种的匿名函数也是闭包函数。而对象中的闭包函数,指向。 闭包概念解释: 闭包(也叫词法闭包或者函数闭包)。 在一个函数parent内声明另一个函数child,形成了嵌套。函数child使用了函数parent的参数...

    MartinDai 评论0 收藏0
  • JS学习系列 01 - 编译原理和作用

    摘要:的抽象语法树中可能如下图所示代码生成将转换为可执行代码的过程被称为代码生成。如果是,编译器会忽略该声明,继续进行编译,否则它会要求在当前作用域的集合中声明一个新的变量,并命名为。 在学习 javascript 的过程中,我们第一步最应该了解和掌握的就是作用域,与之相关还有程序是怎么编译的,变量是怎么查找的,js 引擎是什么,引擎和作用域的关系又是什么,这些是 javascript 这门...

    jkyin 评论0 收藏0
  • 一分钟学习JavaScript系列 -- 闭包

    摘要:此时,中定义的局部变量就被保存在内存中。所以当执行的时候,其真正的作用域是运行时的作用域运行时作用域词法作用域所以第一次调用时,由于是,所以返回而第二次返回是。因此在使用闭包时,需要非常注意内存泄漏的问题。 说起闭包,相信写前端的同学都知道,而且相信在实际的项目中或多或少都已经用到了闭包。那到底什么才是闭包,闭包又是怎么产生的呢? 1. 什么是闭包在阮老师的文章中提到: 闭包就是能够读...

    keke 评论0 收藏0
  • 【Step-By-Step】一周面试题深入解析 / 周刊02

    摘要:关于点击进入项目是我于开始的一个项目,每个工作日发布一道面试题。即使这个时间周期内,小明取得多次满分。创建作用域链在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。这种一层一层的关系,就是作用域链。 关于【Step-By-Step】 Step-By-Step (点击进入项目) 是我于 2019-05-20 开始的一个项目,每个工作日发布一道面试题。每个周末我会仔细阅读大家的答...

    ixlei 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<