《你不知道的JavaScript(上)》第一部分-作用域和闭包

94 阅读6分钟

第1章 作用域是什么

1.1 编译原理

在传统编译语言中,一段代码在执行之前会经历三个步骤:

1.分词/词法分析(Tokenizing/Lexing) 例如var a = 2;,会被分解成为:vara=2;

2.解析/语法分析(Parsing) 将词法单元数组转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树(抽象语法树)。

3.代码生成 代码生成:将AST转换为可执行代码的过程。 🌰将var a = 2;的AST转化为一组机器指令,用来创建一个叫做a的变量(包括分配内存等),并将一个值储存在a中。

理解作用域

1.2.1 演员表

首先要介绍将参与到对程序var a = 2;进行处理的演员们。

1.引擎 从头到尾负责整个JS程序的编译及执行过程。

2.编译器 引擎的好朋友之一,负责语法分析及代码生成等脏活累活。

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

1.2.2 对话

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

1.2.3 编译器有话说

上面在运行时引擎会在作用域中查找该变量

当变量出现在复制操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。

RHS可以理解成retrieve his source value(取到它的源值)。

console.log(a)

上面这个🌰中对a的引用是一个RHS引用,需要查找并取得a的值,这样才能将值传递给console.log(...)

a = 2

这里对a的引用是LHS引用,我们并不关心当前的值,只是想为=2这个赋值操作找到一个目标。

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

上面的🌰既有LHS也有RHS引用:

  1. foo(...)函数的调用需要对foo进行RHS引用。
  2. 当2被当作参数传递给foo(...)函数时,2会被分配给参数a。为了给参数a(隐式地)分配值,需要进行一次LHS查询。
  3. console.log(a)中有对a进行的RHS引用。

1.2.4 小测验

function foo(a){
  var b = a
  return a + b
}
var c = foo(2)
  1. 找到🌰中左右的LHS查询(这里有3处!)
  2. 找到🌰中左右的RHS查询(这里有4处!)

1.3 作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达全局作用域为止。

function foo(a){
  console.log(a+b)
}
var b = 2
foo(2) // 4

上面的🌰中对b进行的RHS引用无法在函数foo内部完成,但可以再上一级作用域(也就是全局作用域)中完成。

作用域处理的过程可视化,就像这个建筑:

image.png

1.4 异常

function foo(a){
  console.log(a+b)
  b=a
}
foo(2)

上面的🌰中,第一次对b进行RHS查询时是无法找到该变量的。如果RHS查询在所有的嵌套的作用域中找不到所需的变量,引擎就会抛出ReferenceError异常。

第2章 词法作用域

2.1 词法阶段

词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。

但this机制某种程度上很像动态作用域。

ES6中的箭头函数引入了一个叫做this词法的行为。

function foo(a){
  var b = a * 2;
  function bar(c){
    console.log(a, b, c)
  }
  bar(b * 3)
}
foo(2)

image.png

2.2 欺骗词法

使用eval或with会导致代码运行变慢。不要使用它们。

2.2.1 eval

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

上面的🌰实际上会在foo(...)内部创建一个变量b,并遮蔽了外部作用域中的同名变量。

当console.log(...)被执行时,会在foo(...)的内部同时找到a和b,但是永远也无法找到外部的b。

2.2.2 with

var obj = {
  a: 1,
  b: 2,
  c: 3,
};
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); // 2
console.log(o1.a);
foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2--不好,a被泄漏到全局作用域上

上面的🌰的原因(自行回忆吧)

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

3.1 函数中的作用域

function foo(a) {
  var b = 2;
  function bar() {
    // ...
  }
  // 更多的代码
  var c = 3;
}
bar();
console.log(a, b, c);

上面的🌰输入什么结果,以及原因。

3.2 隐藏内部实现

function doSomething(a) {
  b = a + doSomethingElse(a * 2);
  console.log(b * 3);
}
function doSomethingElse(a) {
  return a - 1;
}
var bar;
doSomething(2);

变量b和函数doSomethingElse(...)应该是doSomething(...)内部实现的“私有”内容。更“合理”应该怎么做?

规避冲突

function foo() {
  function bar(a) {
    i = 3;
    console.log(a + i);
  }
  for (var i = 0; i < 10; i++) {
    bar(i * 2);
  }
}
foo();

上面的🌰什么结果?怎么解决?

3.3 函数作用域

  1. 具名函数。
  2. 匿名函数。
  3. 立即执行函数表达式。

3.4 块作用域

  1. fo循环。
  2. if语句。
  3. with。
  4. try/catch。
  5. let。
  6. const。

(这块内容后面补充)

第4章 提升

4.1 先有鸡还是先有蛋

  a = 2
  var a
  console.log(a)

上面的🌰1会输出什么?

console.log(a)
var a = 2;

上面的🌰2会输出什么?

4.2 编译器再度来袭

🌰1会以什么形式进行处理?

🌰2会以什么形式进行处理?

提升:变量和函数声明会从它们在代码中出现的位置被"移动"到了最上面。

foo();

function foo(){
  console.log(a)
  var a = 2
}

上面的🌰会输出什么?

foo()

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

上面的🌰会输出什么?

foo()
bar()

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

上面的🌰会输出什么?

4.3 函数优先

foo();

var foo;

function foo() {
  console.log(1)
}

foo = function(){
  console.log(2)
}

上面的🌰会输出什么?

foo()

function foo(){
  console.log(1)
}

var foo = function(){
  console.log(2)
}

function foo(){
  console.log(3)
}

上面的🌰会输出什么?

foo()

var a = true
if(a){
  function foo(){ console.log("a"); }
} else {
  function foo(){ console.log("b"); }
}

上面的🌰会输出什么?

第5章 作用域闭包

5.2 实质问题

function foo(){
  var a = 2
  function bar(){
    console.log(a)
  }
  return bar
}
var baz = foo()
baz()

foo()内部作用域会被回收吗?

无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包

function foo(){
  var a = 2
  function baz(){
    console.log(a)
  }
  bar(baz)
}
funciont bar(fn){
  fn()
}

传递函数也可以是间接的。

var fn;
function foo(){
  var a = 2
  function baz(){
    console.log(a)
  }
  fn = baz
}
function bar(){
  fn()
}
foo()
bar()

5.3 现在我懂了

想想有哪些用到闭包的场景?

5.4 循环和闭包

for(var i = 1; i <= 5; i++){
  setTimeout(function timer(){
    console.log(i)
  }, i*1000)
}

上面的🌰输出什么结果?为什么呢?有什么解决方法吗?

5.5 模块

写一个模块的🌰

对这个模块进行简单的改进来实现单例模式。