逐步解析构造函数、类、原型链问题

299 阅读8分钟

逐步解析构造函数、类、原型链问题

一.静态成员、实例成员、静态方法、实例方法

class Star{
  constructor(uname,age){
    // 实例成员
    this.uname = uname
    this.age = age
    // 实例方法
    this.say = function(){
      console.log('say')
    }
  }
}
Star.work = 'job'  // 静态成员
Star.dance = function(){ // 静态方法
  console.log('dance')
}
var star = new Star('zcl','22')
console.log(star)
console.log(star.work)  // undefined
console.log(Star.work)   // job 
star.dance()  // 报错
Star.dance()  // dance

star:

image.png

从上述图片可以看出实例成员和实例方法会显示在创建的star对象里,但是静态成员work以及静态方法dance并没有添加在star对象中,这也就印证了上述代码中当你去用star对象访问静态成员和静态方法的时候返回的是undefined和报错,因为类Star上的静态成员和静态方法只能通过类去调用,而不能通过类创造的对象来访问。同样的,实例成员和实例方法也只能通过star对象来调用而不能通过Star这个类来调用。

function Father(name,age){
  this.name = name
  this.age = age
  this.sing = function(){
    console.log('sing')
  }
}
var son = new Father('zcl',22)
console.log(son)

同样的以构造函数的形式创建对象,构造函数中的name,age,sing也是添加在son中,用过构造函数本身是访问不到的。

二.prototype上的方法与成员

class Star{
  constructor(uname,age){
    this.uname = uname
    this.age = age
    this.say = function(){
      console.log('say')
    }
  }
  sing(){
    console.log('sing')
  }
}
Star.prototype.work = 'job'
Star.prototype.dance = function(){
  console.log('dance')
}

star:

image.png

我们从上述图片中可以看到Star.prototype上添加的成员wrok以及方法dance都出现在了对象star的__proto__上,而且Star类中的sing方法也出现在了里面。也就意味着sing方法实际上是添加在对象的__proto__上的与dance方法的实质是一样的。由此可知star.__proto__与Start.prototype是等价的

function Father(name,age){
  this.name = name
  this.age = age
  this.sing = function(){
    console.log('sing')
  }
}
Father.prototype.work = 'job'
Father.prototype.dance = function(){
  console.log('dance')
}
var son = new Father('zcl',22)

利用构造函数也是如此,但是构造函数与类的区别在于,类可以通过construcotr中定义实例方法直接显示到对象身上,而构造函数却不可以,它只可以通过在自身构造函数的prototype上添加方法然后显示到对象的__proto__上。

三.prototype与__proto__

3.1 双重身份

为什么我们在类或者构造函数上直接添加属性只能通过类或者构造函数本身去访问得到呢?

原因在于无论是类还是构造函数 首先它是一个函数这是勿用质疑的 但函数的本质是什么 就是对象。所以当我们为一个对象添加属性的时候 当然只能用这个对象去获得该属性。

那么对于构造函数和类具有双重身份,那他们的prototype和__proto__又指向的是什么呢?

function Test(){
  this.a = 1
}
const test = new Test()
console.log(Test.prototype === test.__proto__) // 对象的__proto__保存着该对象的构造函数的prototype
console.log(Test.prototype.__proto__ === Object.prototype)  // Test.prototype也是个对象所以也有__proto__属性
console.log(Object.prototype.__proto__)  // null 走到最顶层之后 就没有了__proto__这个属性

作为函数而言,其构造的对象的__proto__等于其prototype。并且__proto__中的constructor记录着构造该对象的构造函数是谁!!

console.log(Test.__proto__ === Function.prototype)  // true  对象的__proto__等于构造其对象的函数的prototype
console.log(Function._proto_ === Function.prototype)  // 底层规定的 因为它构造了它本身

作为对象而言,对象的__proto__等于构造它的Function的prototype

console.log(typeof Test)  // Function
console.log(Test instanceof Object) // true

