【JavaScript】原生函数
什么是原生函数
原生函数 (Native Functions), 也称为内置函数或内建构造函数, 是 JavaScript 语言内置的全局对象, 用于创建和处理特定类型的数据.
常用原生函数列表
基本类型相关:
String
- 字符串Number
- 数字Boolean
- 布尔值Symbol
- 符号 (ES6+)BigInt
- 任意精度整数 (ES2020+)
引用类型相关:
Object
- 对象Array
- 数组Function
- 函数RegExp
- 正则表达式Date
- 日期Error
- 错误对象
注意: null
和 undefined
虽然是基本类型, 但没有对应的构造函数.
原生函数的两种用法
原生函数可以作为普通函数调用, 也可以作为构造函数使用 (配合 new
关键字).
作为普通函数调用
直接调用原生函数通常用于类型转换:
String(123); // "123"
Number("456"); // 456
Boolean(0); // false
作为构造函数使用
使用 new
关键字会创建对应类型的封装对象:
const str = new String("abc");typeof str; // "object" (不是 "string")
str instanceof String; // true
Object.prototype.toString.call(str); // "[object String]"
通过构造函数创建的是封装对象 (wrapper object), 而非基本类型值本身.
两者的区别
// 普通调用: 返回基本类型值
const a = String("hello");
typeof a; // "string"// 构造函数调用: 返回封装对象
const b = new String("hello");
typeof b; // "object"// 两者的值相等, 但类型不同
a == b; // true (值相等)
a === b; // false (类型不同)
特殊情况
Symbol 和 BigInt 不能使用 new
:
// Symbol 不能作为构造函数
const sym = Symbol("description"); // 正确
// const sym = new Symbol("desc"); // TypeError: Symbol is not a constructor// BigInt 也不能作为构造函数
const big = BigInt(123); // 正确
// const big = new BigInt(123); // TypeError: BigInt is not a constructor
Array 和 Function 可省略 new
:
// 以下两种写法效果相同
const arr1 = new Array(1, 2, 3);
const arr2 = Array(1, 2, 3);const fn1 = new Function("a", "return a * 2");
const fn2 = Function("a", "return a * 2");
基本类型与封装对象
基本类型值 vs 封装对象
JavaScript 有 7 种基本类型:
string
number
boolean
symbol
bigint
null
undefined
基本类型值本身没有属性和方法, 但我们却能调用它们的方法:
const str = "hello";
str.length; // 5
str.toUpperCase(); // "HELLO"
这是因为 JavaScript 引擎会自动进行装箱操作.
自动装箱 (Auto-boxing)
当访问基本类型值的属性或方法时, JavaScript 会自动完成以下步骤:
- 创建对应类型的封装对象
- 调用封装对象上的方法
- 立即销毁该封装对象
// 等价于以下过程:
const str = "hello";
str.toUpperCase();// 1. 创建临时封装对象
// const temp = new String("hello");
// 2. 调用方法
// temp.toUpperCase();
// 3. 销毁临时对象
重要特性: 封装对象是临时的, 无法为基本类型值添加属性:
const str = "test";
str.customProp = "value";
console.log(str.customProp); // undefined (属性丢失)
拆箱 (Unboxing)
获取封装对象中的基本类型值, 使用 valueOf()
方法:
const strObj = new String("abc");
const numObj = new Number(42);
const boolObj = new Boolean(true);strObj.valueOf(); // "abc"
numObj.valueOf(); // 42
boolObj.valueOf(); // true
隐式拆箱会在需要基本类型值的上下文中自动发生:
const a = new String("hello");
const b = a + " world"; // 隐式调用 valueOf()typeof a; // "object"
typeof b; // "string"
b; // "hello world"
使用建议与最佳实践
避免直接使用封装对象:
// 不推荐: 使用封装对象
const str = new String("abc");
const num = new Number(42);// 推荐: 使用基本类型值
const str = "abc";
const num = 42;
封装对象的陷阱:
// Boolean 封装对象始终为真值
const falseObj = new Boolean(false);if (falseObj) {console.log("执行了!"); // 会执行 (对象是真值)
}// 基本类型值的行为符合预期
const falsePrimitive = false;if (falsePrimitive) {console.log("不会执行"); // 不会执行
}
最佳实践:
- 优先使用基本类型值 (
"abc"
,42
,true
) - 避免使用
new String()
,new Number()
,new Boolean()
- 让 JavaScript 引擎自动处理装箱和拆箱
- 仅在类型转换时直接调用构造函数 (不使用
new
)
Object 作为通用包装类构造函数
除了使用 String, Number, Boolean 构造函数创建对应的封装对象, 还可以使用 Object
构造函数, 它会根据参数类型自动返回对应包装类的实例:
// 传入字符串, 返回 String 实例
const strObj = new Object("hello");
strObj instanceof String; // true
typeof strObj; // "object"// 传入数值, 返回 Number 实例
const numObj = new Object(42);
numObj instanceof Number; // true// 传入布尔值, 返回 Boolean 实例
const boolObj = new Object(true);
boolObj instanceof Boolean; // true
这种方式虽然可行, 但不推荐使用, 原因:
- 代码意图不明确 (无法直接看出要创建哪种类型)
- 性能略低于直接使用对应构造函数
- 不符合现代 JavaScript 最佳实践
注意: null
和 undefined
没有对应的包装类, 传入 Object
构造函数会返回空对象:
const nullObj = new Object(null);
const undefinedObj = new Object(undefined);nullObj; // {}
undefinedObj; // {}
对象类型检测
typeof 的局限性
typeof
对于对象类型的检测能力有限:
typeof []; // "object" (无法区分数组)
typeof null; // "object" (历史遗留 bug)
typeof new Date(); // "object"
typeof /regex/; // "object"
typeof new String("abc"); // "object"
Object.prototype.toString() 方法
更准确的类型检测方式是使用 Object.prototype.toString()
:
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
Object.prototype.toString.call(/regex/i); // "[object RegExp]"
Object.prototype.toString.call(new Date()); // "[object Date]"
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]"
Object.prototype.toString.call(Symbol()); // "[object Symbol]"
Object.prototype.toString.call(123n); // "[object BigInt]"
Symbol.toStringTag
ES6 引入了 Symbol.toStringTag
, 允许自定义对象的类型标签:
// 默认行为
class MyClass {}
Object.prototype.toString.call(new MyClass()); // "[object Object]"// 使用 Symbol.toStringTag 自定义
class MyCustomClass {get [Symbol.toStringTag]() {return "MyCustomClass";}
}Object.prototype.toString.call(new MyCustomClass()); // "[object MyCustomClass]"
内置对象的 Symbol.toStringTag:
JavaScript 内置对象 (如 Map, Set, Promise) 内部也定义了 Symbol.toStringTag
属性, 这就是 Object.prototype.toString()
能够识别它们类型的原因:
// 内置对象的 Symbol.toStringTag 属性
const map = new Map();
map[Symbol.toStringTag]; // "Map"const set = new Set();
set[Symbol.toStringTag]; // "Set"const promise = Promise.resolve();
promise[Symbol.toStringTag]; // "Promise"// toString() 方法读取这个属性来生成类型字符串
Object.prototype.toString.call(map); // "[object Map]"
Object.prototype.toString.call(set); // "[object Set]"
Object.prototype.toString.call(promise); // "[object Promise]"
自定义对象使用 Symbol.toStringTag:
我们可以在自定义类中使用相同的机制, 让对象有更友好的类型标识:
class Collection {constructor(items) {this.items = items;}get [Symbol.toStringTag]() {return "Collection";}
}const col = new Collection([1, 2, 3]);// 自定义的类型标签
col[Symbol.toStringTag]; // "Collection"// toString() 会使用这个标签
Object.prototype.toString.call(col); // "[object Collection]"// 对比: 没有定义 Symbol.toStringTag 的普通类
class NormalClass {}
const normal = new NormalClass();
Object.prototype.toString.call(normal); // "[object Object]"
注意事项:
由于 Symbol.toStringTag
可以被随意修改, 不应完全依赖 toString()
进行类型检测:
const fakeArray = {get [Symbol.toStringTag]() {return "Array";},
};Object.prototype.toString.call(fakeArray); // "[object Array]"
Array.isArray(fakeArray); // false (更可靠)
原生原型
原型对象的特性
每个原生构造函数都有对应的 prototype
对象, 包含该类型的方法和属性.
// 字符串方法来自 String.prototype
String.prototype.toUpperCase; // [Function: toUpperCase]
"hello".toUpperCase === String.prototype.toUpperCase; // true// 数组方法来自 Array.prototype
Array.prototype.map; // [Function: map]
[1, 2, 3].map === Array.prototype.map; // true
特殊的原生原型: 某些原生原型本身就是对应类型的实例.
// Function.prototype 是一个函数
typeof Function.prototype; // "function"
Function.prototype(); // undefined (空函数)// Array.prototype 是一个数组
Array.isArray(Array.prototype); // true
Array.prototype.length; // 0// RegExp.prototype 是一个正则表达式
RegExp.prototype.toString(); // "/(?:)/"
"test".match(RegExp.prototype); // [""]
修改原型的风险
虽然可以修改原生原型, 但这是强烈不推荐的做法:
// 不推荐: 修改原生原型
Array.prototype.customMethod = function () {return "custom";
};[1, 2, 3].customMethod(); // "custom"// 风险 1: 影响所有数组
const arr1 = [1, 2];
const arr2 = [3, 4];
arr1.customMethod === arr2.customMethod; // true (共享)// 风险 2: 可能与未来标准冲突
// 如果 ES 未来添加同名方法, 会导致不兼容// 风险 3: 影响 for...in 遍历
for (let key in [1, 2, 3]) {console.log(key); // "0", "1", "2", "customMethod"
}
可修改原型的例外情况:
// Polyfill: 为旧浏览器添加标准方法
if (!Array.prototype.includes) {Array.prototype.includes = function (searchElement) {return this.indexOf(searchElement) !== -1;};
}
现代最佳实践
不要使用原生原型作为默认值:
// 不推荐 (旧做法)
function oldWay(vals, fn, rx) {vals = vals || Array.prototype;fn = fn || Function.prototype;rx = rx || RegExp.prototype;return rx.test(vals.map(fn).join(""));
}// 推荐 (使用默认参数)
function modernWay(vals = [], fn = (v) => v, rx = /.*/) {return rx.test(vals.map(fn).join(""));
}// 或者使用解构赋值
function betterWay({ vals = [], fn = (v) => v, rx = /.*/ } = {}) {return rx.test(vals.map(fn).join(""));
}
原因:
- ES6+ 默认参数更清晰, 性能更好
- 避免意外修改原生原型
- 代码可读性更强
不要修改内置对象的原型:
// 不推荐: 扩展内置对象
String.prototype.reverse = function () {return this.split("").reverse().join("");
};// 推荐: 使用独立函数
function reverseString(str) {return str.split("").reverse().join("");
}// 或者使用工具类
class StringUtils {static reverse(str) {return str.split("").reverse().join("");}
}