JS上卷整理

说点啥

1504的书,现在(2005)才想起好好看,过去5年零1个月了,证明自己的技术能力真是水了5年多。抓紧补齐吧。

  • S11 表示 《不知道系列》 上卷 第一部分 第一章
  • Z11 表示 《不知道系列》 中卷 第一部分 第一章
  • X11 表示 《不知道系列》 下卷 第一部分 第一章

S11-作用域是什么

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对 变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。

赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域 的赋值操作。

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

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

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

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

function foo(a) {
	var b = a;
	return a + b;
}
var c = foo( 2 );

1. 找出所有的 LHS 查询(这里有 3 处!) 
2. 找出所有的 RHS 查询(这里有 4 处!)
  • c = …;、a = 2(隐式变量分配)、b = …
  • foo(2…、= a;、a …、… b

S12-词法作用域是什么

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

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

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

S13-函数作用域和块级作用域

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

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

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

在 ES6 中引入了 let 关键字(var 关键字的表亲),用来在任意代码块中声明变量。if (…) { let a = 2; } 会声明一个劫持了 if 的 { … } 块的变量,并且将变量添加到这个块 中。

有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开 发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。

S14-提升

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

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

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。

要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引 起很多危险的问题!

S15-作用域闭包

闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人 才能够到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的 词法环境中书写代码的。

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

如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循 环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。

模块有两个主要特征:

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

现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用它来做一些有用 的事!

SFA-动态作用域

需要明确的是,事实上 JavaScript 并不具有动态作用域。它只有词法作用域,简单明了。 但是 this 机制某种程度上很像动态作用域。

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

SFB-块作用域的替代方案

最后简单地看一下 try/catch 带来的性能问题,并尝试回答“为什么不直接使用 IIFE 来创 建作用域”这个问题。

首先,try/catch 的性能的确很糟糕,但技术层面上没有合理的理由来说明 try/catch 必 须这么慢,或者会一直慢下去。自从 TC39 支持在 ES6 的转换器中使用 try/catch 后, Traceur 团队已经要求 Chrome 对 try/catch 的性能进行改进,他们显然有很充分的动机来 做这件事情。

其次,IIFE 和 try/catch 并不是完全等价的,因为如果将一段代码中的任意一部分拿出来 用函数进行包裹,会改变这段代码的含义,其中的 this、return、break 和 contine 都会 发生变化。IIFE 并不是一个普适的解决方案,它只适合在某些情况下进行手动操作。

最后问题就变成了:你是否想要块作用域?如果你想要,这些工具就可以帮助你。如果不 想要,继续使用 var 来写代码就好了!

SFC-this词法

var self = this 这种解决方案圆满解决了理解和正确使用 this 绑定的问题,并且没有把 问题过于复杂化,它使用的是我们非常熟悉的工具:词法作用域。self 只是一个可以通过 词法作用域和闭包进行引用的标识符,不关心 this 绑定的过程中发生了什么。

var obj = { count: 0, 
	cool: function coolFn() {
		if (this.count < 1) { 
			setTimeout( () => { // 箭头函数是什么鬼东西? 
				this.count++; 
				console.log( "awesome?" ); 
			}, 100 ); 
		} 
	} 
};

obj.cool(); // 很酷吧 ?

简单来说,箭头函数在涉及 this 绑定时的行为和普通函数的行为完全不一致。它放弃了所 有普通 this 绑定的规则,取而代之的是用当前的词法作用域覆盖了 this 本来的值。

S21-关于this

对于那些没有投入时间学习 this 机制的 JavaScript 开发者来说,this 的绑定一直是一件非常令人困惑的事。this 是非常重要的,但是猜测、尝试并出错和盲目地从 Stack Overflow 上复制和粘贴答案并不能让你真正理解 this 的机制。

学习 this 的第一步是明白 this 既不指向函数自身也不指向函数的词法作用域,你也许被 这样的解释误导过,但其实它们都是错误的。

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

S22-this全面解析

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  • 1.由 new 调用?绑定到新创建的对象。
  • 2.由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  • 3.由上下文对象调用?绑定到那个上下文对象。
  • 4.默认:在严格模式下绑定到 undefined,否则绑定到全局对象。

一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null),以保护全局对象。

ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。

S23-对象

JavaScript 中的对象有字面形式(比如 var a = { … })和构造形式(比如 var a = new Array(…))。字面形式更常用,不过有时候构造形式可以提供更多选项。

许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者是 7 个,取 决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同 的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。

