你不知道的JavaScript中卷-第一二章

36次阅读

共计 9043 个字符,预计需要花费 23 分钟才能阅读完成。

这里的内容是读书笔记,仅供自己学习所用,有欠缺的地方欢迎留言提示。


第一部分 类型和语法

第 1 章 类型
ECMAScript 语言类型包括 Undefined、Null、Boolean、String、Number 和 Object。
类型:对语言引擎和开发人员来说,类型是值得内部特征,它定义了值得行为,以使其区别于其他值。
喜欢强类型(又称静态类型)语言得人也许回认为“类型”一词用在这里不妥。

1.1 类型
强制类型转换是 JavaScript 开发人员最头疼得问题之一。

1.2 内置类型
JavaScript 有七种内置内容(后面跟着 typeof 的类型值):

  • 空置 null     “object”
  • 未定义 undefined     “undefined”
  • 布尔值 boolean     “boolean”
  • 数字 number     “number”
  • 字符串 string    “string”
  • 对象 object     “object”
  • 符号 symbol (ES6 中新增)    “symbol”

除了对象值类,其他统称为“基本类型”,可以用 typeof 运算符来查看值得类型,它返回得是类型的字符串值,有意思的是,这七种类型和它们的字符串值并不一一对应。最特殊的就是 null,但这个 bug 在 JavaScript 中存在了将近二十年,也许永远也不会修复了,因为这牵涉到了太多的 Web 系统,“修复”它会产生更多的 bug,令许多系统无法正常工作。
所以需要使用符合条件来检测 null 值得类型:

let a = null;
(!a && typeof a === 'object'); // true

function 和数组实际上都是 object 的一个“子类型”,所以用 typeof 获取类型值时,返回都是 ”object”。
tip: 判断是否为 null,用 (!a && typeof a === ‘object’) 来判断。

1.3 值和类型
JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。
换个角度来理解就是,JavaScript 不做“类型强制”;也就是说,语言引擎不要求变量总是持有与其初始值同类型的值。
在对变量执行 typeof 操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类型。

typeof typeof 42; // "string"

typeof 42 首先返回字符串 ”number”,然后 typeof “number” 返回 ”string”。

1.3.1 undefined 和 undeclard
变量在未持有值的时候为 undefined。此时 typeof 返回 ”undefined”:
undeclared(未声明),undefined 与 undeclared 是完全不一样的。
已在作用域中声明但还没有赋值的变量,是 undefined 的;相反,还没有在作用域中声明过的变量,是 undeclared 的。

let a;
a; // undefiend
b; // ReferenceError: b is not defined

浏览器对这类情况的处理很让人抓狂,’b is not defined’ 容易让人误以为是 ’b is undefired’。这里再明确一次,’undefined’ 和 ’is not defined’ 是两码事。此时如果浏览器报错成 ’b is not find’ 或者 ’b is not declared’ 会更明确。
更让人抓狂的是 typeof 处理 undeclared 变量的方式,如下:

let a;
typeof a; // "undefined"
typeof b; // "undefined"  而且还没有报错

对于 undeclared(或者 not defined)变量,typeof 照样返回 ”undefined”。请注意虽然 b 是一个 undeclared 变量,但 typeof b 并没有报错。这是因为 typeof 有一个特殊的安全防范机制。
与 undeclared 变量不同,访问不存在的对象属性(设置是在全局对象 window 上)不会产生 ReferenceError 错误。

1.4 小结
JavaScript 有七种内置类型:null、undefined、boolean、number、string、object 和 symbol,可以使用 typeof 来查看。
变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。
很多开发人员将 undefined 和 undeclared 混为一谈,但在 JavaScript 中它们是两码事。undefined 是值的一种。undeclared 则表示变量还没有被声明过。
遗憾的是,JavaScript 却将它们混为一谈,在我们试图访问 ”undeclared” 变量时这样报错:ReferenceError: a is not defined,并且 typeof 对 undefined 和 undeclared 变量都返回 ”undefined”。
然而,通过 typeof 的安全防范机制(阻止报错)来检查 undeclared 变量,有时是个不错的方法。

第 2 章 值
数组(array)、字符串(string)和数字(number)是一个程序最基本的组成部分,但在 JavaScript 中,它们可谓让人喜忧参半。

2.1 数组
和其他强类型语言不同,在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组 (多维数组就是通过这种方式来实现的)。
对数组生命后即可向其中加入值,不需要预先设定大小。
需要注意的是,使用 delete 运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的 length 属性并不会发生变化。
数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内)。

