原型链相关

126 阅读7分钟

原型链相关

1.引入原型对象

1.1构造函数实例化存在的问题

在构造函数中,存在浪费内存的问题,在每一次实例化构造函数(该构造函数里有方法),就会在堆里单独创造一片空间存放该方法,代码如下:

function Star(name, age) {
  this.name = name
  this.age = age
  this.sing = function () {
    console.log('唱歌')
  }
}
const ldh = new Star('刘德华', 18)
const zjl = new Star('周杰伦', 18)
const zxy = new Star('张学友', 18)
console.log(ldh.sing === zxy.sing) // false

该构造函数在实例化过程中,数据存放如图所示:

构造函数的问题.png

在每一次实例化的过程中,都会在堆中开辟一个单独的空间来储存方法,其方法路径也存放在相应的实例化对象中。如果需要实例化的对象比较多,需要开辟空间来储存方法的数目也较多,浪费内存。

1.2 引入原型对象的概念

如果把构造函数里的方法单独拿出来,放在原型对象中,那么在实例化过程中,就不用再单独开辟空间来储存这个方法了,节省了内存。为了更好地理解为什么能够在原型对象中能够访问到方法,我们需要知道相应的原型规则。

引入原型对象.png

1.3 原型规则

  1. 所有的引用类型(数组,对象,函数),都具有对象的属性,并且可以自由扩展属性;
  2. 所有的对象都有 __proto__ (或者 [[prototype]])属性,称为隐式原型,属性值是一个普通对象;
  3. 所有的函数都有 prototype 属性,称为显式原型,属性值也是一个普通的对象;
  4. 所有对象的隐式原型 __proto__ 都指向其构造函数的显式原型prototype
  5. 当试图得到对象的某个属性时,而这个对象没有该属性,那么就会通过其__proto__隐式原型去原型对象中寻找;

1.4原型对象 (通过函数的prototype属性指向)

在JS中,原型对象也称为原型,它本质就是一个对象。字面意思就是原始的模型,原始祖先。

概念

1.所有的函数,都有一个prototype属性(显式原型),这个属性是一个指针,指向了原型对象;

2.原型对象默认有一个叫做constructor的属性,该属性指向了这个构造函数本身;

3.我们可以往这个原型对象上添加属性和方法;

所以所有听过构造函数创建的实例,都共享原型对象上包含的属性和方法;

function Person() {}
//   dir : 查看对象的所有属性和方法
console.dir(Person)

image-20220811191105673.png

举例:

 function Star(name, age) {
   this.name = name
   this.age = age
 }
 //   给对象原型添加方法
 Star.prototype.sing = function () {
   console.log('唱歌')
 }
 //   给对象原型添加属性
 Star.prototype.cheer = '加油'
 const ldh = new Star('刘德华', 18)
 const zxy = new Star('张学友', 18)
 console.log(ldh)
 console.log(ldh.__proto__ === Star.prototype) // 他们都指向原型对象

1.5 构造函数和原型方法中 this 的指向

我们知道,在进行构造函数实例化的过程中,new的执行机制分为以下四步:

  1. 在构造函数内部创建一个空对象;
  2. this指向这个空对象;
  3. 执行构造函数里面的代码,给这个空对象添加相应的属性和方法(引入原型对象后构造函数里尽量没有方法)
  4. 返回this

在构造函数实例化后,其内部的this会被清空,很难判断this的具体指向,所以我们在实例化前,声明一个全局变量that来接收this,代码如下:

let that // 全局变量
function Star(name, age) {
  this.name = name
  // that = this
  // console.log(this)
}
Star.prototype.sing = function () {
  console.log('唱歌')
  that = this
}
const ldh = new Star('刘德华')
console.log(that === ldh) // true

执行以上代码,我们可以知道:

  1. 构造函数里的this,指向的是实例(对象);
  2. 原型对象方法里的this,指向的是实例(对象);

即构造函数里的this和原型对象方法里的this,都指向我们new出来的实例对象

1.6 constructor 属性

function Star(name, age) {
  this.name = name
  this.age = age
}
const ldh = new Star('刘德华', 20)
console.dir(Star) // 构造函数
console.log(Star.prototype) // 原型console.log(Star.prototype.constructor === Star) // true
console.log(ldh.__proto__ === Star.prototype) // true  ===> 都指向原型

执行以上代码,在控制台中我们可以看到,其原型对象有一个constructor属性,其指向了这个构造函数。

image-20220811192840728.png

该属性表示该原型和哪个构造函数相关联,是哪个构造函数的原型。

应用

因为我们前面为了避免构造函数在实例化的过程中浪费内存,将方法添加到原型对象中,但是如果需要添加的方法比较多,一个个添加十分麻烦。

