Skip to content

一. 对象类型

认识对象类型

引言

在 JavaScript 的数据类型中,有一类非常重要且贯穿始终的类型 —— 对象类型(Object)。

"对象类型几乎参与 JS 的所有核心机制(作用域、原型链、this、模块、框架设计等)"。

掌握对象类型,是理解 JavaScript 的关键基础。

什么是对象类型

对象类型是一种用于存储键值对(key-value)的复杂数据类型。

  • 每一组数据由:key: value 组成
  • 对象可以同时描述:

    属性(特性、状态)
    方法(行为,本质是函数)

js
var obj = {
  name: 'later',
  age: 23,
  run: function () {
    console.log('running')
  }
}

对象中的函数,通常称为 方法(method)。

🔔 提示

在 JavaScript 中,对象的方法本质上也是对象的属性,只不过当属性值是函数时,通常称该属性为“方法”。

为什么需要对象类型

基本数据类型(Number、String、Boolean 等)只能表示单一、简单的值,而现实中的事物往往是多维度的

例如:

  • 一个「人」

    特性:姓名、年龄、身高
    行为:学习、工作、跑步

  • 一辆「车」

    特性:颜色、重量、速度
    行为:行驶、刹车

对象类型的作用:将“属性 + 行为”组织成一个整体,用于描述复杂事物

对象中 key 和 value 的规则

  • key(属性名,property name)

    本质是:字符串 或 Symbol(ES6 新增)
    写对象字面量时,字符串 key 的引号通常可以省略
    属性之间是以逗号(comma)分割
    非字符串类型的 key(如 Number)会被隐式调用 toString() 转换为字符串

    js
    var obj = {
      123: 'number key',    // key 实际等价于 '123'
      true: 'boolean key',  // key 实际等价于 'true'
      name: 'later',        // key 实际等价于 'name'
    }
  • value(属性值, property value)

    可以是任意类型:基本类型、函数、对象、数组 等

创建对象的方式

JavaScript 中常见的对象创建方式有三种:

