数组(array)、对象(object)、函数(function)和正则表达式,通常以常量的形式来创建。实际上,使用常量和使用构造函数的效果是一样的(创建的值都是通过封装对象来包装的)。应该尽量避免使用构造函数。

Array(..)

  1. var a = new Array( 1, 2, 3 );
  2. a; // [1, 2, 3]
  3. var b = [1, 2, 3];
  4. b; // [1, 2, 3]

构造函数 Array(..) 不要求必须带 new 关键字。不带时,它会被自动补上。 因此 Array(1,2,3) 和 new Array(1,2,3) 的效果是一样的。
Array 构造函数只有一个数字参数的时候,该参数会被作为数组的预设长度(length),而不是充当数组中的一个元素。
其实数组并没有预设长度这个概念。这样创建出来的只是一个空数组,只不过它的 length 属性被设置成了指定的值。
如果一个数组中没有任何单元,但它的 length 属性中却显示有单元数量,会导致一些怪异的行为。这一切都归咎于已被废止的旧特性(类似 arguments 这样的类数组)。
注:将包含至少一个“空单元”的数组称为“稀疏数组”。
不同浏览器的开发控制台显示的结果也不同,以下例子是Chrome 91

  1. var a = new Array( 3 );
  2. var b = [ undefined, undefined, undefined ];
  3. var c = [];
  4. c.length = 3;
  5. a; // [empty × 3]
  6. b; // [undefined, undefined, undefined]
  7. c; // [empty × 3]

上例中 a 和 b 的行为有时相同,有时不同:

  1. a.join( "-" ); // "--"
  2. b.join( "-" ); // "--"
  3. a.map(function(v,i){ return i; }); // [empty × 3]
  4. b.map(function(v,i){ return i; }); // [ 0, 1, 2 ]

a.map(..) 之所以执行失败,是因为数组中并不存在任何单元,所以 map(..) 无从遍历。而 join(..) 却不一样,它的具体实现可参考下面的代码:

  1. function fakeJoin(arr,connector) {
  2. var str = "";
  3. for (var i = 0; i < arr.length; i++) {
  4. if (i > 0) {
  5. str += connector;
  6. }
  7. if (arr[i] !== undefined) {
  8. str += arr[i];
  9. }
  10. }
  11. return str;
  12. }
  13. var a = new Array( 3 );
  14. fakeJoin( a, "-" ); // "--"

join(..) 首先假定数组不为空,然后通过 length 属性值来遍历其中的元素。而 map(..) 并不做这样的假定,因此结果可能和预期不同。
可以通过下述方式来创建包含 undefined 单元(而非“空单元”)的数组:

  1. var a = Array.apply( null, { length: 3 } );
  2. a; // [ undefined, undefined, undefined ]

apply(..) 是一个工具函数,适用于所有函数对象,它会以一种特殊的方式来调用传递给它的函数。
第一个参数是 this 对象,暂将它设为 null。第二个参数则必须是一个数组(或者类似数组的值,也叫作类数组对象,array-like object),其中的值被用作函数的参数。
于是 Array.apply(..) 调用 Array(..) 函数,并且将 { length: 3 } 作为函数的参数。 可以设想 apply(..) 内部有一个 for 循环(与上述 join(..) 类似),从 0 开始循环到length(即循环到 2,不包括 3)。
假设在 apply(..) 内部该数组参数名为 arr,for 循环就会这样来遍历数组:arr[0]、 arr[1]、arr[2]。然而,由于{ length: 3 }中并不存在这些属性,所以返回值为 undefined。
上述代码执行的实际上是 Array(undefined, undefined, undefined),所以结果是单元值为 undefined 的数组,而非空单元数组。
永远不要创建和使用空单元数组,应使用 Array.apply( null, { length: 3 } )方式创建非空单元数组。

Object(..)、Function(..) 和 RegExp(..)

尽量不要使用 Object(..)/Function(..)/RegExp(..):

  1. var c = new Object();
  2. c.foo = "bar";
  3. c; // { foo: "bar" }
  4. var d = { foo: "bar" };
  5. d; // { foo: "bar" }
  6. var e = new Function( "a", "return a * 2;" );
  7. var f = function(a) { return a * 2; }
  8. function g(a) { return a * 2; }
  9. var h = new RegExp( "^a*b+", "g" );
  10. var i = /^a*b+/g;

