原型链及原型链继承的那些事~

125 阅读7分钟

什么是继承

  1. 继承是面向对象语言的三大特性之一:封装,继承,多态;
  2. 关于继承MDN的解释是:

JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。 (发现看不大懂,我刚接触js的时候,看这个解释也是一脸懵逼)

  1. 继承是子类可以用父类的方法和属性的一个属性,帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来就好,这个属性基于原型链,那什么是原型链呢?

什么是原型及原型链

浏览器给每个对象都赋予了一个属性__proto__,这个属性指向的东西就是原型(也叫原型对象),原型对象也有原型对象,依次不断向上,就构成了原型链 函数也是一个对象,作为对象,他有__proto__属性,指向一个constructor属性是Function函数的对象,作为一个函数,他有一个显式原型prototype指向一个constructor属性是本身的对象,除了Function函数以外的函数,Student.propotype === Student.__proto__false(这里的student为一个构造函数) Function函数是个例外,即:Function.propotype === Function.__proto__true;

function Person() {

}

console.log('1', Person.prototype.__proto__) // 顶层原型对象
console.log('2', Object.getOwnPropertyDescriptors(Person.__proto__))
console.log('3', Object.getOwnPropertyDescriptors(Person.prototype))
console.log('4', Object.propotype === Object.__proto__)
console.log('5', Function.prototype === Function.__proto__); // true 特殊情况

结果如下:

1 [Object: null prototype] {}
2 {
  length: { value: 0, writable: false, enumerable: false, configurable: true },
  name: { value: '', writable: false, enumerable: false, configurable: true },
  arguments: {
    get: [Function (anonymous)],
    set: [Function (anonymous)],
    enumerable: false,
    configurable: true
  },
  caller: {
    get: [Function (anonymous)],
    set: [Function (anonymous)],
    enumerable: false,
    configurable: true
  },
  constructor: {
    value: [Function: Function],
    writable: true,
    enumerable: false,
    configurable: true
  },
  apply: {
    value: [Function: apply],
    writable: true,
    enumerable: false,
    configurable: true
  },
  bind: {
    value: [Function: bind],
    writable: true,
    enumerable: false,
    configurable: true
  },
  call: {
    value: [Function: call],
    writable: true,
    enumerable: false,
    configurable: true
  },
  toString: {
    value: [Function: toString],
    writable: true,
    enumerable: false,
    configurable: true
  },
  [Symbol(Symbol.hasInstance)]: {
    value: [Function: [Symbol.hasInstance]],
    writable: false,
    enumerable: false,
    configurable: false
  }
}
3 {
  constructor: {
    value: [Function: Person],
    writable: true,
    enumerable: false,
    configurable: true
  }
}
4 false
5 true

原型链和原型对象存在的意义

举个例子,我们要创建对象: student, 包含名字,年龄,性别等属性,跑步,唱歌等方法(你可以先思考一下创建对象的几个方法,先思考,再往看哦) 有以下3个方法:

  1. 字面量创建(最常见)
student = {
  name: '李四',
  age: 18,
  gender: '男',
  singing: function() {
    console.log(`${this.name}在唱歌`)
  }
  running: function() {
    console.log(`${this.name}在跑步`)
  }
}
student = {
  name: '王二',
  age: 17,
  gender: '男',
  singing: function() {
    console.log(`${this.name}在唱歌`)
  }
  running: function() {
    console.log(`${this.name}在跑步`)
  }
}

// 但是发现,当学生数据量多了,就会有很多重复的代码
  1. 工厂模式创建
function creatStudent(name, age, gender) {
  let p = {};
  p.name = name;
  p.age = age;
  p.gender = gender;
  p.singing = function() {
    console.log(`${name}在唱歌`);
  };
  p.running = function() {
    console.log(`${name}在跑步`);
  };
  return p;
}

const student1 = creatStudent('李四', 18, '男');
const student2 = creatStudent('王二', 17, '男');
// console.log(Object.getOwnPropertyDescriptors(student1.__proto__)); // 这个时候student的构造器就是Object
// 这个时候可以批量创造对象了,对对象进行了封装,但是没有解决对象标识问题,就是创建的对象是什么类型,创造的对象应该都有一个共同的来源,但是这里没有显示出来
  1. 构造函数创建
