彻底理解JavaScript中的对象

JS对象迷惑性:理解本质

对于C++、Java、C#面向对象的语言而言,对象这个词都是很明确的:“对象”必须以“类(Class)”为模板实例化而来。

而JS中的对象概念,则宽泛得多。例如,以下代码中的staff 、p都是对象,甚至函数Person也是对象:

//普通对象
const staff = {
    name: "Tom",
    age: 18
};

//Promise对象
const p = new Promise(...);

//函数也是一个对象
function Person(name) {
  this.name = name;
}

这不禁让人困惑,JavaScript的对象到底指的是什么?

因为中文把 Object 翻译成“对象”,听起来特别抽象,实际上:

JS 里的“对象”并不是指某一种长相,而是指一种“可以被当成一个整体来管理,并且可以通过属性名找到内部东西”的值。

“对象”本质上只是一个包含键值对(Key-Value)的动态哈希表。

很粗浅的理解,对象就是一个可以装很多“属性”的东西。

认识了这个本质,我们很快就能理解了,staff,p,Person 长相完全不同,但它们在 JavaScript 的底层分类上都属于 Object。以下都是JS中的对象,只是创建它们的“模板”不同而已:

JavaScript Object

├── 普通对象
├── Promise对象
├── Array对象
├── Date对象
├── RegExp对象
├── Map对象
└── …

创建对象有很多种方式

例如,普通对象:

 const a = {};

其它类型对象:

 //Promise 对象
 const b = new Promise(...);
 //Array 对象。
 const c = [];
 //Date 对象
 const d = new Date();
 //Map 对象
 const e = new Map();
 //RegExp 对象
 const f = /abc/;
  //甚至得到的是函数对象
 const g = function() {};

不要再把对象单一理解成:

 { name: “Tom”, age: 18 }

而应该理解成:

对象 = 一个可以拥有属性和方法、可以被独立操作的实体。

于是:

                  Object
                    │
        ┌───────────┼────────────┐
        ↓           ↓           ↓
      普通对象     Array       Promise
        │           │           │
    name/age     push/length   then/catch

它们虽然功能完全不同,但都属于“对象”这个大类别。

函数也是对象,这到底是怎么回事?

这是最让人困惑的地方,在Java/C#/C++等语言中,函数是类的行为,对象的方法,它们世界中函数绝不可能成为一个对象的。JavaScript中,却是另外一种设计思想,函数也是对象!

例如,

function hello() {
    console.log("hi");
}

hello 可以被其它对象直接调用hello(),但它同时也可以拥有属性:hello.name, hello.length ,甚至可以动态添加它的属性:

hello.test = 123;

于是:

hello
├── 可以被调用 → 它是函数
│
└── 可以拥有属性 → 函数也是对象

JavaScript中,函数本身也是一个值,而这个值同时具有“可调用能力”和“对象能力”。所以 JavaScript的“对象”这个概念确实比很多语言更加宽泛。

函数作为对象,是JavaScript 设计非常特别地方,它比普通对象多了一种能力:

普通对象

可以保存数据
可以拥有属性
可以拥有方法


函数对象

可以保存数据
可以拥有属性
可以拥有方法
+
可以被调用()

函数作为对象,它不仅仅可以拥有属性,还可以拥有方法:

function hello() {
console.log("你好");
}

hello.age = 18;

hello.say = function() {
console.log("我是 hello 的方法");
};

所以:

函数并不是“只有一条代码的东西”。它是一个特殊的对象,只不过这个对象额外拥有“可调用”能力。

函数作为对象的深入理解:独特的操作方法

函数外部给函数对象添加属性和方法:

function hello() {
    console.log("你好");
}

hello.say = function() {
    console.log("hello 的 say 方法");
};

hello.run = function() {
    console.log("hello 的 run 方法");
};

这里非常直观:

hello
 │
 ├── say → function
 │
 └── run → function

因为 hello 是一个变量,指向那个函数对象,所以:

hello.say

就是:

找到 hello 指向的函数对象 → 找它的 say 属性。

函数体内部也可以操作

function hello() {
    console.log("你好");

    hello.say = function() {
        console.log("我是 say");
    };

    hello.run = function() {
        console.log("我是 run");
    };
}

然后:

hello();

执行以后,函数自己给自己添加了两个属性,于是:

hello.say();
hello.run();

都可以执行,所以:

调用 hello()
      ↓
进入 hello 函数体
      ↓
hello.say = ...
hello.run = ...
      ↓
hello 这个函数对象被增加了两个属性

甚至可以让函数自己给自己添加属性,比如:

function hello() {
    hello.count++;
}

hello.count = 0;

然后:

hello();
hello();
hello();

console.log(hello.count);

得到:

3

整个过程:

hello.count = 0

第一次 hello()
    ↓
hello.count++
    ↓
1

第二次 hello()
    ↓
hello.count++
    ↓
2

第三次 hello()
    ↓
hello.count++
    ↓