所以可以将需要添加的方法结合成一个对象,再加到原型中,但是!如果我们直接给原型对象赋值一个对象的话,会整个替换了原型对象,这个时候,原型中的constructor属性消失,我们也就不清楚这个原型是哪一个构造函数的原型。所以,我们可以手动添加一个constructor属性,指回原来的构造函数。

 function Star(name, age) {
   this.name = name
 }
 Star.prototype.sing = function () {
   console.log('唱歌')
 }
 Star.prototype.dance = function () {
   console.log('跳舞')
 }
 console.log(Star.prototype)
 
 Star.prototype = {
    constructur: Star, // 指回原来的构造函数
   dance: function () {
     console.log('跳舞')
   },
   sing: function () {
     console.log('唱歌')
   },
 }
 console.log(Star.prototype)

原型的constructor指向相应的构造函数,可以方便批量给构造函数添加相应的方法,只要在添加方法的对象里将constructor指回原来的构造函数即可。

1.7 通过对象的__proto__属性指向原型

前面我们了解到可以通过函数的prototype属性可以指向原型对象,但是我们可以从原型规则第四条中可以得知,可以通过实例对象的__proto__属性(隐式原型)指向原型对象。

function Star(name, age) {
  this.name = name
  this.age = age
}
Star.prototype.sing = function () {
  console.log('唱歌')
}
const ldh = new Star('刘德华', 18)
ldh.__proto__ === Star.prototype

每一个对象都默认有一个 __proto__ 属性,指向它的构造函数的prototype显式原型;

__proto__ 相当于是一个桥梁,链接,实例通过它访问原型对象;

所以,方法属性的查找规则可以为:

首先看ldh 对象身上本身有没有sing这个方法,如果有,就执行这个对象上的sing方法;

如果没有,就会通过__proto__ 去实例的原型上查找;

举例:

function Animal() {
  this.color = 'orange'
}
const cat = new Animal()
console.log(cat.__proto__ === Animal.prototype) // true
console.log(cat.__proto__) // 得到原型
console.log(Animal.prototype) //  得到原型
console.log(Animal.prototype.constructor === Animal)
console.log(cat.__proto__.constructor === Animal)
//   实例.__proto__ === 构造函数.prototype

2.原型链

2.1构造函数,原型和实例之间的关系

function Person(name) {
  this.name = name
}
const person = new Person()

三者关系如图所示:

image-20220811200111597.png

四条线:

  1. 构造函数 有一个prototype 属性,指向原型对象;
  2. 原型对象默认有一个constructor属性,指向构造函数(构造函数和原型 是相互指向);
  3. 构造函数通过 new 创建一个实例;
  4. 实例通过__proto__ 访问到原型;

实例与构造函数原型之间有直接的联系(__proto__),但实例与构造函数之间没有;

2.2 原型链

 function Person() {
   this.name = name
 }
 const person = new Person()

1.Person.prototype也是一个对象,所有,他也有__proto__属性 (原型规则2)

那么Person.prototype的构造函数是谁呢?

Object() 所有的对象,都可以理解为是Object()这个构造函数创建的

根据原型规则4可以知道:

Person.prototype 的隐式原型 指向 它的构造函数的显式原型

console.log(Person.prototype.__proto__ === Object.prototype)  // true

2.Object.prototype 是什么? 是原型,是谁的原型? Object()构造函数的原型

2.1 Object.prototype 原型,默认有一个constructor属性,指向构造函数Object

2.2 Object构造函数通过 prototype 访问到这个原型 Object.prototype

console.log(Person.prototype.__proto__ === Object.prototype)  // true

以下两个,都指向Object() 这个构造函数的原型

console.log(Person.prototype.__proto__)
console.log(Object.prototype)

3.Object是构造函数吗? 是 所以Object也有prototype属性 (原型规则3)

Object.prototype 他是Object的原型对象,所以Object.prototype__proto__属性 (原型规则2)

但是 Object.prototype.__proto__ 指向 null空!

Object.prototype.__proto__ === null

正常的原型链都会终止于 Object 的原型对象

Object 原型的 原型 是 null

我们说的原型, 可以说是构造函数的原型, 也可以说是实例的原型

(Object.prototype) ==> Object的原型

Object.prototype.__proto__ 能访问到它Object.prototype的原型

obj.__proto__ === Object.prototype obj的原型

2.3 原型链小结

  1. 每个对象能够通过 __proto__ 属性访问到它的原型对象,原型对象也有他的原型对象
  2. 当访问一个对象的属性或方法时,先在自身中寻找
  3. 如果没有,就会沿着 __proto__ 这条链向上查找,一直找到最顶层的 Object.prototype为止
  4. object.prototype.__proto__ === null

关系如图所示:

image-20220811172826204.png

主要是通过隐形原型__proto__进行查找!

2.4 数组与函数的原型链

2.4.1 数组的原型链

const arr = new Array(1, 2, 3, 4)

arr --> Array.prototype --> Object.prototype --> null

2.4.2 函数的原型链

fn 相当于是 Function() 创建的

const fn = new Function()

fn --> Function.prototype --> Object.prototype --> null

\