3.2 逐步解析继承的四种主要方式的优缺点

3.2.1 利用原型链继承

function Animal(){
  this.colors = ['black','white']
  this.name = 'zcl'
}
Animal.prototype.getColor = function(){
  return this.colors
}

function Dog(){}
Dog.prototype  = new Animal()   // 通过原型链进行继承
let dog1 = new Dog()
console.log(dog1)

dog1:

image.png

通过Dog.prototype实现继承的原理是 Dog.prototype = new Animal() 实际上就是在Dog实例对象的__proto__上挂载new Animal()即Animal构造的实例。

所以从上图可以看出在__proto__中存在Animal中的两个实例成员colors以及name 同时由于在Animal.prototype上挂载了getColor方法,也就意味着会在Animal实例的__proto__上挂载这个方法。

function Animal(){
  this.colors = ['black','white']
  this.name = 'zcl'
}
Animal.prototype.getColor = function(){
  return this.colors
}
function Dog(){}
Dog.prototype  = new Animal()   // 通过原型链进行继承
let dog1 = new Dog()
dog1.colors.push('red')
dog1.name = 'zzr'
console.log(dog1)

dog1:

image.png

当我们为Dog的实例对象添加成员变量的时候,该成员变量会体现在实例对象中,但是当我们去改变引用类型的时候,Dog的实例对象会跟着__proto__这条链子去寻找colors这个数组,然后修改它,所以不会体现在实例对象中。

function Animal(){
  this.colors = ['black','white']
  this.name = 'zcl'
}
Animal.prototype.getColor = function(){
  return this.colors
}

function Dog(){}
Dog.prototype  = new Animal()   // 通过原型链进行继承
let dog1 = new Dog()
dog1.colors.push('red')
dog1.name = 'zzr'
dog1.age = '23'
console.log(dog1)
let dog2 = new Dog()
console.log(dog2)
console.log(dog2.colors)  // 可以看到dog2实例虽然没有对colors操作 但是返回的确实dog1对colors操作的结果 

dog2:

image.png

当我们再次用Dog实例对象的时候,dog1中的name作为其私有属性是不可被dog2进行访问到的,所以dog2访问到的name是Animal实例对象的成员变量。并且当dog1对colors数组修改之后由于是对原型链上的colors修改所以会辐射到之后利用Dog构建的所有实例上!

产出的问题:

1.利用Dog.prototype = new Animal() 继承Animal构建出来的实例对象后,我们发现Dog构建的实例对象的__proto__上并没有constructor这个属性。而这个属性实际应该是Dog.prototype.constructor = Dog

2.原型链上所有的引用类型譬如colors这个属性将会被所有实例共享

3.子类在实例化的过程中无法给父类构造函数传参

3.2.2 借用构造函数继承

function Animal(name){
  this.colors = ['white','red']
  this.name = name 
  this.getName = function(){
    return this.name
  }
}
function Dog(name){
  Animal.call(this,name)
}
Dog.prototype = new Animal('zcl')

let dog1 = new Dog('zzr')
dog1.colors.push('red')
console.log(dog1)

dog1:

image.png

我们来一步步解析下产出上述结果的步骤:

  1. 首先我们通过 Dog.prototype = new Animal() 先利用Animal实例化了一个对象并传入一个name进行之后再挂载到Dog.prototype上 也就是挂载到了Dog实例对象的__proto__上

  2. 其次当我们实例化Dog对象的时候传入一个参数name 但是我们在Dog这个构造函数中利用call方法调用了Animal这个构造函数并将Dog实例以及name传递进去,再次运行Animal这个构造函数 但是这次this指向是Dog实例了 所以Dog实例上便有了自身的私有属性colors getName 以及 name

带来的问题:

  1. 每次创建实例的时候都会调用两次父类构造函数即new Animal() 以及 Animal.call(this,name)
  2. Animal这个构造函数中定义了一个成员方法 所以当我们每次创建的Animal实例的时候 都会创建一遍这个方法

