# 第一部分 类型和语法

# 第一章 类型

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
1
2
3
4
5
6

null类型比较特殊

typeof null === 'object'; // true
1

正确的返回结果应该是'null',但这个bug由来已久,在JavaScript中已经存在了将近二十年,也许永远也不会修复,因为这牵涉到太多的Web系统,“修复”它会产生更多的bug,令许多系统无法正常工作。

使用复合条件来检测null值的类型

var a = null;
(!a && typeof a === 'object'); // true
1
2
typeof function a() {/** */} === 'function'; // true
1

function(函数)也是JavaScript的一个内置类型。查阅规范,它实际上是object的一个“子类型”。具体来说哦,函数是“可调用对象”,它有一个内部属性[[Call]],该属性使其可以被调用。

函数对象的length属性是其声明的参数的个数:

function a(b, c) {
  /** */
}
a.length; // 2
1
2
3
4
typeof [1,2,3] === "object"; // true
1

数组也是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"
1
2
3
4
5
6
7
8
9
10
11
12

已在作用域中声明还没有赋值的变量,是undefined的。相反,还没有在作用域中声明过的变量,是undeclared的。

var a;

a; // undefined
b; // ReferenceError: b is not defined
1
2
3
4

undefined”和“is not defined”是两码事。此时如果浏览器报错成“b is not found”或者“b is not declared”会更准确。

var a;
typeof a; // "undefined"
typeof b; // "undefined"
1
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" );
}
1
2
3
4
5
6
7
8
9

这不仅对用户定义的变量(比如DEBUG)有用,对内建的API也有帮助:

if (typeof atob === "undefined") {
  atob = function() { /*..*/ };
}
1
2
3

另一种安全防范机制是通过浏览器中的全局对象window来处理

if (window.DEBUG) {
  // ..
}

if (!window.atob) {
  // ..
}
1
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
1
2
3
4
5

对数组声明后即可向其中加入值,不需要预先设定大小

var a = [ ];

a.length;   // 0

a[0] = 1;
a[1] = "2";
a[2] = [ 3 ];

a.length;   // 3
1
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
1
2
3
4
5
6
7
8

如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当做数字索引来处理。

var a = [ ];

a["13"] = 42;

a.length; // 14
1
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"]
1
2
3
4
5
6
7

在ES6中的内置工具函数Array.from()也能实现同样的功能

var arr = Array.from( arguments );
1

# 字符串

字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。

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

b.push( "! " );
b;          // ["f", "O", "o", "! "]
1
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."
1
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", "! "]
1
2
3
4
5
6

我们无法“借用”数组的可变更成员函数,因为字符串是不可变的:

Array.prototype.reverse.call( a );
// 返回值仍然是字符串"foo"的一个封装对象
1
2

变通的办法是先将字符串转换为数组,待处理完后再将结果换回字符串:

var c = a
    // 将a的值转换为字符数组
    .split("")
    // 将数组中的字符进行倒转
    .reverse()
    // 将数组中的字符拼接回字符串
    .join("");

c; // "!oof"
1
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
1
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"
1
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"
1
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"
1
2
3
4
5
6
7

42.toFixed(3)是无效语法,因为.被视为常量42.的一部分,所以没有.属性访问运算符来调用toFixed方法。

下面的语法也是有效的(请注意其中的空格):

42 .toFixed(3); // "42.000"
1

我们还可以用指数形式来表示较大的数字:

var onethousand = 1E3;                      // 即 1 * 10^3
var onemilliononehundredthousand = 1.1E6;   // 即 1.1 * 10^6
1
2

数字字面量还可以用其他格式来表示,如二进制、八进制和十六进制。当前的JavaScript版本都支持这些格式:

0xf3; // 243的十六进制
0Xf3; // 同上

0363; // 243的八进制
1
2
3
4

从ES6开始,严格模式(strict mode)不再支持0363八进制格式。0363格式在非严格模式(non-strict mode)中仍然受支持,但是考虑到将来的兼容性,最好不要再使用

ES6支持以下新格式:

0o363;      // 243的八进制
0O363;      // 同上

0b11110011; // 243的二进制
0B11110011; // 同上
1
2
3
4
5

考虑到代码的易读性,不推荐使用0O363格式,因为0和大写字母O在一起容易混淆。建议尽量使用小写的0x、0b0o

如何判断0.1 + 0.20.3是否相等?

常见的方法是设置一个误差范围值,通常被称为”机器精度“(machineepsilon),对JavaScript的数字来说,这个值通常是2^-52(2.220446049250313e-16)

