js基础回顾-继承

脑子:我感觉我会了 手:你会个鸡

方便查看,先上全图 继承复习图

####一、原型链继承

//父类
function a(name){
  this.name = name
  this.colors = ['red', 'green']
}
a.prototype.say = function(){
  console.log(`姓名:${this.name}`)
}

//子类
function b(age){
  this.age = age
}
//继承父类
//父类的参数只能在此处传递,而不能在子类实例化时传递
b.prototype = new a('张三')
//添加方法,不会影响到父类
//但是添加方法必须在new a之后,如果在之前添加会被覆盖
b.prototype.speak = function(){
  console.log(`姓名:${this.name},年龄:${this.age}`)
}
let obj1 = new b(18)

obj1.say() //姓名:张三
obj1.speak() //姓名:张三,年龄:18

obj1.colors.push('yellow')

let obj2 = new b(24)
console.log(obj2.colors) //[ 'red', 'green', 'yellow' ]

优点: 1.既简单又方便。 2.在子类原型上添加方法不会影响到父类。 3.子类实例可以使用instanceofisPrototypeOf()识别自己的父类 console.log(a.prototype.isPrototypeOf(obj1)) //true console.log(obj1 instanceof a) //true 缺点: 1.必须在new a之后,如果在之前添加会被覆盖(问题不大) 2.无法实现多继承,b.prototype只能指向一个对象(用得不多) 3.子类的原型上如果有引用类型数据,如{}或[], 那么子类在操作这些值时会影响到原型(一般只会在原型上定义方法) 4.无法传递参数(很多时候会需要在子类实例化时向父类的构造函数中传递参数,而不是在b.prototype = new a('张三')时)

####二、借用构造函数继承

//父类a1
function a1(name){
  this.name = name
  this.colors = ['red', 'green']
}
//父类a2
function a2(age){
  this.age = age
}
a1.prototype.a1_say = function(){
  console.log(`姓名:${this.name}`)
}
a2.prototype.a2_say = function(){
  console.log(`年龄:${this.age}`)
}

//子类
function b(name, age){
  //可以实现多继承了
  a1.call(this, name)
  a2.apply(this, [age])
}
b.prototype.speak = function(){
  console.log(`姓名:${this.name},年龄:${this.age}`)
}

let obj1 = new b('张三', 18)
obj1.speak() // 姓名:张三,年龄:18

//无法继承父类的原型方法
//obj1.a1_say() 报错,没有这个方法
//obj1.a2_say() 报错,没有这个方法

obj1.colors.push('yellow')

let obj2 = new b('李四', 24)
console.log(obj2.colors) //[ 'red', 'green' ],没影响

优点: 1.可以实现多继承。 2.子类在实例化父类时,得到的都是父类独立的属性,多个子类互不影响。 3.可以传递参数。 缺点: 1.看着不像是继承,像是投机取巧,因为子类实例 不可以使用instanceofisPrototypeOf()识别自己的父类。 2.父类的原型方法利用不到 3.构造函数本身的缺陷造成的副作用-无法复用

这个无法复用不要迷糊,比如这一条: //obj1.a1_say() 报错,没有这个方法 如果想要让obj1拥有a1_say(),那么就需要改写父类a1

//改写后父类a1
function a1(name){
  this.name = name
  this.colors = ['red', 'green']
  this.a1_say = function(){
    console.log(`姓名:${this.name}`)
  }
}

这么一来,子类b的实例obj1就能拥有a1_say(),但是这个方法是独立的, 如果b在new几个实例obj2,obj3,....都会拥有独立的a1_say()方法,浪费内存 所以3说的无法复用指的是这。 ####三、组合继承 (也叫伪经典继承,=原型链继承 + 借用构造函数继承)

//父类a1
function a1(name){
  this.name = name
}
//父类a2
function a2(age){
  this.age = age
}
a1.prototype.a1_say = function(){
  console.log(`姓名:${this.name}`)
}
a2.prototype.a2_say = function(){
  console.log(`年龄:${this.age}`)
}

//子类
function b(name, age){
  //可以实现多继承
  a1.call(this, name)
  a2.apply(this, [age])
}
//但是依然只能指向一个父类
b.prototype = new a1()
//手动重定向constructor,
//并设置成不可枚举不可删除不可改变
Object.defineProperty(b.prototype, 'constructor', {
  value: b
})
b.prototype.speak = function(){
  console.log(`姓名:${this.name},年龄:${this.age}`)
}

let obj = new b('张三', 18)

obj.a1_say() // 姓名:张三
obj.speak()  // 姓名:张三,年龄:18
//obj.a2_say() // 报错,没有这个方法

看一下obj的全部细节

优点: 他集合了原型链继承借用构造函数继承的全部优点。 缺点: 1.在实现多继承时,子类的原型依然只能指向一个父类的实例,也就意味着子类只能继承一个父类的原型。 2.整个过程中,父类a1的构造函数执行了2次,一次是在 a1.call(this, name),一次是在b.prototype = new a1(),浪费执行时间 3.由于条件2的作用,子类b的实例和原型都存在独立的父类a1的构造函数属性name,只不过原型上的属性值为undifined,由于属性访问的同名屏蔽规则,你永远也访问不到实例的原型上的属性name,但这确实是资源的一种浪费。

