内容大纲:
js 为基本数据类型值提供了封装对象,称为原生函数(如 String、Number、Boolean 等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String#trim() 和 Array#concat(..))。
对于简单标量基本类型值,比如 "abc",如果要访问它的 length 属性或 String.prototype 方法,js 引擎会自动对该值进行封装来实现这些属性和方法的访问。
原生函数
常用的原生函数(内建函数)有:
- String( )
- Numebr( )
- Boolean( )
- Array( )
- Object( )
- Function( )
- RegExp( )
- Date( )
- Error( )
- Symbol( ) ——ES6新加入的!
原生函数可以当作构造函数来用,但其构造出来的对象可能会和我们想象的不一样:
var a = new String( "abc" );
typeof a; // "object"
a instanceof String; // true
Object.prototype.toString.call(a); // "[object String]"
通过构造函数创建出来的时封装了基本类型值得封装对象。
内部属性[[Class]]
所有 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性[[Class]](我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问,一般通过Object.prototype.toString(..)
来查看。例如:
Obejct.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[obejct RegExp]"
大多数情况下,对象的内部 [[Class]] 属性和创建该对象的内建原生构造函数相对应,但并非总是如此。
那么基本类型值呢?下面先来看看 null 和 undefined:
Object.prototype.toString.call( null );
// "[object Null]"
Object.prototype.toString.call( undefined );
// "[object 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"。
封装对象包装
由于基本类型值没有 .length 和 .toString() 这样的属性和方法,所以需要通过封装对象才能访问。此时 js 会自动为基本类型值包装一个封装对象:
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
如果需要经常用这些字符串属性和方法,比如在 for 循环中使用 i < a.length,那么从一开始就创建一个封装对象也许更为方便,这样 js 引擎就不用每次都自动创建了。但实际证明这并不是一个好办法!因为浏览器已经为 .length 这样的常见情况做了性能优化,直接使用封装对象来“提前优化”代码反而会降低执行效率。
封装对象与 truthy 真值
var a = new Boolean(false);
if(!a) {
console.log( "Oops" ); // 执行不到这里
}
我们为 false 创建了一个封装对象,然而该对象是真值("truthy",即总是返回 true),所以这里使用封装对象得到的结果和使用 false 截然相反。
拆封
如果想要得到封装对象中的基本类型值,可以使用 valueOf( ) 函数:
var a = new String( "abc" );
a.valueOf(); // "abc"
隐式拆封:
var a = new String( "abc" );
var b = a + ""; // b的值为“abc”
typeof a; // "object"
typeof b; // "string"
Array(..) 的空单元与 undefined 单元
var a = new Array( 3 );
a.length; // 3
a; // [undefined x 3] in Chrome
a 在 Chrome 中的显示意味着它有三个值为 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(..) 却不一样,它会假定数组不为空,然后通过 length 属性值来遍历其中的元素。而 map(..) 并不做这样的假定,因此结果也往往在预期之外,并可能导致失败。
我们可以通过下述方式来创建包含 undefined 单元(而非“空单元”)的数组:
var a = Array.apply( null, { length: 3 } );
a; // [ undefined, undefined, undefined ]
apply(..) 是一个工具函数,适用于所有函数对象,它会以一种特殊的方式来调用传递给它的函数。
第一个参数是 this 对象,这里不用费心,暂将它设为 null。第二个参数则必须是个数组或类数组的值。
于是 Array.apply(..) 调用 Array(..) 函数,并将 { length: 3 } 作为函数的参数。
我们可以设想 apply(..) 内部有一个 for 循环,从 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) 更准确可靠。
总之,永远不要创建和使用空单元数组。
Date(..) 和 Error(..)
相较于其他的原生构造函数,Date(..) 和 Error(..) 的用处要大得多,因为没有对应的常量形式来作为它们的替代。
创建日期对象必须使用 new Date()。 Date(..) 可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。
Date(..) 主要用来获得当前的 Unix 时间戳。该值可以通过日期对象中的 getTime() 来获得。
从 ES5 开始引入了一个更简单的方法,即静态函数 Date.now()。对 ES5 之前的版本我们可以使用下面的 polyfill:
if(!Date.now){
Date.now = function(){
return (new Date()).getTime();
}
}
如果调用 Date() 时不带 new 关键字,则会得到当前日期的字符串值。其具体格式规范没有规定,浏览器使用“Fri Jul 18 2014 00:31:02 GMT-0500 (CDT)”这样的格式显示。
错误对象通常与 throw 一起使用:
function foo(x) {
if(!x) {
throw new Error( "x wasn't provided" );
}
}
Symbol(..)
ES6 中新加入了一个基本数据类型——符号。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。该类型的引入主要源于 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(..) 便可公开获得对象中的所有符号),但它却主要用于私有或特殊属性。很多开发人员喜欢用它来替代有下划线(_)前缀的属性,而下划线前缀通常用于命名私有或特殊属性。
符号并非对象,而是一种简单标量基本类型。
原生原型
原生构造函数有自己的 .prototype 对象,如 Array.prototype、String.prototype 等。
这些对象包含其对应子类型所特有的行为特征。
根据文档约定,我们将 String.prototype.XYZ 简写为 String#XYZ,对其他 .prototype 也同样如此。
- String#indexOf(..)
在字符串中找到指定子字符串的位置。 - String#charAt(..)
获得字符串指定位置上的字符。 - String#substr(..)、String#substring(..) 和 String#slice(..)
获得字符串的指定部分。 - String#toUpperCase() 和 String#toLowerCase()
- String#trim()
去掉字符串前后的空格,返回新的字符串。
以上方法并不改变原字符串的值,而是返回一个新字符串。
借助原型代理,所有字符串都可以访问这些方法。
网友评论