读书笔记-你不知道的JavaScript金沙官网线上:(上

最近刚刚看完了《你不知道的 JavaScript》上卷,对 JavaScript 有了更进一步的了解。

本文首发在我的个人博客:http://muyunyun.cn/

《你不知道的 JavaScript》上卷由两部分组成,第一部分是《作用域和闭包》,第二部分是《this 和对象原型》。下面我会按照简单介绍一下每一章的主要内容及阅读感受。

《你不知道的JavaScript》系列丛书给出了很多颠覆以往对JavaScript认知的点, 读完上卷,受益匪浅,于是对其精华的知识点进行了梳理。

第一部分《作用域和闭包》

什么是作用域

作用域是一套规则,用于确定在何处以及如何查找变量。

第 1 章 作用域是什么

  1. 编译原理:简单介绍分词/词法分析、解析/语法分析、代码生成的概念;
  2. 理解作用域:介绍引擎、编译器、作用域之间的关系;
  3. 作用域嵌套。

在这一章节中,作者通过引擎、编译器、作用域之间的对话,将这三者之间的关系及作用生动形象地展现出来,并引出了 LHS 查询和 RHS 查询的概念。

编译原理

JavaScript是一门编译语言。在传统编译语言的流程中,程序中一段源代码在执行之前会经历三个步骤,统称为“编译”。

  • 分词/词法分析
    将字符串分解成有意义的代码块,代码块又称词法单元。比如程序var a = 2;会被分解为var、a、=、2、;
  • 解析/语法分析
    将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法接口的书,又称“抽象语法树”。
  • 代码生成
    将抽象语法树转换为机器能够识别的指令。

第 2 章 词法作用域

  1. 词法作用域及其相关概念;
  2. 欺骗词法的方式:
    • 在代码运行时修改词法作用域,如 eval()
    • 在代码运行时创建新的词法作用域,如 with

这一章作者介绍了词法作用域以及欺骗词法的方式。说来惭愧,在看这章之前,我完全没听说过「词法作用域」这个概念,一开始我还以为是个很高大上的东西,看完之后你会觉得其实也没什么,就是你平时都在写的东西,只不过你没有留意而已。

理解作用域

作用域 分别与编译器、引擎进行配合完成代码的解析

  • 引擎执行时会与作用域进行交流,确定RHS与LHS查找具体变量,如果查找不到会抛出异常。
  • 编译器负责语法分析以及生成代码。
  • 作用域负责收集并维护所有变量组成的一系列查询,并确定当前执行的代码对这些变量的访问权限。

对于 var a = 2 这条语句,首先编译器会将其分为两部分,一部分是 var a,一部分是 a = 2。编译器会在编译期间执行 var a,然后到作用域中去查找 a 变量,如果 a 变量在作用域中还没有声明,那么就在作用域中声明 a 变量,如果 a 变量已经存在,那就忽略 var a 语句。然后编译器会为 a = 2 这条语句生成执行代码,以供引擎执行该赋值操作。所以我们平时所提到的变量提升,无非就是利用这个先声明后赋值的原理而已!

第 3 章 函数作用域和块作用域

  1. 函数作用域:函数声明和函数表达式的区别、具名函数和匿名函数;
  2. 块作用域:withtry/catchletconst

这一章作者介绍了 JavaScript 中的函数作用域及块作用域,讲了函数声明和函数表达式的区别,其实很简单,就是看 function 这个关键字是否是在声明中的第一个词,如果是,那就是函数声明,否则就是函数表达式。另外,作者还简单地介绍了下 ES6 中具有块作用域作用的 letconst 关键字。

在这之前,我一直以为 ES6 之前是没有块作用域的,只有全局作用域和函数作用域,看完这章之后,我才知道其实在 ES3 的时候就有块作用域了。比如,with 。再比如,try/catch 中的 catch,一般我们是这样写的:

try {
    // do something
} catch (err) {
    console.log(err)
}

其中这个 err 只存在 catch 分句内部,从别处引用时会抛出错误。这不就是块作用域吗?

异常

对于 var a = 10 这条赋值语句,实际上是为了查找变量 a, 并且将 10 这个数值赋予它,这就是 LHS 查询。 对于 console.log(a) 这条语句,实际上是为了查找 a 的值并将其打印出来,这是 RHS 查询。

为什么区分 LHSRHS 是一件重要的事情?
在非严格模式下,LHS 调用查找不到变量时会创建一个全局变量,RHS 查找不到变量时会抛出 ReferenceError。 在严格模式下,LHS 和 RHS 查找不到变量时都会抛出 ReferenceError。

第 4 章 提升

这一章节作者简单地介绍了一下变量声明提升和函数声明提升。没什么好说的,需要注意的是函数表达式是赋值操作,并不会提升。

作用域的工作模式

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域( JavaScript 中的作用域就是词法作用域)。另外一种是动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等)。

