你不知道的JavaScript(中)读书小记

85 阅读13分钟

参考: 《你不知道的JavaScript(中)》

类型和语法

类型

变量没有类型,但它们持有的值有类型。

  • 空值(null)
  • 未定义(undefined)
    • 已在作用域中声明但还没有赋值的变量,是 undefined 的。
  • 布尔值( boolean)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 符号(symbol,ES6 中新增)

  • typeof undefined === 'undefined'
  • typeof true === 'boolean'
  • typeof 123 === 'number'
  • typeof '123' === 'string'
  • typeof {} === 'object'
  • typeof Symbol() === 'symbol'
  • typeof null === 'object'
  • typeof function a(){ /* .. */ } === "function"
    • 是 object 的一个“子类型”。函数是“可调用对象”,它有一个内部属性 [[Call]],该属性使其可以被调用。

typeof Undeclared

  • typeof 的安全防范机制
  • 提供了一个检测某个变量是否已经声明的工具,这在使用第三方库或者浏览器兼容时会很有用

  • 数值
    • tofixed(..) 方法可指定小数部分的显示位数
    • toPrecision(..) 方法用来指定有效数位的显示位数
  • 怎样来判断 0.1 + 0.2 和 0.3 是否相等
    • Number.EPSILON 来比较两个数字是否相等(在指定的误差范围内):
function numbersCloseEnoughToEqual(n1,n2) {
 return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false
  • 能够呈现的最大浮点数大约是 1.798e+308(这是一个相当大的数字),它定义在 Number. MAX_VALUE 中。最小浮点数定义在 Number.MIN_VALUE 中,大约是 5e-324,它不是负数,但 无限接近于 0 !
  • 整数的安全范围
    • 数字的呈现方式决定了“整数”的安全值范围远远小于 Number.MAX_VALUE。 能够被“安全”呈现的最大整数是 2^53 - 1,即 9007199254740991,在 ES6 中被定义为 Number.MAX_SAFE_INTEGER。最小整数是 -9007199254740991,在 ES6 中被定义为 Number. MIN_SAFE_INTEGER。
  • 整数检测
    • ES6 中的 Number.isInteger(..)
  • 要检测一个值是否是安全的整数,可以使用 ES6 中的 Number.isSafeInteger(..)
Number.isSafeInteger( Number.MAX_SAFE_INTEGER ); // true
Number.isSafeInteger( Math.pow( 2, 53 ) ); // false
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 ); // true
  • a | 0 可以将变量 a 中的数值转换为 32 位有符号整数,因为数位运算符 | 只适用于 32 位 整数(它只关心 32 位以内的值,其他的数位将被忽略)。因此与 0 进行操作即可截取 a 中 的 32 位数位。

特殊值

  • undefined 类型只有一个值,即 undefined。null 类型也只有一个值,即 null。它们的名 称既是类型也是值。
  • null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。
    • undefined 指从未赋值
    • null 指曾赋过值,但是目前没有值
  • NaN
    • 无法返回一个有效的数字,这种情况下返回值为 NaN
    • 用于指出数字类型中的错误情况
    • 它和自身不相等
    • ES6 工具函数 Number.isNaN(..)判断值是否为NaN
  • 无穷数
    • Infinity(即Number.POSITIVE_INfiNITY)
    • Infinity/ Infinity 是一个未定义操作,结果为 NaN。
  • 零值
    • 为什么需要负零呢?
      • 有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位 (sign)用来代表其他信息(比如移动的方向)。此时如果一个值为 0 的变量失去了它的符 号位,它的方向信息就会丢失。所以保留 0 值的符号位可以防止这类情况发生。
  • 特殊等式
    • 能使用 == 和 ===时就尽量不要使用 Object.is(..),因为前者效率更高、 更为通用。Object.is(..) 主要用来处理那些特殊的相等比较。
  • 值和引用
    • 简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值 / 传递,包括 null、undefined、字符串、数字、布尔和 ES6 中的 symbol。
    • 复合值(compound value)——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值 / 传递。
    • 引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向
  • Symbol(..)
    • 符号并非对象,而是一种简单标量基本类型
    • Object.getOwnPropertySymbols(..)