####四、原型式继承(注意,不是原型链)

//如果我们想创建一个继承自普通对象的对象时,需要以下方法
let a = {
  say: function(){
    console.log(`姓名:${this.name},年龄:${this.age}`)
  }
}

function getObject(o) {
  function F() { }
  F.prototype = o;
  return new F();
}

let b = getObject(a)

//等价于(需要支持ES5)
//let b = Object.create(a)

//也等价于(需要支持ES5)
//let b = {}
//b.__proto__ = a

优点: 1.不用兴师动众的创建一个构造函数,写起来更简洁,也更易读。 2.可以使用isPrototypeOf()识别自己的父类 console.log(a.isPrototypeOf(b)) // true 缺点: 1.看着也不像是继承,像是工厂模式创建对象。 2.不可以使用instanceof识别自己的父类,以为它不是一个函数 3.不能设置constructor,因为指向谁都说不通。 4.看如下代码情况

let a = {
  say: function () {
    console.log(`姓名:${this.name},年龄:${this.age}`)
  }
}
let b = {
  name: '张三',
  age: 18
}
//如果b想继承a,那么b就不能直接通过getObject(a)继承,
//而是需要额外的垫片,
b = Object.assign(getObject(a), b) //(需要支持ES5)
//或者使用for...in
let c = getObject(a)
for(let key in b){
  c[key] = b[key]
}
b = c

####五、寄生式继承(原型式继承升级版)

function createAnother(original) {
  var clone = getObject(original); //通过调用函数创建一个新对象
  clone.sayHi = function () { //以某种方式来增强这个对象
    alert("hi");
  };
  return clone; //返回这个对象
}

实际上这个寄生式继承就是在原型式继承的基础上增加了自定义属性的过程,貌似解决了原型式继承的缺点4,但是不灵活,很死板,使用场景会更局限

####六、寄生组合式继承(经典继承,=组合继承 + 寄生式继承 )

//原型式继承
function getObject(o) {
  function F() { }
  F.prototype = o;
  return new F();
}
//寄生式继承
function createAnother(b, a){
  var prototype = getObject(a.prototype); //创建对象
  Object.defineProperty(prototype, 'constructor', {
    value: b
  })//增强对象
  //原型链继承
  b.prototype = prototype; //指定对象
}

//父类
function a(name){
  this.name = name
}
a.prototype.say = function(){
  console.log(`姓名:${this.name}`)
}

//子类
function b(name, age){
  //借用构造函数继承
  a.call(this, name)
  this.age = age
}

createAnother(b, a)

b.prototype.speak = function(){
  console.log(`姓名:${this.name},年龄:${this.age}`)
}

let obj = new b('张三', 18)

obj.say() // 姓名:张三
obj.speak()  // 姓名:张三,年龄:18
console.log(obj)

优点: 可以把此图和组合继承做一下对比,基本上就是利用寄生式继承弥补了组合继承的不足。 缺点: 在实现多继承时,子类的原型依然只能指向一个父类的实例,也就意味着子类只能继承一个父类的原型。 但是多继承用的很少,就当它是完美继承就好了,男人何苦难为男人。 #####寄生组合式继承是最终解决办法,所以叫经典继承或者完美继承应该不过分。 ------------------------------- 分割线 ------------------------------- ####*其他 再谈constructor 先认识两个东西 1. 闭环 在设计产品原型的时候,多数人都会考虑一个“闭环”的概念,闭环是啥,就是画一个圆。 画圆时,无论你的初始点在哪里,终点始终与初始点重合。 比如,你要登录某个网站,发现密码忘了,找回密码就会在登录框附近,你使用了找回密码操作,那么操作成功后提示操作成功,自动跳转登录页面,这就是一个小闭环。 在用户操作完某一个流程时,都会有一个按钮引导你去跳转到另一个页面,这个页面很大概率就是此功能初始的那个页面。 这个闭环流程是必须的吗?当然不是,只不过是提升体验或者完善体系。

2. js既是一门语言,也是一款产品 我们前端根据产品的原型,使用js去生产一个web网页,使用我们网页的人我们称之为用户。 js这门语言本身也是一款产品,他的生产者就是js的作者,那么使用js的人(前端开发)被js的作者称为用户。 这两个报错有区别吗? 站在生产者和用户的角度没有区别。 登录报错是 web生产者对于web用户的错误提示。 控制台报错是js语言生产者对于js语言用户的错误提示。

说回constructor,这个属性就是js作者针对js语言做的一个小的逻辑闭环。 这个闭环帮助js语言用户,可以根据实例化对象的constructor追溯到生产此实例的构造函数【虽然追溯方法还有instanceof isPrototypeOf(),但constructor可以使我们在控制台可以直观感受】,构造函数可以实例化对象,对象还是拥有constructor,如此往复,实现一个完善的体系。 constructor这个闭环是必须的吗?当然不是,如果是必须的也就不会有那么多人怀疑它存在的意义了。