对象字面量(最常见,Object Literal

js
var obj = {
  name: "later"
}

语法最简洁
可读性最好
前端开发中使用频率最高

new Object() + 动态添加属性

js
var obj = new Object() // Object 构造函数

obj.name = "later"

本质上与对象字面量一致
实际开发中较少使用

new 构造函数

js
function Person() {}

var obj = new Person()

用于批量创建对象
与原型、面向对象编程密切相关

当前阶段重点掌握:对象字面量方式 即可。后续学习其他两种方式

对象的基本操作

js
// 定义对象
var obj = {
  name: 'later'
}

// 1.访问对象属性:对象.属性名
console.log(obj.name)

// 2.修改对象属性
obj.name = 'sensen'

// 3.添加对象属性
obj.age = 23

// 4.删除对象属性:delete 操作符
delete obj.age

点语法 vs 方括号语法

点语法的限制

点语法要求属性名必须是合法的变量标识符

不能包含空格
不能以数字开头
不能包含特殊字符($、_ 除外)

js
var obj = {
  'good friend': 'later'
}

// 点语法 不支持
obj.good friend // ❌ 语法错误:';' expected.

方括号语法(更灵活)

方括号在定义或操作属性时更灵活,方括号内可以是任意的字符串、变量、表达式 等。

js
var obj = {
  'good friend': 'later'
}

obj['good friend'] // ‘later'

var key = 'name'
obj[key] // 等价于 => obj.name
  • 常用于:

    key 中包含特殊字符
    key 是变量

对象的遍历(迭代)

遍历对象:表示获取对象中所有的可枚举属性

方式一:Object.keys() + for 循环(推荐)

Object.keys() 方法会返回一个由给定对象的自身可枚举属性组成的字符串数组

js
var info = { name: 'later', age: 23 }
var infoKeys = Object.keys(info)

for (var i = 0; i < infoKeys.length; i++) {
  var key = infoKeys[i]
  var value = info[key]
  console.log(`key: ${key}, value: ${value}`)
}

// output:
// key: name, value: later
// key: age, value: 23

特点:
只遍历对象 自身的可枚举属性
顺序稳定
更可控,工程中更常见

方式二:for...in 遍历

任意顺序迭代一个对象的除 Symbol 以外的可枚举属性, 包括原型链上继承过来的可枚举属性。

js
var info = { name: 'later', age: 23 }
for (var key in info) { 
  var value = info[key]
  console.log(`key: ${key}, value: ${value}`)
}

特点:
遍历对象 自身的可枚举属性 + 从原型链上继承过来的可枚举属性(可搭配 hasOwnProperty() 使用,来排除继承过来的属性)
顺序不固定
不遍历 Symbol 属性

二. 内存中的值类型和引用类型

引言

本节目标:

"理解 JavaScript 中「值是如何存储在内存中的」"
"搞清楚 值类型 vs 引用类型 的本质差异"
"为后续理解:函数参数、对象拷贝、闭包、响应式等打基础"

认识内存(Memory)

计算机存储器(Computer memory)是一种利用半导体、磁性介质等技术制成的存储资料的电子设备,其电子电路中的资料以二进制方式存储,是用来存储程序运行过程中产生的数据的硬件设备

存储器分为主存储器(main memory,简称:主存/内存,又称“内存储器”)和 辅助存储器(auxiliary memory,简称:辅存/外存,又称“外存储器”)。

内存储器与中央处理器(CPU)一起构成主机,用来存放计算机运行时随时需要使用的程序和数据,一切数据要被CPU操作都必须先装入内存。内存的工作速度较快,存储容量较小,主要采用半导体存储器。

目前大部分计算机系统的主存储器主体为动态随机存储器(DRAM),“主存储器”乃至“存储器”一词有时特指DRAM;另外,静态随机存储器(SRAM)与只读存储器(ROM)等也可作主存储器的一部分。

辅助存储器和输入输出设备都属于外设,用来存放CPU运行时暂时不用的各种程序和数据,一般在断电后仍能保存。辅助存储器的存储容量大,工作速度慢,例子如硬盘、U盘、光盘、磁带等。

简单理解:

  • 程序运行前:代码在磁盘(外部存储)
  • 程序运行时:

    被加载到 内存(主存)
    由 CPU 直接读取和执行

内存的特点:

  • 内存是 CPU 可以直接寻址 的存储空间
  • 相比磁盘:

    访问速度快
    容量较小
    断电数据丢失

栈内存(Stack)与 堆内存(Heap)

程序在运行时,先加载到内存中由 CPU 来执行,内存通常可以从逻辑上划分为两个区域:栈内存和堆内存

内存区域特点主要存储
栈内存连续的内存空间、
存取速度快、
生命周期明确(函数调用结束即释放)
基本(原始)类型的值、
变量名与其对应的值、
函数调用相关信息(调用栈)
堆内存非连续的内存空间、
存取速度慢、
生命周期不固定(由 GC 回收)
对象类型(Object / Array / Function 等)的实际数据

原始(基本)类型占据的空间是在栈内存中分配的
对象(复杂)类型占据的空间是在堆内存中分配的

值类型和引用类型

值类型(Primitive / 值拷贝)

原始类型的保存方式:在变量中保存的是值本身,所以原始类型又称为值类型

js
var a = 111
var b = a // a 和 b 互不影响
b = 222

console.log(a) // 111
console.log(b) // 222

特点:
值类型保存的是值本身,所以值类型又称为值拷贝
赋值或传参时,会发生 值拷贝

引用类型(Reference / 引用拷贝)

对象类型的保存方式:在变量中保存的是对象的 "引用",所以对象类型又称为引用类型

js
var obj1 = { name: '111' }
var obj2 = obj1 // 两个变量指向同一个对象

obj2.name = '222'
console.log(obj1.name) // 222

特点:
对象的实际数据存储在 堆内存
变量中保存的是:堆内存地址(引用)

值类型 vs 引用类型

对比项值类型引用类型
存储位置栈(引用) + 堆(实际数据)
变量中保存值本身堆内存地址(引用)
复制行为值拷贝引用拷贝
相互影响

🔔 提示

变量名、原始类型都是存储在栈内存中,只有对象类型的值(实际数据)是存储在堆内存中。
对象类型的变量名存储在栈内存中,其保存的并不是实际数据,而是堆中对象类型值的内存地址(指针/引用)。

现象解析

现象一:比较两个变量

js
var a = 123
var b = 123
console.log(a === b) // true
// 因为原始类型在变量中保存的是值本身,这里是值的比较,123 是等于 123 的

var m = {}
var n = {}
console.log(m === n) // false 
// 因为对象类型在变量中保存的是内存地址,只要写的是个对象字面量形式的大括号,在堆内存中就会创建一个新对象,
// 这里是创建了两个对象,在堆中的内存地址不同,所以比较的时候是不相等的

原因分析:

值类型:比较的是「值」
引用类型:比较的是「内存地址」
每一个对象字面量 {} 都会在堆中创建一个新对象

现象二:引用赋值(共享同一对象)

js
var info = {
  // 对象类型中的基本数据类型,会直接保存在当前对象所在的堆内存中,
  // 因为堆内存本身就是一块空间,空间本身就可以放东西,
  // 因此对象类型中的值类型就存储在该对象所在的堆内存中的,所以'18'这个字符串就存储在该对象所在的堆内存中
  age: '18',
  friend: {
    name: "kobe"
  }
}
// 虽然 info 变量的值本身是一个对象类型,JS引擎会在堆内存中分配一个空间用来存储 info 的值,
// 但 friend 属性的值是对象类型,JS引擎会在堆内存中新分配一个空间用来存储 info.friend 属性的值,
// 而 var friend 变量实际保存的是值在堆内存中的分配的内存地址

var friend = info.friend
friend.name = "james"
console.log(info.friend.name) // james

原因分析:

info.friend 保存的是一个对象引用
friend 拿到的是 同一个引用
修改的是堆中同一块数据

现象三:值类型作为函数参数传递

js
function foo(a) {
  a = 200         // a:100 -> a = 200
}

var num = 100
foo(num)          // 传递的参数是值类型,所以传递的是值的副本
console.log(num)  // 100

原因分析:

函数参数本质是一次 赋值操作
值类型传递的是「值的副本」

现象四:引用类型传递 + 不修改引用值

js
function foo(a) {
  a = { name: 'why' } // 没有修改传过来的 obj,而是创建了一个新对象
}

var obj = { name: 'obj' }
foo(obj)              // 引用传递,传递的 obj 值的内存地址
console.log(obj)      // { name: 'obj' }

原因分析:

传入的是引用(地址)
但函数内部:让参数 a 指向了 新的对象,并没有修改原对象

现象五:引用类型传递 + 修改引用值

js
function foo(a) {
  a.name = 'why'  // 对传递过来的 obj 的堆中的 值进行了修改
}
var obj = {name: 'obj'}
foo(obj)          // 引用传递,传递的 obj 值的内存地址
console.log(obj)  // { name: 'why' }

原因分析:

通过引用,直接修改了堆中的对象数据

三. 函数的 this 执行

引言

本节目标:

"理解 this 为什么存在"
"搞清楚 this 到底指向谁"
"建立一套可推导的 this 判断模型,而不是死记结论"

为什么需要 this?

在常见的编程语言中,几乎都有 this 这个关键字(Objective-C 中使用的是 self),但是 JS 中的 this 和常见的面向对象语言中的 this 不太一样:

大多数面向对象的编程语言中,如 Java、C++、Swift、Dart 等一系列语言中,this 通常只会出现在类的方法中(特别是实例方法)。this 代表的是当前实例对象。

JS 中的特殊性

JavaScript 中的 this 与传统面向对象语言 有本质不同

JS 中 函数是“一等公民”
函数可以:独立调用、作为对象方法调用、作为回调函数、被显式绑定

因此:JS 中的 this,不取决于函数定义在哪,而取决于函数“如何被调用”

不使用 this 的问题

js
var obj = {
  name: 'later',
  running: function () {
    console.log(obj.name + ' running')
  },
  eating: function () {
    console.log(obj.name + ' eating')
  },
}

问题:
方法内部 强依赖外部变量名 obj
一旦对象被:赋值给其他变量、拷贝、作为参数传递,方法立刻失效或语义错误。

使用 this 的好处

this 是为了解决“方法复用 + 动态对象”的问题而存在的。

js
var obj = {
  name: 'later',
  running: function () {
    console.log(this.name + ' running')
  },
  eating: function () {
    console.log(this.name + ' eating')
  },
}

优势:
方法 与对象名解耦
同一方法可以被多个对象复用
this 在调用时动态确定

this 指向什么?

this 的指向,取决于函数的调用方式,而不是定义位置。

目前掌握两个判断方法,足以解释 80% 的 this 问题:

方法一:默认的方式调用一个函数,this 指向全局对象
方法二:通过对象调用,this 指向调用的对象

默认函数调用(独立调用)

js
function foo() {
  console.log(this)
}

foo() // window

解释:
函数独立调用
没有明确的调用者
this 指向全局对象(浏览器中是 window,严格模式下是 undefined)

作为对象的方法调用

js
var obj = {
  bar: function() {
    console.log(this)
  }
}
obj.bar() // obj

解释:
调用点在 obj.bar()
this 指向 调用该方法的对象 obj

四. 工厂函数

创建对象 - 字面量方式的问题

在实际开发中,我们经常会遇到这样的需求:

游戏中创建多个英雄对象(都有名字、技能、价格,但是具体的值又不相同)

学生系统中创建多个学生对象(都有学号、姓名、年龄等,但是具体的值又不相同)

后台系统中创建多个用户对象(都有用户名、密码、权限等,但是具体的值又不相同)

这些对象:

结构相同(属性、方法一致)

数据不同(具体值不同)

当然,最直接的方式是字面量创建一系列的对象:

js
// 一系列的学生对象
// 重复代码的复用: for/函数
var stu1 = {
  name: "why",
  age: 18,
  running: function() {
    console.log("running~")
  }
}
var stu2 = {
  name: "kobe",
  age: 30,
  running: function() {
    console.log("running~")
  }
}
var stu3 = { ... }
var stu4 = { ... }

问题非常明显:

❌ 大量重复代码

❌ 不利于维护

❌ 不符合「抽象」思想

有没有一种方法可以批量创建对象,但是又让它们的属性不一样呢?答案是:工厂函数。

创建对象 - 工厂函数

工厂函数的思想

封装一个函数,专门负责“创建(生产)对象”。

每调用一次函数,就返回一个结构相同、数据不同的新对象。

批量创建对象,只需要重复调用这个函数。

工厂模式其实是一种常见的设计模式

工厂函数示例

js
function createPerson(name, age, address) {
  var p = new Object()
  p.name = name
  p.age = age
  p.address = address
  p.running = function() {
    console.log(this.name + '在跑步')
  }
  return p
}

var p1 = createPerson('张三', 18, '广州')
var p2 = createPerson('李四', 22, '深圳')
console.log(typeof p1) // object

优点:
✅ 封装创建过程
✅ 消除重复代码
✅ 使用简单直观

工厂函数的缺陷

js
console.log(typeof p1) // object
console.log(typeof p2) // object

问题:
创建的所有对象的类型都是 Object。
无法区分:是 Person?还是 Student?

对象“来自哪里”是丢失的。这正是构造函数要解决的问题。

五. 构造函数与类(ES5)

什么是构造函数?

构造函数是在创建对象时会调用的函数,也称之为构造器constructor)。

