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)。
“一等公民”的意思是:只要普通变量/对象能做的事,函数全都可以做。
- 函数可以赋值给变量;
- 函数可以作为参数传给另一个函数(回调函数);
- 函数可以作为另一个函数的返回值(闭包);
- 函数可以拥有自己的属性和方法。