前言
本文作为对本书的一些知识点的收集
正文
1. 类型
ECMAScript 语言类型包括 Undefined、Null、Boolean、String、Number 和 Object。
- 内置类型
- JavaScript 有七种内置类型:
• 空值(null)
• 未定义(undefined)
• 布尔值( boolean)
• 数字(number)
• 字符串(string)
• 对象(object)
• 符号(symbol,ES6 中新增)除对象
object
之外,其他统称为“基本类型”。
- 小结
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. 值
- 数组
和其他强类型语言不同,在
JavaScript
中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的):
使用
delete
运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的length
属性并不会发生变化。
- 在创建“稀疏”数组(sparse array,即含有空白或空缺单元的数组)时要特别注意:
var a = [ ];
a[0] = 1;
// 此处没有设置a[1]单元
a[2] = [ 3 ];
a[1]; // undefined
a.length; // 3
上面的代码可以正常运行,但其中的“空白单元”(empty slot)可能会导致出人意料的结果。a[1] 的值为
undefined
,但这与将其显式赋值为undefined
(a[1] = undefined
)还是有所区别。
- 数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内):
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
在数组中加入字符串键值 / 属性并不是一个好主意。建议使用对象来存放键值 / 属性值,用数组来存放数字索引值。
- 类数组
- 有时需要将类数组(一组通过数字索引的值)转换为真正的数组
function foo() {
var arr = Array.prototype.slice.call( arguments );
arr.push( "bam" );
console.log( arr );
}
foo( "bar", "baz" ); // ["bar","baz","bam"]
如上所示,
slice()
返回参数列表(上例中是一个类数组)的一个数组复本。
- 用
ES6
中的内置工具函数Array.from(..)
也能实现同样的功能:
var arr = Array.from( arguments );
Array.from(..)
还有一些其他非常强大的功能
- 数字
- 应该怎样来判断
0.1 + 0.2
和0.3
是否相等呢?
最常见的方法是设置一个误差范围值,通常称为“机器精度”(machine epsilon),对
JavaScript
的数字来说,这个值通常是2^-52 (2.220446049250313e-16)
。
从
ES6
开始,该值定义在Number.EPSILON
中,我们可以直接拿来用,也可以为 ES6 之前的版本写polyfill
:
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
可以使用 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 !
- 整数检测
要检测一个值是否是整数,可以使用
ES6
中的Number.isInteger(..)
方法:
Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false
也可以为
ES6
之前的版本polyfill
Number.isInteger(..)
方法:
if (!Number.isInteger) {
Number.isInteger = function(num) {
return typeof num == "number" && num % 1 == 0;
};
}
要检测一个值是否是安全的整数,可以使用
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
可以为
ES6
之前的版本polyfill
Number.isSafeInteger(..)
方法:
if (!Number.isSafeInteger) {
Number.isSafeInteger = function(num) {
return Number.isInteger( num ) && Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
};
}
- 不是值的值
undefined
类型只有一个值,即undefined
。null
类型也只有一个值,即null
。它们的名称既是类型也是值。
undefined
和null
常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:
null
指空值(empty value)
undefined
指没有值(missing value)
或者:
undefined
指从未赋值
null
指曾赋过值,但是目前没有值
null
是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而
undefined
却是一个标识符,可以被当作变量来使用和赋值。
- 特殊的数字 -
NaN
NaN
是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive
,即x === x
不成立)的值。
判断一个值是否是
NaN
,从ES6
开始我们可以使用工具函数Number.isNaN(..)
。ES6
之前的浏览器的polyfill
如下:
if (!Number.isNaN) {
Number.isNaN = function(n) {
return (
typeof n === "number" && window.isNaN( n )
);
};
}
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b ); // false——好!
- 特殊等式
如前所述,
NaN
和-0
在相等比较时的表现有些特别。由于NaN
和自身不相等,所以必须使用ES6
中的Number.isNaN(..)
(或者polyfill
)。而-0
等于0
(对于===
也是如此,参见第4 章),因此我们必须使用isNegZero(..)
这样的工具函数。
ES6
中新加入了一个工具方法Object.is(..)
来判断两个值是否绝对相等,可以用来处理上述所有的特殊情况:
var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false
对于
ES6
之前的版本,Object.is(..)
有一个简单的polyfill
:
if (!Object.is) {
Object.is = function(v1, v2) {
// 判断是否是-0
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// 判断是否是NaN
if (v1 !== v1) {
return v2 !== v2;
}
// 其他情况
return v1 === v2;
};
}
能使用
==
和===
(参见第 4 章)时就尽量不要使用Object.is(..)
,因为前者效率更高、更为通用。Object.is(..)
主要用来处理那些特殊的相等比较。
- 值和引用
JavaScript
中没有指针,引用的工作机制也不尽相同。在JavaScript
中变量不可能成为指向另一个变量的引用。
JavaScript
引用指向的是值。如果一个值有 10 个引用,这些引用指向的都是同一个值,它们相互之间没有引用 / 指向关系。
JavaScript
对值和引用的赋值 / 传递在语法上没有区别,完全根据值的类型来决定。
简单值(即标量基本类型值,
scalar primitive
)总是通过值复制的方式来赋值 / 传递,包括null
、undefined
、字符串、数字、布尔和ES6
中的symbol
。
复合值(
compound value
)——对象(包括数组和封装对象,参见第 3 章)和函数,则总是通过引用复制的方式来赋值 / 传递。
- 函数参数相关的代码示例
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// 然后
x = [4,5,6];
x.push( 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[1,2,3,4],不是[4,5,6,7]
我们向函数传递
a
的时候,实际是将引用a
的一个复本赋值给x
,而a
仍然指向[1,2,3]
。
在函数中我们可以通过引用x
来更改数组的值(push(4)
之后变为[1,2,3,4])
。但x = [4,5,6]
并不影响a
的指向,所以a
仍然指向[1,2,3,4]
。
我们不能通过引用x
来更改引用a
的指向,只能更改a
和x
共同指向的值。
- 如果要将
a
的值变为[4,5,6,7]
,必须更改x
指向的数组,而不是为x
赋值一个新的数组。
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// 然后
x.length = 0; // 清空数组
x.push( 4, 5, 6, 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[4,5,6,7],不是[1,2,3,4]
从上例可以看出,
x.length = 0
和x.push(4,5,6,7)
并没有创建一个新的数组,而是更改了当前的数组。于是a
指向的值变成了[4,5,6,7]
。
请记住:我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。
- 小结
JavaScript
中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的行为特征不同,在将字符作为数组来处理时需要特别小心。
JavaScript
中的数字包括“整数”和“浮点型”。
基本类型中定义了几个特殊的值。
null
类型只有一个值null
,undefined
类型也只有一个值undefined
。所有变量在赋值之前默认值都是undefined
。void
运算符返回undefined
。
数 字 类 型 有 几 个 特 殊 值, 包 括NaN
( 意 指 “not a number”, 更 确 切 地 说 是 “invalid number”)、+Infinity
、-Infinity
和-0
。
简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合值(对象等)通过引用复制来赋值 / 传递。
JavaScript
中的引用和其他语言中的引用 / 指针不同,它们不能指向别的变量 / 引用,只能指向值。
3. 原生函数
JavaScript
的内建函数(built-in function
),也叫原生函数(native function
),如String
和Number
。
- 常用的原生函数有:
String()
Number()
Boolean()
Array()
Object()
Function()
RegExp()
Date()
Error()
-
Symbol()
——ES6
中新加入的!
实际上,它们就是内建函数。
- 代码示例
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"
通过构造函数(如
new String("abc")
)创建出来的是封装了基本类型值(如 "abc")的封装对象。
请注意:typeof
在这里返回的是对象(String
)类型的子类型(object
)。
- 将原型作为默认值
Function.prototype
是一个空函数,RegExp.prototype
是一个“空”的正则表达式(无任何匹配),而Array.prototype
是一个空数组。对未赋值的变量来说,它们是很好的默认值。
function isThisCool(vals,fn,rx) {
vals = vals || Array.prototype;
fn = fn || Function.prototype;
rx = rx || RegExp.prototype;
return rx.test(
vals.map( fn ).join( "" )
);
}
isThisCool(); // true
isThisCool(
["a","b","c"],
function(v){ return v.toUpperCase(); },
/D/
); // false
这种方法的一个好处是
.prototypes
已被创建并且仅创建一次。相反,如果将[]
、
function(){}
和/(?:)/
作为默认值,则每次调用 isThisCool(..) 时它们都会被创建一次(具体创建与否取决于JavaScript
引擎,稍后它们可能会被垃圾回收),这样无疑会造成内存和 CPU 资源的浪费。
另外需要注意的一点是,如果默认值随后会被更改,那就不要使用Array.prototype
。上例中的vals
是作为只读变量来使用,更改vals
实际上就是更改Array.prototype
,而这样会导致前面提到过的一系列问题!
- 小结
JavaScript
为基本数据类型值提供了封装对象,称为原生函数(如String
、Number
、Boolean
等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String.prototype.trim()
和Array.prototype.concat(..)
)。
对于简单标量基本类型值,比如 "abc",如果要访问它的
length
属性或String.prototype
方法,JavaScript
引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。
4. 强制类型转换
- 值类型转换
也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在、动态类型语言的运行时(
runtime
)。
然而在
JavaScript
中通常将它们统称为强制类型转换,我个人则倾向于用“隐式强制类型转换”(implicit coercion
)和“显式强制类型转换”(explicit coercion
)来区分。
二者的区别显而易见:我们能够从代码中看出哪些地方是显式强制类型转换,而隐式强制类型转换则不那么明显,通常是某些操作产生的副作用。
var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换
对变量
b
而言,强制类型转换是隐式的;由于+
运算符的其中一个操作数是字符串,所以是字符串拼接操作,结果是数字42
被强制类型转换为相应的字符串"42"
。
而String(..)
则是将a
显式强制类型转换为字符串。
两者都是将数字42
转换为字符串"42"
。然而它们各自不同的处理方式成为了争论的焦点。
- 假值列表
- undefined
- null
- false
- +0、-0 和 NaN
- ""
- 日期显式转换为数字
一元运算符
+
的另一个常见用途是将日期(Date
)对象强制类型转换为数字,返回结果为Unix
时间戳,以微秒为单位(从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间):
var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
+d; // 1408369986000
我们常用下面的方法来获得当前的时间戳,例如:
var timestamp = +new Date();