类数组
有时需要将数组(一组通过数字索引的值)转换为真正的数组,这一般通过数组工具函数(如 indexOf(..)、contat(..)、forEach(..)等)来实现。

2.2 字符串
字符串经常被当成字符数组。字符串的内部实现究竟有没有数组炳皓说,但 JavaScript 中的字符串和字符数组并不是一个回事,最多只是看上去相似而已。
字符串和数组的确很相似,它们都是类数组,都有 length 属性以及 indexOf(..)(从 ES5 开始数组支持此方法)和 concat(..)方法。

let a = "foo";
let b = ["f", "o", "o"];
a.length; // 3
b.length; // 3
a.indexOf("o"); // 1
b.indexOf("o"); // 1
let c = a.concat("bar"); // "foobar"
let d = b.concat(["b", "a", "r"]); // ["f", "o", "o", "b", "a", "r"]

a === c; // false
b === d; // false 对象、数组存的是地址

a[1] = "O";
b[1] = "O";
a; // "foo"
b; // ["f", "o", "o"]

JavaScript 中字符串是不可变的,而数组是可变的。并且 a[1]在 JavaScript 中并非总是合法语法,在老版本的 IE 中就不被允许(现在可以了)。正确的方法应该是 a.charAt(1)。
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数在其原始值上进行操作。

c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"

许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过“借用”数组的非变更方法来处理字符串。

a.join; // undefined
a.map; // undefined
let c = Array.prototype.join.call(a, "-");
let d = Array.prototype.map.call(a, function(v) {return v.toUpperCase() + ".";
}).join("");
c; // "f-o-o"
d; // "F.O.O."

另一个不同点在于字符串反转。数组有一个字符串没有的可变更成员函数 reverse():

a.reverse(); // undefined
b.reverse(); // ["o", "o", "f"]

// 可惜我们无法 "借用" 数组的可变更成员函数,因为字符串是不可变的:Array.prototype.reverse.call(a); // 返回值仍然是字符串 "foo" 的一个封装对象

tip: 通过 Array.ptototype.xxxx.call(str)的方式可以让字符串使用数组函数,但是 reverse()不适用。
一个变通(破解)的方法是先将字符串转换为数组,待处理完后再将结果换回字符串:

let c = a
    // 将 a 的值转换为字符串数组
    .split("")
    // 将数组中的字符进行倒转
    .reverse()
    // 将数组中的字符拼接回字符串
    .join("");
c; // "oof"
// 这种方法简单粗暴,但对简单的字符串却完全适用。// 对于包含复杂字符(Unicode,如星号、多字节字符等)的字符串并不适用。

tip: 对于大多数字符串反转,可以用 str.split().reverse().join(”)的方法。

2.3 数字
JavaScript 只有一种数值类型:number(数字),包括“整数”和带小数的十进制数。此处“整数”之所以加引号是因为和其他语言不同,JavaScript 没有真正意义上的整数,这也是它一直依赖为人诟病的地方。
JavaSctipt 中的“整数”就是没有小数的十进制数。所以 42.0 即等同于“整数”42。

2.3.1 数字的语法
JavaScript 中的数字常量一般用十进制表示。例如:

let a = 42;
let b = 42.3;

// 数字前面的 0 可以省略
let a = 0.42;
let b = .42;

// 小数点后小数部分最后面的 0 也可以省略
let a = 42.0;
let b = 42.;

特别大和特别小的数字默认用指数格式显示,与 toExponential()函数的输出结果相同。例如:

let a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10" 这个是字符串

a == a.toExponential(); // true
a === a.toExponential(); // false

由于数字值可以使用 Number 对象进行封装,因此数字值可以调用 Number.prototype 中的方法。例如,toFixed(..)方法可以指定小数部分的显示位数。
toPrecision(..)方法用来指定 有效数位 的显示位数。

let a = 42.59;
a.toFixed(0); // "43"
a.toFixed(1); // "43.6"
a.toPrecision(1); // "4e+1"

a.toFixed(2); // "42.6"
a.toPrecision(2); // "42.59"

a.toFixed(3); // "42.590"
a.toPrecision(3); // "42.6"

a.toPrecision(4); // "42.59"
a.toPrecision(5); // "42.590"

2.3.2 较小的数值
二进制浮点数最大的问题(不仅 JavaScript,所有遵循 IEEE754 规范的语言都是如此),是回出现如下情况:

0.1 + 0.2 === 0.3; // false

