03-你不知道的js中册

第一部分

1 typeof 有一个特殊的安全防范机制

要为某个缺失的功能写 polyfill,如何避免重复声明变量?

  1. 使用 typeof 判断
1
2
3
if (typeof atob === "undefined") {
atob = function() { /*..*/ };
}

如果要为某个缺失的功能写polyfill(即衬垫代码或者补充代码,用来补充当前运行环境中缺失的功能),一般不会用 var atob 来声明变量 atob。如果在 if 语句中使用 var atob,声明会被提升(hoisted,参见《你不知道的
JavaScript(上卷)》1中的“作用域和闭包”部分)到作用域(即当前脚本或函数的作用域)的最顶层,即使 if 条件不成立也是如此(因为 atob 全局变量已经存在)。在有些浏览器中,对于一些特殊的内建全局变量(通常称为“宿主对象”,host object),这样的重复声明会报错。去掉 var 则可以防止声明被提升

  1. 检查所有全局变量是否是全局对象的属性,浏览器中的全局对象是 window。
1
2
3
4
5
6
if (window.DEBUG) {
// ..
}
if (!window.atob) {
// ..
}
  1. 还有一些人喜欢使用“依赖注入”(dependency injection)设计模式,就是将依赖通过参数显式地传递到函数中,
1
2
3
4
5
6
function doSomethingCool(FeatureXYZ) {
var helper = FeatureXYZ ||
function() { /*.. default feature ..*/ };
var val = helper();
// ..
}

2 值

2.1 怎样来判断 0.1 + 0.2 和 0.3 是否相等?

最常见的方法是设置一个误差范围值,通常称为“机器精度”(machine epsilon),对 JavaScript 的数字来说,这个值通常是 2^-52 (2.220446049250313e-16)。

1
2
3
4
5
6
7
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

Number.EPSILON 为ES6 新增

2.2 整数检测

可以使用 ES6 中的 Number.isInteger(..) 方法

2.3 按惯例我们用 void 0 来获得 undefined

虽然undefined在现在主流的绝大部分浏览器的全局作用域上面都是不能更改了,但是在函数作用域或者块级作用域下面还是能被重写的,当然绝大部分人应该都不会去干这种傻事,但是还是用viod 0吧,这样可以以防万一,同时也更像一个老司机的代码啊

掘金-JS中的null和undefined,undefined为啥用void 0代替?

2.4 很多 JavaScript 程序都可能存在 NaN 方面的问题,所以我们应该尽量使用 Number.isNaN(..)这样可靠的方法,无论是系统内置还是 polyfill。

2.5 特殊等式

ES6 中新加入了一个工具方法 Object.is(..)来判断两个值是否绝对相等,可以用来处理上述所有的特殊情况:

1
2
3
4
5
var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false

2.6 值和引用

简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值 / 传递,包括
null、undefined、字符串、数字、布尔和 ES6 中的 symbol。

复合值(compound value)——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值 / 传递。

3 原生函数

3.1

1
2
3
4
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"

通过构造函数(如 new String(“abc”))创建出来的是封装了基本类型值(如 “abc”)的封装对象。

封 装 对 象(object wrapper) 扮 演 着 十 分 重 要 的 角 色。 由 于 基 本 类 型 值 没 有.length和 .toString()这样的属性和方法,需要通过封装对象才能访问,此时 JavaScript 会自动为基本类型值包装(box 或者 wrap)一个封装对象:

1
2
3
var a = "abc";
a.length; // 3
a.toUpperCase(); // "AB

一般情况下,我们不需要直接使用封装对象。最好的办法是让 JavaScript引擎自己决定什么时候应该使用封装对象。换句话说,就是应该优先考虑使用 “abc” 和 42 这样的基本类型值,而非 new String(“abc”) 和 newNumber(42)。

3.2 内部属性 [[Class]]

所有 typeof 返回值为 “object”的对象(如数组)都包含一个内部属性[[Class]](我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问,一般通过 Object.prototype.toString(..) 来查看。

1
2
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"

4 强制类型转换

将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐式的情况称为强制类型转换(coercion)。

也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)

1
2
3
var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换

4.1 JSON.stringify

JSON.stringify(..) 在对象中遇到undefined、function 和 symbol时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。

