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中的一等公民
函数在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 语言(函数式编程语言的鼻祖之一)。
“一等公民”的意思是:只要普通变量/对象能做的事,函数全都可以做:
- 函数可以赋值给变量;
- 函数可以作为参数传给另一个函数(回调函数);
- 函数可以作为另一个函数的返回值(闭包);
- 函数可以拥有自己的属性和方法。
MDN的正式定义是:”A programming language is said to have First-class functions when functions in that language are treated like any other variable.”
当一门编程语言中的函数被当作其他任何变量一样对待时,就称这门语言拥有一等函数(First-class functions)。)在中文版 MDN 中,它被正式翻译为 “一等公民(First-Class Citizens)”。具体到 JavaScript 的函数中,这意味着函数可以像普通的数字(如 18)或字符串(如 “hello”)一样,被任意操作。 它包含了以下四个核心能力:
① 函数可以被赋值给变量在很多传统语言(如早期的 C/Java)中,函数就是一段死代码,你不能把它塞进变量里。但在 JS 中,你可以像对待数字一样对待函数:
// 就像赋值数字一样:let num = 18; let sayHello = function() { console.log("Hi!"); }; sayHello(); // 执行这个变量② 函数可以作为参数传递给另一个函数
这是前端开发中最常见的操作(即回调函数)。你可以把一个函数当成“跑腿小哥”,传给另一个函数去执行。
function runTask(callback) { console.log("任务开始..."); callback(); // 执行传进来的函数 } // 把 sayHello 函数像参数一样传进去 runTask(sayHello);③ 函数可以作为另一个函数的返回值
一个函数在执行完毕后,不仅可以返回数字、字符串,还可以现场制造并返回一个新的函数。
function createMultiplier(factor) { // 返回了一个全新的匿名函数 return function(number) { return number * factor; }; } let double = createMultiplier(2); // double 现在是一个函数了 console.log(double(5)); // 输出 10④ 函数可以拥有自己的属性和方法
既然“一等公民”意味着函数本质上也是一个“值/对象”,那它就可以像普通对象一样被挂载属性:
function myFunc() {} myFunc.version = "1.0.0"; // 给函数加一个属性 console.log(myFunc.version); // 输出 "1.0.0"
first-class value(一等值) 理解成什么高深术语。它其实是在说:
函数和数字、字符串一样,可以被当成“一个值”来使用。
Java倾向于:
“这是一个类,那是一个对象,这是一个方法。”
而 JavaScript 更倾向于:
“这是一个值。这个值具有哪些能力?”
JS设计之初,完全就与Java走的不是同一条路线
JavaScript 诞生于 1995 年,原名叫做LiveScript,后来为了蹭当时火热的Java热度,改名为JavaScript 。JavaScript 最初的对象系统,与 Java/C++ 那种 class-based 面向对象系统,本来就是两条不同路线。甚至可以说:JavaScript 故意没有采用 Java 那套。
别的语言继承都是“老子生儿子,老子是模板,儿子是实例”。JS 偏不,它没有模板(类),所有东西都是活生生的对象。你想继承?那就拿一个现成的对象当“原型”,通过一条无形的链条(原型链)把它们串起来,找不到属性就沿着链条往上搜。
这其实是一种非常前卫且轻量级的基于原型的面向对象(Prototype-based OOP)。只是因为当年高层逼着 Brendan Eich 加入new关键字,导致 JS 的原型继承套上了一层极其扭曲的、模仿 Java 类的伪装,让几代开发者看得一头雾水。在真正的面向对象语言中,“类”是模板,“函数”是动作,但 JS 在前 20 年(ES6 之前)根本没有
class。设计者 Brendan Eich 为了迎合网景高层“要像 Java”的要求,做了一个极其别扭的操作:直接让“函数对象”兼职做“类/构造函数”——
- 当你直接调用 Person() 时,它是个普通函数;
- 当你 new Person() 时,它又变成了构造函数(类)。
这种“身份分裂”,直接导致了一个函数对象内部必须同时塞入:
- 自己的
[[Call]](普通函数能力);- 准备给子孙的
prototype(类的能力);- 自己的
[[Prototype]](作为对象的继承能力)。高层强加的
new关键字,确实完全没有改变 JS 底层“用函数充当对象工厂,依靠原型链(Prototype)进行属性委托”的核心逻辑。它只是在原本纯粹的原型语言(Self)外表上,强行套了一层“看起来像 Java”的语法外套(Syntactic Sugar)。
JavaScript 不是因为后来受到 Java/C++ 影响,才把“函数硬塞进对象体系”;它从一开始就采用了“对象 + 原型 + 函数是一等值”的设计。后来随着 Java/C++ 等语言的 class 思维成为主流,JavaScript 才在原有模型上增加了 class 语法,让它看起来更像传统面向对象语言。
JavaScript 开始模仿的是原型语言(如 Self),如果我们剥离 new 关键字,看看原型语言最原始、最直观的模样,逻辑其实非常简单顺畅:
// 1. 直接定义一个原型对象(没有任何函数,就是纯对象)
const personPrototype = {
sayHi() {
console.log("你好,我是 " + this.name);
}
};
// 2. 直接基于原型创建一个新对象(利用 Object.create 的思想)
const p1 = Object.create(personPrototype);
p1.name = "张三";
p1.sayHi(); // 输出: 你好,我是 张三在这个纯粹的原型世界里:
- 没有类(Class);
- 没有构造函数(Constructor);
- 没有
new; - 没有混乱的
prototype与[[Prototype]]的双链同名尴尬。 - 有的只是单纯的“对象链接对象”:
p1找不到sayHi,就顺着链条去personPrototype里找。逻辑一目了然。
高层逼迫加入 new 后,发生了什么?
网景公司高层要求:“必须像 Java,要有 new 关键字,要在 new 后面跟一个名字!”
因为 Brendan Eich 根本没有时间去真的写一套像 Java 那样严谨复杂的 Class 编译器,为了在 10 天内搞定这个需求,他做了一个用函数模拟 new 行为的折中手段:
为了让 new Person() 能够运行,必须给 Person 这个函数对象硬塞入一个额外的属性,叫做 prototype,用来存放打算给新对象继承的方法。
这直接把原本极简的逻辑变弯了:
- 原本简洁的路径:实例-直接继承-原型对象
- 加了
new之后变拧巴的路径:实例-通过 new-函数对象-借助函数身上的 .prototype-真正的原型对象
三、 为什么说这个 new 让人看起来极其别扭?
这个外加的 new 创造了一种“伪装成类的幻觉”,直接撕裂了代码的表面语义和底层物理实现:
1. 表面看:像是在实例化一个“类”
var p1 = new Person() 让人误以为 Person 是个 Class,p1 是依据 Person 复制出来的。
2. 底层看:只是触发了一段隐式代码的函数调用
引擎在背后默默帮你做了四件事:
- 隐式创建一个空对象
{}; - 隐式把这个空对象的内部指针
[[Prototype]]连到Person.prototype上; - 隐式把
this绑定到这个空对象,并运行Person()函数体; - 隐式返回这个空对象。