从数学角度来说,上面的条件判断应该为 true,可结果为什么是 false 呢?
简单来说,二进制浮点数中的 0.1 和 0.2 并不是十分精确,它们相加的结果并非刚好等于 0.3,而是一个比较接近的数字 0.3000000000000004,所以条件判断结果为 false。
问题是,如果一些数字无法做到完全精确,是否意味着数字类型毫无用处呢?答案当然是否定的。
在处理带有小数的数字时需要特别注意。很多(也许是绝大多数)程序只需要处理整数,对打不超过百万或者万亿,此时使用 JavaScript 的数字类型是绝对安全的。
那么应该怎样来判断 0.1+0.2 和 0.3 是否相等呢?
最常见的方法是设置一个误差范围值,通常称为“机器精度”,对 JavaScript 的数字来说,这个值通常是 2^-52。从 ES6 开始,该值定义在 Number.EPSILON 中,我们可以直接拿来用,也可以在 ES6 之前的版本写 polyfill:

function numbersCloseEnoughToEqual(n1, n2) {return Math.abs(n1 - n2) < Number.EPSILON;
}
let a = 0.1 + 0.2;
let b = 0.3;
numbersCloseEnoughToEqual(a, b); // true

tip: 小数位运算,因浮点存储的原因会造成误差,可以用小于 Number.EPSILON 来判断是否正确。

2.3.3 整数的安全范围
数字的呈现方式决定了“整数”的安全值范围远远小于 Number.MAX_VALUE。
能够被“安全”呈现的最大整数是 2^53 – 1,即 9007199254740991,在 ES6 中被定义为 Number.MAX_SAFE_INTEGER。最小整数时 -9007199254740991,在 ES6 中被定义为 Number.MIN_SAFE_INTEGER。

2.3.4 整数检测
要检测一个值是否是整数,可以使用 ES6 中的 Number.isInteger(..)方法。

Number.isInteger(42); // true
Number.isInteger(42.0); // true
Number.isInteGer(42.3); // false

检测一个值是否是安全的整数,可以使用 ES6 中的 Number.isSafeInteger(..)方法:

Number.isSafeInteger(Math.pow(2, 53)); // false
Number.isSafeInteger(Math.pow(2, 53) - 1); // true

2.3.5 32 位有符号整数
虽然整数最大能够达到 53 位,但是有些数字操作(如整位操作)只适用于 32 位数字,所以这些操作中数字的安全范围就要小很多,变成从 Math.pow(-2, 31)到 Math.pow(2, 31) – 1。
a | 0 可以将变量 a 中的数值转换为 32 位有符号整数,因为整位运算符 | 只适用于 32 位整数(它只关心 32 位以内的值,其他的数位将被忽略)。因此与 0 进行操作即可截取 a 中的 32 位数位。

2.4 特殊数值
JavaScript 数据类型中有几个特殊的值需要开发人员特别注意和小心使用。

2.4.1 不是值的值
undefined 类型只有一个值,即 undefined。null 类型也只有一个值,即 null。它们的名称既是类型也是值。undefined 和 null 常被用来表示“空的”或是“不是值”的值。二至之间有一些细微的差别。例如:

  • null 值空值(empty value)
  • undefined 指没有值(missing value)

或者

  • undefined 指从未赋值
  • null 指曾赋过值,但是目前没有值

null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。

2.4.2 undefined
在非严格模式下,我们可以位全局标识符 undefined 赋值(这样的设计实在是欠考虑!):