第 5 章 作用域闭包

  1. 闭包;
  2. 作用域和闭包;
  3. 模块机制。

闭包是 JavaScript 中的一大难点,在这章中作者用了 4 个小节来介绍闭包,还有 1 个小节来介绍模块机制。不要看闭包有四个小节,其实也不过 8 页而已,核心的文字加起来也就 2 页,但就是这短短的 2 页,就把闭包给讲得非常清楚。

下面是书中给出关于闭包的定义:

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

看了是不是还是不懂,没关系,让我们来提取关键字:

  • 函数;
  • 记住并访问所在的词法作用域;
  • 当前词法作用域之外执行。

再来看下书中的一段代码,看完之后再结合书中的定义来理解,我相信你对闭包肯定会有更进一步的理解。

function foo() {
    var a = 2;

    function bar() {
        console.log(a);
    }

    return bar;
}

var baz = foo();

baz(); // 2 —— 朋友,这就是闭包的效果。

下面结合我们刚刚提取的关键字来理解。

  • 函数。这里的函数是 bar()
  • 记住并访问。 bar() 当前所在的词法作用域是 foo() 的函数作用域。bar() 的词法作用域能够访问 foo() 的内部作用域。
  • 当前词法作用域之外执行。在上面的代码中,我们将函数 bar() 当做一个值类型传递给外部,在这句代码中 var baz = foo();,我们将 foo() 的返回值(也就是 bar())赋值给变量 baz 并调用 baz(),实际上就是调用 bar()。上面第 2 点里我们说了,bar() 的作用域是 foo() 的函数作用域,但是,在这里,它却是在自己定义的词法作用域以外的地方执行。

怎么样,通过上面的分析,是不是对闭包有了进一步的理解了。

词法作用域

词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则。词法作用域最重要的特征是它的定义过程发生在代码的书写阶段(假设没有使用 eval() 或 with )。来看示例代码:
var a = 10
, b = 20;

function foo() {
  console.log(a);  // 2
}

function bar() {
  var a = 3;
  foo();
}

var a = 2;

bar()

词法作用域让foo()中的a通过RHS引用到了全局作用域中的a,因此会输出2。

附录 A 动态作用域

作者在这一章中简单地分析了下动态作用域,并通过一小段代码将它与词法作用域做了对比。词法作用域与动态作用域的主要区别在于:词法作用域是在定义时确定的,而动态作用域是在运行时确定的。

动态作用域

而动态作用域只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。因此,如果 JavaScript 具有动态作用域,理论上,下面代码中的 foo() 在执行时将会输出3。

function foo() {
  console.log(a);  // 3
}

function bar() {
  var a = 3;
  foo();
}

var a = 2;

bar()

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

这一章简单地介绍了块作用域的替代方案 Traceur,以及因此可能会带来的性能问题。

函数作用域

附录 C this 词法

这一章并没有说明 this 机制 ,只是介绍了 ES6 中的箭头函数引入的行为 —— this 词法。关于 this 机制的详细说明是在第二部分《this 和对象原型》中的第 1 章和 第 2 章。

匿名与具名

对于函数表达式一个最熟悉的场景可能就是回调函数了,比如

setTimeout( function() {
  console.log("I waited 1 second!")
}, 1000 )