在其他面向对象语言中:

类是模板

构造函数只是类中的一个方法,称之为构造方法

但在 JavaScript 中有点不一样:

构造函数是类的扮演者,类的表现形式就是构造函数。

ES6 之前,是通过 function 来声明一个构造函数(类),允许用 new 对其进行调用 (如:系统默认给我们提供的 Date 就是一个构造函数,也可以看作是一个类)

ES6 之后,JS 可以像别的语言一样,使用 class 关键字来声明一个类

js
// ES6 之前
function Person() {}
var p = new Person()

// ES6 之后
class Person {}
var p = new Person()

怎么判断一个函数是不是构造函数呢?

构造函数本质上:仍然是一个普通函数,和普通函数没有什么区别。

关键区别在于:是否通过 new 来调用。

只要某个函数通过 new 调用,这个函数就称之为:构造函数

类与对象的关系

类是模板,对象是实例。

类:一类事物的统称

对象:具体的某个事物

比如:

人(Person 类) → 张三 / 李四

水果(Fruit 类) → 苹果 / 香蕉

new 关键字的执行过程

js
function Coder(name, age, height) { 
  // 2. 将新创建的这个对象的 `[[prototype]]` 属性,指向该构造函数的 `prototype` 属性
  // 3. 将构造函数内部的 `this` 指向新创建的这个对象
  // 在内存中 this 指向的是创建出来的空对象,this.xx=xx 这类操作,
  // 其实就是给创建出来的空对象添加属性或方法
  // 4. 执行构造函数内部的代码(函数体的代码块)
  this.name = name 
  this.age = age
  this.height = height
  this.writeCode = function() {
    console.log('写代码~')
  } 
  // 5. 若函数体内部的代码块没有显式返回一个非空对象,则默认返回新创建的这个对象
}