3

这其实非常有意思:

所以,不是只有函数外面才能给函数添加属性,函数体内也可以:

function hello() {
    hello.say = function() {};
    hello.count = 100;
}

JavaScript中的对象类别

1. 普通对象(Plain Objects / Ordinary Objects)

最基础、最直观的对象类型。它的本质就是一个无序的数据集合,键(Key)必须是字符串或 Symbol,值(Value)可以是任意类型。

字面量对象:{ name: “张三”, age: 18 }
构造出来的普通对象:new Object()、Object.create(null)

2. 内置/内置引用对象(Standard Built-in Objects)

JavaScript 引擎预先为你准备好的对象。它们虽然表现形式各异,但底层全是“对象(Object)”类型,用来处理各种特定领域的数据逻辑:

函数对象(Function Objects):function fn() {} 或 () => {}(可调用的特殊对象)。
集合与键值映射:Array(数组)、Set、Map、WeakSet、WeakMap。
日期与正则:Date、RegExp。
包装对象:Number、String、Boolean(通过 new Number(123) 创建的对象形式)。
错误对象:Error、TypeError、SyntaxError 等。
结构化数据与二进制:JSON、Math、ArrayBuffer、DataView 等(注意:Math 和 JSON 是静态命名空间对象,不能被 new)。

3. 宿主对象(Host Objects)

并非 JavaScript 语言本身自带,而是由运行环境(如浏览器或 Node.js)注入到 JS 运行时环境中的对象。

浏览器环境(DOM/BOM):window、document、HTMLElement(如 <p> 标签对应的节点)、fetch 的 Response 对象等。
Node.js 环境:global、process、fs 模块返回的文件流对象、http 请求对象等。

4. 特殊机制对象(Exotic Objects)

这类对象改变了 JavaScript 底层的默认行为(例如重写了属性查找、拦截了基本操作),是 ES6 之后引入的“能力对象”:

Proxy(代理对象):通过 new Proxy(target, handler) 创建,能够自定义和拦截对目标对象的读取、赋值、遍历等一切底层行为(Vue 3 响应式的核心)。
Promise 对象:用于管理异步状态(Pending / Fulfilled / Rejected)的状态机对象。
Generator / Iterator(生成器与迭代器对象):含有 .next() 方法、能够暂停和恢复执行流程的对象。
总结:JS 的“万物皆对象”

可以用一句话来区分不同语言中“对象”的本质差异:

Java / C#:对象是从类(Class)这套模具里倒出来的石膏像,类型严格,结构固定。
JavaScript:对象是一块橡皮泥(普通的哈希表),只要在上面加上不同的功能模块(例如加上 [[Call]] 就成了函数对象,加上 length 和数字索引就成了数组对象),它就能扮演任何角色。

Javascript中的数据类型大致可以分两大类:

JavaScript 的值
│
├── 基本类型 (Primitive Types,即基本类型)
│   ├── string
│   ├── number
│   ├── boolean
│   ├── null
│   ├── undefined
│   ├── bigint
│   └── symbol
│
└── 对象(Object)—— 引用类型(Reference Types,即 Object 及其衍生类型)
    │
    ├── 普通对象 {}
    │
    ├── Array []
    │
    ├── Function function(){}
    │
    ├── Promise
    │
    ├── Date
    │
    ├── Map
    │
    ├── Set
    │
    ├── RegExp
    │
    └── ...

“对象”是上层分类,不是具体类型。

函数作为对象背后的设计思路:函数是JS中的一等公民

JavaScript 诞生的时候,它并不是为了设计一个像 Java 一样的大型企业级面向对象语言。它最初的目标非常简单:让网页里的东西能够响应用户操作。例如:

button.onclick = function() {
    alert("你点击了按钮");
};

这个设计其实非常自然:

button
 │
 └── onclick → 一个函数

点击的时候:

浏览器
  ↓
找到 onclick
  ↓
调用这个函数

所以 JS 非常强调:

函数可以到处传递。

例如:

function sayHello() {
    console.log("Hello");
}

button.onclick = sayHello;

甚至:

setTimeout(sayHello, 1000);

再比如:

array.map(sayHello);

这里函数实际上就像一件“可以到处传递的东西”。

在 Java、C# 或 C++ 等传统面向对象语言中,函数(方法)确实只能附属于类或对象,不能独立存在。但在 JavaScript 的设计哲学中,函数被赋予了最高地位——一等公民(First-Class Citizens)。这源于 Brendan Eich 借鉴的 Scheme 语言(函数式编程语言的鼻祖之一)。所以 JS 把函数设计成一种非常重要的一等值(first-class value)。

“一等公民”的意思是:只要普通变量/对象能做的事,函数全都可以做。

  • 函数可以赋值给变量;
  • 函数可以作为参数传给另一个函数(回调函数);
  • 函数可以作为另一个函数的返回值(闭包);
  • 函数可以拥有自己的属性和方法。