重读你不知道的JS (上) 第一节二章

pdw00 2019-06-30

你不知道的JS(上卷)笔记

你不知道的 JavaScript

JavaScript 既是一门充满吸引力、简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的 JavaScript 开发者,如果没有认真学习的话也无法真正理解它们.

上卷包括俩节:

  • 作用域和闭包
  • this 和对象原型

作用域和闭包

希望 Kyle 对 JavaScript 工作原理每一个细节的批判性思 考会渗透到你的思考过程和日常工作中。知其然,也要知其所以然。

词法作用域

作用域共有俩种主要的工作模型: 词法作用域和动态作用域。

词法阶段

词法化:大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。
词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋 予单词语义。

词法作用域

  1. 定义在词法阶段的作用域
  2. 由你在写代码时将变量和块作用域写在哪来决定的,因此当词法分析器处理代码时会保持作用域不变。

欺骗词法作用域: 在词法分析器处理过后依然可以修改作用域。

事实上,让词法作用域根据词法关系保持书写时的自然关系不变是一个非常好的最佳实践。

“作用域气泡法” 划分作用域

查找

作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。

  1. 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者向上进行。
  2. 作用域查找会在找到第一个匹配的标识符时停止,或者直至找到最后一个全局作用域处。
  3. window.a的方式可以访问那些被同名变量遮蔽了的全局变量,但非全局变量如果被遮蔽,就无法访问到了

遮蔽效应: 在多层的嵌套作用域中可以定义同名的标识符。

欺骗词法

俩种欺骗手段:eval和with;
社区认为使用这俩种机制并不是什么好主意,因为使用这俩种机制会导致性能下降
另外一个不推荐使用 eval(..) 和 with 的原因是会被严格模式所影响(限 制)。with 被完全禁止,而在保留核心功能的前提下,间接或非安全地使用 eval(..) 也被禁止了。

eval

JavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并 运行,就好像代码是写在那个位置的一样。

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

eval通常被用来执行动态创建的代码
在这个例子中,为了展示的方便和简洁,我们传递进去的“代码”字符串是 固定不变的。而在实际情况中,可以非常容易地根据程序逻辑动态地将字符 拼接在一起之后再传递进去。eval(..) 通常被用来执行动态创建的代码,因 为像例子中这样动态地执行一段固定字符所组成的代码,并没有比直接将代 码写在那里更有好处。

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

类似:setTimeout的第一个参数为字符串时;new Function的最后一个字符串参数;等都不提倡,不要使用。

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——不好,a 被泄漏到全局作用域上了!

可以注意到一个奇怪的副作用,实际上 a = 2 赋值操作创建了一个全局的变量 a。这 是怎么回事?

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

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

with 这种将对象及其属性放进一个作用域并同时分配标识符的行为很让人费解。

性能

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

小结

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段 基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。

JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..) 和 with。前者可以对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作 用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

相关推荐