JS原型链

基本概念

JavaScript 使用原型链来实现继承机制。

每个对象内部有一个 [[Prototype]] 对象(可以通过 __proto__进行访问),指向它的原型对象。

关键概念

  1. 构造函数:用于创建对象的函数
  2. prototype:函数特有的属性
  3. proto:对象实例指向原型对象的属性
  4. constuctor:原型对象指向构造函数的属性

原型链关系

javascript 复制代码
function Person() {}
const p = new Person();

// 关系链
p.__proto__ === Person.prototype
Person.prototype.constructor === Person
Person.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

一些讨论

2.1 我们需要『继承』这种特性

"继承"是人类构建复杂事物的关键能力。

这种需求或许源于两重限制:

  • 认知层面,人脑处理能力有限,所以衍生出分类、拆解、解构此类需求
  • 时间层面,生命有限性促使我们将知识代际传递以延续文明。

2.2 编程语言中的继承是什么?

继承本质上源于人类对现实世界的抽象。------通过分层归纳来简化认知与记忆。

例如,要描述鸟、老鹰:

  1. 先定义鸟的通用特征。
  2. 再基于鸟扩展老鹰、大鹏的【特有属性】

有趣的是,现实世界的认知过程往往是相反的。

人类先观察到鸟、老鹰等具体实例,而后才抽象出鸟这一通用概念。

这种差异解释了早期编程语言的诸多设计局限:受限于当时的认知范式,导致了一些不符合直觉的实现方式。

2.3 ES6之前,如何实现继承

ES6之前的JS没有现成语法可用,人们另辟蹊径,间接实现继承:

  • JS创建函数时,在对象的内部会创建一个 prototype对象
  • 使用 new 关键字调用函数时,实例对象的 __proto__属性指向构造函数的prototype
  • 访问一个对象的属性,如果没找到该属性,会顺着 __proto__继续向上查找,如还没找到,则继续向上,直到为 null

基于以上3条原理,前人【发现】了旧JS实现继承的路径。

  • 原型链的尽头是 null。
  • 一切JS对象均有原型

2.4 一些无需记忆的旧实现

javascript 复制代码
function F(name, age) {
  this.name = name
  this.age = age
}
F.prototype.say = function() {
  console.log(this.name)
}
function S(name, age, sex) {
  F.call(this, name, age)
  this.sex = sex
}
// 此方法是ES6之前的实现方式
// S.prototype = new F()
// ES6之后可以这样实现
S.prototype = Object.create(F.prototype)
const son = new S('chp', 18, 'male')
son.say() // chp

上述代码是经典的 组合构造模式 的继承。

缺陷也比较明显,我们发现我们调用了 F.call,ES6之前我们第二次调用了 new F()

不过ES6之后弥补了缺陷

2.5 Class语法糖的出现,ES6之后如何实现

新时代的继承

scala 复制代码
class F {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  say() {
    console.log(this.name)
  }
}
class S extends F {
  constructor(name, age, sex) {
    super(name, age)
    this.sex = sex
  }
}
const son = new S('chp', 18, 'male')
son.say() // chp

2.6 所以什么是原型链?

  • 原型链是 JS 实现继承的机制
  • 当访问一个属性时,如果对象自身有该属性,直接返回。
  • 如果没有,则沿着 [[Prototype]] (__proto__)向上查找,直到找到对象的属性、或到达 null。
  • 这条自底向上,逐级查找的链路,就叫做【原型链】。

原型链的终点是 null

注意事项

  • 不要随意覆盖函数的原型 prototype,例如对 Function.prototype进行赋值。比较好的方式是只对 Function.prototype.attr进行赋值。
  • 原型链的终点是 null

继承模式

发展顺序:

原型链继承 -> 构造函数继承 -> 组合 -> 原型 -> 寄生式

原型链继承

javascript 复制代码
function Parent() {}
function Child() {}
Child.prototype = new Parent();

子类原型 = 父类实例

直接修改 prototype。

  • ✅简单直接
  • ❌所有子类实例共享父类引用属性
  • ❌无法向父类构造函数传参

构造函数继承

csharp 复制代码
function Parent() {}
function Child() {
  Parent.call(this);
}

在函数内,借用调用。

在子类函数中使用 call 方法。

  • ✅解决引用属性共享问题
  • ✅可以向父类传参
  • ❌无法继承父类原型上的方法

组合继承(最常用)

