细读《你不知道的JavaScript·上卷》1-2 词法作用域
栏目: JavaScript · 发布时间: 5年前
内容简介:在第作用域共有两种主要的工作模型,一是在第1章学习了,大部分标准语言编译器的
在第 1
章中,学习了 作用域,它是一套规则,用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
作用域共有两种主要的工作模型,一是 词法作用域 ( JavaScript
等);二是 动态作用域 ( bash
脚本等)。
词法作用域
- 什么是词法作用域
- 为什么废弃欺骗词法作用域的两种机制
2.1 词法阶段
在第1章学习了,大部分标准语言编译器的 第一个工作阶段 是词法化( token
化 )。
词法作用域就是在词法分析时定义的作用域,即在写代码时,由变量和块作用域的位置决定的。因此,在词法分析时也是固定不变的了(不考虑欺骗词法作用域情况)。
下面这段示例代码有三个嵌套作用域:
- 圈1 包含了全局作用域,只有一个标识符号
foo
- 圈2 包含
foo
作用域,有三个标识符a
、bar
、b
- 圈3 包含
bar
作用域 ,有一个标识符c
作用域的范围是根据作用域代码块定义的位置决定的,在这里每个函数创建了一个作用域。
这里作用域嵌套是严格的,一个函数不能同时存在于两个外部函数中。
2.1.1 查找
-
作用域查找会在找到第一个匹配的标识符时停止。
-
遮蔽效应:在多层嵌套作用域中可以定义同名的标识符,内部的标识符会 遮蔽 外部的标识符。
-
全局变量是全局对象的属性,被覆盖的非全局对象则无法被访问到。
window.a 复制代码
-
词法作用域查找只会查找一级标识符,比如
a
、b
、c
。如果代码中引用了foo.bar.baz
,词法作用域只会查找foo
标识符,找到后, 对象属性访问规则 会分别接管对bar
和baz
属性的访问。
2.2 欺骗词法
欺骗词法作用域会导致性能下降,以下两种方法都 不推荐使用 。
2.2.1 eval
-
eval(...)
函数可以接受一个字符串作为参数,并把字符串的内容当作代码运行,从而实现对词法作用域环境的修改。 - 在执行
eval()
之后的那些代码,引擎不知道、也不去关心前面的代码是 动态编译 的,且 修改 了词法作用域环境。引擎只会一如既往地进行词法作用域查找。
非严格模式下:
function foo(a, str){ eval(str); // 欺骗! //console.log(evla(str));//Uncaught ReferenceError: evla is not defined console.log(a, b); // 1 3 console.log(a, window.b); //1 2 } var b = 2; foo(1, " var b = 3 ;"); // 1 3 复制代码
-
eval()
被调用时,字符串参数“ var b = 3; ”
被当作真正的代码声明了变量 b ,并修改了foo()
的词法作用域。在foo()
内部创建了一个变量b ,
遮蔽了外部全局作用域中的同名变量b
。 -
console.log()
被执行时,会在foo()
的内部同时找到a
和b
, 但是永远也无法找到外部的b
。因此会输出1 , 3
,而不是正常情况下会输出的1 ,2
。
严格模式下:
function foo(a, str){ "use strict"; eval(str); // console.log(evla(str));//Uncaught ReferenceError: evla is not defined console.log(a, b);// 1 2 console.log(a, window.b); //1 2 } var b = 2; foo(1, " var b = 3 ;");// 1 2 复制代码
-
eval()
在严格模式下,有自己的词法作用域,其中的声明无法修改作用域。 -
setTimeout(...)
和setInterval(...)
的第一个参数可以是字符串,字符串的内容会被解释为一段动态生成的函数代码。 已废弃使用 。 - 构造函数
new Function()
的最后一个参数可以接受代码字符串(前面的参数是新生成的函数的形参), 避免使用 。
2.2.2 width
with
通常被当作重复引用同一个对象中的多个属性快捷方式, 可不用重复引用对象本身 。
var obj = { a:1, b:2, c:3 }; //单调乏味的重复“ obj ” obj.a = 2; obj.b = 3; obj.c = 4; //简单的快捷方式 with(obj){ a = 3; b = 4; c = 5; } 复制代码
不仅仅是一个属性访问的 快捷方式 。如下:
function foo(obj){ with(obj){ a = 2; } } var o1 = { a : 3 }; var o2 = { b : 4 }; console.log(o1.a);//3 foo(o1); console.log(o1.a);//2 foo(o2); console.log(02.a);//undefined console.log(a);//2 -> 不好,a 被泄露到全局作用域上了! 复制代码
-
o1
传进后,with
声明的作用域是o1
,a = 2
赋值操作找到o1.a
并将2
赋值给它。 -
o2
传进后,作用域o2
中没有a
属性,则进行LHS
标识符查找,o2
的作用域、foo()
的作用域 和全局作用域都没找到标识符a
,因此当a = 2
执行时,产生副作用,自动创建了一个全局变量(非严格模式)a
,并将2
赋值给a
,所以o2.a
保持undefined
。
在严格模式下,with 语句被完全禁用,eval() 则只保留核心功能,都不推荐使用。
2.2.3 性能
JavaScript
引擎在 编译阶段 进行各种性能优化,一些优化在词法分析阶段,静态分析了代码,预先确定了变量和函数声明的位置,所以在执行期间就可以快速解析标识符。
2.3小结
词法作用域只由函数被声明时所处的位置决定。
以下两个机制可以 欺骗 词法作用域:
-
eval(...)
: 对一段包含一个或多个声明的 代码 字符串进行演算,借此来修改已经存在的词法作用域( 运行时 )。 -
with
: 将一个对象的引用 当作 作用域,将对象的属性当作作用域的标识符,创建一个新的词法作用域( 运行时 )。
副作用是引擎无法在编译时对作用域查找进行优化。因为引擎只能谨慎地认为这样的优化是无效的,使用任何一个机制都将导致代码运行变慢。 废弃它们。
最后,读书是由厚到薄,又由薄到厚的双向过程,注重领悟、实践,不断踩坑、提升,若有帮助,请点个赞,谢谢您的支持与指教。
参考文献:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 手写一个词法分析器
- 模板引擎实现(一)词法分析
- 深入ECMAScript系列(一):词法环境
- 图解词法作用域与作用域链
- 【PHP源码学习】2019-03-20 PHP词法分析
- 百度深度学习中文词法分析工具LAC试用之旅
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。