你不懂js系列学习笔记-做用域和闭包- 02

第2章:词法做用域

原文:You-Dont-Know-JSjavascript

做用域共有两种主要的工做模型:java

  1. 第一种是最为广泛的,被大多数编程语言所采用的词法做用域
  2. 另一种叫做动态做用域,仍有一些编程语言在使用(好比 Bash 脚本、Perl 中的一些模式等)。

词法做用域 和 动态做用域的区别:git

function foo() {
    console.log( a ); // 2
  }
  function bar() {
    var a = 3;
	foo();
  }
  var a = 2;
  bar();
复制代码

以上代码:词法做用域让 foo() 中的 a 经过 RHS 引用到了全局做用域中的 a,所以会输出 2。 JavaScript只有词法做用域github

若是是动态做用域 :由于当 foo() 没法找到 a 的变量引用时,会顺着调用栈在调用 foo() 的地方查找 a,而不是在嵌套的词法做用域链中向上查找。因为 foo() 是在 bar() 中调用的,引擎会检查 bar() 的做用域,并在其中找到值为 3 的变量 a。chrome

须要明确的是,事实上 JavaScript 并不具备动态做用域。它只有词法做用域,简单明了。可是 this 机制某种程度上很像动态做用域。编程

主要区别:词法做用域是在写代码或者说定义时肯定的,而动态做用域是在运行时肯定的。(this 也是!)词法做用域关注函数在何处声明,而动态做用域关注函数从何处调用。浏览器

1 词法阶段

  1. 包含着整个全局做用域,其中只有一个标识符:foo。
  2. 包含着 foo 所建立的做用域,其中有三个标识符:a、bar 和 b。 . 包含着 bar 所建立的做用域,其中只有一个标识符:c。

1.1 查找

以上代码的查找过程:安全

在上一个代码片断中,引擎执行 console.log(..) 声明,并查找 a、b 和 c 三个变量的引用。它首先从最内部的做用域,也就是 bar(..) 函数的做用域气泡开始查找。引擎没法在这里找到 a,所以会去上一级到所嵌套的 foo(..) 的做用域中继续查找。在这里找到了 a,所以引擎使用了这个引用。对 b 来说也是同样的。而对 c 来讲,引擎在 bar(..) 中就找到了它。性能优化

若是 a、c 都存在于 bar(..) 和 foo(..) 的内部,console.log(..) 就能够直接使用 bar(..)中的变量,而无需到外面的 foo(..) 中查找。编程语言

  1. 做用域查找会在找到第一个匹配的标识符时中止。(外部被遮蔽)
  2. 不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处 的位置决定。
  3. 词法做用域查找只会查找一级标识符

2 欺骗词法做用域

在运行时“修改”词法做用域 JavaScript 有两种这样的机制:eval 和 with。(正常应用场景不多,并且会影响性能在代码中应当被避免。)

2.1 eval

JavaScript 中的 eval(..) 函数接收一个字符串做为参数值,并将这个字符串的内容看做是好像它已经被实际编写在程序的那个位置上。换句话说,你能够用编程的方式在你编写好的代码内部生成代码,并且你能够运行这个生成的代码,就好像它在编写时就已经在那里了同样。

eval(..) 被执行的后续代码行中,引擎 将不会“知道”或“关心”前面的代码是被动态翻译的,并且所以修改了词法做用域环境。引擎 将会像它一直作的那样,简单地进行词法做用域查询。

考虑以下代码:(非 strict 模式)

function foo(str, a) {
	eval( str ); // 做弊!
	console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3

// 1,执行 foo();
// 2,执行 eval(str);
// 3,在 eval(..) 调用的位置上 生成var b = 3,修改了现存的 foo(..) 的词法做用域
// 4,执行console.log(..) 在foo(..) 的做用域中找到 a 和 b (并不会在全局做用域中查找)
复制代码

strict 模式下会报错:

function foo(str) {
 "use strict";
  eval(str);
  console.log(a); // ReferenceError: a is not defined
}
foo("var a = 2");
复制代码

2.2 with

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: 3
};

foo(o1);
console.log(o1.a); // 2

foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2 -- 哦,全局做用域被泄漏了!