使用new Object()来创建对象时,无法像常量形式那样一次设定多个属性,必须逐一设定。
构造函数 Function 只在极少数情况下很有用,比如动态定义函数参数和函数体的时候。不要把 Function(..) 当作 eval(..) 的替代品,基本上不会通过这种方式来定义函数。
建议使用常量形式(如 /^a*b+/g)来定义正则表达式,不仅语法简单,执行效率也更高,因为 JavaScript 引擎在代码执行前会对它们进行预编译和缓存。与前面的构造函数不同,RegExp(..) 有时还是很有用的,比如动态定义正则表达式时:

  1. var someText = 'The quick brown fox jumps over the lazy dog. It barked.';
  2. var name = "quick";
  3. var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" );
  4. var matches = someText.match( namePattern ); // ["quick"]

Date(..) 和 Error(..)

创建日期对象必须使用new Date()Date(..)可以带参数,用来指定日期和时间,而不带 参数的话则使用当前的日期和时间。
Date(..) 主要用来获得当前的 Unix 时间戳(从 1970 年 1 月 1 日开始计算,以秒为单位)。 该值可以通过日期对象中的 getTime() 来获得。
ES5 引入了一个更简单的方法,即静态函数 Date.now()。对 ES5 之前的版本我们可 以使用下面的 polyfill:

  1. if (!Date.now) {
  2. Date.now = function(){
  3. return (new Date()).getTime();
  4. };
  5. }

如果调用 Date() 时不带 new 关键字,则会得到当前日期的字符串值。

  1. new Date() // Tue Jun 22 2021 19:17:26 GMT+0800 (中国标准时间)
  2. Date() // "Tue Jun 22 2021 19:17:57 GMT+0800 (中国标准时间)"

构造函数 Error(..) (与前面的 Array() 类似)带不带 new 关键字都可。
创建错误对象(error object)主要是为了获得当前运行栈的上下文(大部分 JavaScript 引擎通过只读属性 .stack 来访问)。栈上下文信息包括函数调用栈信息和产生错误的代码行号, 以便于调试(debug)。

Error.prototype.stack ——MDN

错误对象通常与 throw 一起使用:

  1. function foo(x) {
  2. if (!x) {
  3. throw new Error( "x wasn’t provided" );
  4. }
  5. // ..
  6. }

通常错误对象至少包含一个 message 属性,有时也不乏其他属性(必须作为只读属性访问),如 type。除了访问 stack 属性以外,最好的办法是调用(显式调用或者通过强制类型转换隐式调用) toString() 来获得经过格式化的便于阅读的错误信息。
注:Error(..) 之外,还有一些针对特定错误类型的原生构造函数,如 EvalError(..)RangeError(..)ReferenceError(..)SyntaxError(..)TypeError(..)URIError(..)。这些构造函数很少被直接使用,它们在程序发生异常(比如试图使用未声明的变量产生 ReferenceError 错误)时会被自动调用。

Symbol(..)

ES6 中新加入了一个基本数据类型 ——符号(Symbol)。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。该类型的引入主要源于 ES6 的一些特殊构造,此外符号也可以自行定义。
符号可以用作属性名,但无论是在代码还是开发控制台中都无法查看和访问它的值,只会显示为如:Symbol(Symbol.create) 这样的值。
ES6 中有一些预定义符号,以 Symbol 的静态属性形式出现,如 Symbol.createSymbol. iterator 等:

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

可以使用 Symbol(..) 原生构造函数来自定义符号。不能带 new 关键字,否则会出错:

  1. var mysym = Symbol( "my own symbol" );
  2. mysym; // Symbol(my own symbol)
  3. mysym.toString(); // "Symbol(my own symbol)"
  4. typeof mysym; // "symbol"
  5. var a = { };
  6. a[mysym] = "foobar";
  7. Object.getOwnPropertySymbols( a ); // [ Symbol(my own symbol) ]

虽然符号实际上并非私有属性(通过 Object.getOwnPropertySymbols(..) 可获得对象中的所有符号),但它却主要用于私有或特殊属性。很多开发人员喜欢用它来替代有下划线(_)前缀的属性,下划线前缀通常用于命名私有或特殊属性。
注意:符号并非对象,而是一种简单标量基本类型。

