## 3.3 原生函数
常用的原生函数有:
• String()
• Number()
• Boolean()
• Array()
• Object()
• Function()
• RegExp()
• Date()
• Error()
• Symbol()——ES6 中新加入的!
实际上,它们就是内建函数。
原生函数可以被当作构造函数来使用,通过构造函数(如new String("abc"))创建出来的是**封装了基本类型值(如"abc")的封装对象**。
~~~
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"
~~~
可以这样来查看封装对象:
~~~
console.log( a );
~~~
### 3.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"。
其他基本类型值(如字符串、数字和布尔)的情况有所不同,通常称为“**包装**”(boxing)。
~~~
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"。
### 3.3.2 封装对象包装
由于基本类型值没有`.length`和`.toString()` 这样的属性和方法,需要通过封装对象才能访问,此时JavaScript 会自动为基本类型值包装(box 或者wrap)一个封装对象:
~~~
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
~~~
#### 封装对象释疑
使用封装对象时有些地方需要特别注意。
比如Boolean:
~~~
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // 执行不到这里
}
~~~
我们为false 创建了一个封装对象,然而该对象是真值(“truthy”,即总是返回true),所以这里使用封装对象得到的结果和使用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.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.3.4 原生函数作为构造函数
关于数组(array)、对象(object)、函数(function)和正则表达式,通常以常量的形式来创建它们。实际上,使用常量和使用构造函数的效果是一样的(创建的值都是通过封装对象来包装)。
**1. Array()**
~~~
var a = new Array( 1, 2, 3 );
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [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;
~~~
b 在当前版本的Chrome 中显示为[ undefined, undefined, undefined ],而a 和c 则显示为[ undefined x 3 ]。
更糟糕的是,上例中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(..) 是一个工具函数,适用于所有函数对象,它会以一种特殊的方式来调用传递给它的函数。
第一个参数是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 } ) `在创建`undefined `值的数组时有些奇怪和繁琐,但是其结果远比Array(3) 更准确可靠。总之,**永远不要创建和使用空单元数组**。
**2. Object(..)、Function(..) 和RegExp(..)**
同样,除非万不得已,否则尽量不要使用`Object(..)/Function(..)/RegExp(..)`:
~~~
var c = new Object();
c.foo = "bar";
c; // { foo: "bar" }
var d = { foo: "bar" };
d; // { foo: "bar" }
var e = new Function( "a", "return a * 2;" );
var f = function(a) { return a * 2; }
function g(a) { return a * 2; }
var h = new RegExp( "^a*b+", "g" );
var i = /^a*b+/g;
~~~
在实际情况中没有必要使用new Object() 来创建对象,因为这样就无法像常量形式那样一次设定多个属性,而必须逐一设定。
构造函数Function 只在极少数情况下很有用,比如动态定义函数参数和函数体的时候。
强烈建议使用常量形式(如`/^a*b+/g`)来定义正则表达式,这样不仅语法简单,执行效率也更高,因为JavaScript 引擎在代码执行前会对它们进行预编译和缓存。与前面的构造函数不同,RegExp(..) 有时还是很有用的,比如动态定义正则表达式时:
~~~
var name = "Kyle";
var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" );
var matches = someText.match( namePattern );
~~~
上述情况在JavaScript 编程中时有发生,这时`new RegExp("pattern","flags") `就能派上用场。
**3. Date(..) 和Error(..)**
创建日期对象必须使用`new Date()`。`Date(..) `可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。`Date(..) `主要用来获得当前的Unix 时间戳(从1970 年1 月1 日开始计算,以秒为单位)。该值可以通过日期对象中的getTime() 来获得。
从ES5 开始引入了一个更简单的方法,即静态函数Date.now()。对ES5 之前的版本可以使用下面的polyfill:
~~~
if (!Date.now) {
Date.now = function(){
return (new Date()).getTime();
};
}
~~~
构造函数`Error(..)`(与前面的Array() 类似)带不带new 关键字都可。创建错误对象(error object)主要是为了获得当前运行栈的上下文(大部分JavaScript 引擎通过只读属性.stack 来访问)。栈上下文信息包括函数调用栈信息和产生错误的代码行号,
以便于调试(debug)。
错误对象通常与throw 一起使用:
~~~
function foo(x) {
if (!x) {
throw new Error( "x wasn’t provided" );
}
// ..
}
~~~
通常错误对象至少包含一个`message` 属性,有时也不乏其他属性(必须作为只读属性访问),如type。除了访问stack 属性以外,最好的办法是调用(显式调用或者通过强制类型转换隐式调用)`toString()` 来获得经过格式化的便于阅读的错误信息。
**4. Symbol(..)**
ES6 中新加入了一个基本数据类型 ——**符号(Symbol)**。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。该类型的引入主要源于ES6 的一些特殊构造,此外符号也可以自行定义。
符号可以用作属性名,但无论是在代码还是开发控制台中都无法查看和访问它的值,只会显示为诸如`Symbol(Symbol.create)` 这样的值。
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) ]
~~~
虽然符号实际上并非私有属性(通过`Object.getOwnPropertySymbols(..)` 便可以公开获得对象中的所有符号),但它却主要用于私有或特殊属性。很多开发人员喜欢用它来替代有下划线`(_)`前缀的属性,而下划线前缀通常用于命名私有或特殊属性。
**5. 原生原型**
原生构造函数有自己的`.prototype` 对象,如Array.prototype、String.prototype 等。这些对象包含其对应子类型所特有的行为特征。
例如,将字符串值封装为字符串对象之后,就能访问String.prototype 中定义的方法。(将`String.prototype.XYZ` 简写为`String#XYZ`, 对其他`.prototypes `也同样如此。)
~~~
• String#indexOf(..)
在字符串中找到指定子字符串的位置。
• String#charAt(..)
获得字符串指定位置上的字符。
• String#substr(..)、String#substring(..) 和String#slice(..)
获得字符串的指定部分。
• String#toUpperCase() 和String#toLowerCase()
将字符串转换为大写或小写。
• String#trim()
去掉字符串前后的空格,返回新的字符串。
~~~
以上方法并不改变原字符串的值,而是返回一个新字符串。
借助原型代理(prototype delegation),所有字符串都可以访问这些方法:
~~~
var a = " abc ";
a.indexOf( "c" ); // 3
a.toUpperCase(); // " ABC "
a.trim(); // "abc"
~~~
其他构造函数的原型包含它们各自类型所特有的行为特征,比如`Number#tofixed(..)`(将数字转换为指定长度的整数字符串)和`Array#concat(..)`(合并数组)。所有的函数都可以调用`Function.prototype 中的apply(..)、call(..) 和bind(..)`。
然而,有些原生原型(native prototype)并非普通对象那么简单:
~~~
typeof Function.prototype; // "function"
Function.prototype(); // 空函数!
RegExp.prototype.toString(); // "/(?:)/"——空正则表达式
"abc".match( RegExp.prototype ); // [""]
~~~
更糟糕的是,我们甚至可以修改它们(而不仅仅是添加属性):
~~~
Array.isArray( Array.prototype ); // true
Array.prototype.push( 1, 2, 3 ); // 3
Array.prototype; // [1,2,3]
// 需要将Array.prototype设置回空,否则会导致问题!
Array.prototype.length = 0;
~~~
这里,`Function.prototype` 是一个函数,`RegExp.prototype` 是一个正则表达式,而`Array.prototype` 是一个数组。
**将原型作为默认值**
`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简介
- 第三章 基本概念
- 3.1-3.3 语法、关键字和变量
- 3.4 数据类型
- 3.5-3.6 操作符、流控制语句(暂略)
- 3.7函数
- 第四章 变量的值、作用域与内存问题
- 第五章 引用类型
- 5.1 Object类型
- 5.2 Array类型
- 5.3 Date类型
- 5.4 基本包装类型
- 5.5 单体内置对象
- 第六章 面向对象的程序设计
- 6.1 理解对象
- 6.2 创建对象
- 6.3 继承
- 第七章 函数
- 7.1 函数概述
- 7.2 闭包
- 7.3 私有变量
- 第八章 BOM
- 8.1 window对象
- 8.2 location对象
- 8.3 navigator、screen与history对象
- 第九章 DOM
- 9.1 节点层次
- 9.2 DOM操作技术
- 9.3 DOM扩展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件处理程序
- 10.3 事件对象
- 10.4 事件类型
- 第十一章 JSON
- 11.1-11.2 语法与序列化选项
- 第十二章 正则表达式
- 12.1 创建正则表达式
- 12.2-12.3 模式匹配与RegExp对象
- 第十三章 Ajax
- 13.1 XMLHttpRequest对象
- 你不知道的JavaScript
- 一、作用域与闭包
- 1.1 作用域
- 1.2 词法作用域
- 1.3 函数作用域与块作用域
- 1.4 提升
- 1.5 作用域闭包
- 二、this与对象原型
- 2.1 关于this
- 2.2 全面解析this
- 2.3 对象
- 2.4 混合对象“类”
- 2.5 原型
- 2.6 行为委托
- 三、类型与语法
- 3.1 类型
- 3.2 值
- 3.3 原生函数