// 1,执行foo(o1) 赋值 a = 2 , 找到属性 o1.a, o1.a = 2
// 2,执行foo(o2) 赋值 a = 2 , o2 没有 a 属性 o2.a = undefined, 
//(这里赋值 a = 2 建立了一个全局变量 a,若是 a = 2 加 var 则a属于foo函数的做用域)
复制代码

“做用域” o2 中没有,foo(..) 的做用域中也没有,甚至连全局做用域中都没有找到标识符 a,因此当 a = 2 被执行时,其结果就是自动全局被建立(由于咱们没有在 strict 模式下)。

3 性能

JavaScript引擎在编译阶段期行许多性能优化工做。其中的一些优化原理都归结为实质上在进行词法分析时能够静态地分析代码,并提早决定全部的变量和函数声明都在什么位置,这样在执行期间就能够少花些力气来解析标识符。

但若是引擎在代码中发现一个 eval(..)with,它实质上就不得不假定本身知道的全部的标识符的位置多是无效的,由于它不可能在词法分析时就知道你将会向eval(..)传递什么样的代码来修改词法做用域,或者你可能会向with传递的对象有什么样的内容来建立一个新的将被查询的词法做用域。

换句话说,悲观地看,若是 eval(..)with 出现,那么它将作的几乎全部的优化都会变得没有意义,因此它就会简单地根本不作任何优化。

在旧的浏览器中若是你使用了eval,性能会降低10倍。在现代浏览器中有两种编译模式:fast pathslow pathfast path 是编译那些稳定和可预测(stable and predictable)的代码。而明显的,eval不可预测,因此将会使用slow path ,因此会慢。

使用with关键字或者eval(..)对性能的影响还有一点就是js压缩工具,它没法对代码进行压缩,这也是影响性能的一个因素。

复习

词法做用域意味着做用域是由编写时函数被声明的位置的决策定义的。编译器的词法分析阶段实质上能够知道全部的标识符是在哪里和如何声明的,并如此在执行期间预测它们将如何被查询。

在 JavaScript 中有两种机制能够“欺骗”词法做用域:eval(..)with。前者能够经过对一个拥有一个或多个声明的“代码”字符串进行求值,来(在运行时)修改现存的词法做用域。后者实质上是经过将一个对象引用看做一个“做用域”,并将这个对象的属性看做做用域中的标识符,(一样,也是在运行时)建立一个全新的词法做用域。

这些机制的缺点是,它压制了引擎在做用域查询上进行编译期优化的能力,由于引擎不得不悲观地假定这样的优化是无效的。这两种特性的结果就是代码将会运行的更慢。不要使用它们。

附录:关于eval的一些问题

原文:www.nczonline.net/blog/2013/0…

eval()这个简单的函数被设计用来执行一个字符串做为JavaScript代码,有几点须要了解:

滥用

滥用与性能或安全无关,而是与不理解如何构建和使用JavaScript中的引用有关。假设您有多个表单输入,其名称包含一个数字,例如“option1”和“option2”,一般会看到:

function isChecked(optionNumber) {
  return eval("forms[0].option" + optionNumber + ".checked");
}
var result = isChecked(1);
复制代码

在这种状况下,开发人员正在尝试编写,forms[0].option1.checked但没有意识到如何在不使用的状况下作到这一点eval()。你会看到这种类型的模式在大约十岁以上的代码中不少,由于当时的开发人员不明白如何正确使用该语言。在eval()这里使用不合适,由于它不是没必要要的,不是由于它很差。您能够轻松地将此功能重写为:

function isChecked(optionNumber) {
  return forms[0]["option" + optionNumber].checked;
}
var result = isChecked(1);
复制代码

可调试

eval()不容易调试。用 chromeDev 等调试工具没法打断点调试,这意味着你将代码运行到一个黑盒子中,而后从中取出。Chrome开发者工具如今能够调试 eval() 编码,但仍然很痛苦。您必须等待代码执行一次后,才会显示在“来源”面板中。避免 eval() 编辑代码使调试变得更加容易,使您能够轻松查看和逐步浏览代码。

性能

上面有提到,使用时性能确实是一个大问题。

安全

若是你正在接受用户输入并eval()以某种方式传递它,那么你是在寻求麻烦。永远不要这样作。可是,若是您使用的eval()输入只有您本身控制而且不能被用户修改,那么就没有安全风险。

因此,只要你的信息源不安全,你的代码就不安全。不仅仅是由于eval引发的。

相关文章
相关标签/搜索