// 1.在堆内存中创建一个新的空对象
var coder1 = new Coder('张三', 18, 1.8) 
var coder2 = new Coder('李四', 22, 1.8)

如果通过 new 操作符调用一个函数,JS 引擎会做以下的事情:

  1. 在堆内存中创建一个新的空对象
  2. 将该对象的 [[prototype]] 属性,指向该构造函数的 prototype 属性
  3. 将构造函数内部的 this 指向新创建的这个对象
  4. 执行构造函数内部的代码(函数体的代码块)
  5. 若函数体内部的代码块没有显式返回一个非空对象,则默认返回新创建的这个对象

创建对象 - 构造函数

js
function Coder(name, age, height) { 
  this.name = name 
  this.age = age
  this.height = height
  this.writeCode = function() {
    console.log('写代码~')
  } 
}

var coder1 = new Coder('张三', 18, 1.8) 
console.log(coder1) // Coder {name: '张三', age: 18, height: 1.8, writeCode: ƒ}

使用构造函数创建对象:

对象有明确的类型(实际是 constructor 的属性,这个后续再探讨)

这是工厂函数做不到的

事实上构造函数还有很多其他的特性:

如:原型、原型链、实现继承的方案

比如 ES6 中类、继承的实现

但它也有问题:

方法会被重复创建(后续由 prototype 解决)