原生原型

原生构造函数有自己的 .prototype 对象,如 Array.prototypeString.prototype 等。
这些对象包含其对应子类型所特有的行为特征。 例如,将字符串值封装为字符串对象之后,就能访问 String.prototype 中定义的方法。
String.prototype.indexOf(..) 在字符串中找到指定子字符串的位置。
String.prototype.charAt(..) 获得字符串指定位置上的字符。
String.prototype.substr(..)String.prototype.substring(..)String.prototype.slice(..) 获得字符串的指定部分。
String.prototype.toUpperCase()String.prototype.toLowerCase() 将字符串转换为大写或小写。
String.prototype.trim() 去掉字符串前后的空格,返回新的字符串。
以上方法并不改变原字符串的值,而是返回一个新字符串。
借助原型代理(prototype delegation),所有字符串都可以访问这些方法:

  1. var a = " abc ";
  2. a.indexOf( "c" ); // 3
  3. a.toUpperCase(); // " ABC "
  4. a.trim(); // "abc"

其他构造函数的原型包含它们各自类型所特有的行为特征,比如 Number.prototype.tofixed(..)(将 数字转换为指定长度的整数字符串)和 Array.prototype.concat(..)(合并数组)。所有的函数都可以 调用 Function.prototype 中的 apply(..)、call(..) 和 bind(..)。
有些原生原型(native prototype)并非普通对象那么简单:

  1. typeof Function.prototype; // "function"
  2. Function.prototype(); // undefined 空函数!
  3. RegExp.prototype.toString(); // "/(?:)/"——空正则表达式
  4. "abc".match( RegExp.prototype ); // [""] Chrome91 报错 Uncaught TypeError: Method RegExp.prototype.exec called on incompatible receiver [object Object]
  5. // 修改后
  6. "abc".match( RegExp.prototype.toString() ); // null

甚至可以修改它们(而不仅仅是添加属性):

  1. Array.isArray( Array.prototype ); // true
  2. Array.prototype.push( 1, 2, 3 ); // 3
  3. Array.prototype; // [1,2,3]
  4. // 需要将Array.prototype设置回空,否则会导致问题!
  5. Array.prototype.length = 0;

Function.prototype 是一个函数,RegExp.prototype 是一个正则表达式,而 Array. prototype 是一个数组

将原型作为默认值

Function.prototype 是一个空函数,RegExp.prototype 是一个“空”的正则表达式(无任何匹配),而 Array.prototype 是一个空数组。对未赋值的变量来说,是很好的默认值。例如:

  1. // 原书案例Chrome91会报错:
  2. function isThisCool(vals,fn,rx) {
  3. vals = vals || Array.prototype;
  4. fn = fn || Function.prototype;
  5. rx = rx || RegExp.prototype;
  6. return rx.test(
  7. vals.map( fn ).join( "" )
  8. );
  9. }
  10. isThisCool(); // true
  11. isThisCool(
  12. ["a","b","c"],
  13. function(v){ return v.toUpperCase(); },
  14. /D/
  15. ); // false
  16. // 修改后
  17. function isThisCool(vals,fn,rx) {
  18. vals = vals || Array.prototype;
  19. fn = fn || Function.prototype;
  20. if (!rx) {
  21. rx = RegExp.prototype.toString()
  22. rx = new RegExp(rx.slice(1,-1));
  23. }
  24. return rx.test(
  25. vals.map( fn ).join( "" )
  26. );
  27. }
  28. isThisCool();
  29. isThisCool(
  30. ["a","b","c"],
  31. function(v){ return v.toUpperCase(); },
  32. /D/
  33. );

这种方法的一个好处是 .prototypes 已被创建并且仅创建一次。相反,如果将[]function(){}/(?:)/ 作为默认值,则每次调用 isThisCool(..) 时它们都会被创建一次 (具体创建与否取决于JavaScript 引擎,稍后它们可能会被垃圾回收),这样就会造成内存和 CPU 资源浪费。
需要注意的是,如果默认值随后会被更改,那就不要使用 Array.prototype。上例中的 vals 是作为只读变量来使用,更改 vals 实际上就是更改 Array.prototype,这样会导致一系列问题!