JS 原型链:prototype / 实例 / 构造函数 / 顶层原型
引言:为什么需要理解原型链?
在 ES6 的 class 语法出现之前,JavaScript 就已经是一门成熟的面向对象语言------只是它走了一条与众不同的路:基于原型的面向对象 。即使今天你日常写的是 class,它底层依然是原型链的语法糖。理解原型链,才能真正理解 JS 里 this 的指向、方法的共享机制、以及 instanceof 的原理。
这篇文章从"构造函数 ↔ 原型对象 ↔ 实例"这个核心三角出发,配合可运行的代码示例,把原型链上每一个节点讲清楚。
核心三角关系
javascript
┌─────────────────────────────────────┐
│ │
▼ │
┌─────────┐ new ┌──────────┐ │
│ 构造函数 │ ──────────► │ 实例 │ │
│Greeting │ │ xxx │ │
└────┬─────┘ └────┬─────┘ │
│ .prototype │ │
│ │ .__proto__│
│ │ (或 │
▼ ▼ Object │
┌──────────┐ .getProto- │
│ 原型对象 │ ◄────────────── typeOf) │
│Greeting │ │
│.prototype│── .constructor ──────────────┘
└────┬─────┘
│ .__proto__
▼
┌──────────┐
│ Object. │
│ prototype│ ← 顶层原型
│ │
└────┬─────┘
│ .__proto__
▼
null ← 原型链的终点
1. prototype --- 构造函数上的属性
js
function Greeting(name) {
this.name = name;
}
// Greeting.prototype 是 Greeting 这个构造函数自带的一个对象
// 它就是一个普通对象,用来存放所有实例共享的方法
console.log(Greeting.prototype); // { constructor: ƒ Greeting }
关键点 :只有函数才有 prototype 属性(箭头函数除外)。这个 prototype 对象就是未来所有 new Greeting() 产生的实例的"模板"。
prototype 上默认有一个 constructor 属性,指向构造函数本身:
js
console.log(Greeting.prototype.constructor === Greeting); // true
2. 实例 --- new 出来的对象
new 一个构造函数,就得到了一个实例。我们把 Greeting 用 new 调用,拿到实例 xxx:
js
const xxx = new Greeting("xxx");
这行代码背后,JS 引擎实际上做了四件事:
- 创建一个空对象
{} - 把这个空对象的
__proto__指向Greeting.prototype this指向这个空对象,执行构造函数体,给this添加属性- 如果构造函数没有
return一个对象,则返回this(新创建的那个对象);如果return了一个对象,则返回那个对象
结果 :实例 xxx 本身只有构造函数里 this.xxx = ... 的属性,方法不在实例身上,而是通过原型链向上查找。
js
console.log(xxx.name); // "xxx" --- 自己的属性
console.log(xxx.say); // ƒ --- 从 Greeting.prototype 上找到的
console.log(xxx.hasOwnProperty('name')); // true
console.log(xxx.hasOwnProperty('say')); // false (say 在原型上)
3. __proto__ --- 实例通向原型的桥梁
实例是怎么找到原型上的方法的?靠的就是 __proto__ 这条隐式链接。每一个实例的 __proto__ 都指向它的构造函数的 prototype:
js
xxx.__proto__ === Greeting.prototype // true
这行等式是整个原型链理论的基石。当你访问 xxx.say 时,JS 引擎的查找顺序是这样的:
xxx自身有没有say?→ 没有xxx.__proto__(即Greeting.prototype)上有没有?→ 有!调用它
这就是原型链 的本质:一个沿着 __proto__ 向上查找属性的链条。
4. 顶层的原型 --- Object.prototype
Greeting.prototype 本身也是一个对象,它也有自己的 __proto__:
js
Greeting.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true --- 链的终点
完整的查找链:
javascript
xxx.say()
xxx 自身 → Greeting.prototype → 找到了 → 调用
xxx.toString()
xxx 自身 → Greeting.prototype → Object.prototype → 找到了 → 调用
xxx.someProp
xxx 自身 → Greeting.prototype → Object.prototype → null → undefined
这也是为什么所有对象都能用 toString()、hasOwnProperty() --- 它们定义在 Object.prototype 上。
5. 一张图不够?来个"孔子"类比
如果你觉得上面那套"构造函数 → 原型对象 → 实例"太抽象,我们换一个更形象的视角。
在 JS 的世界里没有"类",只有"对象"。那怎么让一群对象共享行为?答案就是------指定一个典范对象,所有新对象都链接到它,从它身上继承方法。
这很像儒家文化中的"孔子":
- 孔子(
prototype) 本身是一个具体的人,但他是"人"的典范。他定义了"人"应该怎么说话(say)、怎么工作(work)。 - 具体的人(实例) 并不自己发明这些行为,而是"向孔子看齐"------通过
__proto__这条线链接到孔子,从而拥有了这些行为。 - 当别人问"你会怎么介绍自己?"(调用
xxx.say()),你不需要自己定义say,沿着__proto__找到孔子那儿自然就有了。
这套机制的妙处在于:孔子只需要一个,所有以他为典范的人共享同一套行为,不需要每个人自己存一份。
| 概念 | 类比 | 代码 |
|---|---|---|
| 构造函数 | "人"这个概念/模板 | function Greeting(){} |
| 原型对象 | 孔子 --- 典范的具体化身 | Greeting.prototype |
| 实例 | 以孔子为典范的具体的人 | const xxx = new Greeting("xxx") |
__proto__ |
"向孔子看齐"的那条线 | xxx.__proto__ === Greeting.prototype |
当然,孔子自己也是一个对象,所以他也有自己的"典范"------Object.prototype(更顶层的行为来源,比如 toString)。一层一层往上找,最终到达 null,这就是原型链。
类比只是辅助理解,不要过度延伸。记住三个角色就好:构造函数 负责造人,原型对象 负责教人,实例是被造出来、向原型学习的人。
6. 完整结构图
把上面的三角关系拉直,就是一条清晰的链:
scss
构造函数 Greeting
┌──────────────┐
│ .prototype │──┐
└──────────────┘ │
▼
原型对象 Greeting.prototype ◄─── .constructor
┌──────────────────────┐
│ say() │
│ work() │
│ .__proto__ │
└──────────┬───────────┘
│
▼
Object.prototype(顶层原型)
┌──────────────────────┐
│ toString() │
│ hasOwnProperty() │
│ .__proto__ │
└──────────┬───────────┘
│
▼
null(终点)
▲
│
实例 xxx │ 属性查找方向:沿 __proto__ 从下往上
┌──────────────────────┐
│ name: "xxx" │ ← 自身属性
│ .__proto__ ──────────┘
└──────────────────────┘
原型链查找过程(从实例出发,一路向上):
javascript
xxx.say()
xxx 自身 → Greeting.prototype → 找到了 → 调用
xxx.toString()
xxx 自身 → Greeting.prototype → Object.prototype → 找到了 → 调用
xxx.someProp
xxx 自身 → Greeting.prototype → Object.prototype → null → undefined
三条核心等式
前面讲了这么多概念,其实可以浓缩成三条等式。这三条等式是你理解整个原型链的"骨架",面试中但凡问到原型链,本质上就是在考你知不知道这三条:
js
// 1. 实例 → 原型
xxx.__proto__ === Greeting.prototype // true
// 2. 原型 → 构造函数
Greeting.prototype.constructor === Greeting // true
// 3. 原型 → 顶层原型 → null
Greeting.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
记住这三条,后面所有代码示例本质上都是在验证和展开它们。
7. 代码示例大全 --- 验证核心三角关系
以下所有示例基于 3.js 中的 Greeting 构造函数:
js
function Greeting(name) {
this.name = name;
}
Greeting.prototype.say = function () {
console.log(`我叫${this.name} 很高兴认识你`);
};
Greeting.prototype.work = function () {
console.log(`我叫${this.name} 正在打csgo`);
};
const xxx = new Greeting("xxx");
7.1 prototype --- 只有函数才有的属性
prototype 是函数独有的属性,普通对象和实例都没有。下面这段代码可以让你一次性看清"谁有、谁没有":
js
// ✅ 普通函数 / 构造函数 --- 有 prototype
console.log(Greeting.prototype);
// → { say: ƒ, work: ƒ, constructor: ƒ Greeting }
// ✅ 函数表达式 --- 有 prototype
const fn = function () { };
console.log(fn.prototype);
// → { constructor: ƒ }
// ✅ 内置构造函数 --- 有 prototype
console.log(Array.prototype);
// → [constructor: ƒ, push: ƒ, pop: ƒ, ...](数组方法的来源)
console.log(Function.prototype);
// → ƒ () { }(注意:是一个函数对象!)
console.log(Date.prototype);
console.log(RegExp.prototype);
// ❌ 箭头函数 --- 没有 prototype
const arrow = () => { };
console.log(arrow.prototype); // → undefined
// ❌ 普通对象 --- 没有 prototype
const obj = {};
console.log(obj.prototype); // → undefined
// ❌ 实例对象 --- 没有 prototype
console.log(xxx.prototype); // → undefined
// ❌ 数组实例 --- 没有 prototype
console.log([1, 2, 3].prototype); // → undefined
// ❌ null / undefined --- 没有 prototype(直接报错)
// null.prototype → TypeError
// undefined.prototype → TypeError
// 特殊的 prototype
console.log(typeof Function.prototype); // → "function"(唯一的 prototype 是函数的)
console.log(typeof Array.prototype); // → "object"
console.log(typeof Date.prototype); // → "object"
7.2 prototype.constructor --- 指回构造函数
prototype 对象上默认有一个 constructor 属性,指回构造函数本身。这个环保证了实例可以通过原型链找到自己的"出身":
js
// ✅ 默认就有 constructor
console.log(Greeting.prototype.constructor === Greeting); // true
// ✅ 实例通过原型链也能访问 constructor
console.log(xxx.constructor === Greeting); // true
// 查找路径:xxx 自身没有 constructor → xxx.__proto__(即 Greeting.prototype)上有 → 找到了
// ✅ 数组实例的 constructor
console.log([].constructor === Array); // true
// ✅ 函数对象的 constructor
console.log(Greeting.constructor === Function); // true
// ⚠ 手动覆盖 prototype 会丢失 constructor
Greeting.prototype = {
say: function () { console.log('hi'); }
};
// 现在 Greeting.prototype.constructor 指向 Object,因为对象字面量的 constructor 是 Object
console.log(Greeting.prototype.constructor === Object); // true
// ✅ 手动补回 constructor
Greeting.prototype = {
say: function () { console.log('hi'); },
constructor: Greeting // 手动加回来
};
console.log(Greeting.prototype.constructor === Greeting); // true
7.3 __proto__ --- 实例通向原型的桥梁
前面已经验证过一次了,这里我们用不同类型的对象来全面验证------实例、普通对象、数组、函数,甚至原型对象自己,它们的 __proto__ 分别指向谁:
js
// ✅ 实例的 __proto__ 指向构造函数的 prototype
console.log(xxx.__proto__ === Greeting.prototype); // true
// ✅ 等价的标准写法(推荐)
console.log(Object.getPrototypeOf(xxx) === Greeting.prototype); // true
// ✅ 普通对象的 __proto__
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
// ✅ 数组实例的 __proto__
console.log([].__proto__ === Array.prototype); // true
// ✅ 函数对象的 __proto__
console.log(Greeting.__proto__ === Function.prototype); // true
// ✅ 原型对象自己的 __proto__
console.log(Greeting.prototype.__proto__ === Object.prototype); // true
// ✅ Object.prototype.__proto__ --- 原型链的终点
console.log(Object.prototype.__proto__); // → null
// ❌ Object.create(null) 创建的对象没有 __proto__
const noProto = Object.create(null);
console.log(noProto.__proto__); // → undefined
console.log(Object.getPrototypeOf(noProto)); // → null
7.4 完整的原型链查找路径表
把上面的零散验证串成一条完整的链。从实例出发,沿着 __proto__ 一路走到 null,中间经过的每一站都列在这里:
javascript
实例 xxx 的原型链(一条线走到底):
xxx 自身(name 属性在此)
│ .__proto__
▼
Greeting.prototype(say、work 方法在此)
│ .__proto__
▼
Object.prototype(toString、hasOwnProperty、valueOf 等在此)
│ .__proto__
▼
null(终点)
逐一验证:
console.log(xxx.__proto__ === Greeting.prototype); // true
console.log(xxx.__proto__.__proto__ === Object.prototype); // true
console.log(xxx.__proto__.__proto__.__proto__ === null); // true
// 完整遍历原型链
let current = xxx;
while (current !== null) {
console.log(current);
current = Object.getPrototypeOf(current);
}
// 输出:
// Greeting { name: 'xxx' }
// { say: ƒ, work: ƒ, constructor: ƒ }
// { constructor: ƒ Object, toString: ƒ, ... }
// → 最后 current 为 null 停止
7.5 属性查找的详细演示
理解了链的结构,接下来看实际的属性查找过程。我们设计 5 个场景,从"属性在自身"到"属性哪里都不存在",完整走一遍原型链的查找逻辑:
js
// 场景1:属性在实例自身 --- 直接返回,不往上找
console.log(xxx.name); // "xxx"
console.log(xxx.hasOwnProperty('name')); // true
// 场景2:属性在 Greeting.prototype 上 --- 往上找一层
console.log(xxx.say); // ƒ say()
console.log(xxx.hasOwnProperty('say')); // false
console.log(xxx.__proto__.hasOwnProperty('say')); // true
// 场景3:属性在 Object.prototype 上 --- 往上找两层
console.log(xxx.toString); // ƒ toString()
console.log(xxx.hasOwnProperty('toString')); // false
console.log(Greeting.prototype.hasOwnProperty('toString')); // false
console.log(Object.prototype.hasOwnProperty('toString')); // true
// 场景4:属性在任何一层都不存在 --- 返回 undefined
console.log(xxx.someRandomProp); // undefined
// 查找路径:xxx → Greeting.prototype → Object.prototype → null → 没有 → undefined
// 场景5:实例属性"遮蔽"原型属性(同名覆盖)
Greeting.prototype.name = "默认名字"; // 原型上设置一个 name
const yyy = new Greeting("yyy");
console.log(yyy.name); // "yyy" --- 先找到实例自身的
// 如果构造函数没传 name:
function G() { }
G.prototype.name = "default";
const z = new G();
console.log(z.name); // "default" --- 实例没有,走原型链找到
7.6 new 运算符的返回值情况(本质只有两种:return 对象 vs 其他)
前面第 2 节列了 new 的四步过程,其中第 4 步值得单独拿出来深挖。构造函数里写 return 会怎样?答案只有两种情况------返回对象就生效,返回原始值就被无视。下面用 5 个函数逐一验证:
js
// 情况1:构造函数没有 return 语句 → 返回新创建的对象(默认行为)
function A(name) {
this.name = name;
// 没有 return
}
const a = new A("a");
console.log(a); // A { name: 'a' }
console.log(a instanceof A); // true
// 情况2:构造函数 return 了一个原始值 → 忽略,仍返回新创建的对象
function B(name) {
this.name = name;
return 123; // 返回原始值 → 被忽略
}
const b = new B("b");
console.log(b); // B { name: 'b' }
console.log(b instanceof B); // true
function C(name) {
this.name = name;
return "hello"; // 返回原始值 → 被忽略
}
const c = new C("c");
console.log(c); // C { name: 'c' }
function D(name) {
this.name = name;
return true; // 返回原始值 → 被忽略
}
const d = new D("d");
console.log(d); // D { name: 'd' }
function E(name) {
this.name = name;
return null; // null 是原始值(typeof null === "object" 是 JS 的历史遗留 bug),→ 被忽略!
}
const e = new E("e");
console.log(e); // E { name: 'e' }
console.log(e instanceof E); // true
function F(name) {
this.name = name;
return undefined; // 返回原始值 → 被忽略
}
const f = new F("f");
console.log(f); // F { name: 'f' }
// 情况3:构造函数 return 了一个对象 → 返回那个对象(this 被丢弃!)
function G(name) {
this.name = name;
return { custom: "我覆盖了 this" }; // 返回对象 → 生效!
}
const g = new G("g");
console.log(g); // { custom: '我覆盖了 this' }
console.log(g.name); // undefined(this 上绑的属性丢了)
console.log(g instanceof G); // false!
// 情况4:构造函数 return 了一个数组(也是对象)→ 返回那个数组
function H(name) {
this.name = name;
return [1, 2, 3]; // 数组也是对象 → 生效!
}
const h = new H("h");
console.log(h); // [1, 2, 3]
console.log(h instanceof H); // false
// 情况5:构造函数 return 了一个函数(也是对象)→ 返回那个函数
function I(name) {
this.name = name;
return function () { console.log('我是返回的函数'); }; // 函数也是对象 → 生效!
}
const i = new I("i");
console.log(typeof i); // "function"
console.log(i instanceof I); // false
7.7 不使用 new 调用构造函数的后果
如果忘了写 new,构造函数就变成了普通函数调用。this 不会指向新实例,而是指向全局对象(严格模式下是 undefined),后果很严重:
js
// ✅ 用 new 调用 → this 指向新创建的对象
const withNew = new Greeting("张三");
console.log(withNew); // Greeting { name: '张三' }
// ❌ 不用 new 调用 → this 指向全局对象(浏览器中是 window,Node 中是 global)
// 在严格模式下 this 为 undefined,会报错
// const withoutNew = Greeting("李四"); // 普通函数调用,this → globalThis
// console.log(window.name); // "李四"(在浏览器中)
// ❌ 不用 new 调用 → 返回值是 undefined(函数没有 return)
const result = Greeting("李四");
console.log(result); // undefined
7.8 instanceof 的原理 --- 检查原型链
instanceof 不是什么魔法,它的本质就是在原型链上做查找:检查右边构造函数的 prototype 是否出现在左边对象的原型链上。理解了这个,就能看懂下面所有的判断结果:
js
// instanceof 就是沿着右边构造函数的 .prototype 在左边对象的原型链上查找
console.log(xxx instanceof Greeting); // true
// 等价于:xxx.__proto__ === Greeting.prototype 或在更上层
console.log(xxx instanceof Object); // true
// 因为 xxx.__proto__.__proto__ === Object.prototype
console.log(xxx instanceof Array); // false
// Array.prototype 不在 xxx 的原型链上
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true(万物皆对象)
// ⚠ 注意:手动改变 prototype 后,旧实例仍指向旧 prototype
function Animal() { }
const oldInst = new Animal();
Animal.prototype = { newMethod: function () { } }; // 换了新的 prototype
const newInst = new Animal();
console.log(oldInst instanceof Animal); // false!(oldInst.__proto__ 指向旧 prototype)
console.log(newInst instanceof Animal); // true
7.9 原型对象上的方法与实例的关系
原型上的方法被多个实例共享,但 this 始终指向调用它的那个实例。同时要注意:基础类型的属性写不坏原型,引用类型的属性却可能"连坐"所有实例:
js
// 原型方法中的 this 指向调用者(即实例本身)
Greeting.prototype.introduce = function () {
console.log(this); // 这里的 this 是谁调用就指向谁
console.log(this === xxx); // true(当 xxx.introduce() 调用时)
};
xxx.introduce(); // → true
// 多个实例共享同一个原型方法(比较的是引用)
const aaa = new Greeting("aaa");
const bbb = new Greeting("bbb");
console.log(aaa.say === bbb.say); // true --- 同一个函数引用
console.log(aaa.say === Greeting.prototype.say); // true
// 每个实例自身的属性是独立的
console.log(aaa.name); // "aaa"
console.log(bbb.name); // "bbb"
aaa.name = "改了";
console.log(aaa.name); // "改了"
console.log(bbb.name); // "bbb" --- 不受影响
// 但如果修改原型上的引用类型属性,所有实例都会被影响!
Greeting.prototype.friends = [];
aaa.friends.push("小明");
console.log(bbb.friends); // ["小明"] --- 共享的同一个数组!
// 这就是为什么属性通常放构造函数里(this.xxx = ...),方法放 prototype 上
7.10 __proto__ vs prototype 的速查表(经典易错题)
面试最高频的坑来了:__proto__ 和 prototype 到底谁是谁?哪个角色有哪个?下面这段代码,把构造函数、实例、原型对象、Object.prototype、Function.prototype 五个角色放在一起对比,一次性搞清楚:
js
// 构造函数 Greeting(它自己是函数,也是对象)
console.log(typeof Greeting); // "function"
console.log(Greeting.prototype); // { say, work, constructor } --- 原型对象
console.log(Greeting.__proto__); // ƒ () {} = Function.prototype
// 因为 Greeting 是 Function 的实例
// 实例 xxx(普通对象)
console.log(typeof xxx); // "object"
console.log(xxx.prototype); // undefined!实例没有 prototype
console.log(xxx.__proto__); // Greeting.prototype
// 原型对象 Greeting.prototype(也是普通对象)
console.log(typeof Greeting.prototype); // "object"
console.log(Greeting.prototype.prototype); // undefined!普通对象没有 prototype
console.log(Greeting.prototype.__proto__); // Object.prototype
// Object.prototype(顶层原型)
console.log(typeof Object.prototype); // "object"
console.log(Object.prototype.__proto__); // null --- 终点!
console.log(Object.prototype.prototype); // undefined
// Function.prototype(特殊 --- 它是函数对象)
console.log(typeof Function.prototype); // "function"
console.log(Function.prototype.__proto__); // Object.prototype
console.log(Function.prototype.prototype); // undefined(即使它自己是函数,也没有 prototype 属性的实用价值,它是 undefined)
// Object / Function / Array 这些内置构造函数(它们是函数)
console.log(Object.__proto__ === Function.prototype); // true(所有函数都是 Function 的实例)
console.log(Function.__proto__ === Function.prototype); // true(Function 自己是自己的实例!)
console.log(Array.__proto__ === Function.prototype); // true
7.11 用 Object.create() 手动指定原型链
原型链不是只能靠 new 来建立。Object.create() 让你手动指定一个对象的原型,甚至可以用 Object.create(null) 创建一个没有任何原型的"纯净"对象:
js
// Object.create(proto) --- 创建一个对象,其 __proto__ 指向传入的对象
const customProto = { hello: function () { return 'hi'; } };
const child = Object.create(customProto);
console.log(child.__proto__ === customProto); // true
console.log(child.hello()); // "hi"
console.log(child.hasOwnProperty('hello')); // false --- hello 在原型上
// Object.create(null) --- 创建"纯净"对象
const pure = Object.create(null);
console.log(pure.__proto__); // undefined
console.log(pure.toString); // undefined --- 没有原型链,没有 toString!
console.log(Object.getPrototypeOf(pure)); // null
7.12 Object.setPrototypeOf() 动态修改原型链
原型链甚至可以在运行时动态修改------虽然这属于"你可以做但你不该做"的操作。Object.setPrototypeOf() 能让一个对象当场"换个爹":
js
// 可以"换个爹"
const a = { x: 1 };
const b = { y: 2 };
Object.setPrototypeOf(a, b); // 把 a 的原型设为 b
console.log(a.y); // 2 --- 从 b 上找到的
console.log(a.__proto__ === b); // true
// ⚠ 性能很差的写法,一般不要在生产代码中修改原型链
7.13 for...in vs Object.keys() --- 原型链属性的可见性
遍历对象属性时,有些方法会"看到"原型链上的属性,有些不会。这个差异是日常开发中容易踩的坑。下面的示例在原型链上分别挂了属性,然后用不同方式遍历,对比一目了然:
js
function F() { this.a = 1; }
F.prototype.b = 2;
Object.prototype.c = 3; // ⚠ 极不推荐!仅做演示
const obj = new F();
// for...in --- 遍历自身 + 原型链上所有【可枚举】属性
for (let key in obj) {
console.log(key); // "a" → "b" → "c"(沿着原型链一路往上)
}
// Object.keys() --- 只返回【自身】【可枚举】属性
console.log(Object.keys(obj)); // ["a"](b 和 c 都在原型上,不算)
// Object.getOwnPropertyNames() --- 只返回【自身】属性(含不可枚举)
console.log(Object.getOwnPropertyNames(obj)); // ["a"]
// "in" 操作符 --- 检查属性是否在对象自身或原型链上(无论可枚举性)
console.log('a' in obj); // true --- 自身
console.log('b' in obj); // true --- 在 F.prototype 上
console.log('c' in obj); // true --- 在 Object.prototype 上
console.log('toString' in obj); // true --- toString 在 Object.prototype 上且不可枚举
// hasOwnProperty --- 只看自身
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // false --- b 在原型上
console.log(obj.hasOwnProperty('c')); // false
console.log(obj.hasOwnProperty('toString')); // false
7.14 赋值的"遮蔽"行为 --- 读是共享,写是创建
这是最容易误解的地方:原型链上的属性是"读共享"但"写独立"。
js
function Person(name) {
this.name = name;
}
Person.prototype.age = 18; // 原型上的默认年龄
const p1 = new Person("张三");
const p2 = new Person("李四");
// === 读取 ===
console.log(p1.age); // 18 --- 从原型上读到的
console.log(p2.age); // 18 --- 从原型上读到的
console.log(p1.hasOwnProperty('age')); // false
// === 写入 p1.age ===
// 关键:这不会修改原型!而是在 p1 自身上创建了一个新属性!
p1.age = 25;
console.log(p1.age); // 25 --- 读自己的(遮蔽了原型的)
console.log(p2.age); // 18 --- 依然读到原型的
console.log(p1.hasOwnProperty('age')); // true --- 现在 p1 自己有 age 了
console.log(p2.hasOwnProperty('age')); // false
// 原型上的值完全没变
console.log(Person.prototype.age); // 18
// === 验证:如果要修改原型自身,必须显式操作 ===
Person.prototype.age = 30;
console.log(p1.age); // 25 --- p1 自己的 age 遮蔽了原型,原型改了什么也看不到
console.log(p2.age); // 30 --- p2 没有自己的 age,所以读到新的原型值
// === 怎么"揭开"遮蔽?===
delete p1.age;
console.log(p1.age); // 30 --- 删掉自己的 age 后,又重新暴露原型上的 age
console.log(p1.hasOwnProperty('age')); // false
// === 引用类型的遮蔽更容易踩坑 ===
Person.prototype.hobbies = [];
const p3 = new Person("王五");
// 情况A:push --- 修改的是引用指向的对象,并没触发赋值,所以影响所有人!
p3.hobbies.push("游戏");
console.log(Person.prototype.hobbies); // ["游戏"] --- 原型被改了!
// 因为 p3.hobbies.push() 不是赋值操作,而是沿着原型链找到数组后调用它的方法
// 情况B:赋值 --- 这才触发遮蔽
p3.hobbies = ["运动"]; // 在 p3 自身上创建了新的 hobbies 属性
console.log(p3.hobbies); // ["运动"] --- p3 自己的
console.log(Person.prototype.hobbies); // ["游戏"] --- 原型没变
console.log(p3.hasOwnProperty('hobbies')); // true
7.15 delete 操作符对原型链的影响
delete 和赋值一样,只作用于对象自身。你永远无法通过实例去删除原型上的属性------想删原型属性,必须直接操作原型对象本身:
js
function F() { this.own = 1; }
F.prototype.shared = 2;
const obj = new F();
// delete 只能删除【自身属性】,永远不会影响原型链!
console.log(obj.own); // 1
console.log(obj.shared); // 2
delete obj.own;
console.log(obj.own); // undefined --- 删掉了
console.log(obj.hasOwnProperty('own')); // false
delete obj.shared; // 试图删除原型上的属性
console.log(obj.shared); // 2 --- 还在!delete 删不掉原型上的属性
console.log(F.prototype.shared); // 2 --- 原型完全不受影响
// 要真正删除原型属性,必须直接操作原型对象:
delete F.prototype.shared;
console.log(obj.shared); // undefined --- 这次真没了
7.16 综合对比:3.js + 4.html 中的实际例子
js
// === 来自 3.js 的 Greeting ===
function Greeting(name) {
this.name = name;
}
Greeting.prototype.say = function () {
console.log(`我叫${this.name} 很高兴认识你`);
};
Greeting.prototype.work = function () {
console.log(`我叫${this.name} 正在打csgo`);
};
const xxx = new Greeting("xxx");
// 三条核心等式:
console.log(xxx.__proto__ === Greeting.prototype); // true (1)
console.log(Greeting.prototype.constructor === Greeting); // true (2)
console.log(Greeting.prototype.__proto__ === Object.prototype); // true (3)
console.log(Object.prototype.__proto__ === null); // true (4)
// === 来自 4.html 的 Person ===
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.poem = "仁义礼智信";
Person.prototype.say = function () {
console.log(`My name is ${this.name}, nice to meet you`);
};
Person.prototype.timeMF = function () {
console.log("Magic of time");
};
const p = new Person("张三", 17);
// poem 是原型上的属性(不是方法,是数据属性)
console.log(p.poem); // "仁义礼智信"
console.log(p.hasOwnProperty('poem')); // false --- poem 在原型上
// toString 来自 Object.prototype
console.log(p.toString()); // "[object Object]"
console.log(p.hasOwnProperty('toString')); // false
console.log(Object.prototype.hasOwnProperty('toString')); // true
// === 来自 1.js 的 MyQueue(无 class 的面向对象)===
const MyQueue = function () {
this.stack1 = [];
this.stack2 = [];
};
MyQueue.prototype.push = function (x) {
this.stack1.push(x);
};
const queue = new MyQueue();
console.log(queue.__proto__ === MyQueue.prototype); // true
console.log(queue.stack1); // [] --- 实例自己的属性
// === 来自 2.js 的验证:函数也是对象 ===
function greeting() {
console.log("hello world");
}
greeting.a = '1'; // 函数上可以直接挂属性
console.log(greeting.a); // "1"
console.log(greeting.prototype); // { constructor: ƒ greeting }
console.log(greeting.__proto__ === Function.prototype); // true(函数也是对象)
8. 速查:所有关键等式一览
前面拆开讲了很多,这里把全文最重要的等式汇总到一块。考试前扫一眼这一节就够了:
js
// ===== 实例 ↔ 原型 =====
xxx.__proto__ === Greeting.prototype // 任何实例都满足
Object.getPrototypeOf(xxx) === Greeting.prototype // 标准写法,等价
// ===== 原型 → 构造函数 =====
Greeting.prototype.constructor === Greeting // 默认成立
// 但手动覆盖 prototype 后可能不成立!
// ===== 原型 → 顶层原型 → null =====
Greeting.prototype.__proto__ === Object.prototype // 原型的原型是 Object.prototype
Object.prototype.__proto__ === null // 终点
// ===== 内置函数关系 =====
Function.__proto__ === Function.prototype // Function 自身也是 Function 的实例
Object.__proto__ === Function.prototype // Object 构造函数是 Function 的实例
Array.__proto__ === Function.prototype // 所有内置构造函数同理
Function.prototype.__proto__ === Object.prototype // Function.prototype 的原型是 Object.prototype
9. 2.js 深度解析:普通函数 vs 构造函数
回到项目里那个最短的文件 2.js------它只有 8 行代码,但揭示了一个重要的真相:JS 里的函数既是函数,也是对象。来看代码:
js
function greeting() {
console.log("hello world");
}
greeting.a = '1';
console.log(greeting.a); // "1"
greeting(); // "hello world" --- 普通函数调用
这段代码同时用到了函数的"对象身份"(greeting.a = '1')和"函数身份"(greeting()),正好说明了 JS 函数的双重身份:
| 维度 | 说明 | 代码 |
|---|---|---|
| 作为函数调用 | this 指向全局对象(非严格模式),执行函数体 |
greeting() |
| 作为对象使用 | 可以像普通对象一样添加属性 | greeting.a = '1' |
| 作为构造函数使用 | 用 new 调用会产生实例 |
new greeting() |
拥有 prototype |
因为它是函数,自带 prototype 属性 | greeting.prototype |
js
// 进一步验证:
function greeting() { console.log("hello world"); }
greeting.a = '1';
console.log(greeting.a); // "1" --- 函数的"静态属性"
console.log(greeting.prototype); // { constructor: ƒ } --- 原型对象
console.log(greeting.__proto__); // ƒ () --- Function.prototype
console.log(greeting.__proto__ === Function.prototype); // true --- 函数是 Function 的实例
// 普通调用
greeting(); // "hello world"
// new 调用
const g = new greeting(); // "hello world"(构造函数体执行)
console.log(g); // greeting {}(空实例)
console.log(g.a); // undefined --- a 在 greeting 函数对象上,不在实例上
console.log(g.__proto__ === greeting.prototype); // true
10. 实战易错题汇总
学完理论,用 6 道高频易错题来检验一下。每一道都来自真实的面试场景,建议先自己想一遍答案,再看解析:
题1:prototype 是什么类型?
js
function F() { }
console.log(typeof F.prototype); // "object"
// 唯一例外:typeof Function.prototype === "function"
题2:箭头函数有 prototype 吗?
js
const f = () => { };
console.log(f.prototype); // undefined --- 没有!所以箭头函数不能当构造函数
// new f(); → TypeError: f is not a constructor
题3:实例的 constructor 是谁?
js
function F() { }
const f = new F();
// f 自身没有 constructor,沿原型链在 F.prototype 上找到
console.log(f.constructor === F); // true
console.log(f.hasOwnProperty('constructor')); // false
题4:覆盖 prototype 后,constructor 去哪了?
js
function F() { }
F.prototype = { x: 1 }; // 用字面量替换了整个 prototype
console.log(F.prototype.constructor === F); // false!
console.log(F.prototype.constructor === Object); // true(字面量的 constructor 来自 Object.prototype)
题5:null 在 new 的 return 中生效吗?
js
function F() { this.x = 1; return null; }
const f = new F();
console.log(f); // F { x: 1 } --- null 被忽略了!
console.log(f instanceof F); // true
// 只有 return 对象(含数组、函数)才会生效,return 原始值一律被忽略
题6:Object.create(null) 的对象能用 toString 吗?
js
const obj = Object.create(null);
console.log(obj.toString); // undefined --- 原型链断了,没有 Object.prototype
// obj + '' → TypeError: Cannot convert object to primitive value
总结
回到最开始那张三角关系图,现在你应该能看懂里面的每一条线了:
prototype只有函数才有,它是构造函数身上指向原型对象的指针__proto__是每个普通对象都有的隐式属性,指向它"师父"的原型对象constructor在原型对象上,指回构造函数,形成一个闭环
原型链的威力在于共享 :不用每个实例存一份方法,所有实例沿着 __proto__ 向上查找,找到同一个原型对象上的同一个方法。理解了这一点,你就能理解为什么 class 语法里的方法在底层也是挂在 prototype 上的------class 只是语法糖,原型链才是 JS 面向对象的灵魂。
原型链的查找方向(牢记):实例自身 → 构造函数.prototype → Object.prototype → null
速查表
| 属性 | 谁有 | 指向谁 |
|---|---|---|
prototype |
只有函数有(箭头函数除外) | 指向原型对象(实例的模板) |
__proto__ |
绝大多数对象都有(Object.create(null) 除外) |
指向构造函数的 prototype |
constructor |
原型对象上有 | 指回构造函数本身 |