在ES6中,该值定义在Number.EPSILON中,ES6之前的版本写polyfill:

if (!Number.EPSILON) {
  Number.EPSILON = Math.pow(2, -52);
}
1
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
1
2
3
4
5
6
7
8
9

能够呈现的最大浮点数大约是1.798e+308(这是一个相当大的数字),它定义在Number.MAX_VALUE中。最小浮点数定义在Number.MIN_VALUE中,大约是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
1
2
3

ES6之前的polyfill版本的Number.isInteger()方法:

if (! Number.isInteger) {
  Number.isInteger = function(num) {
    return typeof num == "number" && num % 1 == 0;
  };
}
1
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
1
2
3

ES6之前的polyfill版本的Number.isSafeInteger()方法:

if (! Number.isSafeInteger) {
  Number.isSafeInteger = function(num) {
  return Number.isInteger( num ) &&
    Math.abs( num ) <= Number.MAX SAFE INTEGER;
  };
}
1
2
3
4
5
6

# 32位有符号整数

a | 0可以将变量a中的数值转换为32位有符号整数,因为数位运算符|只适用于32位整数(它只关心32位以内的值,其他的数位将被忽略)。因此与0进行OR操作本质上没有意义。

# 不是值的值

undefinednull常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:

  • 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();
1
2
3
4
5
6
7
8
9
10
11
12

在非严格和严格两种模式下,我们可以声明一个名为undefined的局部变量。再次强调最好不要这样做!

function foo() {
  "use strict";
  var undefined = 2;
  console.log( undefined ); // 2
}

foo();
1
2
3
4
5
6
7

表达式void______没有返回值,因此返回结果是undefinedvoid并不改变表达式的结果,只是让表达式不返回值:

var a = 42;

console.log( void a, a ); // undefined 42
1
2
3

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

不让表达式返回任何结果(即时有副作用):

function doSomething() {
  // 注:APP.ready由程序自己定义
  if (! APP.ready) {
    // 稍后再试
    return void setTimeout( doSomething,100 );
  }

  var result;

  // 其他
  return result;
}

// 现在可以了吗?
if (doSomething()) {
  // 立即执行下一个任务
}
1
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
1
2
3

NaN是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

NaN是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即x === x不成立)的值。而NaN != NaNtrue,很奇怪吧?

使用内建的全局工具函数isNaN(..)来判断一个值是否是NaN

var a = 2 / "foo";
var b = "foo";

a; // NaN
b; "foo"

window.isNaN( a ); // true
window.isNaN( b ); // true——晕!
1
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——晕!
1
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——好!
1
2
3
4
5
6
7
8
9
10
11
12
13
14

另一个更简单的方法是利用NaN不等于自身这个特点

if (! Number.isNaN) {
  Number.isNaN = function(n) {
    return n ! == n;
  };
}
1
2
3
4
5

我们应该尽量使用Number.isNaN()这样可靠的方法,无论是系统内置还是polyfill

# 无穷数

var a = 1 / 0;
1

在JavaScript中上例的结果为Infinity(即Number.POSITIVE_INfINITY)。同样:

var a = 1 / 0;  // Infinity
var b = -1 / 0; // -Infinity
1
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
1
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
1
2

加法和减法运算不会得到负零(negative zero)。

根据规范,对负零进行字符串化会返回"0"

var a = 0 / -3;

// 至少在某些浏览器的控制台中显示是正确的
a;                           // -0

// 但是规范定义的返回结果是这样!
a.toString();               // "0"
a + "";                     // "0"
String( a );                // "0"

// JSON也如此,很奇怪
JSON.stringify( a );   // "0"
1
2
3
4
5
6
7
8
9
10
11
12

如果反过来将其从字符串转换为数字,得到的结果是准确的

+"-0";              // -0
Number( "-0" );     // -0
JSON.parse( "-0" ); // -0
1
2
3

JSON.stringify(-0)返回"0",而JSON.parse("-0")返回-0。

-00的区分方式:

function isNegZero(n) {
  n = Number( n );
  return (n === 0) && (1 / n === -Infinity);
}

isNegZero( -0 );        // true
isNegZero( 0 / -3 );    // true
isNegZero( 0 );         // false
1
2
3
4
5
6
7
8

我们为什么需要负零?

有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位(sign)用来代表其他信息(比如移动的方向)。此时如果一个值为0的变量失去了它的符号位,它的方向信息就会丢失。所以保留0值的符号位可以防止这类情况发生。

# 特殊等式

