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

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


第一部分 类型和语法

第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中的引用和其它语言中的引用/指针不同,它们不能指向别的变量/引用,只能指向值。

相关推荐