强制类型转换

  • 将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐式的情况称为强制类型转换
  • 假值列表:
    • undefined
    • null
    • false
    • ""
    • 0 +0 -0 NaN
  • ~ 运算符(字位操作“非”)
    • 过字位运算符只适用于 32 位整数,运算符会强制操作数使用 32 位 格式。这是通过抽象操作 ToInt32 来实现的。
    • 它首先将值强制类型转换为 32 位数字,然后执行字位操作“非”(对每一个字 位进行反转)。
    • 对 ~ 还可以有另外一种诠释,源自早期的计算机科学和离散数学:~ 返回 2 的补码。这样 一来问题就清楚多了! ~x 大致等同于 -(x+1)。
  • 解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停 止。而转换不允许出现非数字字符,否则会失败并返回 NaN。
    • parseInt、parseFloat 字符串解析
      • arseInt(..) 针对的是字符串值。向 parseInt(..) 传递数字和其他类型的参数是 没有用的,比如 true、function(){...} 和 [1,2,3]。
    • Number 强制类型转换
var a = "42";
var b = "42px";

Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42
  • ToPrimitive 抽象操作
    • 为了将值转换为相应的基本类型值,抽象操作 ToPrimitive会首先检查该值是否有 valueOf() 方法。 如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。 如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
  • 如果 + 的其中一个操作数是字符串, 则执行字符串拼接;否则执行数字加法
  • 可以将数字和空字符串 "" 相 + 来将其转换为字符串
  • a + ""(隐式)和前面的 String(a)(显式)之间有一个细微的差别需要注意。根据 ToPrimitive 抽象操作规则,a + "" 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象 操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()。
