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

403 阅读3分钟

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

20250310220634

JavaScript 数据类型

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

📌 类型分类

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

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

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

🧱 原始类型(Primitive Types)

共有 7 种原始类型:

1. Undefined

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

let a;
console.log(a); // undefined

2. Null

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

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

3. Boolean

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

let isLoggedIn = false;

常用于条件判断:

if (isLoggedIn) {
  console.log("欢迎回来!");
}

4. Number

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

let count = 42;
let pi = 3.14159;

支持特殊数值:

Infinity;
-Infinity;
NaN; // Not-a-Number,表示非数字

5. String

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

let name = "Alice";
let greeting = `Hello, ${name}!`;

6. Symbol(ES6 引入)

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

const sym1 = Symbol("id");
const sym2 = Symbol("id");

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

7. BigInt(ES2020 引入)

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

const big = 900719925474099100000n;
console.log(typeof big); // "bigint"

🧩 引用类型(Reference Types)

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

Object(通用对象)

const user = {
  name: "Alice",
  age: 25,
};

Array(数组)

const numbers = [1, 2, 3];

Function(函数)

function greet() {
  console.log("Hello!");
}

其他内建对象

  • Date

  • RegExp

  • Map / Set

  • WeakMap / WeakSet

  • Error

⚖️ 原始类型 vs 引用类型

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

📚 小贴士与陷阱

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

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

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

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

    typeof value === "object" && value !== null;
    

typeof

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

我们来看一些例子:

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]],使其可以被调用。

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 属性。

const foo = [];

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

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

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 关键字调用一个函数,会执行以下几件事:

const instance = new ConstructorFunction();
  • 创建一个全新的对象;

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

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

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

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

📦 new 调用的返回值

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

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 返回值。如果返回的是一个对象类型(引用),它会覆盖默认返回的新实例;如果返回的是原始类型,则会被忽略,仍然返回新实例。

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(),内部大致相当于以下逻辑:

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;
}

使用示例:

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:

var a;
console.log(tyoeof a); // undefined

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

20250212165935

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

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(...) 来查看。例如:

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() 函数,例如:

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 的行为,但对于非基本类型(即对象和函数),它在可能的情况下返回更详细的类型名。

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]]属性值
// 创建一个自定义对象,并定义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 实现精确的类型检测

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

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属性,使其能够被准确识别:

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
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. 创建类型安全的函数:确保传入参数符合预期类型
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. 实现多态行为:根据对象类型执行不同操作
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) 方法。该方法返回更精确的类型标签,例如:

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 属性,可以“伪装”成其他类型:

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

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

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