面试必问的 JS 原型链,我用 16 个示例给你彻底讲明白

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 一个构造函数,就得到了一个实例。我们把 Greetingnew 调用,拿到实例 xxx

js 复制代码
const xxx = new Greeting("xxx");

这行代码背后,JS 引擎实际上做了四件事:

  1. 创建一个空对象 {}
  2. 把这个空对象的 __proto__ 指向 Greeting.prototype
  3. this 指向这个空对象,执行构造函数体,给 this 添加属性
  4. 如果构造函数没有 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 引擎的查找顺序是这样的:

  1. xxx 自身有没有 say?→ 没有
  2. 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.prototypeFunction.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:nullnew 的 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 原型对象上有 指回构造函数本身
相关推荐
丷丩1 小时前
12. 渲染:MapLibre GL JS 集成与多源瓦片联动
javascript·矢量瓦片·maplibre gl js·地图服务器
橘子星2 小时前
别再懵圈!JS 执行机制的 “千层套路” 全揭秘
前端·javascript
拾年2752 小时前
__proto__ vs prototype:90% 的人分不清的 JavaScript 核心
前端·javascript·面试
提子拌饭1332 小时前
个人月事记录表应用 - 鸿蒙PC Electron框架完整实现指南
前端·javascript·华为·electron·前端框架·开源·鸿蒙系统
超人气王2 小时前
新手学前端JS浅拷贝和深拷贝:对象复制竟然是个“替身文学”?
javascript·面试
YHL2 小时前
📚 JS执行机制(执行上下文 + 调用栈 + 编译流程)
前端·javascript
不简说2 小时前
这次真香!sv-print 可视化打印设计器更新:插件脚手架、Excel 导出、弹窗 API 三连发
前端·javascript·前端框架
无聊的老谢2 小时前
Web GIS 最佳实践:Vue 集成 Leaflet/OpenLayers 实现基站海量点位渲染
前端·javascript·vue.js
东风破_2 小时前
V8 如何执行你的代码——编译、上下文与调用栈
javascript