七. 函数本身也是对象

函数的本质:也是对象

在 JavaScript 中,function 本身也是一种对象类型,同样是创建在堆内存中的,只不过它的结构和普通对象略有不同。

既然是对象,那么函数天然具备两个能力:

✅ 可以被变量引用

✅ 可以拥有自己的属性和方法

从普通对象推导到函数对象

我们先看最常见的对象创建方式:

js
// 字面量形式
var obj1 = {}

// 构造函数形式
var obj2 = new Object()

这两种方式,本质都是:

在堆内存中创建一个对象,然后将引用赋值给变量

同理,函数也是如此:

js
var foo1 = function () {}
var foo2 = new Function()

可以推导出结论:

foo1、foo2 中保存的,都是堆内存中函数对象的引用

函数和普通对象一样,都存在于堆内存中

js
console.log(typeof foo1) // function

⚠️ 注意:

typeof 返回 function,只是为了更精确地区分

从数据结构角度看,函数本质上仍然属于 object 类型

函数对象的内存行为

创建函数时发生了什么?

在堆内存中开辟一块空间,用来存放:

函数体代码

作用域相关信息

一些内部属性(如 [[Call]]prototype 等)

js
function sayHello() {}

此时:

sayHello 是一个变量,存储在栈内存中

指向堆内存中的函数对象

函数调用时发生了什么?

JS 引擎会创建一个函数执行上下文

执行上下文会被压入调用栈(Call Stack)

阶段内存位置说明
函数创建堆内存函数对象本身
函数调用栈内存函数执行上下文

函数对象:可以添加属性

既然函数是对象,那么它也可以像普通对象一样添加属性:

js
function sayHello() {}

sayHello.age = 18
sayHello.name = 'hello'

这在语法上是完全合法的。

函数对象 vs 普通对象

js
var info = {}
info.age = 18

function sayHi() {}
sayHi.age = 18

从 JS 引擎的角度来看:

info 是对象、sayHi 也是对象

二者的本质操作是完全一致的

构造函数上的类方法

直接挂载在构造函数(类)本身上的方法,又称为静态方法

通过「类名」直接调用

js
function Dog() {}

// 构造函数上(类上面)添加的函数, 称之为类方法
Dog.running = function() {}
Dog.running()

七. 全局对象 window

什么是全局对象 window?

在浏览器环境中,存在一个全局对象:window

window 是 浏览器提供的顶层对象,也是 JavaScript 在浏览器中的 全局宿主对象(host object)

在浏览器中:

全局作用域 ≈ window 对象

所有未被包裹在函数或块级作用域中的标识符,都会尝试挂载或访问 window

⚠️ 注意

"window 是 浏览器特有 的"

"在 Node.js 中,对应的全局对象是 global(不是 window)"

作用一:作为作用域链的“最终查找对象”

JavaScript 在访问变量 / 函数时,会遵循 作用域链(Scope Chain) 的查找规则:

当前作用域 → 上层作用域 → ... → 全局作用域(window)

