4 学习对象的原型链 +使用原型链实现继承

110 阅读7分钟

1. js的继承

  1. 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要继承过来即可

  2. 而继承的关键是使用JavaScript原型链机制

  3. 可以使用原型链机制来实现继承

2. JavaScript对象的原型链

JavaScript 中的每个对象都有一个隐藏属性 [[Prototype]](可以通过 __proto__ 访问),它指向另一个对象,我们称之为“原型”。

当你访问对象的某个属性或方法时,JS 引擎会:

  1. 先查对象本身
  2. 如果找不到,就沿着它的 __proto__ (原型)往上找
  3. 一直找到顶层 Object.prototype,如果还没有,就返回 undefined

这个查找的路径,就是 原型链

关键点说明
对象通过 __proto__ 指向其原型查找属性时就会沿着原型链往上查找
原型链以 Object.prototype 结束它的 __proto__null
prototype 是函数专属只有函数才有 prototype 属性,用于构造实例的原型对象
多对象共享一个原型可以节省内存,方法共享

原型链是有终点的。且终点的指向为NUll

object的原型
var obj = {name: 'why'}

console.log(obj.__proto__) //[Object: null prototype] {}
//看到[Object: null prototype] {} 说明已经到了obj这个对象的最顶层的原型了

console.log(obj.__proto__.__proto__) //null
//可以看出最顶层的原型的 __proto__ 为 null 
顶层的原型

方法1:var obj1 = {}

方法2: var obj2 = new Object()

方法1是方法2的语法糖形式

var obj1 = {}
var obj2 = new Object()
console.log(obj1.__proto__ === obj2.__proto__) //true

本质都是:obj1.__proto__ = Object.prototype | obj2.__proto__ = Object.prototype

console.log(obj1.__proto__) //[Object: null prototype] {}
console.log(obj1.__proto__.__proto__) //null
console.log(obj2.__proto__) //[Object: null prototype] {}
console.log(obj2.__proto__.__proto__) //null

可以看到Object的函数原型对象是obj1 和 obj2的对象原型链的顶层了

此时我们可以把 var obj2 = new Object()中的Object()对于对象来说当成一个特殊的函数

Snipaste_2025-04-18_17-31-06.png

当我们改变了obj2的__proto__会发生什么

当obj2 中找不到相关的属性,obj2会沿着原型链obj2 -> obj1 -> Object的原型对象 -> __proto__ : null。找到就返回,找不到就返回undefined

Snipaste_2025-04-18_17-34-16.png

那么,Object()和我们之前的Person()被称为构造函数有什么区别呢?

我们可以看到Object的原型对象有__proto__,那么Person函数的原型对象是否也有__proto__

答案:Person函数的原型对象 __proto__

function Person() {
  
}
var obj1 = new Object()
var obj2 = new Person()

Snipaste_2025-04-18_17-45-26.png

那么Person的原型对象的__proto__属性指向哪里?
function Person() {

}
var obj1 = new Object()
var obj2 = new Person()
console.log(Object.prototype === obj1.__proto__) //true
console.log(Object.prototype === Person.prototype.__proto__) //true
console.log(obj1.__proto__ === Person.prototype.__proto__) //true

Snipaste_2025-04-18_17-48-03.png

3. 怎么使用原型链实现继承

使用父类:存放公共的属性和方法

使用子类:存放特有的属性和方法

function Father() {
  this.name = 'why'
}
Father.prototype.eating = function() {
  console.log(this.name + 'eating😮')
}

var father = new Father()

function Son() {
  this.son = 111
}
Son.prototype.studying = function () {
  console.log(this.name + 'studying📚')
}
var sonOne = new Son()
console.log(sonOne.name)
console.log(sonOne.studying)

我们的代码较少,在很多代码中。我们的sonOne对象想要通原型链,在自己没有特定属性的情况下,可以使用某种方法找到fatherOne对象中的属性和Father的默认原型对象

Snipaste_2025-04-18_18-32-10.png

function Father() {
  this.name = 'why'
}
Father.prototype.eating = function() {
  console.log(this.name + 'eating😮')
}

var father = new Father()
//增加的代码(只有这一行)
Son.prototype = father

function Son() {
  this.son = 111
}
Son.prototype.studying = function () {
  console.log(this.name + 'studying📚')
}
var sonOne = new Son()
console.log(sonOne.name)
console.log(sonOne.studying)

Snipaste_2025-04-18_18-39-36.png 这样sonOne中没有的对象就可以沿着原型链查找fatherOne对象和Father函数默认原型对象了

4. 原型链继承的弊端

代码1
function Father() {
  this.name = 'why'
  this.friends = []
}
Father.prototype.eating = function() {
  console.log(this.name + 'eating😮')
}

var father = new Father()
Son.prototype = father

function Son() {
  this.son = 111
}
Son.prototype.studying = function () {
  console.log(this.name + 'studying📚')
}
var sonOne = new Son()
var sonTwo = new Son()

sonOne.name  = 'kobe'
console.log(sonOne)
console.log(sonTwo)

