你不知道的JavaScript读书笔记(二)
词法作用域
概念
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。
==来看一个例子==
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 1
在这个例子中有三个逐级嵌套的作用域。由里向外逐层作用域为bar所创建的作用域,foo所创建的作用域,最外层是全局作用域
- 在全局作用域中,只有一个标识符:foo。
- 在foo 所创建的作用域,其中有三个标识符:a、bar 和 b
- 在 bar 所创建的作用域,其中只有一个标识符:c。
在执行console.log(...)声明,会去查找a,b,c三个变量的引用。首先先从它最内部的作用域,也就是 bar(..)函数的作用域开始查找。引擎无法在这里找到 a,因此会去上一级到所嵌套的 foo(..)的作用域中继续查找。在这里找到了 a,因此引擎使用了这个引用。对 b 来讲也是一样的。而对 c 来说,引擎在 bar(..) 中就找到了它。
如果 a、c 都存在于 bar(..) 和 foo(..) 的内部,console.log(..) 就可以直接使用 bar(..)中的变量,而无需到外面的 foo(..) 中查找作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
欺骗语法
如果词法作用域完全由写代码期间的函数所声明的位置来定义,我们如何能在运行时候去修改词法作用域呢?JavaScript中有两种机制来实现这个目的——evel和with。虽然这两个机制都不是什么好方法,都会导致性能下降。
- evel
JavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并 运行,就好像代码是写在那个位置的一样。
根据这个原理,可以达到代码欺骗。假设代码就在那里,在执行时修改词法作用域环境。引擎是无法识别代码是否是动态插入,只会如往常地进行词法作用域查找。
==eval欺骗语法例子==
function foo(str, a) {
eval( str ); // 欺骗
console.log( a, b );
}var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
eval(..) 调用中的 "var b = 3;" 这段代码会被当作本来就在那里一样来处理。由于那段代码声明了一个新的变量 b,因此它对已经存在的 foo(..) 的词法作用域进行了修改。这段代码实际上在 foo(..) 内部创建了一个变量 b,并遮蔽 了外部(全局)作用域中的同名变量。当 console.log(..) 被执行时,会在 foo(..) 的内部同时找到a和b,但是永远也无法找到外部的b。因此会输出“1, 3”而不是正常情况下会输出的“1, 2”。
在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损 失。
- with
说起js中的with关键字,很多小伙伴们的第一印象可能就是with关键字的作用在于改变作用域,然后最关键的一点是js不推荐使用with关键字。听到不推荐with关键字后,我们很多人都会忽略掉with关键字,包括我在内。但是有时候,我们在看一些代码或者面试题的时候,又会有with关键字的相关问题,所以还是有必要说一说with关键字的相关知识的。
with 语句的原本用意是为逐级的对象访问提供命名空间式的速写方式,也就是在指定的代码区域,直接通过节点名称调用对象。 with 通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
比如,目前现在有一个这样的对象:
var obj = {
a: 1,
b: 2,
c: 3
};
如果想要改变 obj 中每一项的值,一般写法可能会是这样:
// 重复写了3次的"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
在这段代码中,使用了 with 语句关联了 obj对象,这就以为着在with代码块内部,每个变量首先被认为是一个局部变量,如果局部变量与 obj 对象的某个属性同名,则这个局部变量会指向 obj 对象属性。
- with弊端
with 可以很好地帮助我们简化代码。但是为什么不推荐使用呢?下面我们来说说with的缺点
- 导致数据泄漏
==来看一段代码==
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
}
foo(o1);
console.log(o1.a); //2
foo(o2);
console.log(o2.a); //underfined
console.log(a); //不好,a被泄漏到全局作用域上了
在上面例子中创建了 o1 和 o2 两个对象。其中一个有 a 属性,另外一个没有。foo(obj) 函数接受一个 obj 的形参,该参数是一个对象引用,并对该对象引用执行了 with(obj) {...}。在 with 块内部,对 a 有一个词法引用,实际上是一个 LHS引用,将 2 赋值给了它。 当我们将 o1 传递进去,a = 2 赋值操作找到了 o1.a 并将 2 赋值给它。而当 o2 传递进去,o2 并没有 a 的属性,因此不会创建这个属性,o2.a 保持 undefined!
但是可以注意到一个奇怪的副作用,实际上 a = 2 赋值操作创建了一个全局的变量 a,导致数据的泄漏呢? 这是怎么回事?
这里需要回到对 LHS查询 的机制问题。 当我们传递 o2 给 with 时,with 所声明的作用域是 o2, 从这个作用域开始对 a 进行 LHS查询。o2 的作用域、foo(…) 的作用域和全局作用域中都没有找到标识符 a,因此在非严格模式下,会自动在全局作用域创建一个全局变量),在严格模式下,会抛出ReferenceError 异常。
- 性能下降
with 会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域。with 可以使代码更具有扩展性,虽然有着上面的数据泄漏的可能,但只要稍加注意就可以避免,难道不是可以创造出很好地功能吗? 答案是否定的,具体原因我们先来看下面的这部分代码。
function func() {
console.time("func");
var obj = {
a: [1, 2, 3]
};
for(var i = 0; i < 100000; i++)
{
var v = obj.a[0];
}
console.timeEnd("func");
}
func();
function funcWith() {
console.time("funcWith");
var obj = {
a: [1, 2, 3]
};
with(obj) {
for(var i = 0; i < 100000; i++) {
var v = a[0];
}
}
console.timeEnd("funcWith");
}
funcWith();
在处理相同逻辑的代码中,没用 with 的运行时间仅为 3.44 ms。而用 with 的运用时间长达 65.24ms。 这是为什么呢?
原因是 JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。 但如果引擎在代码中发现了 with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法知道传递给with用来创建新词法作用域的对象的内容到底是什么。
最悲观的情况是如果出现了with,所有的优化都可能是无意义的。因此引擎会采取最简单的做法就是完全不做任何优化。如果代码大量使用 with 或者eval(),那么运行起来一定会变得非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代码会运行得更慢的事实。
作者简介:张敏,芦苇科技web前端开发工程师,低调冷幽默,深藏不露。代表作品:微鱼娃娃机系统、TopShow活动报名小程序。擅长网站建设、微信公众号开发、微信小程序开发、小游戏制作、企业微信制作、H5建设,专注于前端框架、交互设计、图像绘制、数据分析等研究。
欢迎和我们一起并肩作战: web@talkmoney.cn 访问 www.talkmoney.cn 了解更多
提供专业的微信公众号外包,靠谱的钉钉开发,深圳企业微信建设,高质量的微信小程序开发,广东小游戏开发,东莞H5制作