var a = {
 valueOf: function() { return 42; },
 toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4"
  • && 和 || 运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值
  • == 允许在相等比较中进行强制类型转换,而 === 不允许
  • 字符串和数字之间的相等比较
    • a == b 是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型 转换。 具体怎么转换?是 a 从 42 转换为字符串,还是 b 从 "42" 转换为数字?
    • ES5 规范 11.9.3.4-5 这样定义:
      • (1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
      • (2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。
  • 他类型和布尔类型之间的相等比较
    • (1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
    • (2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
  • null 和 undefined 之间的相等比较
    • 在 == 中 null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不存在这种 情况。
// 这也就是说在 == 中 null 和 undefined 是一回事,可以相互进行隐式强制类型转换:
var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false
  • 对象和非对象之间的相等比较
    • (1) 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
    • (2) 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果
  • 安全运用隐式强制类型转换
    • 如果两边的值中有 true 或者 false,千万不要使用 ==。
    • 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==。
  • 还有一个坑常被提到:
[] + {}; // "[object Object]"
{} + []; // 0
    • 表面上看 + 运算符根据第一个操作数([] 或 {})的不同会产生不同的结果,实则不然。 第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理。第 4 章讲过 [] 会被强制类型转换为 "",而 {} 会被强制类型转换为 "[object Object]"。 但在第二行代码中,{} 被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需 要分号,所以这里不存在语法上的问题。最后 + [] 将 [] 显式强制类型转换(参见第 4 章) 为 0。

异步和性能

异步控制台

  • 某些浏览器的 console.log(..) 并不会把传入的内容立 即输出。出现这种情况的主要原因是,在许多程序(不只是 JavaScript)中,I/O 是非常低 速的阻塞部分。所以,(从页面 /UI 的角度来说)浏览器在后台异步处理控制台 I/O 能够提 高性能,这时用户甚至可能根本意识不到其发生

回调函数

  • 可能出现的状况
    • 调用回调过早(在追踪之前);
    • 调用回调过晚(或没有调用);
    • 调用回调的次数太少或太多;
    • 没有把所需的环境 / 参数成功传给你的回调函数;
    • 吞掉可能出现的错误或异常
    • 控制反转(inversion of control),也就是把自己程序一部分的执行控制交给某 个第三方。在你的代码和第三方工具(一组你希望有人维护的东西)之间有一份并没有明 确表达的契约。
  • 第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。 我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。
  • 第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三 方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期

Promise

错误处理

  • 错误处理最自然的形式就是同步的 try..catch 结构。遗憾的是,它只能是同步的,无法用于异步代码模式
  • then中的reject()回调用于处理promise抛出的error,无法捕捉到then中的error。例:
var p = Promise.resolve( 42 );
p.then(
 function fulfilled(msg){
 // 数字没有string函数,所以会抛出错误
 console.log( msg.toLowerCase() );
 },
 function rejected(err){
 // 永远不会到达这里
 }
);

生成器、迭代器

  • 回调表达异步控制流程的两个关键缺陷:
    • 基于回调的异步不符合大脑对任务步骤的规划方式;
    • 由于控制反转,回调并不是可信任或可组合的。
  • 多个迭代器

通过一个迭代器控制生成器的时候,似乎是在控制声明的生成器 函数本身。但有一个细微之处很容易忽略:每次构建一个迭代器,实际上就隐式构建了生 成器的一个实例,通过这个迭代器来控制的是这个生成器实例。

  • 迭代器

迭代器是一个定义良好的接口,用于从生成器中得到一系列值。每次想要从生产者得到下一个值的时候调用 next()。

next() 调用返回一个对象。这个对象有两个属性:done 是一个 boolean 值,标识迭代器的 完成状态;value 中放置迭代值。

  • 生成器

生成器是 ES6 的一个新的函数类型,它并不像普通函数那样总是运行到结束。取而代之 的是,生成器可以在运行当中(完全保持其状态)暂停,并且将来再从暂停的地方恢复 运行。

生成器的关键优点是:生成器内部的代码是以自然的同步 / 顺序方 式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字 yield 的后面。

可以把生成器看作一个值的生产 者,我们通过迭代器接口的 next() 调用一次提取出一个值。

同步错误处理

生成器 yield 暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还 可以同步捕获来自这些异步函数调用的错误

  • 把错误抛到生成器中
function *main() {
 try {
   var text = yield foo( 11, 31 ); 
   console.log( text );
 }
 catch (err) {
   console.error( err );
 }
}
var it = main();
// 这里启动!
it.next(); 
  • 生成器向外抛出错误
function *main() {
 var x = yield "Hello World";
 yield x.toLowerCase(); // 引发一个异常!
}
var it = main();
it.next().value; // Hello World
try {
 it.next( 42 );
}
catch (err) {
 console.error( err ); // TypeError
} 
  • 以捕获通过 throw(..) 抛入生成器的错误
function *main() {
 var x = yield "Hello World";
 // 永远不会到达这里
 console.log( x );
}
var it = main();
it.next();
try {
 // *main()会处理这个错误吗?看看吧!
 it.throw( "Oops" );
}
catch (err) {
 // 不行,没有处理!
 console.error( err ); // Oops
} 

程序性能

Web Worker

在浏览器这样的环境中,很容易提供多个 JavaScript 引擎实例,各自运行在自己 的线程上,这样可以在每个线程上运行不同的程序。程序中每一个这样的独立的多线程 部分被称为一个(Web)Worker。这种类型的并行化被称为任务并行,因为其重点在于把 程序划分为多个块来并发运行。

从 JavaScript 主程序(或另一个 Worker)中,可以这样实例化一个 Worker:

var w1 = new Worker( "some.url.1/mycoolworke…" );

这个 URL 应该指向一个 JavaScript 文件的位置(而不是一个 HTML 页面!),这个文件将 被加载到一个 Worker 中。然后浏览器启动一个独立的线程,让这个文件在这个线程中作 为独立的程序运行。

Worker 之间以及它们和主程序之间,不会共享任何作用域或资源,通过一个基本的事件消息机制相互联系。

以下是如何侦听事件:

w1.addEventListener( "message", function(evt){ // evt.data } );

也可以发送 "message" 事件给这个 Worker:

w1.postMessage( "something cool to say" );

要在创建 Worker 的程序中终止 Worker,可以调用 Worker 对象(就像前面代码中的 w1) 上的 terminate()。突然终止 Worker 线程不会给它任何机会完成它的工作或者清理任何资 源。这就类似于通过关闭浏览器标签页来关闭页面。

如果浏览器中有两个或多个页面(或同一页上的多个 tab !)试图从同一个文件 URL 创 建 Worker,那么最终得到的实际上是完全独立的 Worker。