你不知道的JavaScript(1)

《你不知道的JavaScript》上卷 阅读笔记

本篇讲述:【JavaScript编译】【函数作用域】【块作用域】

第一部分 作用域和闭包

第一章 作用域是什么

1.1 编译原理

  • 分词/词法分析

    例:var a = 2;对这段代码进行词法分析,通常会分解成var a = 2 ;

  • 解析/语法分析

    生成“抽象语法树(Abstract Syntax Tree, AST)”

  • 代码生成

    通过AST编译成一组机器指令

注:JavaScript引擎的编译要比以上复杂的多。例如:在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化。

1.2 理解作用域

  • 引擎

    从头到尾负责整个JavaScript程序的编译及执行过程。

  • 编译器

    引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。

  • 作用域

    引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

接下来看一段代码执行过程中他们是如何进行交流
代码:

1
2
3
4
5
function foo(a) {
console.log( a ); // 2
}
foo( 2 );

对话:

“引擎:我说作用域,我需要为foo进行RHS引用。你见过它吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下foo。
引擎:作用域,还有个事儿。我需要为a进行LHS引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为foo的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把2赋值给a。
引擎:哥们,不好意思又来打扰你。我要为console进行RHS引用,你见过它吗?
作用域:咱俩谁跟谁啊,再说我就是干这个的。这个我也有,console是个内置对象。给你。
引擎:么么哒。我得看看这里面是不是有log(..)。太好了,找到了,是一个函数。
引擎:哥们,能帮我再找一下对a的RHS引用吗?虽然[…]”

摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.

1.3 作用域嵌套

在当前作用域中无法找到某个变量时,引擎就会在外层嵌套作用域中继续查找,知道找到该变量,或抵达最外层的全局作用域为止。

1.4 异常

如果在全局作用域中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下

如果ES5中引入了“严格模式”,那么严格模式会禁止自动的或隐式的创建全局变量。因此在查询失败的时候会抛出类似ReferenceError异常。

如果你对一个已查询到的变量进行不合理的操作时,比如对一个非函数类型的值进行函数调用,或者引用null undefined类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫做TypeError

1.5 小结

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

var a = 2

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

摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.

第二章 词法作用域

2.2 词法阶段

气泡1包含着整个全局作用域,其中只有一个标识符:foo
气泡2包含着foo所创建的作用域,其中有三个标识符:s bar b
气泡3包含着bar所创建的作用域,其中只有一个标识符:c

作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域中可以定义同名的标识符,这叫做“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)

全局变量会自动成为全局对象(比如浏览器中的window对象)的属性。

1
2
3
4
5
6
7
8
9
10
11
var a = 1;
function fun1() {
var a = 2;
console.log(a)//输出2
}
function fun2() {
var a = 2;
console.log(window.a)//输出1
}

对浏览器中window全局对象的解释可以进一步到以下网址了解:

2.2 欺骗词法

eval with 等语法会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域。而且会导致JavaScript引擎在编译阶段无法对它们做任何的优化处理。
简单介绍一下这两个语法:

  • eval

    可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。

  • with

    本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)

这两个机制的副作用会导致代码运行变慢。不要使用它们。

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

  • 每一个函数都会创建一个自己的作用域,如果你在该作用域中声明了变量,且该变量存在于外部作用域,那么它会遮蔽外部作用域中的变量。

  • 为了不污染全局作用域,可以将代码写入一个匿名函数中调用

    1
    2
    3
    4
    5
    6
    7
    8
    var a = 2;
    (function() {
    var a = 3;
    console.log( a ); // 3
    })();
    console.log( a ); // 2
  • 变量声明应该距离使用的地方越近越好,并最大限度的本地化

  • let

    1
    2
    3
    4
    5
    6
    7
    {
    let a = 10;
    var b = 1;
    }
    a // ReferenceError: a is not defined.
    b // 1
  • const

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var foo = true;
    if (foo) {
    var a = 2;
    const b = 3; // 包含在if中的块作用域常量
    a = 3; // 正常!
    b = 4; // 错误!
    }
    console.log( a ); // 3
    console.log( b ); // ReferenceError!