原生函数

常用的原生函数有:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()——ES6 中新加入的!

实际上,它们就是内建函数。

原生函数可以被当作构造函数来使用,但是你得到的就不是常量了,而是对象。

var a = new String("abc");

typeof a; // 是"object",不是"String"

a instanceof String; // true

Object.prototype.toString.call(a); // "[object String]"

new String("abc") 创建的是字符串 "abc" 的封装对象,而非基本类型值 "abc"。

内部属性 [[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"。

对象包装器

由于基本类型值没有.length.toString()这样的属性和方法,需要通过封装对象才能访问,此时 JavaScript 会自动为基本类型值转为一个包装对象(封装对象):

var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"

理论上,如果我们频繁的去调用基本类型的方法,我们应该一开始就创建成一个包装对象会更加合适,比如for循环中调用.length属性,这样JavaScript 引擎就不用每次都自动创建了。

但实际证明这并不是一个好办法,因为浏览器已经为 .length 这样的常见情况做了性能优化,直接使用封装对象来“提前优化”代码反而会降低执行效率。

一般情况下,我们不需要直接使用封装对象。最好的办法是让 JavaScript 引擎自己决定什么时候应该使用封装对象。换句话说,就是应该优先考虑使用 "abc" 和 42 这样的基本类型值,而非 new String("abc") 和 new Number(42)。

注意:

使用封装对象时有些地方需要特别注意。

比如 Boolean:

var a = new Boolean(false);
if (!a) {
    console.log("Oops"); // 执行不到这里
}

因为得到的是一个对象,而不是基本类型了。

如果想要自行封装基本类型值,可以使用 Object(..) 函数(不带 new 关键字)。

Object构造函数将给定的值包装为一个新对象。

  • 如果给定的值是 null 或 undefined, 它会创建并返回一个空对象
  • 否则,它将返回一个和给定的值相对应的类型的对象
  • 如果给定值是一个已经存在的对象,则会返回这个已经存在的值(相同地址)

其实可以把Object(..) 函数看做是一个自动判断类型的包装器。

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),但它们偶尔也会派上用场。

拆封

如果我们想要得到包装对象的基本类型,可以使用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"

原生函数作为构造函数

我们应该尽量避免使用构造函数,除非十分必要,因为它们经常会产生意想不到的结果

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 属性中却显示有单元数量,这样奇特的数据结构会导致一些怪异的行为。

而不同的浏览器去展示空单元时却又不太一样,有的用undefined去表示空单元,有的则显示空格,有的展示为<empty slots>

事实上undefined和空单元在一些行为上是不一样的:

var a = new Array(3);
var b = [undefined, undefined, undefined];

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) 更准确可靠。

总之,永远不要创建和使用空单元数组。

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

同样,除非万不得已,否则尽量不要使用 Object(..)/Function(..)/RegExp(..)。

Object的形式繁琐,还不能直接定义属性。

Function一般都是拿来当eval的替代品,但是用的也非常少。

RegExp在一些需要动态正则的时候使用,但是用的也比较少。

Date(..) 和 Error(..)

创建日期必须使用new Date(),常见的用法可能就是获取当前时间的时间戳:

new Date().getTime()

es5提供了一个更加便捷的方法:

Date.now()

Error()和数字Array类似,都可以不带new关键字使用,创建一个错误对象主要是为了获取当前运行栈的上下文。栈上下文信息包括函数调用栈信息和产生错误的代码行号,以便于调试(debug)。

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

function foo(x) {
    if (!x) {
        throw new Error("x wasn’t provided");
    }
    // ..
}

Symbol(..)

ES6 中新加入了一个基本数据类型 ——符号(Symbol)。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。该类型的引入主要源于 ES6 的一些特殊构造,此外符号也可以自行定义。

符号可以用作属性名,但无论是在代码还是开发控制台中都无法查看和访问它的值,只会显示为诸如 Symbol(Symbol.create) 这样的值。

ES6 中有一些预定义符号,以 Symbol 的静态属性形式出现,如 Symbol.create、Symbol.iterator 等,可以这样来使用:

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

Symbol构造函数比较特殊,不能使用new关键词,否则会报错。

虽然符号实际上并非私有属性(通过 Object.getOwnPropertySymbols(..) 便可以公开获得对象中的所有符号),但它却主要用于私有或特殊属性。很多开发人员喜欢用它来替代有下划线(_)前缀的属性,而下划线前缀通常用于命名私有或特殊属性。

符号并非对象,而是一种简单标量基本类型。

原生原型

原生构造函数有自己的 .prototype 对象,如 Array.prototype、String.prototype 等。

这些对象包含其对应子类型所特有的行为特征。

但是并不是原型都是键值对对象:

typeof Function.prototype; // "function"
Function.prototype(); // 空函数!

RegExp.prototype.toString(); // "/(?:)/"——空正则表达式
"abc".match(RegExp.prototype); // [""]

Array.isArray(Array.prototype); // true

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

使用这种做法的好处就是不需要频繁创建默认值,如果使用字面量每次运行都是创建一次,这样无疑会造成内存和 CPU 资源的浪费。

但是如果默认值会被修改,那么就不要使用这种方式,因为这是原型,是可以影响到所有实例的。

es6开始为函数提供了默认值,所以我们一般都不需要这么处理了。

分类: 你不知道的JavaScript 标签: 函数原生函数包装器拆包拆封原生原型

评论

暂无评论数据

暂无评论数据

目录