一、原型对象的定义
1. 两个关键概念
在 JavaScript 中,"原型"涉及两个不同但相关的概念:
| 概念 | 含义 | 访问方式 |
|---|---|---|
[[Prototype]] | 对象的内部链接,指向其原型对象 | Object.getPrototypeOf(obj) 或 obj.__proto__ |
prototype | 函数对象的属性,指向实例的原型对象 | Constructor.prototype |
关键区别:
- **
[[Prototype]]** 存在于** 所有对象 **(除null)上,是实例与原型之间的链接 - **
prototype** 只存在于** 函数对象 **上,用于构造函数创建实例时设置[[Prototype]]
2. 原型对象的图形化结构
function Person(name) {
this.name = name;
}
const person1 = new Person('Alice');** 上述代码的内存结构 **:
person1 (实例对象)
├─ name: "Alice" ← 自有属性
└─ [[Prototype]] ─────────┐
↓
Person.prototype (原型对象) ← person1.__proto__ 或 Object.getPrototypeOf(person1)
├─ constructor: Person ← 指向构造函数
└─ [[Prototype]] ─────────┐
↓
Object.prototype (顶层原型)
├─ toString(), valueOf() ← 所有对象共享的方法
└─ [[Prototype]]: null ← 原型链终点二、** 原型链的工作原理 **
** 属性查找机制(原型链搜索) **
当访问 person1.sayHello() 时,JavaScript 引擎执行以下步骤:
- ** 检查自有属性 **:
person1自身是否有sayHello?否。 - ** 查找原型 **:通过
person1.[[Prototype]]找到Person.prototype,是否有sayHello?是 → 执行。 - ** 若未找到 **:继续查找
Person.prototype.[[Prototype]]→Object.prototype。 - ** 到达终点 **:若
Object.prototype也未找到,返回undefined或抛出错误。
** 代码验证 **:
person1.__proto__ === Person.prototype; // true
Person.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true ← 原型链终点三、** 构造函数与原型对象的关系 **
** 1. 构造函数的原型属性 **
每个函数创建时,JS 引擎自动为其添加 prototype 属性:
function Foo() {}
Foo.prototype; // { constructor: Foo, [[Prototype]]: Object.prototype }- **
constructor**:指向函数本身(Foo.prototype.constructor === Foo) - **
[[Prototype]]**:默认指向Object.prototype
** 2. new 运算符的底层操作 **
const f = new Foo() 内部执行了:
// 伪代码
const f = {}; // 1. 创建空对象
f.[[Prototype]] = Foo.prototype; // 2. 链接原型
Foo.call(f); // 3. 执行构造函数(this 指向新对象)
return f; // 4. 返回新对象四、** 原型对象的三大作用 **
** 1. 实现方法共享 **
** 错误方式 **(每个实例都复制一份方法):
function Person(name) {
this.name = name;
this.say = function() { console.log('Hi'); }; // ❌ 每个实例独立函数
}** 正确方式 **(通过原型共享):
function Person(name) {
this.name = name;
}
Person.prototype.say = function() { console.log('Hi'); }; // ✅ 所有实例共享
const p1 = new Person('A');
const p2 = new Person('B');
p1.say === p2.say; // true ← 同一个函数** 2. 实现继承 **
// 父类
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function() { console.log('eating'); };
// 子类
function Dog(name) {
Animal.call(this, 'dog'); // 调用父构造函数
this.name = name;
}
// 设置原型链:Dog.prototype ← Animal.prototype ← Object.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor 指向
Dog.prototype.bark = function() { console.log('woof'); };
const myDog = new Dog('旺财');
myDog.eat(); // 继承自 Animal.prototype
myDog.bark(); // 自有方法** 3. 动态扩展所有实例 **
function Person() {}
const p1 = new Person();
const p2 = new Person();
// 动态添加原型方法
Person.prototype.walk = function() { console.log('walking'); };
p1.walk(); // ✅ 可用
p2.walk(); // ✅ 可用 ← 即使实例已经创建五、** 现代 ES6+ 的类语法 **
class 是原型继承的** 语法糖 **,底层仍然是原型机制:
class Person {
constructor(name) { this.name = name; }
say() { console.log('Hi'); }
}
// 等价于 ES5
function Person(name) { this.name = name; }
Person.prototype.say = function() { console.log('Hi'); };
// 验证
typeof Person; // 'function' ← 仍是函数
Person === Person.prototype.constructor; // true** 类继承的底层 **:
class Dog extends Animal {
bark() { console.log('woof'); }
}
// 等价于
function Dog() { Animal.call(this); }
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;六、** 核心要点总结 **
- ** 原型是对象**:每个对象(除
null)都有[[Prototype]]内部链接 - 函数有
prototype:构造函数的prototype属性是其创建实例的原型 - 原型链是链接:
obj → 原型 → 原型的原型 → ... → Object.prototype → null - 属性查找是委托:找不到属性时沿原型链向上搜索,而非复制
- 共享机制:原型上的方法被所有实例共享,节省内存
- 动态性:修改原型会影响所有已创建和未来的实例
一句话概括:JavaScript 的原型对象是对象之间共享属性和方法的桥梁,通过 [[Prototype]] 链接形成原型链,实现了"无类"的继承机制。
一、历史渊源:向Self语言致敬
"原型"一词源自**原型继承(Prototype-based Inheritance)**这一编程范式,其命名有深厚的历史和设计哲学背景:
JavaScript的原型机制直接借鉴了Self语言(一种基于原型的面向对象语言)。1995年Brendan Eich在10天内创造JavaScript时,刻意避开了传统的"类继承"模型,选择了更灵活的原型委托机制。"Prototype"在英文中意为:
- 原始模型:第一个用于测试和复制的模板
- 蓝本:后续对象以此为基础创建
在JavaScript中,prototype对象正是扮演了 "对象蓝本" 的角色。
二、"原型"的工程学内涵
1. 字面意义:对象的"原始模板"
就像制造业中用**原型(Prototype)**来批量生产产品一样:
// Person.prototype 是"原型对象"——所有Person实例的原始模板
function Person(name) {
this.name = name;
}
Person.prototype.say = function() {
console.log(`我是${this.name}`);
};
// 所有实例都从这个"模板"复制链接行为
const p1 = new Person('Alice'); // p1 以 Person.prototype 为原型
const p2 = new Person('Bob'); // p2 也以 Person.prototype 为原型在这个模型中:
Person.prototype是原型对象(模板)p1和p2是实例对象(从原型复制链接的产品)- 实例不复制方法,而是委托给原型查找
2. 与传统"类"的命名对比
| 范式 | 术语 | 含义 | 实例创建方式 |
|---|---|---|---|
| 基于类 | Class | 抽象蓝图,定义结构但不直接可用 | new Class() → 实例 |
| 基于原型 | Prototype | 可使用的对象,既是模板也是对象 | Object.create(proto) → 新对象 |
关键区别:
- 类是抽象概念:你无法直接调用类上的方法
- 原型是真实对象:你可以直接调用
Person.prototype.say(),它本身就是对象
三、为什么不用"Class"这个名字?
1. JavaScript诞生时的妥协
1995年JavaScript初创时,Java正流行。Netscape要求"看起来像Java",但Brendan Eich坚持函数式+原型的设计:
"我被告知'必须看起来像Java',但我偷偷塞进了函数式特性和原型继承。" —— Brendan Eich
如果叫"Class",会误导开发者认为这是Java那种静态类继承,而Prototype更能体现其动态、可变的对象委托本质。
2. 动态原型 vs 静态类
// 类的静态性(Java/C++):定义后结构固定
class Person {
void say() { /* 无法运行时修改 */ }
}
// 原型的动态性(JavaScript):可随时修改
function Person() {}
Person.prototype.say = function() { console.log('Hi'); };
// 运行后还能给所有实例增加新方法
Person.prototype.walk = function() { console.log('Walking'); };
// 即使p1已经创建,也能用新方法
const p1 = new Person();
p1.walk(); // ✅ 可用!这种 "模板即对象,对象可修改" 的特性,只有"原型"这个词能准确表达。
四、"原型"在中文语境的契合
翻译成"原型"而非"样本"或"模板",是因为:
- 原型可实例化:汽车原型本身能驾驶,
Person.prototype本身也是对象 - 原型可演进:产品原型会迭代,
prototype可在运行时修改 - 实例与原型联动:批量产品随原型升级而升级,实例自动获得原型的新方法
这与面向对象中的"类"(静态蓝图)形成鲜明对比。
五、ES6 Class语法的命名妥协
2015年引入的class关键字只是语法糖,底层仍是原型:
class Person {
constructor(name) { this.name = name; }
say() { console.log(this.name); }
}
// 编译后仍是原型链
console.log(typeof Person); // 'function'
console.log(Person.prototype.say); // [Function: say]JavaScript 保留 class 名称是为了降低学习成本,但文档仍强调"基于原型的继承",因为这才是本质。
核心总结
| 概念 | 为什么叫"原型" | 体现 |
|---|---|---|
| 词源 | 源自Self语言,向原型继承范式致敬 | Prototype-based Programming |
| 工程学 | 它是可复用的"原始模型" | Person.prototype 是实例的蓝本 |
| 动态性 | 它是可修改的真实对象 | 运行时增减方法影响所有实例 |
| 区别于类 | 避免与静态类继承混淆 | 体现JavaScript"无类"的设计哲学 |
一句话:因为它既是对象的模板,又是可使用的对象本身,这种"模板即实例"的动态特性,只有"原型"一词能精准表达。
为什么 JavaScript 的函数也是对象?
在 JavaScript 中,函数(function)被设计成**"可调用的对象"(callable objects)**。这一设计并非偶然,而是根植于语言的历史目标、技术实现和哲学理念。下面从 6 个维度彻底解释"为什么"。
一、历史原因:10 天内的"偷懒"设计
1995 年,Brendan Eich 被要求"让网页动起来",并被告知**"必须看起来像 Java"**。
| 需求 | 决策 |
|---|---|
| 要有类、但来不及实现完整类体系 | 把函数当构造函数用(new Foo()) |
| 要有方法、但不想新增语法 | 允许函数作为属性值(obj.method()) |
| 要有回调、但不想单独设计类型 | 让函数可以像变量一样传递 |
于是,函数被直接做成了一种特殊对象,一次性解决三大需求。
"我必须在 10 天内让 JS 跑起来,把函数做成对象是最快路径。" —— Brendan Eich
二、技术实现:引擎内部如何"既是函数又是对象"
1. 对象模型的底层结构
V8(Chrome 引擎)内部伪代码:
// 普通对象
JSObject {
Map* shape; // 隐藏类,记录属性布局
Object* properties; // 属性存储
}
// 函数对象 = JSObject + 可调用标记
JSFunction : JSObject {
Code* code; // 机器码入口地址
SharedFunctionInfo* info; // 源码、参数长度等
bool is_callable; // 特殊标记,设为 true
}关键:
- 内存布局上,函数对象就是 JSObject 的扩展
- 多了一个
[[Call]]内部方法,让引擎知道"此对象可以被执行"
2. 规范视角:ECMAScript 内部插槽
| 插槽 | 普通对象 | 函数对象 |
|---|---|---|
[[Prototype]] | ✅ 指向原型 | ✅ 指向 Function.prototype |
[[Call]] | ❌ 不存在 | ✅ 存在,指向调用逻辑 |
[[Construct]] | ❌ 不存在 | ✅ 存在(若函数可做构造函数) |
结论:函数对象比普通对象多两个内部插槽,其余完全一样。
三、语言表现:代码层面的"对象证据"
1. 可以添加属性(像对象一样)
function greet(name) {
console.log('Hello ' + name);
}
// 函数作为对象,随意增删属性
greet.counter = 0;
greet.inc = function () { this.counter++; };
greet.inc();
console.log(greet.counter); // 12. 可以赋值、传参、返回(像变量一样)
// 高阶函数:函数作参数,也作返回值
function createGreeter(greeting) {
return function (name) {
console.log(greeting + ', ' + name);
};
}
const sayHi = createGreeter('Hi');
sayHi('Alice'); // Hi, Alice3. 可以原型继承(像对象一样)
function Foo() {}
Foo.prototype.speak = () => console.log('Speaking');
// 函数本身也继承自 Function.prototype
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Foo.toString()); // 继承自 Function.prototype.toString四、哲学理念:统一的最小化设计
JavaScript 采用**"一切皆对象"**的极端统一哲学:
| 类型 | 在 JS 中的对待 |
|---|---|
| 函数 | 可调用的对象 |
| 数组 | 特殊的对象(索引是键) |
| 原始值 | 临时包装为对象(如 new Number(1)) |
好处:
- 语法简洁:无需区分"函数指针""方法""委托"
- 元编程能力强:可在运行时给函数加属性、方法、元数据
- 易于实现模式:装饰器、依赖注入、插件系统都依赖"函数即对象"
五、对比其他语言:JS 的独特之处
| 语言 | 函数类型 | 能否给函数加属性 | 能否运行时修改 |
|---|---|---|---|
| Java | 单独的 Function 接口 | ❌ 不行 | ❌ 不行 |
| C/C++ | 函数指针 | ❌ 不行 | ❌ 不行 |
| Python | 第一类对象(可调用) | ✅ 可以 | ✅ 可以 |
| JavaScript | 可调用的对象 | ✅ 可以 | ✅ 可以 |
JS 把函数提升到**"一等公民"**地位,甚至超越 Python:函数既是对象,又能被 new 实例化。
六、函数作对象的副作用与注意点
1. 内存占用略大
函数对象比普通对象多持有字节码/机器码指针,大量闭包可能更耗内存。
2. 命名函数 vs 匿名函数
// 命名函数:函数对象有 name 属性
function foo() {}
console.log(foo.name); // "foo"
// 匿名函数:name 为空或推断
const bar = function () {};
console.log(bar.name); // "bar"(引擎推断)3. 可构造性判断
function Foo() {}
console.log(typeof Foo); // "function"
console.log(Foo instanceof Object); // true
console.log(Foo instanceof Function); // true
console.log(Foo.prototype); // 存在 → 可构造核心总结
| 维度 | 回答 |
|---|---|
| 历史 | 10 天速成需求,函数兼作构造器、方法、回调最省事 |
| 技术 | 引擎内部=普通对象+[[Call]]/[[Construct]]插槽 |
| 语言 | 可以增删属性、继承原型、当变量传递,完全对象化 |
| 哲学 | "最小化、统一化"——用一套模型解决多种场景 |
| 结果 | 函数是**"可调用对象"**,JavaScript 最具特色的设计之一 |
一句话:因为 Brendan Eich 需要一种结构同时胜任函数、方法、构造器、模块四种角色,而"对象"是唯一能在 10 天内满足所有需求的超集类型。