function Student(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.singing = function() {
    console.log(`${name}在唱歌`);
  };
  this.running = function() {
    console.log(`${name}在跑步`);
  };
}

const student1 = new Student('李四', 18, '男'); // new关键字的作用是创建一个对象,将该对象的__proto__赋值给Student的一个显式对象prototype,这个就是原型对象;
const student2 = new Student('王二', 17, '男');
// console.log(Object.getOwnPropertyDescriptors(p1.__proto__)); // 这个时候student1的构造器是Student, 我们知道他的来源就是Student,父类就是Student;
// 但是这样创造对象也有缺点,就是每创造一个对象,都会创建一个单独的函数方法,不太好,这时候,我们就可以把以上代码改为一下:
function Student(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
}

Student.prototype.running = function() {
  console.log(this.name + " running~")
}
Student.prototype.singing = function() {
  console.log(this.name + " running~")
}

const student1 = new Student('李四', 18, '男');
const student2 = new Student('王二', 17, '男');
 // 这个时候,不会每次创建对象,都重复创建一次相同的方法,造成内存浪费

为什么需要继承

以上利用在构造函数原型上增加方法来减少内存浪费,就是为什么需要‘继承’的原因,可以复用相同的方法和属性,student1和student2从Student的原型上继承了running和singing这两个方法; 当我们调用student1.singing()方法时,在student1上没有这个方法,就顺着student1__proto__属性来找这个方法,直到找不到为止,这个就是‘原型链’;

常见继承方法

  1. 原型链继承 当我们想在一个对象student上使用另一个对象person的方法时,可以将这个对象person放到该对象student的原型链上