这叫作匿名函数表达式。函数表达式可以匿名,而函数声明则不可以省略函数名。匿名函数表达式书写起来简单快捷,很多库和工具也倾向鼓励使用这种风格的代码。但它也有几个缺点需要考虑。

  • 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  • 如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
  • 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

始终给函数表达式命名是一个最佳实践:

setTimeout( function timeoutHandler() { // 我有名字了
  console.log("I waited 1 second!")
}, 1000 )

附录 D 致谢

这一章作者致谢了一大堆的人,光人名的排版就占了两页多,说真的,我都怀疑是不是在凑字数了(纯调侃,没别的意思)。


第二部分《this 和对象原型》

提升

第 1 章 关于 this

  1. this 的指向;
  2. this 的作用域。

这一章中作者先是提出我们「为什么要使用 this?」这个问题,然后再指出「this 到底是什么?」,为第 2 章做铺垫。

这一章我个人认为最核心的就是两句话。第一句是「当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录的一个属性,会在函数执行的过程中用到」。也就是说,this 是活动记录里的一个属性,与函数执行的过程有关。

第二句话是「this 实际上是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。」。第 2 章实际上就是在讲这个绑定。

先有声明还是先有赋值

考虑以下代码:

a = 2;

var a;

console.log(a); // 2

考虑另外一段代码

console.log(a); // undefined

var a = 2;

我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个是执行阶段的任务。
这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程称为提升。

可以看出,先有声明后有赋值。

再来看以下代码:

foo();  // TypeError
bar();  // ReferenceError

var foo = function bar() {
  // ...
};

这个代码片段经过提升后,实际上会被理解为以下形式:

var foo;

foo();  // TypeError
bar();  // ReferenceError

foo = function() {
  var bar = ...self...
  // ...
};

这段程序中的变量标识符 foo() 被提升并分配给全局作用域,因此 foo() 不会导致 ReferenceError。但是 foo 此时并没有赋值(如果它是一个函数声明而不是函数表达式就会赋值)。foo()由于对 undefined 值进行函数调用而导致非法操作,因此抛出 TypeError 异常。另外即时是具名的函数表达式,名称标识符(这里是 bar )在赋值之前也无法在所在作用域中使用。

第 2 章 this 全面解析

  1. 调用位置;
  2. 绑定规则:
  • 默认绑定;
    • 隐式绑定;
    • 显式绑定:call()apply()bind()
    • new 绑定;
    • 箭头函数的绑定;
    • 一些例外的绑定。
  1. 绑定规则的优先级。

作者在这一章中全面介绍了 this 的绑定规则。

要弄清楚 this 的绑定对象,需要明白以下两点:

  • 调用位置
  • 绑定规则

什么是调用位置?简单来说,就是函数在代码中被调用的位置。为了找到调用位置,我们需要分析调用栈,也就是为了到达当前执行位置所调用的所有函数,而调用位置就在当前正在执行的函数的前一个调用中。

而绑定规则就是说 this 绑定的对象是有规则的,并且这些规则是有优先级的,总的来说有下面四点:

  1. new 调用的,绑定到新创建的对象;
  2. callapplybind 调用的,绑定到指定的对象;
  3. 由上下文对象调用的,绑定到该上下文对象;
  4. 默认的,在严格模式下绑定到 undefined,在非严格模式下绑定到全局对象。

当然了,ES6 中新增的箭头函数并不在这四条规则里面,而是继承外层第一个非箭头函数调用的 this 绑定。

在看这一章之前,我对 this 一知半解,网上找的答案也是五花八门,根本不知道哪个对哪个错。在看完这一章之后,我算是对 this 的所绑定的对象有了较为清晰的认识,以后再遇到类似的问题时,直接套用上面的规则就可以了。

闭包

之前写过关于闭包的一篇文章深入浅出JavaScript之闭包(Closure)

本文由金沙官网线上发布于Web前端,转载请注明出处:读书笔记-你不知道的JavaScript金沙官网线上:(上

您可能还会对下面的文章感兴趣: