别再被 JavaScript 类型困扰了!深入剖析 toString.call() 与 Symbol.toStringTag 的秘密

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777

JavaScript 数据类型

在 JavaScript 中,所有的值都归属于某种类型。理解这些类型及其特性,是掌握 JavaScript 的基础。

📌 类型分类

JavaScript 的类型系统分为两大类:

  • 原始类型(Primitive Types):不可变,按值传递。

  • 引用类型(Reference Types):可变,按引用传递。

🧱 原始类型(Primitive Types)

共有 7 种原始类型:

1. Undefined

表示变量已声明但尚未赋值,或访问了未定义的属性。

js 复制代码
let a;
console.log(a); // undefined

2. Null

表示"无值"或"空对象引用"。通常用来主动清空某个变量。

js 复制代码
let b = null;
console.log(typeof b); // "object"(这是历史遗留 bug)

3. Boolean

表示逻辑值,仅有两个:truefalse

js 复制代码
let isLoggedIn = false;

常用于条件判断:

js 复制代码
if (isLoggedIn) {
  console.log("欢迎回来!");
}

4. Number

JavaScript 中的数字类型包括整数和浮点数,统一为 number 类型。

js 复制代码
let count = 42;
let pi = 3.14159;

支持特殊数值:

js 复制代码
Infinity;
-Infinity;
NaN; // Not-a-Number,表示非数字

5. String

用于表示文本,使用 '单引号'"双引号"模板字符串

js 复制代码
let name = "Alice";
let greeting = `Hello, ${name}!`;

6. Symbol(ES6 引入)

表示独一无二的值,常用于对象属性的私有键。

js 复制代码
const sym1 = Symbol("id");
const sym2 = Symbol("id");

console.log(sym1 === sym2); // false

7. BigInt(ES2020 引入)

用于表示超过 Number.MAX_SAFE_INTEGER 的大整数。

js 复制代码
const big = 900719925474099100000n;
console.log(typeof big); // "bigint"

🧩 引用类型(Reference Types)

引用类型可以存储多个值或复杂结构。它们通过引用进行赋值和比较。

Object(通用对象)

js 复制代码
const user = {
  name: "Alice",
  age: 25,
};

Array(数组)

js 复制代码
const numbers = [1, 2, 3];

Function(函数)

js 复制代码
function greet() {
  console.log("Hello!");
}

其他内建对象

  • Date

  • RegExp

  • Map / Set

  • WeakMap / WeakSet

  • Error

⚖️ 原始类型 vs 引用类型

特性 原始类型 引用类型
可变性 不可变 可变
赋值方式 按值传递 按引用传递
比较方式 值比较(===) 引用地址比较(===)
存储位置 栈内存 堆内存

📚 小贴士与陷阱

  • typeof null 返回 "object" 是 JavaScript 的一个历史 bug。

  • 原始类型是不可扩展的,不能给它们添加属性(尽管可以临时包装成对象)。

  • 使用 ===(严格等于)可以避免类型转换带来的误解。

  • 判断是否是引用类型可以用:

    js 复制代码
    typeof value === "object" && value !== null;

typeof

typeof 是 JavaScript 中的一个一元运算符,用于检测变量或值的类型。它会返回一个字符串,表示该值的类型。

我们来看一些例子:

js 复制代码
console.log(typeof 777); // "number"
console.log(typeof 3.14); // "number"
console.log(typeof 0); // "number"
console.log(typeof Infinity); // "number"
console.log(typeof Number("moment")); // "number"

console.log(typeof 77n); // "bigint"

console.log(typeof "1"); // "string"
console.log(typeof typeof 1); // "string"(typeof 返回的是字符串)
console.log(typeof String(777)); // "string"

console.log(typeof true); // "boolean"
console.log(typeof false); // "boolean"
console.log(typeof Boolean(5)); // "boolean"
console.log(typeof !!1); // "boolean"

console.log(typeof Symbol()); // "symbol"
console.log(typeof Symbol("foo")); // "symbol"
console.log(typeof Symbol.iterator); // "symbol"

console.log(typeof { a: 1 }); // "object"
console.log(typeof [1, 2, 4]); // "object"
console.log(typeof new Date()); // "object"
console.log(typeof /regex/); // "object"

console.log(typeof null); // "object"(这是 JavaScript 的一个历史 bug)

console.log(typeof function () {}); // "function"
console.log(typeof class T {}); // "function"

为什么 typeof null 是 "object"?

这是 JavaScript 的一个广为人知的历史遗留问题。

在最初的实现中,JavaScript 使用一种内部表示来存储类型信息。每个值都有一个"类型标签",用于表示它属于哪一类。对象的类型标签是 0。由于 null 被表示为空指针(在底层通常是 0x00),其类型标签也被错误地解析为 0,因此 typeof null 返回 "object"

虽然这个行为被认为是一个 bug,但由于兼容性问题,它从未被修复。

函数是对象的子类型

虽然 typeof function () {} 返回 "function",但从规范角度看,函数本质上是对象的一个子类型。函数是"可调用对象",它们具有一个内部属性 [[Call]],使其可以被调用。

js 复制代码
function f() {}

console.log(f.__proto__.constructor === Function); // true
console.log(f.__proto__.__proto__.constructor === Object); // true

// 函数是对象,也可以拥有属性
console.log(f.name); // "f"
console.log(f.arguments); // null(在非调用环境中为 null)

数组也是对象的子类型

数组在 JavaScript 中也被归为对象类型。它具有特殊的结构:按数字索引、具备 length 属性。

js 复制代码
const foo = [];

console.log(foo.__proto__.constructor === Array); // true
console.log(foo.__proto__.__proto__.constructor === Object); // true

虽然 typeof foo 返回 "object",但更推荐使用:

js 复制代码
Array.isArray(foo); // true

来判断一个值是否为数组。

typeof 返回值总结

值类型 typeof 返回值
Number "number"
BigInt "bigint"
String "string"
Boolean "boolean"
Symbol "symbol"
Undefined "undefined"
Null "object"(历史 bug)
Object "object"
Array "object"
Function "function"

new 操作符详解

在 JavaScript 中,new 是一个关键字,用于通过构造函数创建并返回一个新的对象实例。

🔧 new 的基本用途

通过 new 关键字调用一个函数,会执行以下几件事:

js 复制代码
const instance = new ConstructorFunction();
  • 创建一个全新的对象;

  • 将这个新对象的 __proto__ 链接到构造函数的 prototype

  • 将构造函数内部的 this 绑定到这个新对象;

  • 执行构造函数中的代码;

  • 如果构造函数返回一个对象,则使用这个对象作为 new 表达式的结果;否则返回创建的新对象。

📦 new 调用的返回值

使用 new 调用构造函数时,返回的永远是一个 引用类型(对象或函数)

js 复制代码
const str = new String("hello");
const num = new Number(123);
const bool = new Boolean(true);
const func = new Function("return 42");

console.log(typeof str); // "object"
console.log(typeof num); // "object"
console.log(typeof bool); // "object"
console.log(typeof func); // "function"

✅ 注意:构造函数 Function 返回的是一个可调用的函数,因此 typeof"function",但它本质上仍然是对象的子类型。

🧠 构造函数返回值行为

构造函数中可以显式使用 return 返回值。如果返回的是一个对象类型(引用) ,它会覆盖默认返回的新实例;如果返回的是原始类型,则会被忽略,仍然返回新实例。

js 复制代码
function A() {
  this.name = "A";
  return { msg: "custom object" };
}

function B() {
  this.name = "B";
  return 123;
}

const a = new A();
const b = new B();

console.log(a); // { msg: "custom object" }
console.log(b); // B { name: "B" }

🔍 new 运算符的底层执行流程

当你执行 new Constructor(),内部大致相当于以下逻辑:

js 复制代码
function customNew(Constructor, ...args) {
  // 1. 创建一个新对象,并设置其原型
  const obj = Object.create(Constructor.prototype);

  // 2. 执行构造函数,将 this 指向新对象
  const result = Constructor.apply(obj, args);

  // 3. 如果构造函数显式返回对象,则返回该对象,否则返回新对象
  return result !== null && typeof result === "object" ? result : obj;
}

使用示例:

js 复制代码
function Person(name) {
  this.name = name;
}
const p1 = customNew(Person, "Alice");
console.log(p1.name); // "Alice"

🚫 使用 new 的注意事项

  • 不要对非构造函数使用 new,否则会抛出错误。

  • 避免滥用 new 包装原始类型(如 new Number()),可能导致类型混乱。

  • 自定义构造函数必须使用大写开头(惯例)以示区分。

new 与构造函数的关系总结

构造函数 返回类型 typeof 返回值
new Object() 对象 "object"
new Array() 数组(对象) "object"
new Function() 函数 "function"
new String() 包装对象 "object"
new Number() 包装对象 "object"
new Boolean() 包装对象 "object"

new 是 JavaScript 中构造对象的重要机制。掌握它的执行原理,有助于你更深入理解构造函数、原型链、继承机制等高级概念。在 ES6 引入 class 语法之后,new 的使用更加普遍,了解它的本质仍然是非常必要的。

undefined 和 undeclared

变量在未持有值的时候为 undefined。此时 typeof 返回 undefined:

js 复制代码
var a;
console.log(tyoeof a); // undefined

大多数的开发者倾向于将 undefined 等同于 undeclared(未声明),但在 JavaScript中它们完全是两回事。在作用域中声明但是还没有赋值的变量,是 undefined。相反,还没有在作用域中声明过的变量,是undeclared 的。

在上列中, bar is not defined 容易让人误以为是 bar is undefined。但是 undefinedis undefined 是两码事,但是 typeof 处理 undeclared 返回的结果竟然是 undefined,例如:

js 复制代码
var foo;

console.log(typeof foo); // undefined
console.log(typeof bar); // undefined

它们两个原样返回 "undefined",并且 typeof bar 并没有报错,这是因为 typeof 有一个特殊的安全防范机制。

内部属性 [[class]]

在前面的例子中,使用 typeof 进行判断,无论是 nullObjectArray等类型,都返回的是 "object",那么是否有一种机制可以判断它具体为什么类型的值呢?答案是有的。

所有 typeof 返回值为 object 的对象(如数组)都包含一个内部属性 [[class]],我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类。这个属性无法直接访问,一般通过 Object.prototype.toString(...) 来查看。例如:

js 复制代码
console.log(Object.prototype.toString.call([1, 2, 3])); // [object Array]
console.log(Object.prototype.toString.call(1)); // [object Number]
console.log(Object.prototype.toString.call("moment")); //[object String]
console.log(Object.prototype.toString.call(true)); //[object Boolean]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call(function f() {})); // [object Function]
console.log(Object.prototype.toString.call(class C {})); // [object Function]
console.log(Object.prototype.toString.call(new Date())); // [object Date]
console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]
console.log(Object.prototype.toString.call(new Boolean(1))); // [object Boolean]
console.log(Object.prototype.toString.call(new RegExp())); // [object RegExp]

上例中,数组内部[[class]]属性值是 Array,正则表达式的值是 RegExp。多数情况下,对象的内部 [[class]] 属性和创建该对象的内建原生构造函数相对应,但并不是所有的情况都是这样,例如一些基本类型,例如 nullundefined,虽然 Null()undefined() 这样的原生构造函数并不存在,但是内部 [[class]] 属性值仍然是 NullUndefined

其他基本类型,例如 字符串、数值和布尔值 的情况有所不同,由于基本类型值没有 .length.toString() 这样的属性和方法,需要通过封装对象才能访问,此时 JavaScript 会自动为基本类型封装为一个对象,例如 var foo = 'moment';,实际上进行的是 var foo =new String('moment'); ,使其变成一个对象,让其拥有自己的属性和方法,如果想要得到封装对象中的基本类型值,可以使用 valueOf() 函数,例如:

js 复制代码
var foo = new String("moment");

console.log(foo); // [String: 'moment']
console.log(foo.valueOf()); // moment
console.log(typeof foo.valueOf()); // string

手写 typeof

typeof 是非常有用的,但它不像需要的那样万能。例如,typeof []"object",以及 typeof new Date()typeof /abc/ 等。

为了明确地检查类型, mdn 上提供了一个自定义的 type(value) 函数,它主要模仿 typeof 的行为,但对于非基本类型(即对象和函数),它在可能的情况下返回更详细的类型名。

js 复制代码
function type(value) {
  // 如果传入的值是 null ,则返回 null
  if (value === null) {
    return "null";
  }

  const baseType = typeof value;
  // 如果是基本类型
  if (!["object", "function"].includes(baseType)) {
    return baseType;
  }

  // Symbol.toStringTag 通常指定对象类的"display name"
  const tag = value[Symbol.toStringTag];
  if (typeof tag === "string") {
    return tag;
  }

  // 如果他是一个函数,其源代码以 class 关键字开头的
  if (
    baseType === "function" &&
    Function.prototype.toString.call(value).startsWith("class")
  ) {
    return "class";
  }

  // 构造函数的名称;例如 `Array`、`GeneratorFunction`、`Number`、`String`、`Boolean` 或 `MyCustomClass`
  const className = value.constructor.name;
  if (typeof className === "string" && className !== "") {
    return className;
  }

  // 没有合适的方法来获取值的类型,直接返回
  return baseType;
}

Symbol.toStringTag 与类型判断

在 JavaScript 中,Symbol.toStringTag 是一个内置的 Symbol 属性,它在类型判断中起着核心作用。这个特殊的 Symbol 允许自定义对象在被Object.prototype.toString()方法调用时返回的字符串标签。

Symbol.toStringTag 的工作原理

当我们调用Object.prototype.toString.call(value)时,JavaScript 引擎会查找该值是否具有Symbol.toStringTag属性:

  1. 如果对象有Symbol.toStringTag属性,则使用它的值作为类型标识
  2. 如果没有,则回退到默认的内部[[Class]]属性值
js 复制代码
// 创建一个自定义对象,并定义Symbol.toStringTag
const myObject = {};
Object.defineProperty(myObject, Symbol.toStringTag, {
  value: "CustomType",
});

console.log(Object.prototype.toString.call(myObject)); // "[object CustomType]"

// 内置对象也使用Symbol.toStringTag
console.log(Object.prototype.toString.call(new Map())); // "[object Map]"
console.log(Object.prototype.toString.call(Promise.resolve())); // "[object Promise]"

利用 Symbol.toStringTag 实现精确的类型检测

我们可以创建一个更强大的类型检测函数,它能够识别包括内置类型和自定义类型在内的所有对象类型:

js 复制代码
function getExactType(value) {
  if (value === null) {
    return "null";
  }

  if (value === undefined) {
    return "undefined";
  }

  // 处理原始类型
  if (typeof value !== "object" && typeof value !== "function") {
    return typeof value;
  }

  // 从Object.prototype.toString中提取类型信息
  const objectString = Object.prototype.toString.call(value);
  const type = objectString.slice(8, -1); // 移除 "[object " 和 "]"

  return type;
}

// 测试各种类型
console.log(getExactType(42)); // "number"
console.log(getExactType("hello")); // "string"
console.log(getExactType(true)); // "boolean"
console.log(getExactType(undefined)); // "undefined"
console.log(getExactType(null)); // "null"
console.log(getExactType(Symbol())); // "Symbol"
console.log(getExactType([])); // "Array"
console.log(getExactType({})); // "Object"
console.log(getExactType(new Date())); // "Date"
console.log(getExactType(new Map())); // "Map"
console.log(getExactType(new Set())); // "Set"
console.log(getExactType(() => {})); // "Function"

为自定义类型实现 Symbol.toStringTag

我们可以为自定义类添加Symbol.toStringTag属性,使其能够被准确识别:

js 复制代码
class Person {
  constructor(name) {
    this.name = name;
  }

  // 定义Symbol.toStringTag getter
  get [Symbol.toStringTag]() {
    return "Person";
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }

  get [Symbol.toStringTag]() {
    return "Student";
  }
}

const person = new Person("Alice");
const student = new Student("Bob", 12);

console.log(Object.prototype.toString.call(person)); // "[object Person]"
console.log(Object.prototype.toString.call(student)); // "[object Student]"
console.log(getExactType(person)); // "Person"
console.log(getExactType(student)); // "Student"

Symbol.toStringTag 与类型检查的实际应用

使用Symbol.toStringTag进行类型检查在实际开发中十分有用,尤其是在以下场景:

  1. 检测环境内置对象:例如检查是否支持某些特定的 API
js 复制代码
function isArrayBuffer(obj) {
  return Object.prototype.toString.call(obj) === "[object ArrayBuffer]";
}

function isBlob(obj) {
  return Object.prototype.toString.call(obj) === "[object Blob]";
}

// 更通用的解决方案
function isTypeOf(obj, typeName) {
  return Object.prototype.toString.call(obj) === `[object ${typeName}]`;
}

console.log(isTypeOf(new Blob(), "Blob")); // true
console.log(isTypeOf(new ArrayBuffer(8), "ArrayBuffer")); // true
  1. 创建类型安全的函数:确保传入参数符合预期类型
js 复制代码
function processCollection(collection) {
  const type = getExactType(collection);

  if (type === "Array") {
    // 处理数组...
    return collection.length;
  }

  if (type === "Set") {
    // 处理Set...
    return collection.size;
  }

  if (type === "Map") {
    // 处理Map...
    return collection.size;
  }

  throw new TypeError(`Expected Array, Set or Map, got ${type}`);
}
  1. 实现多态行为:根据对象类型执行不同操作
js 复制代码
function stringify(value) {
  const type = getExactType(value);

  switch (type) {
    case "Date":
      return value.toISOString();
    case "RegExp":
      return value.toString();
    case "Array":
    case "Object":
      return JSON.stringify(value);
    case "Map":
    case "Set":
      return JSON.stringify(Array.from(value));
    default:
      return String(value);
  }
}

通过正确使用Symbol.toStringTagObject.prototype.toString.call(),我们可以实现比原生typeof运算符更精确可靠的类型检测系统,这在复杂应用的开发中尤为重要。

总结

typeof 运算符用于检查变量的类型,返回一个表示数据类型的字符串。对于基本类型(如 numberstringbooleanundefinedsymbolbigint),它的返回值非常明确。然而,对于所有对象类型(如数组、正则表达式、日期、null 等),typeof 都返回 "object",这在实际开发中可能造成误判,比如 typeof null === "object" 就是一个广为人知的历史遗留问题。

为了解决 typeof 在判断复杂对象类型时的局限性,可以使用 Object.prototype.toString.call(value) 方法。该方法返回更精确的类型标签,例如:

js 复制代码
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(new Date()); // "[object Date]"

这种方式可以准确识别对象的原始内部类型标签([[Class]]),是进行类型判断的推荐方案。

此外,ES6 引入了 Symbol.toStringTag,允许我们自定义 Object.prototype.toString.call() 的返回值。通过在对象上定义该 Symbol 属性,可以"伪装"成其他类型:

js 复制代码
const customType = {
  [Symbol.toStringTag]: "Custom",
};
console.log(Object.prototype.toString.call(customType)); // "[object Custom]"

这在库设计中非常有用,可提升类型信息的可读性和表达力。

总之,typeof 适合快速判断基本类型,而对于复杂类型,应优先使用 Object.prototype.toString.call(),在需要自定义类型表现时,可结合 Symbol.toStringTag 提升语义清晰度和可控性。

相关推荐
好_快36 分钟前
Lodash源码阅读-baseMatchesProperty
前端·javascript·源码阅读
好_快37 分钟前
Lodash源码阅读-hasPath
前端·javascript·源码阅读
好_快40 分钟前
Lodash源码阅读-hasIn
前端·javascript·源码阅读
Jasmin Tin Wei43 分钟前
蓝桥杯 web 展开你的扇子(css3)
前端·css·css3
好_快44 分钟前
Lodash源码阅读-basePropertyDeep
前端·javascript·源码阅读
vvilkim4 小时前
深入理解 TypeScript 中的 implements 和 extends:区别与应用场景
前端·javascript·typescript
GISer_Jing4 小时前
前端算法实战:大小堆原理与应用详解(React中优先队列实现|求前K个最大数/高频元素)
前端·算法·react.js
振鹏Dong6 小时前
超大规模数据场景(思路)——面试高频算法题目
算法·面试
uhakadotcom6 小时前
Python 与 ClickHouse Connect 集成:基础知识和实践
算法·面试·github
uhakadotcom6 小时前
Python 量化计算入门:基础库和实用案例
后端·算法·面试