// 父类: 公共属性和方法,抽离复用的方法和属性,不涉及业务处理
function Person() {
  this.name = "why"
  this.friends = []
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法,在子类中进行业务处理
function Student() {
  this.sno = 111
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}

var stu = new Student()

// 原型链实现继承的弊端:
// 1.第一个弊端: 打印stu对象, 继承的属性是看不到的
// console.log(stu.name)

// 2.第二个弊端: 创建出来两个stu的对象
var stu1 = new Student()
var stu2 = new Student()

// 直接修改对象上的属性, 是给本对象添加了一个新属性
stu1.name = "kobe"
console.log(stu2.name)

// 获取引用, 修改引用中的值, 会相互影响
stu1.friends.push("kobe")

console.log(stu1.friends)
console.log(stu2.friends)

// 3.第三个弊端: 在前面实现类的过程中,不能向父类传递参数
var stu3 = new Student("lilei", 112)
  1. 借用(盗用)构造函数继承
function Person(name, friends) {
  this.name = name
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

function Student(name, friends, sno) {
  Person.call(this, name, friends) // 使用Person的属性
  this.sno = sno
}

const stu1 = new Student('lisi', ['ming', 'zhangsan'], 111);
const stu2 = new Student('wanger', ['ming', 'zhangsan'], 222);
stu1.eating() // 不能调用
stu1.friends.push('hanmeimei')
console.log('stu1>>>>>', stu1, stu2);

// 解决了继承的属性看不到问题,原型共享导致的引用更改的问题,以及可以向父类函数中传递参数的问题
// 但是也有弊端

// 1.第一个弊端: Person函数至少被调用了两次
// 2.第二个弊端: stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要: stu.__proto__
// 3.不能调用父类原型上的方法
  1. 组合式继承
  • 父类对象赋值给子类原型
function Person(name, friends) {
  this.name = name
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

function Student(name, friends, sno) {
  Person.call(this, name, friends) // 使用Person的属性
  this.sno = sno
}

const stu1 = new Student('lisi', ['ming', 'zhangsan'], 111);
const stu2 = new Student('wanger', ['ming', 'zhangsan'], 222);
stu1.prototype = new Person() // 继承Person的方法
stu2.prototype = new Person() // 继承Person的方法
stu1.friends.push('hanmeimei')
console.log('stu1>>>>>', stu1, stu2);

// 1.第一个弊端: Person函数至少被调用了两次
// 2.第二个弊端: stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要: stu.__proto__
  • 父类原型赋值给子类原型 在上一个的基础上完善
function Person(name, friends) {
  this.name = name
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

function Student(name, friends, sno) {
  Person.call(this, name, friends) // 使用Person的属性
  this.sno = sno
}

const stu1 = new Student('lisi', ['ming', 'zhangsan'], 111);
const stu2 = new Student('wanger', ['ming', 'zhangsan'], 222);
const p = new Person()
stu1.prototype = p.__proto__ // 继承Person的方法
stu2.prototype = p.__proto__ // 继承Person的方法
stu1.friends.push('hanmeimei')
console.log('stu1>>>>>', stu1, stu2);

// 1.第一个弊端: Person函数至少被调用了两次
// 第二个弊端被解决了
// 严格意义上说,这个也属于借用(盗用)构造函数继承
  1. 原型式继承(可以关注一下Object.create()的实现)
// 继承的目的:复用另一个对象的属性和方法
const person = {
  friends: ['zhangshan', 'lihua'],
  singing: function() {
    console.log(`${this.name}在唱歌`)
  }
}

const student1 = Object.create(person, {
  name: {
    value: 'zhangshan',
  },
  age: {
    value: 18
  },
}) // 第二个参数可以定义student1中的属性
const student2 = Object.create(person, {
  name: {
    value: 'buzhi',
  },
  age: {
    value: 19
  },
})

student1.friends.push('hanmeimei')
console.log('student>>>>>', student1, student2);
student1.singing()
// 缺陷一:student1中的friends改变,student2中的也会改变,Object.create()是一个浅拷贝的方法
// 缺陷二:当student中有重复的属性时,比如这里的age,name还是造成了代码冗余
  1. 寄生式继承(使用原型式继承对目标函数进行浅拷贝)
var person = {
  singing: function() {
    console.log(`${this.name}在singing`)
  },
  friends: ['zhangshan', 'lihua'],
}

function createStudent(name) {
  var stu = Object.create(person)
  stu.name = name
  stu.studying = function() {
    console.log(`${this.name}在studying~`)
  }
  return stu
}

var stu1 = createStudent("why") // 每调用一次这个函数,studing方法都会被创建一次
var stu2 = createStudent("kobe")
var stu3 = createStudent("james")
stu1.friends.push('lihua')
console.log('stu1>>>>>', stu1, stu2, stu3); // friends改变还是会造成其他friends改变
stu1.singing()
// 缺陷1: 每调用一次这个函数,studing方法都会被创建一次
// 缺陷2: 父类的引用属性是共享的
  1. 寄生组合式继承(寄生式继承和借用构造函数继承的组合)
function createObject(o) {
  function Fn() {}
  Fn.prototype = o
  return new Fn()
}

function inheritPrototype(SubType, SuperType) {
  // SubType.prototype = Object.create(SuperType.prototype)
  SubType.prototype = createObject(SuperType.prototype)
  Object.defineProperty(SubType.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: SubType
  })
}

function Person(name, age, friends) {
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.running = function() {
  console.log("running~")
}

Person.prototype.eating = function() {
  console.log("eating~")
}


function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends)
  this.sno = sno
  this.score = score
}

inheritPrototype(Student, Person)

Student.prototype.studying = function() {
  console.log("studying~")
}

var stu = new Student("why", 18, ["kobe"], 111, 100)
console.log(stu)
stu.studying() // studying~
stu.running() // running~
stu.eating() // eating~

console.log(stu.constructor.name) // Student

不断对继承方法优化,最终得到现在的继承方法,优点如下:(当然这里没说class,class继承其实就是寄生组合式的语法糖)

  1. 只调用一次父类构造函数
  2. 子类构造器可以向父类构造器传参
  3. 父类方法可以复用
  4. 父类的引用属性不会被共享