第1章 类型
- 函数对象的length属性是其声明的参数的个数
- JavaScript中变量是没有类型的,只有值有。变量可以随时持有任何类型的值。
第2章 值
数组
- 使用 delete 运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的 length 属性并不会发生变化。
- 在创建“稀疏”数组(即含有空白或空缺单元的数组)时要特别注意:
var a = [];
a[0] = 1;
// 此处没有设置a[1]单元
a[2] = [ 3 ];
a[1]; // undefined
a.length; // 3
- 数组通过数字进行索引,但它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内):
var a = [];
a[0] = 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2
但是,如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理。
var a = [];
a["13"] = 42;
a.length; // 14
- 类数组:可以通过索引值属性访问元素,并拥有length属性,但不能使用数组的方法。比如DOM查询操作返回的DOM元素列表和arguments对象。可以使用Array.prototype.slice.call或Array.from来将伪数组转换为数组。
字符串
- 许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过“借用”(使用call方法)数组的非变更方法(如join和map)来处理字符串。可惜我们无法“借用”数组的可变更成员函数(如reverse)。翻转字符串时,可以使用str.split("").reverse().join(""),前提是str不能包含复杂字符(如多字节字符等)。
数字
- JavaScript 中的“整数”就是没有小数的十进制数。所以 42.0 即等同于“整数” 42。
- 特别大和特别小的数字默认用指数格式显示,与 toExponential() 函数的输出结果相同。
var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11
- toFixed方法可以指定小数部分的显示位数。toFixed返回值是字符串类型的。
- toPrecision用来指定有效数位的显示位数。
- 上面的方法不仅适用于数字变量,也适用于数字常量。不过对于 . 运算符需要给予特别注意,因为它是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。
// 无效语法:
42.toFixed( 3 ); // SyntaxError
// 下面的语法都有效:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42 .toFixed(3); // "42.000" (注意空格)
- 0x、0o、0b开头的数字分别表示十六进制、八进制、二进制。
- 0.1 + 0.2 === 0.3为false,遵循IEEE 754标准的语言都存在类似问题。解决方案是设置一个机器精度(即误差范围值)。从ES6开始,该值定义在Number.EPSILON中。
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
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.MAX_VALUE为1.798e+308,最小浮点数Number.MIN_VALUE为5e-324。
- 数字的呈现方式决定了“整数”的安全值范围远远小于 Number.MAX_VALUE。能够被“安全”呈现的最大整数是 2^53 - 1 ,即 9007199254740991 ,在 ES6 中被定义为Number.MAX_SAFE_INTEGER 。最小整数是-9007199254740991 ,在 ES6 中被定义为 Number.MIN_SAFE_INTEGER 。
- Number.isInteger检查是否是整数。
Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false
// polyfill
if (!Number.isInteger) {
Number.isInteger = function(num) {
return typeof num == "number" && num % 1 == 0;
};
}
Number.isSafeInteger检查是否是安全的整数。
Number.isSafeInteger( Number.MAX_SAFE_INTEGER ); // true
Number.isSafeInteger( Math.pow( 2, 53 ) ); // false
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 ); // true
// polyfill
if (!Number.isSafeInteger) {
Number.isSafeInteger = function(num) {
return Number.isInteger( num ) &&
Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
};
}
- 不要使用window身上的isNaN,要使用Number.isNaN:
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; "foo"
window.isNaN( a ); // true
window.isNaN( b ); // true——晕!
很明显 "foo" 不是一个数字,但是它也不是 NaN 。这个 bug 自 JavaScript 问世以来就一直存在。从 ES6 开始我们可以使用工具函数 Number.isNaN:
Number.isNaN( a ); // true
Number.isNaN( b ); // false——好!
- 在JavaScript中,1 / 0为Infinity,-1 / 0为-Infinity。
- 在JavaScript中,我们无法决定是值复制还是引用复制,一切由值的类型来决定。
第3章 原生函数
- 如果想要自行封装基本类型值,也可以使用Object()函数。
var a = "abc";
var b = new String( a );
var c = Object( a );
typeof a; // "string"
typeof b; // "object"
typeof c; // "object"
b instanceof String; // true
c instanceof String; // true
Object.prototype.toString.call( b ); // "[object String]"
Object.prototype.toString.call( c ); // "[object String]"
- 如果想要拆封对象中的基本类型的值,可以使用valueOf()方法。
var a = new String( "abc" );
var b = new Number( 42 );
var c = new Boolean( true );
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
- 我们可以创建包含空单元的数组,比如将length属性设置为超过实际单元数的值,或者通过delete删除数组中的某个元素。
- 我们可以通过下述方式来创建包含undefined单元(而非空单元)的数组:
let arr = Array.apply(null, { length: 3 });
a; // [ undefined, undefined, undefined ];
- 如果需要动态定义正则表达式,则可以使用RegExp函数。
var name = "Kyle";
var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" );
var matches = someText.match( namePattern );
- 有些原生原型并非普通对象那么简单。Function.prototype是一个函数,Array.prototype是一个数组,RegExp.prototype是一个正则表达式。
typeof Function.prototype // 'function'
Array.isArray(Array.prototype) // true
RegExp.prototype.toString() // "/(?:)/" —— 空正则表达式
第4章 强制类型转换
- 如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
- 所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..) 字符串化。安全的JSON 值是指能够呈现为有效 JSON 格式的值。常见的不安全的JSON 值: undefined 、 function 、 symbol(ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的对象。
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}"
如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回值来进行序列化。
- 我们可以向 JSON.stringify(..) 传递一个可选参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。如果 replacer 是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此之外其他的属性则被忽略。如果 replacer 是一个函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数,键和值。如果要忽略某个键就返回 undefined ,否则返回指定的值。
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
JSON.stringify( a, function(k,v){
if (k !== "c") return v;
});
// "{"b":42,"d":[1,2,3]}"
如果 replacer 是函数,它的参数 k 在第一次调用时为 undefined (就是对对象本身调用的那次)。 if 语句将属性 "c" 排除掉。由于字符串化是递归的,因此数组 [1,2,3] 中的每个元素都会通过参数 v 传递给 replacer,即 1 、 2 和 3 ,参数 k 是它们的索引值,即 0 、 1 和 2 。
JSON.string 还有一个可选参数 space,用来指定输出的缩进格式。space 为正整数时是指定每一级缩进的字符数,它还可以是字符串,此时最前面的十个字符被用于每一级的缩进:
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, null, 3 );
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }"
JSON.stringify( a, null, "-----" );
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"
- JSON.stringify(..) 并不是强制类型转换,但其具有以下表现:
- 字符串、数字、布尔值和 null 的 JSON.stringify(..) 规则与 ToString 基本相同。
- 如果传递给 JSON.stringify(..) 的对象中定义了 toJSON() 方法,那么该方法会在字符串化前调用,以便将对象转换为安全的 JSON 值。
- 对于ToNumber这一抽象操作:
- true转换为1,false转换为0,undefined转换为NaN,null转换为0
- 对字符串的处理基本遵循数字常量的相关规则/语法。
- 对象会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其转换为数字。
- 将对象转换为基本类型的值:将对象转换为基本类型的值,涉及到的抽象操作是ToPrimitive。如果有valueOf,则调用该方法;没有的话调用toString方法。
- JavaScript中的值可以分为以下两类:
- 假值:可以被强制类型转换为false的值。
- 真值:其他(被强制类型转换为true的值)。
JavaScript定义了一小撮假值: - undefined
- null
- false
- +0, -0, NaN
- ""(字符串中唯一一个假值)
假值对象:浏览器在JavaScript基础上定义了一些宿主对象,比如document.all,将它们强制类型为布尔值的结果为false。Chrome中Boolean(document.all)为false。但IE中Boolean(document.all)为true,有时用此来判断宿主环境是否是IE。
if (document.all) { /* It's IE. */ }
- 示例代码
-'1' // -1
- -'1' // 1
- ~x大致等同于-(x+1),因此可以使用if (~str.indexOf(char))代替if (str.indexOf(char) !== -1)。
- 《你不知道的JavaScript(中卷)》将字符串和数字之间的类型转换分为了两节:4.3.1 字符串和数字之间的显示转换、4.3.2 显式解析数字字符串。4.3.1中讲解了Number等函数的用法,4.3.2中讲解了parseInt和parseFloat的用法。这样的讲解顺序是我见过的最合理的。它并没有笼统地把Number、parseInt、parseFloat放在一起讲,而是通过小标题的形式分成了两节,这样我就能牢牢记住——Number和parseInt、parseFloat之间是存在一定差别的。在此之前,我很容易把Number('42px')的结果也误记为42,我觉得就是因为知识点的学习和总结方式不恰当。其实Number('42px')为NaN,而parseInt('42px')的结果才是42。强制类型转换和解析数字字符串是有区别的——解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止解析;而转换不允许出现非数字字符,否则会失败并返回NaN。
- parseInt传递第二个参数的含义是什么?在此之前我一直记不清。其实就是指定按哪种进制解析第一个参数。第二个参数其实是ES5中加入的特性。因为在ES5之前,parseInt会根据字符串的第一个字符来指定转换的基数,如果第一个字符是X或x,则基数为16;如果第一个字符是0,则基数为8。这导致了很多问题,比如我处理时分秒时,parseInt('09')会被错误地转换成0(因为9并非合法的八进制数)。因此,从ES5开始,parseInt采用的默认基数是10,并且新增了第二个参数。
- 其次,parseInt的第一个参数如果不是字符串,会先转换为字符串。这也就不难理解,为什么parseInt(1 / 0, 19);为18了。因为1/0为Infinity,相当于parseInt('Infinity', 19)。在19进制中,I表示18,而n为非法字符,因此解析到n时便停止解析(n不包含在解析结果内)。结果为18。
- a + ""(隐式)和前面的 String(a)(显式)之间有一个细微的差别需要注意。根据ToPrimitive 抽象操作规则,a + "" 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()。
第5章 语法
- ES6中定义了一个新概念,叫做暂时性死区。指的是由于代码中的变量还没有初始化而不能被引用的情况。比如在let a;之前访问a变量。注意:对未声明的变量使用typeof不会产生错误,但在暂时性死区中却会。
附录A 混合环境JavaScript
- polyfill能够有效地为不符合最新规范的老版本浏览器填补缺失的功能。
- 使用script标签引入或编写js代码,如果script中的代码发生错误,它会像独立的js程序那样停止,但是后续的script中的代码依然会接着执行,不受影响。
- 保留字有四类:关键字、预留关键字、null常量和true/false布尔值。像function和switch都是关键字,预留关键字包括enum等,它们中很多已经在ES6中被用到(如class、extend等)。另外还有一些在严格模式中使用的保留字,如interface。