# 第一部分 类型和语法
# 第一章 类型
ECMAScript类型细分为语言类型和规范类型
ECMAScript语言类型包括Undefined、Null、Boolean、String、Number、Object
。
# 内置类型
JavaScript有七种内置类型:
- 空值(
null
) - 未定义(
undefined
) - 布尔值(
boolean
) - 数字(
number
) - 字符串(
string
) - 对象(
object
) - 符号(
symbol
,ES6中新增)
除对象之外,其他统称为“基本类型”
typeof undefined === 'undefined'; // true
typeof true === 'boolean'; // true
typeof 42 === 'number'; // true
typeof '42' === 'string'; // true
typeof {life: 2} === 'object'; // true
typeof Symbol() === 'symbol'; // true
2
3
4
5
6
null
类型比较特殊
typeof null === 'object'; // true
正确的返回结果应该是'null'
,但这个bug由来已久,在JavaScript中已经存在了将近二十年,也许永远也不会修复,因为这牵涉到太多的Web系统,“修复”它会产生更多的bug,令许多系统无法正常工作。
使用复合条件来检测null
值的类型
var a = null;
(!a && typeof a === 'object'); // true
2
typeof function a() {/** */} === 'function'; // true
function
(函数)也是JavaScript的一个内置类型。查阅规范,它实际上是object
的一个“子类型”。具体来说哦,函数是“可调用对象”,它有一个内部属性[[Call]]
,该属性使其可以被调用。
函数对象的length
属性是其声明的参数的个数:
function a(b, c) {
/** */
}
a.length; // 2
2
3
4
typeof [1,2,3] === "object"; // true
数组也是object
的子类型
# 值和类型
JavaScript中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。
JavaScript不做“类型强制”;语言引擎不要求变量总是持有与其初始值同类型的值。一个变量可以现在被赋值为字符串类型值,随后又被赋值为数字类型值。
# undefined和undclared
变量在未持有值的时候为undefined
。此时typeof
返回“undefined
”。
var a;
typeof a; // "undefined"
var b = 42;
var c;
// later
b = c;
typeof b; // "undefined"
typeof c; // "undefined"
2
3
4
5
6
7
8
9
10
11
12
已在作用域中声明还没有赋值的变量,是undefined
的。相反,还没有在作用域中声明过的变量,是undeclared
的。
var a;
a; // undefined
b; // ReferenceError: b is not defined
2
3
4
“undefined
”和“is not defined
”是两码事。此时如果浏览器报错成“b is not found
”或者“b is not declared
”会更准确。
var a;
typeof a; // "undefined"
typeof b; // "undefined"
2
3
对于undeclared
(或者not defined
)变量,typeof
照样返回'undefined
'。虽然b
是一个undeclared
变量,但typeof b
并没有报错。这是因为typeof
有一个特殊的安全防范机制。
此时typeof
如果能返回undeclared
(而非undefined
)的话,情况会好很多。
# typeof Undeclared
typeof
的安全防范机制如下:
// 这样会抛出错误
if (DEBUG) {
console.log( "Debugging is starting" );
}
// 这样是安全的
if (typeof DEBUG ! == "undefined") {
console.log( "Debugging is starting" );
}
2
3
4
5
6
7
8
9
这不仅对用户定义的变量(比如DEBUG
)有用,对内建的API也有帮助:
if (typeof atob === "undefined") {
atob = function() { /*..*/ };
}
2
3
另一种安全防范机制是通过浏览器中的全局对象window
来处理
if (window.DEBUG) {
// ..
}
if (!window.atob) {
// ..
}
2
3
4
5
6
7
通过window
来访问全局对象有一定的局限性,尤其当代码需要运行在多种JavaScript环境中(比如:浏览器、服务器端或其他JavaScript运行平台),因为此时全局对象并非总是window
。
# 第二章 值
# 数组
在JavaScript中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object
),甚至是其他数组(多维数组就是通过这种方式来实现的)
var a = [ 1, "2", [3] ];
a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true
2
3
4
5
对数组声明后即可向其中加入值,不需要预先设定大小
var a = [ ];
a.length; // 0
a[0] = 1;
a[1] = "2";
a[2] = [ 3 ];
a.length; // 3
2
3
4
5
6
7
8
9
使用delete
运算符可以将单元从数组中删除,通过delete
运算法删除单元后,数组的length
属性并不会发生变化。
数组通过数字进行索引,但它们也是对象,所以可以包含字符串键值和属性(但这些并不计算在数组长度内):
var a = [ ];
a[0] = 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2
2
3
4
5
6
7
8
如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当做数字索引来处理。
var a = [ ];
a["13"] = 42;
a.length; // 14
2
3
4
5
工具函数slice()
经常用于类数组转换
function foo() {
var arr = Array.prototype.slice.call( arguments );
arr.push( "bam" );
console.log( arr );
}
foo( "bar", "baz" ); // ["bar", "baz", "bam"]
2
3
4
5
6
7
在ES6中的内置工具函数Array.from()
也能实现同样的功能
var arr = Array.from( arguments );
# 字符串
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。
var a = 'foo';
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
b.push( "! " );
b; // ["f", "O", "o", "! "]
2
3
4
5
6
7
8
许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过“借用”数组的非变更方法来处理字符串
var a = 'foo';
a.join; // undefined
a.map; // undefined
var c = Array.prototype.join.call(a, "-" );
var d = Array.prototype.map.call(a, function(v){
return v.toUpperCase() + ".";
}).join( "" );
c; // "f-o-o"
d; // "F.O.O."
2
3
4
5
6
7
8
9
10
11
常见面试题:字符串反转
var a = 'foo!';
var b = ['f', 'o', 'o', '!']
a.reverse; // undefined
b.reverse(); // ["! ", "o", "O", "f"]
b; // ["f", "O", "o", "! "]
2
3
4
5
6
我们无法“借用”数组的可变更成员函数,因为字符串是不可变的:
Array.prototype.reverse.call( a );
// 返回值仍然是字符串"foo"的一个封装对象
2
变通的办法是先将字符串转换为数组,待处理完后再将结果换回字符串:
var c = a
// 将a的值转换为字符数组
.split("")
// 将数组中的字符进行倒转
.reverse()
// 将数组中的字符拼接回字符串
.join("");
c; // "!oof"
2
3
4
5
6
7
8
9
上述方法对于包含复杂字符串(Unicode,如星号、多字节字符等)的字符串并不适用。这时则需需要功能更加完备、能够处理Unicode的工具库。可以参考Mathias Bynen的Esrever(https://github.com/mathiasbynents/esrever)。
# 数字
特别大和特别小的数字默认用指数格式显示,与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
2
3
4
5
6
7
8
9
toFixed()
方法可以指定小数部分的显示位数:
var a = 42.59;
a.toFixed( 0 ); // "43"
a.toFixed( 1 ); // "42.6"
a.toFixed( 2 ); // "42.59"
a.toFixed( 3 ); // "42.590"
a.toFixed( 4 ); // "42.5900"
2
3
4
5
6
7
例子中的输出结果实际上是给定数字的字符串形式,如果指定的小数部分的显示位数多余实际位数就用0补齐。
toPrecision()
方法用来指定有效位数的显示位数:
var a = 42.59;
a.toPrecision(1); // "4e+1"
a.toPrecision(2); // "43"
a.toPrecision(3); // "42.6"
a.toPrecision(4); // "42.59"
a.toPrecision(5); // "42.590"
a.toPrecision(6); // "42.5900"
2
3
4
5
6
7
8
上面的方法不仅适用于数字变量,也适用于数字字面量。不过对于.
运算符需需要给予特别注意,因为她是一个有效的数字字符,会被优先识别为数字字面量的一部分,然后才是对象属性访问运算法。
// 无效语法:
42.toFixed( 3 ); // SyntaxError
// 下面的语法都有效:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
2
3
4
5
6
7
42.toFixed(3)
是无效语法,因为.
被视为常量42.
的一部分,所以没有.
属性访问运算符来调用toFixed
方法。
下面的语法也是有效的(请注意其中的空格):
42 .toFixed(3); // "42.000"
我们还可以用指数形式来表示较大的数字:
var onethousand = 1E3; // 即 1 * 10^3
var onemilliononehundredthousand = 1.1E6; // 即 1.1 * 10^6
2
数字字面量还可以用其他格式来表示,如二进制、八进制和十六进制。当前的JavaScript版本都支持这些格式:
0xf3; // 243的十六进制
0Xf3; // 同上
0363; // 243的八进制
2
3
4
从ES6开始,严格模式(strict mode)不再支持0363
八进制格式。0363
格式在非严格模式(non-strict mode)中仍然受支持,但是考虑到将来的兼容性,最好不要再使用
ES6支持以下新格式:
0o363; // 243的八进制
0O363; // 同上
0b11110011; // 243的二进制
0B11110011; // 同上
2
3
4
5
考虑到代码的易读性,不推荐使用0O363
格式,因为0和大写字母O在一起容易混淆。建议尽量使用小写的0x、0b
和0o
。
如何判断0.1 + 0.2
和0.3
是否相等?
常见的方法是设置一个误差范围值,通常被称为”机器精度“(machineepsilon),对JavaScript的数字来说,这个值通常是2^-52(2.220446049250313e-16)
。
在ES6中,该值定义在Number.EPSILON
中,ES6之前的版本写polyfill:
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2, -52);
}
2
3
在指定误差范围内,使用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
2
3
4
5
6
7
8
9
能够呈现的最大浮点数大约是1.798e+308
(这是一个相当大的数字),它定义在Number.MAX_VALUE
中。最小浮点数定义在Number.MIN_VALU
E中,大约是5e-324
,它不是负数,但无限接近于0!
# 整数的安全范围
能够被“安全”呈现的最大整数是2^53-1
,即9007199254740991
,在ES6中被定义为Number.MAX_SAFE_INTEGER
。最小整数是-9007199254740991
,在ES6中被定义为Number.MIN_SAFE_INTEGER
。
# 整数检测
使用ES6中的Number.isInteger()
方法来检测一个值是否是整数
Number.isInteger(42); // true
Number.isInteger(42.000); // true
Number.isInteger(42.3); // false
2
3
ES6之前的polyfill版本的Number.isInteger()
方法:
if (! Number.isInteger) {
Number.isInteger = function(num) {
return typeof num == "number" && num % 1 == 0;
};
}
2
3
4
5
可以用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
2
3
ES6之前的polyfill版本的Number.isSafeInteger()
方法:
if (! Number.isSafeInteger) {
Number.isSafeInteger = function(num) {
return Number.isInteger( num ) &&
Math.abs( num ) <= Number.MAX SAFE INTEGER;
};
}
2
3
4
5
6
# 32位有符号整数
a | 0
可以将变量a
中的数值转换为32
位有符号整数,因为数位运算符|
只适用于32
位整数(它只关心32
位以内的值,其他的数位将被忽略)。因此与0
进行OR
操作本质上没有意义。
# 不是值的值
undefined
和null
常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:
null
指空值(empty value)undefined
指没有值(missing value)
或者:
undefined
指从未赋值null
指曾赋过值,但是目前没有值
null
是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而undefined
却是一个标识符,可以被当作变量来使用和赋值。
# undefined
在非严格模式下,我们可以为全局标识符undefined
赋值
function foo() {
undefined = 2; // 非常糟糕的做法!
}
foo();
function foo() {
"use strict";
undefined = 2; // TypeError!
}
foo();
2
3
4
5
6
7
8
9
10
11
12
在非严格和严格两种模式下,我们可以声明一个名为undefined
的局部变量。再次强调最好不要这样做!
function foo() {
"use strict";
var undefined = 2;
console.log( undefined ); // 2
}
foo();
2
3
4
5
6
7
表达式void______
没有返回值,因此返回结果是undefined
。void
并不改变表达式的结果,只是让表达式不返回值:
var a = 42;
console.log( void a, a ); // undefined 42
2
3
按惯例我们用void 0
来获得undefined
(这主要源自C语言,当然使用void true
或其他void
表达式也是可以的)。void 0、void 1
和undefined
之间并没有实质上的区别。
不让表达式返回任何结果(即时有副作用):
function doSomething() {
// 注:APP.ready由程序自己定义
if (! APP.ready) {
// 稍后再试
return void setTimeout( doSomething,100 );
}
var result;
// 其他
return result;
}
// 现在可以了吗?
if (doSomething()) {
// 立即执行下一个任务
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 特殊的数字
NaN
意指“不是一个数字”(not a number),这个名字容易引起误会,后面将会提到。将它理解为“无效数值”“失败数值”或者“坏数值”可能更准确些。
var a = 2 / "foo"; // NaN
typeof a === "number"; // true
2
3
NaN
是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
NaN
是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即x === x
不成立)的值。而NaN != NaN
为true
,很奇怪吧?
使用内建的全局工具函数isNaN(..)
来判断一个值是否是NaN
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; "foo"
window.isNaN( a ); // true
window.isNaN( b ); // true——晕!
2
3
4
5
6
7
8
isNaN(..)
有一个严重的缺陷,它的检查方式过于死板,就是“检查参数是否不是NaN,也不是数字”。
很明显"foo
"不是一个数字,但是它也不是NaN
。这个bug自JavaScript问世以来就一直存在,至今已超过19年。
我们也可以使用ES6中的工具函数:Number.isNaN()
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; "foo"
Number.isNaN( a ); // true
Number.isNaN( b ); // true——晕!
2
3
4
5
6
7
8
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——好!
2
3
4
5
6
7
8
9
10
11
12
13
14
另一个更简单的方法是利用NaN
不等于自身这个特点
if (! Number.isNaN) {
Number.isNaN = function(n) {
return n ! == n;
};
}
2
3
4
5
我们应该尽量使用Number.isNaN()
这样可靠的方法,无论是系统内置还是polyfill
# 无穷数
var a = 1 / 0;
在JavaScript中上例的结果为Infinity
(即Number.POSITIVE_INfINITY
)。同样:
var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity
2
JavaScript的运算结果有可能溢出,此时结果为Infinity
或者-Infinity
var a = Number.MAX VALUE; // 1.7976931348623157e+308
a + a; // Infinity
a + Math.pow( 2, 970 ); // Infinity
a + Math.pow( 2, 969 ); // 1.7976931348623157e+308
2
3
4
规范规定,如果数学运算(如加法)的结果超出处理范围,则由IEEE 754规范中的“就近取整”(round-to-nearest)模式来决定最后的结果。例如,相对于Infinity,Number.MAX_VALUE + Math.pow(2, 969)
与Number.MAX_VALUE
更为接近,因此它被“向下取整”(round down);而Number.MAX_VALUE + Math.pow(2,970)
与Infinity
更为接近,所以它被“向上取整”(round up)。
无穷除以无穷是什么结果?
从数学运算和JavaScript语言的角度来说,Infinity/Infinity
是一个未定义操作,结果为NaN
。
如果是有穷正数除以Infinity
呢?很简单,结果为0。
# 零值
-0
除了可以用作常量以外,也可以是某些数学运算的返回值:
var a = 0 / -3; // -0
var b = 0 * -3; // -0
2
加法和减法运算不会得到负零(negative zero)。
根据规范,对负零进行字符串化会返回"0"
:
var a = 0 / -3;
// 至少在某些浏览器的控制台中显示是正确的
a; // -0
// 但是规范定义的返回结果是这样!
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
// JSON也如此,很奇怪
JSON.stringify( a ); // "0"
2
3
4
5
6
7
8
9
10
11
12
如果反过来将其从字符串转换为数字,得到的结果是准确的
+"-0"; // -0
Number( "-0" ); // -0
JSON.parse( "-0" ); // -0
2
3
JSON.stringify(-0)返回"0",而JSON.parse("-0")返回-0。
-0
和0
的区分方式:
function isNegZero(n) {
n = Number( n );
return (n === 0) && (1 / n === -Infinity);
}
isNegZero( -0 ); // true
isNegZero( 0 / -3 ); // true
isNegZero( 0 ); // false
2
3
4
5
6
7
8
我们为什么需要负零?
有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位(sign)用来代表其他信息(比如移动的方向)。此时如果一个值为0的变量失去了它的符号位,它的方向信息就会丢失。所以保留0值的符号位可以防止这类情况发生。
# 特殊等式
使用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
2
3
4
5
6
7
ES6之前的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;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 值和引用
JavaScript中没有指针,引用的工作机制也不尽相同。在JavaScript中变量不可能成为指向另一个变量的引用。
JavaScript中引用指向的是值。如果一个值有10个引用,这些引用指向的都是同一个值,它们相互之间没有引用/指向关系。
简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值/传递,包括null、undefined、字符串、数字、布尔和ES6中的symbol
。
复合值(compound value)——对象(包括数组和封装对象,参见第3章)和函数,则总是通过引用复制的方式来赋值/传递。
var a = 2;
var b = a; // b是a的值的一个复本
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
2
3
4
5
6
7
8
9
10
11
由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。
var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]
// 然后
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]
2
3
4
5
6
7
8
9
b=[4,5,6]
并不影响a
指向值[1,2,3]
,除非b
不是指向数组的引用,而是指向a
的指针,但在JavaScript中不存在这种情况!
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]
2
3
4
5
6
7
8
9
10
11
12
13
14
我们向函数传递a
的时候,实际是将引用a
的一个复本赋值给x
,而a
仍然指向[1, 2, 3]
。在函数中我们可以通过引用x
来更改数组a
的值(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]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x.length = 0
和x.push(4, 5, 6, 7)
并没有创建一个新的数组,而是更改了当前的数组。于是a
指向的值变成了[4, 5, 6, 7]
。
如果通过值赋值的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的就不再是原始值的引用复本。例如
foo(a. slice());
slice()
不带参数会返回当前数组的一个浅复本(shallow copy)。由于传递给函数的是指向该复本的引用,所以foo()
中的操作不会影响a
指向的数组。
如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合对象(对象、数组等)中,然后通过引用复制的方式传递。
function foo(wrapper) {
wrapper.a = 42;
}
var obj = {
a: 2
};
foo( obj );
obj.a; // 42
2
3
4
5
6
7
8
9
10
11
如果传递指向数字对象的引用复本,我们并不能通过它来更改其中的基本类型值:
function foo(x) {
x = x + 1;
x; // 3
}
var a = 2;
var b = new Number(a); // Object(a)也一样
foo(b);
console.log(b); // 是2, 不是3
2
3
4
5
6
7
8
9
10
原因是标量基本类型值是不可更改的(字符串和布尔也是如此)。如果一个数字对象的标量基本类型值是2
,那么该值就不能更改,除非创建一个包含新值的数字对象。
x = x + 1
中,x
中的标量基本类型值2
从数字对象中拆封(或提取)出来后,x
就神不知鬼不觉地从引用变成了数字对象,它的值为2 + 1 等于 3
。然而函数外的b
仍然指向原来那个值为2
的数字对象。
# 第三章 原生函数
常用的原生函数有:
- String()
- Number()
- Boolean()
- Array()
- Object()
- Function()
- RegExp()
- Date()
- Error()
- Symbol()
通过构造函数(如 new String('abc')
)创建出来的是封装了基本类型值(如'abc'
)的封装对象。
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"
2
3
4
5
6
7
再次强调,new String('abc')
创建的是字符串'abc'
的封装对象,而非基本类型值'abc'
# 内部属性[[Class]]
所有typeof
返回值为"Object"
的对象(如数组)都包含一个内部属性[[Class]]
(我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问,一般通过Object.prototype.toString()
来查看
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"
2
3
4
虽然Null()
和Undefined()
这样的原生构造函数并不存在,但是内部[[Class]]
属性值仍然是"Null"
和"Undefined"
。
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
2
本例中基本类型值被各自的封装对象自动包装,所以它们的内部[[Class]]
属性值分别为"String"
、"Number"
、"Boolean"
。
Object.prototype.toString.call("abc");
// "[object String]"
Object.prototype.toString.call(42);
// "[object Number]"
Object.prototype.toString.call(true);
// "[object Boolean]"
2
3
4
5
6
7
8
# 封装对象包装
由于基本类型值没有.length
和.toString()
这样的属性和方法,需要通过封装对象才能访问,此时JavaScript会自动为基本类型值包装(box或者wrap)一个封装对象:
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
2
3
4
一般情况下,我们不需要直接使用封装对象。最好的方式是让JavaScript引擎自己决定什么时候应该是用封装对象。这是因为浏览器已经为.length
这样的情况做了性能优化,直接使用封装对象来“提前优化”代码反而会降低执行效率。
为false创建了一个封装对象,然而该对象是真值,所以这里使用封装对象得到的结果和使用false截然相反:
var a = new Boolean(false);
if (!a) {
console.log('Oops'); // 执行不到这里
}
2
3
4
如果想要得到封装对象中的基本类型值,可以使用valueOf()
函数:
var a = new Boolean(false);
if (!a.valueOf()) {
console.log('Oops'); // 可以执行到这里了
}
2
3
4
在需要用到封装对象中的基本类型值的地方会发生隐式拆封。即发生强制类型转换
var a = new String( "abc" );
var b = a + ""; // b的值为"abc"
typeof a; // "object"
typeof b; // "string"
2
3
4
5
# 原生函数作为构造函数
构造函数Array()
不要求必须带new
关键字。不带时,它会被自动补上。因此Array(1, 2, 3)
和new Array(1, 2, 3)
的效果是一样的。
var a = new Array(1, 2, 3);
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [1, 2, 3]
2
3
4
5
Array
构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length
),而非只充当数组中的一个元素。
var a = Array(12);
a.length; // 12
2
var a = new Array( 3 );
var b = [ undefined, undefined, undefined ];
var c = [];
c.length = 3;
a;
b;
c;
2
3
4
5
6
7
8
如例子中的c
,只要将length
属性设置为超过实际单元数的值,就能隐式地制造出空单元。另外还可以通过delete b[1]
在数组b
中制造一个空单元。
上例中a和b的行为有时相同,有时又大相径庭
a.join( "-" ); // "--"
b.join( "-" ); // "--"
a.map(function(v, i){ return i; }); // [空 ã10]
b.map(function(v, i){ return i; }); // [0, 空, 2, 3]
2
3
4
5
a.map()
之所以会执行失败,是因为数组中并不存在任何单元,所以map()
无从遍历。而join()
却不一样,它的具体实现可参考下面的代码
function fakeJoin(arr, connector) {
var str = "";
for (var i = 0; i < arr.length; i++) {
if (i > 0) {
str += connector;
}
if (arr[i] ! == undefined) {
str += arr[i];
}
}
return str;
}
var a = new Array( 3 );
fakeJoin( a, "-" ); // "--"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们可以通过下述方式来创建包含undefined
单元的数组:
var a = Array.apply(null, {length: 3});
a; // [undefined, undefined. undefined]
2
虽然Array.apply(null, {length: 3})
在创建undefined
值的数组时有些奇怪和繁琐,但是其结果远比Array(3)
更准确可靠。
创建日期对象必须使用new Date()
。Date()
可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。
Date()
主要用来获得当前的Unix
时间戳(从1970年1月1日开始计算,以秒为单位)。该值可以通过日期对象中的getTime()
来获得。
ES5开始引入了静态函数Date.now()
。对ES5之前的版本,可以使用下面的polyfill:
if (!Date.now) {
Date.now = function(){
return (new Date()).getTime();
};
}
2
3
4
5
如果调用Date()
时不带new
关键字,则会得到当前日期的字符串值。其具体格式规范没有规定,浏览器中使用"Thu Dec 01 2022 10:39:53 GMT+0800 (中国标准时间)"这样的格式来显示。
构造函数Error()
(与前面的Array()
类似)带不带new
关键字都可以。错误对象通常与throw
一起使用。
function foo(x) {
if (! x) {
throw new Error( "x wasn't provided" );
}
// ..
}
2
3
4
5
6
错误对象至少包含一个message
属性,有时也不乏其他属性(必须作为只读属性访问),如type
。除了访问stack
属性以外,最好的办法是调用(显示调用或者通过强制类型转换隐式调用)toString()
来获得经过格式化的便于阅读的错误信息。
除Error()
之外,还有一些针对特定错误类型的原生构造函数,如EvalError()
、RangeError()
、ReferenceError()
、SyntaxError()
、TypeError()
和URIError()
。这些构造函数很少被直接使用,它们在程序发生异常(比如试图使用未声明的变量产生ReferenceError错误)时会被自动调用
# Symbol()
符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。该类型的引入主要源于ES6的一些特殊构造,此外符号也可以自行定义。
ES6中有一些预定义符号,以Symbol
的静态属性形式出现,如Symbol.create
、Symbol.iterator
等
obj[Symbol.iterator] = function(){ /*..*/ };
我们可以使用Symbol()
原生构造函数来自定义符号。但它比较特殊,不能带new
关键字,否则会报错
var mysym = Symbol( "my own symbol" );
mysym; // Symbol(my own symbol)
mysym.toString(); // "Symbol(my own symbol)"
typeof mysym; // "symbol"
var a = { };
a[mysym] = "foobar";
Object.getOwnPropertySymbols( a );
// [ Symbol(my own symbol) ]
2
3
4
5
6
7
8
9
10
虽然符号实际上并非私有属性(通过Object.getOwnPropertySymbols()
便可以公开获得对象中的所有符号),但它却主要用于私有或特殊属性。很多开发人员喜欢用它来替代下划线方法前缀的属性,而下划线前缀通常用于命名私有或特殊属性。
符号并非对象,而是一种简单标量基本类型。
# 原生原型
有些原生原型并非普通对象那么简单:
typeof Function.prototype; // "function"
Function.prototype(); // 空函数!
RegExp.prototype.toString(); // "/(? :)/"——空正则表达式
"abc".match( RegExp.prototype ); // [""]
2
3
4
5
更糟糕的是,我们甚至可以修改它们(而不仅仅是添加属性):
Array.isArray( Array.prototype ); // true
Array.prototype.push( 1, 2, 3 ); // 3
Array.prototype; // [1,2,3]
// 需要将Array.prototype设置回空,否则会导致问题!
Array.prototype.length = 0;
2
3
4
5
6
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这种方法的一个好处是.prototype
已被创建并且仅创建一次。相反,如果将[]
、function(){}
和/(? :)/
作为默认值,则每次调用isThisCool(..)
时它们都会被创建一次(具体创建与否取决于JavaScript引擎,稍后它们可能会被垃圾回收),这样无疑会造成内存和CPU资源的浪费。
另外需要注意的一点是,如果默认值随后会被更改,那就不要使用Array.prototype
。上例中的vals
是作为只读变量来使用,更改vals
实际上就是更改Array.prototype
,而这样会导致前面提到过的一系列问题!