1
2
3
4
5
6
7
8
JSON.stringify( undefined ); // undefined
JSON.stringify( function(){} ); // undefined
JSON.stringify(
[1,undefined,function(){},4]
); // "[1,null,null,4]"
JSON.stringify(
{ a:2, b:function(){} }
); // "{"a":2}

4.2 ~ 和 ~~ 的作用 (了解)

~x 大致等同于 -(x+1)。

1
~42; // -(42+1) ==> -43

~~x 能将值截除为一个 32 位整数,x | 0 也可以,而且看起来还更简洁

4.3 parseInt

ES5 之前的 parseInt(..) 有一个坑导致了很多 bug。即如果没有第二个参数来指定转换的
基数(又称为 radix),parseInt(..) 会根据字符串的第一个字符来自行决定基数

将第二个参数设置为 10,即可避免这个问题:

从 ES5 开始 parseInt(..) 默认转换为十进制数,除非另外指定。如果你的代码需要在 ES5
之前的环境运行,请记得将第二个参数设置为 10

4.4 a + “”(隐式)和前面的 String(a)(显式)之间有一个细微的差别

1
2
3
4
5
6
var a = {
valueOf: function() { return 42; },
toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4

根据ToPrimitive 抽象操作规则,a + “” 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象
操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()

5 语法

5.1 语句的结果值

如果你用开发控制台(或者 JavaScript REPL——read/evaluate/print/loop 工具)调试过代
码,应该会看到很多语句的返回值显示为 undefined,只是你可能从未探究过其中的原因。
其实控制台中显示的就是语句的结果值。

代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值;

5.2 上下文规则

1 标签

但 foo: bar() 这样奇怪的语法:叫作“标签语句”。而 JavaScript 通过标签跳转能够实现 goto的部分功能。continue 和 break 语句都可以带一个标签,因此能够像 goto 那样进行跳转。

5.3 运算符优先级

1 && 运算符先于 || 执行

false && true || true 的执行顺序如下:

1
2
false && true || true; // true
(false && true) || true; // true

&& 先执行,然后是 ||。

&& 运算符先于 || 执行

2 短路

1
2
3
4
5
function doSomething(opts) {
if (opts && opts.cool) {
// ..
}
}
1
2
3
4
5
function doSomething(opts) {
if (opts.cache || primeCache()) {
// ..
}
}

3 && 运算符的优先级高于 ||,而 || 的优先级又高于 ? :。

1
2
3
a && b || c ? c || b ? a : c && b : a

(a && b || c) ? (c || b) ? a : (c && b) : a

4 关联

(1)

a ? b : c ? d : e;

它的组合顺序是

1
a ? b : (c ? d : e)。

(2)

1
2
var a, b, c;
a = b = c = 42;

它首先执行 c = 42,然后是 b = ..,最后是 a = ..。因为是右关联,所以它实际上是这样
来处理的:a = (b = (c = 42))。

(3)

1
2
3
4
5
var a = 42;
var b = "foo";
var c = false;
var d = a && b || c ? c || b ? a : c && b : a;
d; // 42

分解如下

1
((a && b) || c) ? ((c || b) ? a : (c && b)) : a

第二部分

1 省点回调

为了更优雅地处理错误,有些 API设计提供了分离回调(一个用于成功通知,
一个用于出错通知)

1
2
3
4
5
6
7
function success(data) { 
console.log( data );
}
function failure(err) {
console.error( err );
}
ajax( "http://some.url.1", success, failure );

2 Node 风格

还有一种常见的回调模式叫作“error-first 风格”(有时候也称为“Node 风格”,因为几乎
所有 Node.js API都采用这种风格),其中回调的第一个参数保留用作错误对象(如果有的话)。如果成功的话,这个参数就会被清空 / 置假(后续的参数就是成功数据)。如果产生了错误结果,那么第一个参数就会被置起 / 置真(通常就不会再传递其他结果):

1
2
3
4
5
6
7
8
9
10
11
function response(err,data) { 
// 出错?
if (err) {
console.error( err );
}
// 否则认为成功
else {
console.log( data );
}
}
ajax( "http://some.url.1", response );

promise

实现 JavaScript 异步方法 Promise.all
从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理