前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JS学习系列 02 - 词法作用域

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

作者头像
leocoder
发布2019-07-09 10:10:43
1.1K0
发布2019-07-09 10:10:43
举报
文章被收录于专栏:前端进阶之路前端进阶之路
1. 两种作用域

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

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

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

2. 词法

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

举个例子:

代码语言:javascript
复制
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(...) 之后的代码时,引擎并不知道或在意前面的代码是以动态形式插入进来并对词法作用域环境进行修改的,引擎只会像往常一样正常进行词法作用域的查找。

举个例子:

代码语言:javascript
复制
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(...) 在运行时有自己的词法作用域,意味着其中的声明无法修改所在的作用域。

代码语言:javascript
复制
'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 通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

举个例子:

代码语言:javascript
复制
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 被完全禁止使用。

代码语言:javascript
复制
'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 部分包含错误发生时运行的代码。

举个例子:

代码语言:javascript
复制
try {
   foo();
} catch (err) {
   console.log(err);   

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

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

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

4. 性能

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

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

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

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

5. 结论

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年04月11日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 两种作用域
  • 2. 词法
  • 3. 欺骗词法
  • 4. 性能
  • 5. 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档