javascript 复制代码
function Parent() {}
function Child() {
  Parent.call(this);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
  • ✅实例属性独立
  • ✅方法共享
  • ❌父类被调用了2次【为了修复指向,会连续2次调用 Parent()

原型式继承

本质是克隆

ini 复制代码
const obj = Object.create(parent);
  • ✅简单对象继承方便
  • ❌引用属性共享问题

寄生式继承

优化版的克隆

ini 复制代码
function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}
  • ✅ 避免组合继承的两次调用问题
  • ✅ 保持原型链完整

总结 ES6 最终实现

ES6最后采用的是 【寄生组合式】继承。

这是最理想的继承方式。

scala 复制代码
class Parent {}
class Child extends Parent {}

// 转译后的核心代码:
function _inherits(subClass, superClass) {
  subClass.prototype = Object.create(superClass.prototype); // 寄生式继承
  subClass.prototype.constructor = subClass;                // 修复构造函数
  Object.setPrototypeOf(subClass, superClass);              // 继承静态属性
}

它有以下优势:

  • 只调用一次父类函数 【性能最优,同时正确修复了 Child constructor``prototype 的指向】
  • 原型链纯净无冗余属性【内存友好】
  • 完整继承静态属性 + 实例方法【功能完整】

ES6 的 class 继承是语法糖,其底层采用 寄生组合式继承,解决了传统继承方式的缺陷,成为 JavaScript 中最完善、最高效的继承实现方案。这也是为什么现代 JavaScript 开发推荐始终使用 class 语法而非手动实现继承。

目的主要是:

  • 方法共享
  • 属性独立

相关案例

🔥 超经典的题目

javascript 复制代码
function F(){}
function O(){}
var obj = new O();
O.prototype = new F();
// 如果此时有个 obj2
var obj2 = new O();
  • obj.__proto__ 是否指向 O.prototype? // ❌
  • obj2.__proto__.__proto__ === F.prototype // ✅
  • obj.__proto__.constructor === O// ✅

答案:

obj.__proto__并不指向 O.prototpye,因为后者被修改了。

由于对象本身 this 上没挂在东西,所以此时它指向了【实例对象】

{constructor: O(), [[prototype]]: Object}

  • var obj = new O()O.prototype=> 默认的原型对象,constructor=> O。
  • 重定向 O.prototype,其被赋值为 F()的实例。
  • 【此操作不影响已创建的 obj,因为 obj 内部 [[prototype]]仍然指向旧的 O.prototype
  • obj instanceof O ? // ❌

instanceof 的本质是,【一个对象】的【原型链上】,是否存在某个函数的 prototype属性。

obj.__proto__此时指向了一个实例对象。

对于实例对象的 __proto__,只会指向 Object.prototype

由于 O.prototype 已经被改变,他们注定无缘。

  • obj instanceof F ? // ❌ 同理。
  • obj intanceof Object ? // ✅,因为

obj.__proto__.__proto__ === Object.prototype

一切对象都是 Object 的继承,原型链的终点是 null

javascript 复制代码
let o = new Object()
o.__proto__ // Object.prototype
o.__proto__.__proto__ // null
Object.__proto__ // Function.prototype,Object和Function的关系
Object.__proto__.__proto__ === Object.prototype // 函数也是对象
Object.__proto__.__proto__.__proto__ // null

手写 instanceof

instanceof 的本质是,【一个对象】的【原型链上】,是否存在某个函数的 prototype属性。

javascript 复制代码
export const myInstanceof = (ins, targetCls) => {
  let p = ins.__proto__
  while (p) {
    if (p === targetCls.prototype) {
      return true
    }
    p = p.__proto__
  }
  return false
}

不断寻找自己的 __proto__实例,判断该属性是否与【指定类型的原型prototype】相等。

自底向上,直到 null

由于 __proto__是一个非标准属性,更推荐使用 Object.getPrototypeOf

javascript 复制代码
export const myInstanceof = (ins, targetCls) => {
  if (typeof targetCls !== 'function') {
    return TypeError('第二个参数必须是函数')
  }
  let p = Object.getPrototypeOf(ins)
  while (p) {
    if (p === targetCls.prototype) {
      return true
    }
    p = Object.getPrototypeOf(p)
  }
  return false
}

多层继承

javascript 复制代码
function A() { this.x = 1; }
function B() { this.y = 2; }
function C() { this.z = 3; }

B.prototype = new A();
C.prototype = new B();

