【读书笔记】《你不知道的 JavaScript(上卷)》


第一部分 作用域和闭包

1.1 作用域是什么?

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。

JavaScript 引擎首先会在代码执行前对其进行编译(词法分析->语法分析->代码生成),在这个过程中,像var a = 2这样的声明会被分解成两个独立的步骤:

  1. 首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
  2. 接下来,a = 2会查询(LHS 查询)变量 a 并对其进行赋值。

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说他们没有找到所需的标识符),就会向上级作用域继续查找标识符,这样每次上升一级作用域,最后抵达全局作用域,无论找到或没找到都将会停止。

不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。

1.2 词法作用域

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

JavaScript 中有两个机制可以“欺骗”词法作用域:

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

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

1.3 函数作用域和块作用域

函数是 JavaScript 中最常见的作用域单元。本质上,声明一个函数内部的变量或函数,会在其所处的作用域中“隐藏”起来,这是有意为之的良好的设计原则。

但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{...}内部)。

从 ES3 开始,try/catch结构在 catch 分句中具有块作用域。

在 ES6 引入了 let 关键字,用来在任意代码块中声明变量;const 关键字声明常量。

1.4 提升

我们习惯将var a = 2;看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将var aa = 2当做两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。(回顾下:编译-解析-执行)

因此无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

声明本身会被提升,而包括函数表达式(var a = function(){...})的赋值在内的赋值操作并不会提升。

在写代码时我们要避免重复声明!

1.5 作用域闭包

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

闭包可以用多种形式来实现模块等模式。

模板有两个主要特征:

  1. 为创建内部作用域而调用了一个包装函数;
  2. 包装函数的返回值必须至少包括一个队内部函数的引用,这样就会创建覆盖整个包装函数内部作用域的闭包。

附录A-动态作用域

动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心他们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。

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

附录B-块作用域的替代方案

ES6之前是使用 catch 实现:

try{trow undefined}catch(a){
  a = 2;
  consloe.log(a); // 2
}

第二部分 this 和对象原型

2.1 关于 this

2.2 this 全面解析

2.3 对象

2.4 混合对象“类”

2.5 原型

2.6 行为委托