function foo() {undefined = 2; // 非常糟糕的做法!}
function foo() {
    "use strict";
    undefined = 2; // TypeError!
}

void 运算符
undefined 是一个内置标识符(除非被重新定义),它的值为 undefined,通过 void 运算符既可得到该值。
表达式 void 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只要让表达式不返回值:

let a = 42;
console.log(void a, a); // undefined 42

按惯例我们用 void 0 来获得 undefined(这主要源自 C 语言,当然使用 void true 或其他 void 表达式也是可以的)。void 0,void 1 和 undefined 之间并没有实质上的区别。

2.4.3 特殊的数字
数字类型中有几个特殊的值。

  1. 不是数字的数字

如果数学运算的操作数不是数字类型,就无法返回一个有效的数字,这种情况下返回值为 NaN。
NaN 意指“不是一个数字”(not a number),这个名字容易引起误会。将它理解为“无效数值”或者“坏数值”可能更准确些。

let a = 2 / "foo"; // NaN
typeof a === "number"; // true

tip:typeof 并不能完全判断是否为数字类型,还包括 NaN。
换句话说,“不是数字的数字”仍然是数字类型。
NaN 是一个“警戒值”(有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果。”
也许有人认为如果要检查变量的值是否为 NaN,可以直接和 NaN 进行比较,就像比较 null 和 undefeind 那样,实则不然。

null === null; // true
undefined = undefined; // true
let a = 2 / "foo";
a == NaN; //false

NaN 是一个特殊值,他和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN 竟然为 true。
tip: NaN 是唯一一个非自反的值。
既然我们无法对 NaN 进行比较(结果永远为 false),那应该怎样来判断它呢?可以使用内建的全局工具函数 isNaN(..)来判断一个值是否是 NaN。

let a = 2 / "foo";
isNaN(a); // true

isNaN(..)有一个严重的缺陷,它的检查方式过于死板,就是“检查参数是否不是 NaN,也不是数字”。这样做的结果并不太准确。

let a = 2 / "foo";
let b = "foo";
window.isNaN(a); // true
window.isNaN(b); // true  ????? 'foo' 不是一个数字,但是它也不是 NaN
// 从 ES6 开始,使用工具函数 Number.isNaN(..)
Number.isNaN(a); // true
Number.isNaN(b); // false 

tip: 判断是否为 NaN,用 Number.isNaN(..)来判断;也可以用非自反来判断。

2. 无穷数
熟悉传统编译型语言(如 C)的开发人员可能都遇到过编译错误(compiler error)或者运行时错误(runtime exception),例如“除以 0”:

let a = 1 / 0;

然而在 JavaScript 中上例的结果为 Infinity(即 Number.POSITIVE_INFINITY)。
如果除法运算中的一个操作数为负数。则结果为 -Infinity(即 Number.NEGATIVE_INFINITY)。

let a = 1 / 0; // Infinity
let b = -1 / 0; // -Infinity
Infinity === Infinity; // true

3. 零值
JavsScript 有一个常规的 0(也叫做 +0)和一个 -0。
加法和减法运算不会得到负零。
tip: JSON.stringify(-0)返回 ”0″,而 JSON.parse(“-0”)返回 -0。

-0 === 0 ; // true emmm, 有待深究

2.4.4 特殊等式
NaN 和 - 0 在相等比较时的表现有些特别。由于 NaN 和自身不相等,所以必须使用 ES6 中的 Number.isNaN(..),而 - 0 等于 0(对于 === 也是如此),因此我们必须使用 isNegZero(..) 这样的工具函数。
ES6 中新加入了一个工具方法 Object.is(..)来判断两个值是否绝对相等,可以用来处理上述所有的特殊情况:

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

tip: 能使用 == 和 === 时就尽量不要使用 Object.is(..),因为前者效率更高、更为通用,后者主要用来处理那些特殊的相等比较。

2.5 值和引用
在许多编程语言中,赋值和参数传递可以通过值复制(value-copy)或者引用复制(reference-copy)来完成,这取决于我们使用什么语法。
JavaScript 引用指向的是值。如果一个值有 10 个引用,这些引用指向的都是同一个值,它们相互之间没有应用 / 指向关系。
JavaScript 对值和引用的赋值 / 传递在语法上没有区别,完全根据值的类型来决定。

let a = 2;
let b = 2; // b 是 a 的值的一个副本
b++;
a; // 2
b; // 3

let c = [1, 2, 3];
let d = c; // d 是 [1, 2, 3] 的一个引用
d.push(4);
c; // [1, 2, 3, 4]
d; // [1, 2, 3, 4]

简单值(即标量基本类型值)总是通过值复制的方式来赋值 / 传递,包括 null、undefined、字符串、数字、布尔和 ES6 中的 symbol。
复合值——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值 / 传递。
由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。

let a = [1, 2, 3];
let b = a;
b = [4, 5, 6]; // 因为赋值,b 改变了自己的引用,并不会改变 a 的引用,所以 b 变了,而 a 没有。a; // [1, 2, 3]
b; // [4, 5, 6]

2.6 小结
JavaScript 中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的行为特征不同,在将字符作为数组来处理时需要特别小心。JavaScript 中的数字包括“整数”和“浮点型”。
基本类型中定义了几个特殊的值。
null 类型只有一个值 null,undefined 类型也只有一个值 undefined。所有变量在赋值之前默认值都是 undefined。void 运算符返回 undefined。
数字类型有几个特殊值,包括 NaN(意指“not a number”,更准确地说是“invalid number”)、+Infinity、–Infinity 和 -0。
简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合值(对象等)通过引用复制来赋值 / 传递。
JavaScript 中的引用和其它语言中的引用 / 指针不同,它们不能指向别的变量 / 引用,只能指向值。

正文完
 0