使用ES6新加的工具方法Object.is()来判断两个值是否绝对相等:

var a = 2 / "foo";
var b = -30;

Object.is( a, NaN );    // true
Object.is( b, -0 );     // true

Object.is( b, 0 );      // false
1
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;
  };
}
1
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]
1
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]
1
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]
1
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的指向,只能更改ax共同的值。

如果要将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]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

x.length = 0x.push(4, 5, 6, 7)并没有创建一个新的数组,而是更改了当前的数组。于是a指向的值变成了[4, 5, 6, 7]

如果通过值赋值的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的就不再是原始值的引用复本。例如

foo(a. slice());
1

slice()不带参数会返回当前数组的一个浅复本(shallow copy)。由于传递给函数的是指向该复本的引用,所以foo()中的操作不会影响a指向的数组。

如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合对象(对象、数组等)中,然后通过引用复制的方式传递。

function foo(wrapper) {
  wrapper.a = 42;
}

var obj = {
  a: 2
};

foo( obj );

obj.a; // 42
1
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
1
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]"
1
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]"
1
2
3
4

虽然Null()Undefined()这样的原生构造函数并不存在,但是内部[[Class]]属性值仍然是"Null""Undefined"

Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
1
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]"
1
2
3
4
5
6
7
8

# 封装对象包装

由于基本类型值没有.length.toString()这样的属性和方法,需要通过封装对象才能访问,此时JavaScript会自动为基本类型值包装(box或者wrap)一个封装对象:

var a = "abc";

a.length; // 3
a.toUpperCase(); // "ABC"
1
2
3
4

一般情况下,我们不需要直接使用封装对象。最好的方式是让JavaScript引擎自己决定什么时候应该是用封装对象。这是因为浏览器已经为.length这样的情况做了性能优化,直接使用封装对象来“提前优化”代码反而会降低执行效率。

为false创建了一个封装对象,然而该对象是真值,所以这里使用封装对象得到的结果和使用false截然相反:

var a = new Boolean(false);
if (!a) {
  console.log('Oops'); // 执行不到这里
}
1
2
3
4

如果想要得到封装对象中的基本类型值,可以使用valueOf()函数:

var a = new Boolean(false);
if (!a.valueOf()) {
  console.log('Oops'); // 可以执行到这里了
}
1
2
3
4

在需要用到封装对象中的基本类型值的地方会发生隐式拆封。即发生强制类型转换

var a = new String( "abc" );
var b = a + ""; // b的值为"abc"

typeof a;       // "object"
typeof b;       // "string"
1
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]
1
2
3
4
5

Array构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。

var a = Array(12);
a.length; // 12
1
2
var a = new Array( 3 );
var b = [ undefined, undefined, undefined ];
var c = [];
c.length = 3;

a;
b;
c;
1
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]
1
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, "-" ); // "--"
1
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]
1
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();
  };
}
1
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" );
  }
  // ..
}
1
2
3
4
5
6

错误对象至少包含一个message属性,有时也不乏其他属性(必须作为只读属性访问),如type。除了访问stack属性以外,最好的办法是调用(显示调用或者通过强制类型转换隐式调用)toString()来获得经过格式化的便于阅读的错误信息。

Error()之外,还有一些针对特定错误类型的原生构造函数,如EvalError()RangeError()ReferenceError()SyntaxError()TypeError()URIError()。这些构造函数很少被直接使用,它们在程序发生异常(比如试图使用未声明的变量产生ReferenceError错误)时会被自动调用

# Symbol()

符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。该类型的引入主要源于ES6的一些特殊构造,此外符号也可以自行定义。

ES6中有一些预定义符号,以Symbol的静态属性形式出现,如Symbol.createSymbol.iterator

obj[Symbol.iterator] = function(){ /*..*/ };
1

我们可以使用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) ]
1
2
3
4
5
6
7
8
9
10

虽然符号实际上并非私有属性(通过Object.getOwnPropertySymbols()便可以公开获得对象中的所有符号),但它却主要用于私有或特殊属性。很多开发人员喜欢用它来替代下划线方法前缀的属性,而下划线前缀通常用于命名私有或特殊属性。

符号并非对象,而是一种简单标量基本类型。

# 原生原型

有些原生原型并非普通对象那么简单:

typeof Function.prototype;          // "function"
Function.prototype();               // 空函数!

RegExp.prototype.toString();        // "/(? :)/"——空正则表达式
"abc".match( RegExp.prototype );    // [""]
1
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;
1
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
1
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,而这样会导致前面提到过的一系列问题!