const c = new C();

console.log(c.x); // 1,从 A.prototype 继承
console.log(c.y); // 2,从 B.prototype 继承
console.log(c.z); // 3,从 C.prototype 继承
console.log(c.w); // undefined,未定义

c -> C.prototype (B的实例) -> B.prototype (A的实例) -> A.prototype -> Object.prototype -> null

动态修改原型的影响

javascript 复制代码
function Person() {}
const p1 = new Person();

Person.prototype = {
  name: 'Alice'
};

const p2 = new Person();

console.log(p1.name); // undefined,还是指向原来的引用
console.log(p2.name); // Alice
console.log(p1 instanceof Person); // false
console.log(p2 instanceof Person); // true

/*
解释:
- p1创建时使用的是原始的Person.prototype
- 之后Person.prototype被完全替换,但p1的[[Prototype]]仍然指向旧的原型
- p2创建时使用的是新的Person.prototype
- instanceof检查当前构造函数的prototype是否在对象的原型链上
*/

构造函数返回对象的影响

javascript 复制代码
function Foo() {
  this.name = 'Foo';
  return { name: 'Bar' };
}

function Bar() {
  this.name = 'Bar';
}

Bar.prototype = new Foo();

const b = new Bar();

console.log(b.name); // 'Bar'
console.log(b instanceof Bar); // true
console.log(b instanceof Foo); // false
console.log(b instanceof Object); // true

/*
原型链分析:
- Foo构造函数返回了一个新对象,所以Bar.prototype = new Foo() 
  实际上使Bar.prototype指向{name: 'Bar'}这个对象
- b的原型链:b -> Bar.prototype ({name: 'Bar'}) -> Object.prototype -> null
- Foo.prototype不在b的原型链上
*/

如果 Foo 没有 return 一个对象,则:

b instanceof Foo // true

b instanceof Bar // true

但是因为 Foo return 了一个对象。所以这里的链条断掉了。

原型链和静态属性

ini 复制代码
function Parent() {}
Parent.staticProp = 'Parent Static';
Parent.prototype.protoProp = 'Parent Proto';

function Child() {}
Child.staticProp = 'Child Static';
Child.prototype = Object.create(Parent.prototype);
Child.prototype.protoProp = 'Child Proto';

const c = new Child();

console.log(c.protoProp); // 'Child Proto'
console.log(c.staticProp); // undefined
console.log(Child.staticProp); // 'Child Static'
console.log(Child.prototype.protoProp); // 'Child Proto'

/*
静态属性不会被实例继承,它们只是构造函数的属性
只有添加到prototype上的属性才会被实例继承
*/

复杂原型链与属性屏蔽

ini 复制代码
const grandparent = { a: 1 };
const parent = Object.create(grandparent);
parent.a = 2;
parent.b = 3;

const child = Object.create(parent);
child.b = 4;
child.c = 5;

console.log(child.a); // 2 屏蔽了父级的 a
console.log(child.b); // 4 屏蔽了 parent 的b
console.log(child.c); // 5
console.log(child.d); // undefined

delete child.b;
console.log(child.b); // 3 解除了屏蔽

循环原型链

javascript 复制代码
function A() {}
function B() {}

A.prototype = new B();
B.prototype = new A();

const a = new A();
const b = new B();

console.log(a instanceof A); // true
console.log(a instanceof B); // true
console.log(b instanceof A); // true
console.log(b instanceof B); // true

/*
这种设计会导致原型链循环引用:
A.prototype -> B实例 -> B.prototype -> A实例 -> A.prototype -> ...

浏览器处理:
现代浏览器能检测这种循环引用,不会导致无限循环
但这是非常糟糕的设计,会导致难以预测的行为和性能问题
*/

综合原型链分析

javascript 复制代码
function Foo() {}
Foo.prototype.bar = function() { return 1; };

const foo = new Foo();

Foo.prototype = {
  bar: function() { return 2; },
  baz: function() { return 3; }
};

const foo2 = new Foo();

console.log(foo.bar()); // 1
console.log(foo2.bar()); // 2
console.log(foo.baz); // // undefined
console.log(foo2.baz()); // 3

/*
原型链差异:
- foo的原型指向最初的Foo.prototype
- foo2的原型指向替换后的新Foo.prototype
替换原型对象不会影响已存在的实例
*/

原型链污染+防御

javascript 复制代码
Object.prototype.hack = function() {
  console.log('Prototype polluted!');
};