3.2.3 组合继承

function Animal(name) {
  this.name = name
  this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
  return this.name
}
function Dog(name, age) {
  Animal.call(this, name)
  this.age = age
}
Dog.prototype =  new Animal('zcl')
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
console.log(dog1)

dog1:

image.png

这种方法优化了什么:

  1. 我们将成员方法放到了Animal.prototype上 这样就不会在每次创建实例的时候都生成一遍成员方法
  2. 并且我们将Dog.prototype.constructor这个属性重新扩充了出来指回了Dog这个构造函数

3.2.4 寄生式组合继承

function Animal(name) {
  this.name = name
  this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
  return this.name
}
function Dog(name, age) {
  Animal.call(this, name)
  this.age = age
}
Dog.prototype =  Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
console.log(dog1)

dog1:

image.png

这里我们解决了只调用一次Animal这个构造函数,这里Object.create的作用是将Aninam.prototype挂载到Dog.prototype的__proto__上。所以我们可以看到Dog.prototype上是没有Animal的成员变量的因为并没有在这一层面进行操作!!

做到这一步不由得想到另一种方式:

function Animal(name) {
  this.name = name
  this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
  return this.name
}
function Dog(name, age) {
  Animal.call(this, name)
  this.age = age
}
Dog.prototype =  Animal.prototype
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
console.log(dog1)

image.png

我们直接将Dog.prototype = Animal.prototype 也就是在dog的__proto__上直接挂载Animal的prototype有的所有东西!!

四.基于原型链的一些应用

4.1 实现new关键字

function newFactory(){
  var obj = new Object()
  Constructor = Array.prototype.slice.call(arguments)[0]  // 传入的构造函数
  arguments = Array.prototype.slice.call(arguments).slice(1)  // 传入的成员属性
  // obj.__proto__ = Constructor.prototype  // person的prototype和person实例的__proto__  中是有constructor这个属性的以及其他一些方法 但是现在obj.__proto__是空的
  obj.__proto__.constructor  = Constructor   // 和上面这个形式是等价的
  var ret = Constructor.apply(obj,arguments)  // 继承

  return typeof ret === 'object' ? ret || obj:obj  // 返回这个对象
}

原理就是 我们新创建的实例是由constructor来创建的,这个constructor属性是在新创建的实例的__proto__里面的,其次我们要调用父类构造方法并传递参数进去所以就想到了用apply方法

4.2 实现Instanceof关键字

instanceof的主要作用用白话来讲就是 A instanceof B 即A是否是B创建的!

所以显而易见 想要看A是否是B创建的 即A是否是B的实例对象 那么我们可以依据B创建的实例对象的__proto__等于B这个构造函数的prototype。

function instanceOf(left,right){
  let proto = left.__proto__
  while(true){
    if(proto === null) return false
    if(proto === right.prototype){
      return true
    }
    proto = proto.__proto__ // 沿着原型链继续往下找
  }
}

4.3 实现Object.create

Object.create的作用就是 我们要在新创建的对象的__proto__上挂载现有的对象。

参数其一新建对象的__proto__上需要挂载的对象是谁 其二为新建对象添加属性(可选)


Object.createFunction = function(proto, propertyObject = undefined) {
  if (typeof proto !== 'object' && typeof proto !== 'function') {
      throw new TypeError('Object prototype show be an object')
  }
  if (propertyObject == null) {
      new TypeError('Cannot convert undefined or null to object')
  }
  function F() {}   // 新建一个构造函数 
  F.prototype = proto  // 构造函数的prototype相当于obj的__proto__ 让他等于传入的参数一 即一个对象
  const obj = new F()
  if (propertyObject != undefined) {
    // 处理属性
      Object.defineProperties(obj, propertyObject)
  }
  if (proto === null) {   // 对于 Object.create(null)
      obj.__proto__ = null
  }
  return obj  // 返回新建对象
}

五.Tips

上述描述若有不妥指出请指正