你不知道的JavaScript(中卷)|原生函数

常见的原生函数有:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()..ES6 拞怴壛擖揑両
    实际上它们就是内建函数。

内部属性[[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]]属性值是“Array”,正则表达式的值是“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”。

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

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

封装对象释疑

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

我们为false创建了一个封装对象,然而该对象是真值,所以这里使用封装对象得到的结果和使用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]"

但一般不推荐直接使用封装对象,但它们偶尔也会派上用场。

拆封
如果想要得到封装对象中的基本类型值,可以使用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)、对象(object)、函数(function)和正则表达式,我们通常喜欢以常量的形式来创建它们。实际上,使用常量和使用构造函数的效果是一样的(创建的值都是通过封装对象来包装)。

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),而非只充当数组中的一个元素。

var a = new Array( 3 );
var b = [ undefined, undefined, undefined ];
var c = [];
c.length = 3;
a;//[ undefined x 3 ]
b;// [ undefined, undefined, undefined ]
c;//[ undefined x 3 ]

我们可以创建包含空单元的数组,如上例中的c。只要将length属性设置为超过实际单元数的值,就能隐式地制造出空单元。另外还可以通过delete b[1]在数组b中制造出一个空单元。
在当前版本的Firefox中a和c显示为[ , , , ]。仔细看来,这其中有三个逗号,代表四个孔单元,而不是三个。Firefox在输出结果后面多添了一个,,原因是从ES5规范开始就允许在列表(数组值、属性列表等)末尾多加一个逗号(在实际处理中会被忽略不计)。
针对这种情况,Firefox将[ , , , ]改为显示Array[<3 empty slots>],这无疑是个很大 的提升。

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(..)是一个工具函数,适用于所有函数对象,它会以一种特殊的方式来调用传递给它的函数。Array.apply(..)调用Array(..)函数,并且将{length:3}作为函数的参数。虽然Array.apply(null,{length:3})在创建undefined值的数组时有些奇怪和繁琐,但是其结果远比Array(3)更准确可靠。
总之,永远不要创建和使用空单元数组。

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只在极少数情况下很有用,比如动态定义函数参数和函数体的时候。不要把Function(..)当做eval(..)的替代品,你基本上不会通过这种方式来定义函数。

Date(..) 和Error(..)
相较于其他原生构造函数,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();
    };
}

如果调用Date()时不带new关键字,则会得到当前日期的字符串值。其具体格式规范没有规定,浏览器使用"Fri Jul 18 2014 00:31:02 GMT-0500 (CDT)"这样的格式来显示。
构造函数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()来获得经过格式化的便于阅读的错误信息。

除Error(..) 之外, 还有一些针对特定错误类型的原生构造函数, 如
EvalError(..)、 RangeError(..)、 ReferenceError(..)、 SyntaxError(..)、
TypeError(..) 和URIError(..)。这些构造函数很少被直接使用,它们在程序
发生异常(比如试图使用未声明的变量产生ReferenceError 错误)时会被自
动调用。

Symbol(..)
ES6中新加入了一个基本数据类型——符号(Symbol)。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。
符号可以用作属性名,但无论是在代码还是开发控制台中都无法查看和访问它的值,只会显示为诸如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) ]

原生原型
原生构造函数有自己的.prototype对象,如Array.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()
    去掉字符串前后的空格,返回新的字符串。
    以上方法并不改变原字符串的值,而是返回一个新字符串。
    借助原型代理,所有字符串都可以访问这些方法:
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/
);

从ES6开始,我们不再需要使用vals = vals || ...这样的方式来设置默认值,因为默认值可以通过函数声明中的内置语法来设置。

这种方法的一个好处是.prototype已被创建并且仅创建一次。相反,如果将[]、function(){}和/(?:)/作为默认值,则每次调用isThisCool(..)时它们都会被创建一次(具体创建与否取决于JavaScript引擎,稍后它们可能会被垃圾回收),这样无疑会造成内存和CPU资源的浪费。
另外需要注意的一点是,如果默认值随后会被更改,那就不要使用Array.prototype。上例中的vals是作为只读变量来使用,更改vals实际上就是更改Array.prototype,而这样会导致前面提到过的一系列问题!

。。。。。。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容