中卷(1.3)

2018-08-13  本文已影响5人  风声233

内容大纲:
js 为基本数据类型值提供了封装对象,称为原生函数(如 String、Number、Boolean 等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String#trim() 和 Array#concat(..))。
对于简单标量基本类型值,比如 "abc",如果要访问它的 length 属性或 String.prototype 方法,js 引擎会自动对该值进行封装来实现这些属性和方法的访问。

原生函数

常用的原生函数(内建函数)有:

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 也同样如此。

以上方法并不改变原字符串的值,而是返回一个新字符串。
借助原型代理,所有字符串都可以访问这些方法。

阅读下一篇

上一篇下一篇

猜你喜欢

热点阅读