一文搞懂 JavaScript 原型与原型链

41 阅读3分钟

什么是原型

举个例子

let a = [1,2,3]

image.png 可以看到,有一个属性是[[ProtoType]]: Array(0),这行代码相当于a有一个属性__proto__,

image.png 是不是和上面[[ProtoType]]: Array(0)展开的内容一致

image.png 也就是a.__proto__ === Array.prototype ,这个__proto__称为隐式原型,prototype称为显示原型。

所有的引用类型(函数,数组,对象)都具有__proto__属性,但函数比较特殊,除了有__proto__属性之外还有prototype属性

隐式原型与显示原型之间的关系

  • 隐式原型(__proto__ :每个引用类型实例都有的属性,指向它的原型对象,是实例查找原型的 “通道”。
  • 显式原型(prototype :只有函数(如 ArrayObject 这些构造函数)才有的属性,指向它创建的实例的原型对象。

什么是原型链

原型链1.webp 可以发现a.__proto__A.prototype也属于对象,所以他们也有__proto__属性

a.__proto__指向A.prototype,而A.prototype指向了Object.prototype所有普通对象的原型默认指向 Object.prototype(这是 JavaScript 原型链的顶层之一),也就是等同于a.__proto__.__proto__ === Object.prototype,这种链式结构,就是 原型链

  • 当调用某种方法或者查找某种属性时,首先会在自身调用和查找,如果自身并没有该属性或方法,则回去它的__proto__属性中调用查找,也就是构造函数的prototype中调用查找,还没找到就继续向上查找,以此类推 通过这种特性可以实现原型的继承

提出一个问题帮助理解: 下图中为什么p.b = b,p.a = undefined

image.png 这个问题是原型链的简单问题

稍微进阶一下 为什么 p.b = 'b',p.a = undefined?

假设图示中的代码结构如下(符合原型链常规场景):

// 1. 定义构造函数 Person,原型上有属性 a = 'a'
function Person() {}
Person.prototype.a = 'a'; // 原型上的属性 a

// 2. 创建实例 p
let p = new Person();

// 3. 给 p 赋值属性 b
p.b = 'b';

2. 为什么 p.b = 'b'?(赋值逻辑)

当给实例赋值时,JavaScript 不会去原型链上查找,而是直接在实例自身添加该属性(无论原型上有没有同名属性)。

  • 这里 p 自身原本没有 b 属性,执行 p.b = 'b' 后,会直接在 p 上新增 b 属性,所以 p.b 能拿到 'b'。

3. 为什么 p.a = undefined?(读取逻辑)

当读取实例属性时,会先查自身,再顺着原型链找:

  • 第一步:先看 p 自身有没有 a 属性?根据代码,p 是新创建的实例,自身没有定义 a,所以继续找;

  • 第二步:顺着 p.__proto__ 找 Person.prototype,发现原型上有 a = 'a',按理说应该返回 'a'?

  • 关键矛盾点:如果图示中 p.a 是 undefined,说明原型链的某个环节断了,最可能的原因是:

    • 场景 1:Person.prototype 被重新赋值成了空对象(如 Person.prototype = {}),导致原型上的 a 属性丢失;
    • 场景 2:实例 p 是直接用 let p = {} 创建的普通对象,没有关联 Person.prototype,所以找不到 a

本质总结:赋值属性只影响实例自身,读取属性才会走原型链;读取不到时返回 undefined,要么是自身和原型链都没有该属性,要么是原型链被修改导致属性丢失