第 3 章原生函数
常用的原生函数有:
-
String()
-
Number()
-
Boolean()
-
Array()
-
Object()
-
Function()
-
RegExp()
-
Date()
-
Error()
-
Symbol()——ES6 中新加入的!
JavaScript 中 的 String() 和 Java 中 的 字 符 串 构 造 函 数
String(..) 非常相似,可以这样来用:
var s = new String( "Hello World!" );
console.log( s.toString() ); // "Hello World!"
原生函数可以被当作构造函数来使用,但其构造出来的对象可能会和我们设想的有所
出入:
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 在这里返回的是对象类型的子类型。
可以这样来查看封装对象:
console.log( a );
在本书写作期间, Chrome 的最新版本是这样显示的: String {0: "a", 1:
"b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"},而老版本这样显示:
String {0: "a", 1: "b", 2: "c"}。最新版本的 Firefox 这样显示: String
["a","b","c"];老版本这样显示: "abc",并且可以点击打开对象查看器。
这些输出结果随着浏览器的演进不断变化,也带给人们不同的体验。
再次强调, new String("abc") 创建的是字符串 "abc" 的封装对象,而非基本类型值 "abc"。
3.1 内部属性 [[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]"
对象的内部 [[Class]] 属性和创建该对象的内建原生构造函数相对应,但并非
总是如此。
基本类型值
Object.prototype.toString.call( null );
// "[object Null]"
Object.prototype.toString.call( undefined );
// "[object Undefined]"
虽然 Null() 和 Undefined() 这样的原生构造函数并不存在,但是内部 [[Class]] 属性值仍
然是 "Null" 和 "Undefined"。
其他基本类型值(如字符串、数字和布尔)的情况有所不同,通常称为“包装”
Object.prototype.toString.call( "abc" );
// "[object String]"
Object.prototype.toString.call( 42 );
// "[object Number]"
Object.prototype.toString.call( true );
// "[object Boolean]
上例中基本类型值被各自的封装对象自动包装,所以它们的内部 [[Class]] 属性值分别为
"String"、 "Number" 和 "Boolean"。
从 ES5 到 ES6, toString() 和 [[Class]] 的行为发生了一些变化
3.2 封装对象包装
由 于 基 本 类 型 值 没 有 .length
和 .toString() 这样的属性和方法,需要通过封装对象才能访问,此时 JavaScript 会自动为
基本类型值包装( box 或者 wrap)一个封装对象:
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
如果需要经常用到这些字符串属性和方法,比如在 for 循环中使用 i < a.length,那么从
一开始就创建一个封装对象也许更为方便,这样 JavaScript 引擎就不用每次都自动创建了。
但实际证明这并不是一个好办法,因为浏览器已经为 .length 这样的常见情况做了性能优
化,直接使用封装对象来“提前优化”代码反而会降低执行效率。
一般情况下,我们不需要直接使用封装对象。最好的办法是让 JavaScript 引擎自己决定什
么时候应该使用封装对象。换句话说,就是应该优先考虑使用 "abc" 和 42 这样的基本类型
值,而非 new String("abc") 和 new Number(42)。
封装对象释疑
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // 执行不到这里
}
我们为 false 创建了一个封装对象,然而该对象是真值(“ truthy”,即总是返回 true,参见
第 4 章),所以这里使用封装对象得到的结果和使用 false 截然相反。
如果想要自行封装基本类型值,可以使用 Object(..) 函数(不带 new 关键字):
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]"
一般不推荐直接使用封装对象(如上例中的 b 和 c),但它们偶尔也会派上
用场。
3.3 拆封
如果想要得到封装对象中的基本类型值,可以使用 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
在需要用到封装对象中的基本类型值的地方会发生隐式拆封。具体过程(即强制类型转
换)
var a = new String( "abc" );
var b = a + ""; // b的值为"abc"
typeof a; // "object"
typeof b; // "string"
3.4 原生函数作为构造函数
关于数组( array)、对象( object)、函数( function)和正则表达式,我们通常喜欢以常
量的形式来创建它们。实际上,使用常量和使用构造函数的效果是一样的(创建的值都是
通过封装对象来包装)。
如前所述,应该尽量避免使用构造函数,除非十分必要,因为它们经常会产生意想不到的
结果。
3.4.1 Array(..)
var a = new Array( 1, 2, 3 );
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [1, 2, 3]
构造函数 Array(..) 不要求必须带 new 关键字。不带时,它会被自动补上。
因此 Array(1,2,3) 和 new Array(1,2,3) 的效果是一样的。
Array 构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度( length),而
非只充当数组中的一个元素。
这实非明智之举:一是容易忘记,二是容易出错。
更为关键的是,数组并没有预设长度这个概念。这样创建出来的只是一个空数组,只不过
它的 length 属性被设置成了指定的值。
如若一个数组没有任何单元,但它的 length 属性中却显示有单元数量,这样奇特的数据结
构会导致一些怪异的行为。而这一切都归咎于已被废止的旧特性(类似 arguments 这样的
类数组)。
我们将包含至少一个“空单元”的数组称为“稀疏数组”。
var a = new Array( 3 );
a.length; // 3
a;
a 在 Chrome 中显示为 [ undefined x 3 ](目前为止),这意味着它有三个值为 undefined
的单元,但实际上单元并不存在(“空单元” 这个叫法也同样不准确)。
var a = new Array( 3 );
var b = [ undefined, undefined, undefined ];
var c = [];
c.length = 3;
a;
b;
c;
我们可以创建包含空单元的数组,如上例中的 c。只要将 length 属性设置为
超过实际单元数的值,就能隐式地制造出空单元。另外还可以通过 delete
b[1] 在数组 b 中制造出一个空单元。
b 在当前版本的 Chrome 中显示为 [ undefined, undefined, undefined ],而 a 和 c 则显示
为 [ undefined x 3 ]。是不是感到很困惑?
更令人费解的是在当前版本的 Firefox 中 a 和 c 显示为 [ , , , ]。仔细看来,这其中有三
个逗号,代表四个空单元,而不是三个。
Firefox 在输出结果后面多添了一个 ,,原因是从 ES5 规范开始就允许在列表(数组值、属性列表等)末尾多加一个逗号(在实际处理中会被忽略不计)。所以如果你在代码或者调
试控制台中输入 [ , , , ],实际得到的是 [ , , ](包含三个空单元的数组)。这样做虽
然在控制台中看似令人费解,实则是为了让复制粘贴结果更为准确。
针对这种情况, Firefox 将 [ , , , ] 改为显示 Array [<3 empty slots>],这
无疑是个很大的提升。
更糟糕的是,上例中 a 和 b 的行为有时相同,有时又大相径庭:
a.join( "-" ); // "--"
b.join( "-" ); // "--"
a.map(function(v,i){ return i; }); // [ undefined x 3 ]
b.map(function(v,i){ return i; }); // [ 0, 1, 2 ]
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, "-" ); // "--"
从中可以看出, join(..) 首先假定数组不为空,然后通过 length 属性值来遍历其中的元
素。而 map(..) 并不做这样的假定,因此结果也往往在预期之外,并可能导致失败。
我们可以通过下述方式来创建包含 undefined 单元(而非“空单元”)的数组:
var a = Array.apply( null, { length: 3 } );
a; // [ undefined, undefined, undefined ]
apply(..) 是一个工具函数,适用于所有函数对象,它会以一种特殊的方式来调用传递给
它的函数。
假设在 apply(..) 内部该数组参数名为 arr, for 循环就会这样来遍历数组: arr[0]、
arr[1]、 arr[2]。 然 而, 由 于 { length: 3 } 中 并 不 存 在 这 些 属 性, 所 以 返 回 值 为
undefined
。
换句话说,我们执行的实际上是 Array(undefined, undefined, undefined),所以结果是单
元值为 undefined 的数组,而非空单元数组。
虽然 Array.apply( null, { length: 3 } ) 在创建 undefined 值的数组时有些奇怪和繁琐,
但是其结果远比 Array(3) 更准确可靠。
总之, 永远不要创建和使用空单元数组。
网友评论