对象就是键 / 值对的集合。可以通过 .propName 或者 [“propName”] 语法来获取属性值。访 问属性时,引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]]), [[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]] 链(参见第 5 章)。

属性的特性可以通过属性描述符来控制,比如 writable 和 configurable。此外,可以使用 Object.preventExtensions(…)、Object.seal(…) 和 Object.freeze(…) 来设置对象(及其 属性)的不可变性级别。

属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是 可枚举或者不可枚举的,这决定了它们是否会出现在 for…in 循环中。

你可以使用 ES6 的 for…of 语法来遍历数据结构(数组、对象,等等)中的值,for…of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。

S24-混合对象”类“

类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有类 似的语法,但是和其他语言中的类完全不同。

类意味着复制。

传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类 中。

多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父 类,但是本质上引用的其实是复制的结果。

JavaScript 并不会(像类那样)自动创建对象的副本。

混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态(OtherObj.methodName.call(this, …)),这会让代码更加难 懂并且难以维护。

此外,显式混入实际上无法完全模拟类的复制行为,因为对象(和函数!别忘了函数也 是对象)只能复制引用,无法复制被引用的对象或者函数本身。忽视这一点会导致许多 问题。

总地来说,在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋 下更多的隐患。

S25-原型

如果要访问对象中并不存在的一个属性,[[Get]] 操作(参见第 3 章)就会查找对象内部 [[Prototype]] 关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。

所有普通对象都有内置的 Object.prototype,指向原型链的顶端(比如说全局作用域),如 果在原型链中找不到指定的属性就会停止。toString()、valueOf() 和其他一些通用的功能 都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。

关联两个对象最常用的方法是使用 new 关键词进行函数调用,在调用的 4 个步骤(第 2 章)中会创建一个关联其他对象的新对象。

使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”。带 new 的函数调用 通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的类构造函数不一样。

虽然这些 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很相似,但 是 JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。

出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无 法帮助你理解 JavaScript 的真实机制(不仅仅是限制我们的思维模式)。

相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是复制而是委托。

S26-行为委托

在软件架构中你可以选择是否使用类和继承设计模式。大多数开发者理所当然地认为类是 唯一(合适)的代码组织方式,但是本章中我们看到了另一种更少见但是更强大的设计模式:行为委托。

行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努 力实现类机制(参见第 4 和第 5 章),也可以拥抱更自然的 [[Prototype]] 委托机制。

当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。

对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把 它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。

S2A-ES6的class

除了语法更好看之外,ES6 还解决了什么问题呢?

  • 1.(基本上,下面会详细介绍)不再引用杂乱的 .prototype 了。
  • 2.Button 声 明 时 直 接“ 继 承 ” 了 Widget, 不 再 需 要 通 过 Object.create(…) 来 替 换 .prototype 对象,也不需要设置 .proto 或者 Object.setPrototypeOf(…)。
  • 3.可以通过 super(…) 来实现相对多态,这样任何方法都可以引用原型链上层的同名方 法。这可以解决第 4 章提到过的那个问题:构造函数不属于类,所以无法互相引用—— super() 可以完美解决构造函数的问题。
  • 4.class 字面语法不能声明属性(只能声明方法)。看起来这是一种限制,但是它会排除 掉许多不好的情况,如果没有这种限制的话,原型链末端的“实例”可能会意外地获取 其他地方的属性(这些属性隐式被所有“实例”所“共享”)。所以,class 语法实际上 可以帮助你避免犯错。
  • 5.可以通过 extends 很自然地扩展对象(子)类型,甚至是内置的对象(子)类型,比如 Array 或 RegExp。没有 class …extends 语法时,想实现这一点是非常困难的,基本上 只有框架的作者才能搞清楚这一点。但是现在可以轻而易举地做到!

class 很好地伪装成 JavaScript 中类和继承设计模式的解决方案,但是它实际上起到了反作 用:它隐藏了许多问题并且带来了更多更细小但是危险的问题。

class 加深了过去 20 年中对于 JavaScript 中“类”的误解,在某些方面,它产生的问题比 解决的多,而且让本来优雅简洁的 [[Prototype]] 机制变得非常别扭。

结论:如果 ES6 的 class 让 [[Prototype]] 变得更加难用而且隐藏了 JavaScript 对象最重要 的机制——对象之间的实时委托关联,我们难道不应该认为 class 产生的问题比解决的多 吗?难道不应该抵制这种设计模式吗?

我无法替你回答这些问题,但是我希望本书能从前所未有的深度分析这些问题,并且能够 为你提供回答问题所需的所有信息。