如果在当前作用域和其父级作用域中都找不到对应标识符,会一层层往上查找,最终会尝试从 window 对象上查找

js
var name = 'later'  // 挂载到全局对象 window 上 => window.name

function foo() {
  console.log(name) // 在当前作用域中找不到变量 name,往上查找到 window.name
}

foo()               // later

作用二:浏览器内置 API 的统一入口

浏览器为 JavaScript 提供的 大量内置 API 都挂载在 window 对象上

js
// 常见示例:
window.setTimeout
window.setInterval
window.console
window.location
window.history
window.document
window.alert

在实际使用中,window 通常可以省略不写

js
setTimeout(() => {}, 1000)
// 等价于
window.setTimeout(() => {}, 1000)

这是因为 JS 在解析标识符时,会自动从全局对象中查找

var 声明的“历史遗留问题”

在全局作用域中,var 声明的变量,会被 自动添加到 window 对象上,这是 JavaScript 语言早期设计上的缺陷

js
console.log(window.a) // undefined
var a = 10
console.log(window.a) // 10

同理,使用 function 声明的全局函数,也会成为 window 的属性

js
function foo() {}
console.log(window.foo === foo) // true

⚠️ 注意

ES6 之后的 let、const 声明的全局变量,不会挂载到 window 上。

八. Console 输出行为详解

一个容易踩坑的现象

js
var obj = { name: 'later' }
console.log(obj)
obj.name = 'baby'

很多人在控制台中展开 obj 时,会看到:

js
{ name: 'baby' }

从而产生疑问:

❓ 我明明是在修改之前就 console.log(obj) 了,为什么看到的是修改后的值?

这并不是 JS 的问题,而是浏览器 DevTools 的设计行为

控制台输出对象的本质:引用 + 延迟查看

当你执行:

js
console.log(obj)

浏览器实际做的是:

保存一份对该对象的“引用”,而不是当时对象的完整值副本

当你稍后在控制台中点击展开该对象时:

浏览器会重新读取该对象当前的属性值并展示

所以你看到的:

展开时的当前状态

而不是 console.log 执行那一刻的状态

window 对象的经典示例

js
console.log(window)

var msg = '000'

在控制台中展开 window,你会看到:

js
window.msg === '000'

即使 console.log(window) 写在 msg 定义之前

原因

window 是一个巨大的全局对象

浏览器对其采用了实时视图(Live View)策略

后续对 window 的任何修改,都会反映到之前打印的 window 引用上

⚠️ 这是 DevTools 的调试优化行为,不是 JS 语言规范

如何得到“打印当时的真实值”?

使用 JSON 快照法

js
var obj = { name: 'later' }

console.log(obj)  // devtools 中展开时显示 { name: 'baby' }
console.log('----')
console.log(
  JSON.parse(JSON.stringify(obj))
) // devtools 中展开时显示永远是 { name: 'later' }

obj.name = 'baby'

这个写法的本质是:

JSON.stringify:把对象序列化为字符串(值拷贝)

JSON.parse:再从字符串生成一个全新的对象

最终打印的是:

一个与原对象“值相同、引用无关”的新对象

因此:

后续对原对象的修改,不会影响这次打印结果

JSON.stringify 的重要限制

⚠️ JSON 序列化 不是通用的深拷贝方案

undefined、function、symbol,在 JSON.stringify 过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)

对象中:

js
JSON.stringify({ a: undefined, b: () => {}, c: Symbol() })
// '{}'

数组中:

js
JSON.stringify([undefined, function(){}, Symbol()])
// '[null,null,null]'

浏览器为什么要这样设计?

原因只有一个:提升调试体验

对象往往很大

复制完整快照成本高

实时查看更方便追踪变化

但代价是:

console.log 对象 ≠ 输出当时的值

避免使用 console.log(obj),推荐使用 console.log(JSON.parse(JSON.stringify(obj))), 除非你能确保后续不会修改 obj

这样可以确保 obj 的值是 console.log语句执行时输出的值, 否则大多数浏览器会提供一个随着值的变化而不断更新的实时视图。这可能不是你想要的

https://developer.mozilla.org/zh-CN/docs/Web/API/console/log#输出对象

🔔 提示

像查找 window 对象上的某些属性或函数,打印在控制台的时候,浏览器可能会展示,也可能会隐藏

如有转载或CV请标注本站原文地址