const obj = {};

// 问题1: obj.hack() 输出什么? 'Prototype polluted!'
// 问题2: 如何防御这种原型污染?
// * Object.freezee 冻结
// * 创建无原型对象Object.create(null)
// 问题3: Object.create(null) 创建的对象会受到污染吗?
// 不会,因为它没有原型链,不继承 Object.prototype

ES6 class 与原型链的对应关系

javascript 复制代码
class A {
  static staticMethod() { return 'A static'; }
  instanceMethod() { return 'A instance'; }
}

class B extends A {
  static staticMethod() { return 'B static'; }
  instanceMethod() { return 'B instance'; }
}

const b = new B();

// 问题1: 用 ES5 语法重写上述 class 定义
// 问题2: console.log(b.instanceMethod()) 输出什么?'B instance'
// 问题3: console.log(B.staticMethod()) 输出什么?'B static'
// 问题4: 如何通过原型链访问父类的实例方法?通过 super 调用父类方法

使用 ES5 去实现。

javascript 复制代码
function A() {}
A.staticMethod = function () {
  return 'A static'
}
A.prototype.instanceMethod = function () {
  return 'A instance'
}

function B() {}

const prototype = Object.create(A.prototype)
prototype.constructor = B
B.prototype = prototype

B.staticMethod = function () {
  return 'B static'
}
B.prototype.instanceMethod = function () {
  return 'B instance'
}

const b = new B()
console.log('🍀🍀🍀🍀', b.instanceMethod())
console.log('🍀🍀🍀🍀', B.staticMethod())

console.log('🍀🍀🍀🍀', b instanceof B) // true
console.log('🍀🍀🍀🍀', b instanceof A) // true

原型链与访问优先级

javascript 复制代码
function Foo() {
  this.name = 'Foo';
}

Foo.prototype.name = 'Foo Prototype';

Object.prototype.name = 'Object Prototype';

const foo = new Foo();

console.log(foo.name); // 'Foo'
console.log(foo.__proto__.name); // 'Foo Prototype'
console.log(foo.__proto__.__proto__.name); // 'Object Prototype'
console.log(foo.toString()); // 输出什么?'[object Object]'
delete foo.name;
console.log(foo.name); // 输出什么?'Foo Prototype'

原型链与性能优化

javascript 复制代码
// 现有以下性能较差的代码
function HeavyComponent() {
  this.id = Math.random();
  this.data = new Array(10000).fill('data');
}

HeavyComponent.prototype.render = function() {
  console.log('Rendering:', this.id);
};

// 问题:如何优化创建大量HeavyComponent实例时的内存使用?
// 要求:
// 1. 保持render方法的可用性
// 2. 避免每个实例都创建大数组
// 3. 写出优化后的代码实现
// 4. 解释优化原理
javascript 复制代码
function F() {
  this.id = Math.random()
}
F.prototype.data = new Array(10000).fill('data')
F.prototype.render = function () {
  console.log('Rendering:', this.id)
}

let list = []
for (let i = 0; i < 100; i++) {
  list.push(new F())
}
  • 将占用内存大的都移动到原型上,所有实例共享
  • 每个实例只保留独有的 id
  • render放在原型上共享
  • 保留原来 api
相关推荐
布兰妮甜10 分钟前
单例模式在前端(JavaScript)中的实现与应用
前端·javascript·单例模式
Mintopia11 分钟前
Three.js 加载模型文件:从二进制到像素的奇幻漂流
前端·javascript·three.js
StrongerIrene20 分钟前
rs build 的process.env的值undefined解决方案
开发语言·javascript·ecmascript
前端小巷子29 分钟前
跨域问题解决方案:JSONP
前端·javascript·面试
eric*168837 分钟前
尚硅谷张天禹老师课程配套笔记
前端·vue.js·笔记·vue·尚硅谷·张天禹·尚硅谷张天禹
程序员爱钓鱼1 小时前
Go语言中的反射机制 — 元编程技巧与注意事项
前端·后端·go
GIS之路1 小时前
GeoTools 结合 OpenLayers 实现属性查询(二)
前端·信息可视化
just小千1 小时前
重学React(二):添加交互
javascript·react.js·交互
烛阴1 小时前
一文搞懂 Python 闭包:让你的代码瞬间“高级”起来!
前端·python
AA-代码批发V哥1 小时前
HTML之表单结构全解析
前端·html