sonOne.friends.push('kebo')
console.log(sonOne.friends)
console.log(sonTwo.friends)
//Father { son: 111, name: 'kobe' }
//Father { son: 111 }
//[ 'kebo' ]
//[ 'kebo' ]
解释:
  • sonOne.name = 'kobe' :

    • 当你给 sonOne.name 赋值时,JavaScript 引擎首先查找 sonOne 对象自身是否有 name 属性。它没有。
    • 因为是赋值操作 (=) ,引擎不会去修改原型链上的属性。相反,它会在sonOne 对象自身上创建一个新的 name 属性,并赋值为 'kobe'。这叫做属性屏蔽 (Property Shadowing)
    • 此时 sonOne 变成: { son: 111, name: 'kobe', proto: father }
    • sonTwo 保持不变: { son: 111, proto: father }
  • console.log(sonOne) : 输出 sonOne 对象,它现在有自己的 name 属性,值为 'kobe'。

  • console.log(sonTwo) : 输出 sonTwo 对象,它没有自己的 name 属性。当你访问 sonTwo.name 时,会沿着原型链找到 father 对象上的 name 属性,其值为 'why'。(虽然 console.log(sonTwo) 可能不会直接显示原型链上的属性,但访问 sonTwo.name 会得到 'why')。

  • 这就是为什么前两个打印不一样的原因。


  • sonOne.friends.push('kebo') :

    • 当你访问 sonOne.friends 时,JavaScript 引擎首先查找 sonOne 自身是否有 friends 属性。它没有。

    • 于是引擎沿着原型链查找,在 sonOne.proto(也就是 father 对象)上找到了 friends 属性。这个 friends 属性引用的是上面提到的那个具体的数组实例 Array A

    • 所以 sonOne.friends 解析为 father.friends,也就是 Array A。

    • .push('kebo') 操作是直接修改了这个 Array A 数组本身,在数组末尾添加了 'kebo'。

    • 现在 father 对象(也就是 sonOne 和 sonTwo 共享的原型)变成了:

      {
          name: 'why',
          friends: ['kebo'], // Array A 被修改了!
          studying: function(){...},
          __proto__: Father.prototype
      }
      
  • console.log(sonOne.friends) : 访问 sonOne.friends,沿着原型链找到 father.friends,它现在是 ['kebo']。

  • console.log(sonTwo.friends) : 访问 sonTwo.friends,同样沿着原型链找到 father.friends,它也是 ['kebo']。

  • 这就是为什么后两个打印一样的的原因。

核心问题总结:

这种继承方式(将子类的原型直接指向父类的一个实例)会导致:

  1. 父类实例的属性(如 name, friends)被所有子类实例共享。
  2. 对于原始类型(如 string, number)的属性,当子类实例尝试修改它们时,会在子类实例自身创建新属性(屏蔽),看起来像是独立的。
  3. 对于引用类型(如 Array, Object)的属性,所有子类实例访问的都是原型上同一个引用。当任何一个子类实例通过这个引用修改了对象或数组内部时,这个修改对所有其他子类实例都可见,因为它们共享同一个引用。
代码2
function Father() {
  this.name = 'why'
}
Father.prototype.eating = function() {
  console.log(this.name + 'eating😮')
}

var father = new Father()
Son.prototype = father

function Son() {
  this.son = 111
}
Son.prototype.studying = function () {
  console.log(this.name + 'studying📚')
}
var sonOne = new Son()
var sonTwo = new Son()

sonOne.name  = 'kobe'
console.log(sonOne)
console.log(sonTwo)

sonOne.friends = ['aaa']
console.log(sonOne.friends)
console.log(sonTwo.friends)
//Father { son: 111, name: 'kobe' }
//Father { son: 111 }
//[ 'aaa' ]
//undefined
代码解释
  • sonOne.name = 'kobe' :

    • 和上一个例子逻辑相同:赋值操作 (=) 在 sonOne 上查找 name 属性,未找到。
    • 于是,在 sonOne 实例自身上创建了一个新的 name 属性,并赋值为 'kobe'(属性屏蔽)。
    • sonOne 变为: { son: 111, name: 'kobe', proto: father }
    • sonTwo 保持不变: { son: 111, proto: father }
  • console.log(sonOne) : 打印 sonOne,它有自己的 name: 'kobe'。

  • console.log(sonTwo) : 打印 sonTwo。如果访问 sonTwo.name,它会通过原型链找到 father 上的 'why'。

  • 结果:  前两个打印结果不同,原因和之前一样(对实例属性的赋值创建了自身属性)。


  • sonOne.friends = ['aaa'] :

    • 这是与上一个例子 (.push) 的关键区别
    • JavaScript 引擎在 sonOne 上查找 friends 属性。未找到。
    • 因为这是一个赋值操作 (=) ,引擎不会去原型链上查找并修改。相反,它会在sonOne 对象实例自身上创建一个全新的 friends 属性,并将一个新的数组 ['aaa'] 赋值给它。
    • sonOne 现在变为: { son: 111, name: 'kobe', friends: ['aaa'], proto: father }
    • sonTwo 保持不变: { son: 111, proto: father }
    • 共享的原型对象 (father) 完全没有受到这次赋值的影响。
  • console.log(sonOne.friends) :

    • 访问 sonOne.friends 时,直接在 sonOne 实例上找到了该属性。
    • 输出:  ['aaa']
  • console.log(sonTwo.friends) :

    • 访问 sonTwo.friends。
    • 在 sonTwo 实例上查找 friends。未找到。
    • 去 sonTwo.proto(即 father 对象)上查找 friends。在这个版本的代码里,father 对象自身也没有 friends 属性。未找到。
    • 去 father.proto(即 Father.prototype)上查找 friends。未找到。
    • 去 Father.prototype.proto(即 Object.prototype)上查找 friends。未找到。
    • 到达原型链末端。
    • 输出:  undefined

结论:

在这个修改后的版本中:

  1. 对 sonOne.name 的赋值仍然导致 sonOne 和 sonTwo 在 name 属性上表现不同(一个有自身属性,一个继承原型属性)。
  2. 对 sonOne.friends 进行赋值 (= ['aaa']),创建的是 sonOne 实例自身的属性,并没有修改共享的原型。
  3. 由于 sonTwo 没有自身的 friends 属性,并且这次共享的原型 father 上也不存在 friends 属性,因此访问 sonTwo.friends 的结果是 undefined。