JavaScript 原生函数

680 阅读7分钟

常见的原生函数有:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

原生函数可以被当作构造函数来使用,但其构造出来的对象可能会和我们设想的有所出入:

var a = new String()

type of a // "object"

a instanceof String // true

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

console.log(a) // String { 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc" }

通过构造函数创建出来的是封装了基本类型值的封装函数

内部属性 [[Class]]

所有 typeof 返回值为 object的对象(如数组)都包含一个内部属性 [[Classs]]

我们可以把它看作一个内部的分类,该属性无法直接访问,通常通过 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“

其他基本类型值得情况有所不同,通常成为 ”包装“:

Object.prototype.toString.call("abc") // "[[object, String]]"

Object.prototype.toString.call(42) // "[[object, Number]]"

Object.prototype.toString.call(true) // "[[object, Boolean]]"

很显然, 基本类型值被各自的封装对象自动包装,所以它们的内部 [[Class]] 属性值分别为对应的内建原生构造函数

封装对象包装

封装对象扮演者十分重要的角色,由于基本类型值没有 .length.toString 这样的属性方法,需要通过封装对象才能访问,此时 JavaScript 会自动为基本类型值包装一个封装对象

var a = "abc"

a.length // 3

a.toUpperCase() // "ABC"

如果需要经常用到这些字符串属性和方法,比如在for循环中使用i < a.length,那么从 一开始就创建一个封装对象也许更为方便,这样 JavaScript 引擎就不用每次都自动创建了

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

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

注意

var a = new Boolean(false)

if (!a) {
    console.log('handsome boy') // 执行不到这里
}

这里为 false 创建了一个封装对象,显然该对象是真值

如果想要自行封装基本类型值,可以使用 Object(...) 函数

var a = "abc"
var b = new String(a)
var c = Object(a)

typeof a // "string"
typeof b // "obejct"
typeof c // "object"

b instanceof String // true
c instanceof String // true

Obejct.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 // "obejct"
typeof b // "string"

原生函数作为构造函数

关于 arrayobjectfunction正则表达式, 我们通常喜欢以常量的形式来创建它们,实际上,使用常量和使用构造函数的效果是一样的———创建的值都是通过封装函数对象来包装

Array(..)

var a = new Array(1, 2, 3)
a // [1, 2, 3]

var b = [1, 2, 3]
b // [1, 2, 3]

构造函数 Array只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素

var a = new Array(2)
a // []
a.length // 2

属实耐人寻味,这样奇特的数据结构会导致一些怪异行为,而一切都归咎于已被废止的就特性(类似于 arguments 这样的类数组)

我们将包含至少一个 ”空单元“ 的数组称为 ”稀疏数组“

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

我们可以创建包含空单元的数组,如上例中的 c,只要将 length 属性设置为超过实际单元数的值,就能隐式地制造出空单元,另外还可以通过 delete b[1] 在数组 b 中制造出一个空单元

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

a.join("-") // "- -"
b.join("-") // "- -"

a.map((v, i) => {
    return i
})
// [ undefined x 3 ]

b.map((v, i) => {
    return i
})
// [0, 1, 2]

join(...) 首先假定数组不为空,然后通过 length 属性值来遍历其中的元素。而 map(...) 并不做这样的假定

我们可以通过以下方式创建包含 undefined 单元(而非 ”空单元“)的数组:

var a = Array.apply(null, { length: 3 })
a // [ undefined, undefined, undefined ]

虽然 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 只在极少数情况下很有用,比如动态定义函数参数和函数体的时候

强烈建议使用常量形式 (如/^a*b+/g)来定义正则表达式,这样不仅语法简单,执行效率也更高,因为 JavaScript 引擎在代码执行前会对它们进行预编译和缓存

RegExp(..) 有时候还是很有用的,比如动态定义正则表达式

var name = "Kyle"
var namePattern = new RegExp("\\b(?:" + name + ")+\\b", "ig");

var matches = someText.match(namePattern)

Date(...)Error(...)

相较于其他原生构造函数, Date(...)Error(...) 的用处要大很多,因为没有对应的常量形式来作为它们的替代

Date(...) 主要用来获得当前的 Unix 时间戳,该值可以通过日期对象中的 getTime() 来获得, ES5 静态函数 Date.now() 也可以

如果调用 Date() 时不带 new 关键字,则会得到当前日期的字符串值

构造函数 Error(...) 带不带 new 关键字都可

创建错误对象,主要是为了获得当前运行栈的上下文,大部分 JavaScript 引擎通过只读属性 .stack 来访问,栈上下文信息包括函数调用栈信息和产生错误的代码行号,以便于 debug

function foo (x) {
  if(!x){
    throw new Error("x wasn't provided")
  }
}

通常错误对象至少包含一个 message 属性,有时也不乏其他属性(必须作为只读属性访问),如 type。除了访问 stack 属性外,最好的办法是调用 toString() 来获得经过格式化的便于阅读的错误信息

除了 Error(...) 之外,还有一些针对特定错误类型的原生构造函数,如 EvalError(...)RangeError(...)ReferenceError(...)SyntaxError(...)TypeError(...)URIError(...) , 这些构造函数很少直接被使用,它们在程序发生异常的时候会被自动调用

Symbol(...)

ES6 中新加了一个基本数据类型 ———— 符号(Symbol)

符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名

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

我们可以使用 Symbol(..) 原生构造函数来自定义符号

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.prototypeString.prototype

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

例如,将字符串值封装为字符串对象之后,就能访问 String.prototype 中定义的方法

  • String.prototype.indexOf(...)

    在字符串中找到指定子字符串的位置

  • String.prototype.charAt(...)

    获得字符串指定位置的字符

  • String.prototype.substr(...)String.prototype.substring(...)String.prototype.slice(...)

    获得字符串指定部分

  • String.ptototype.toUpperCase()String.prototype.toLowerCase()

    将字符串转换大小写

  • String.prototype.trim()

    去掉字符串前后的空格,返回新的字符串

以上方法并不改变原字符串的值,而是返回一个新字符串

其他构造函数的原型包含它们各自类型所特有的行为特征,比如 Number.prototype.toFixed(..)Array.protptype.concat(...),所有你的函数都可以调用 Function.prototype 中的 apply(...)call(...)bind(...)

然而,有些原生函数并非普通对象那么简单

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

总结

JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 StringNumberBoolean 等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如 String.prototype.trim()Array.prototype.cancat(...)

对简单标量基本类型值,比如 “abc”, 如果要访问它的 length 属性 或 String.prototype 方法, JavaScript 引擎会自动对该值进行封装来实现对这些属性和方法的访问