前端必刷手写题系列 [19]

735 阅读5分钟

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

这个系列也没啥花头,就是来整平时面试的一些手写函数,考这些简单实现的好处是能看出基本编码水平,且占用时间不长,更全面地看出你的代码实力如何。一般不会出有很多边界条件的问题,那样面试时间不够用,考察不全面。

平时被考到的 api 如果不知道或不清楚,直接问面试官就行, api 怎么用这些 Google 下谁都能马上了解的知识也看不出水平。关键是在实现过程,和你的编码状态习惯思路清晰程度等。

注意是简单实现,不是完整实现,重要的是概念清晰实现思路清晰,建议先解释清楚概念 => 写用例 => 写伪代码 => 再实现具体功能,再优化,一步步来。

28. 各类继承方式的手写(下)

问题是什么

继承是 OOP (面向对象编程)(Object-oriented programming) 的核心概念,本文是介绍js 中各种继承的简单手写。

关于原型相关知识请看我的核心概念系列一文说透JS中的原型和继承(上) 继承相关核心概念正在写作中,敬请期待。本篇只是列举下手写方式,作为核心概念的实例补充。那篇会针对各种继承方式进行详细分析。

我们先简单理解,继承就是为了重用, 比如你有 people 这个对象及其属性和方法,并希望将 student 和 worker 作为基于 people 稍加修改的变体。我们想重用 people 中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象。那么我们看下面几种继承方式的简单实现。

上篇我们主要讲了

  • 原型链继承
  • 构造函数继承
  • 组合继承

接下来是下篇, 如果没看过继承上篇的建议先读一下,这是循序渐进的

  • 寄生式继承
  • 寄生组合式继承

寄生式继承

寄生式继承简单来说就是把继承过程封装在一个函数中

它创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来增强对象,最后返回这个增强对象。

就是在原来继承方式上,换了一种写法,但是还是存在原型链继承或者构造函数继承的缺陷。至于是那种缺陷,是看你封装函数用的哪种继承方式。所以这种继承方式是一种思想或者说写法。

  • 用原型链继承的话
    • 引用类型的属性被所有实例共享
    • 在创建 Child 的实例时,不能向 Parent 传参
  • 用构造函数继承
    • 每次创建对象都会创建一遍方法, 造成内存浪费

那么下面是代码

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

function createEnhanceObj(obj) {
  // 以 obj 为 新对象的原型
  let clone = Object.create(obj);
  // 增强该对象, 可添加方法
  clone.coding = function () {
    console.log('I am coding !');
  }
  return clone
}


let parent = { 
  name: 'ParentName', 
  actions: ['eat', 'reading'] 
}

// 我们利用封装的继承函数 来增强 child 对象
let child = createEnhanceObj(parent)

console.log(child.name) // ParentName
console.log(child.actions) // [ 'eat', 'reading' ]
child.coding() //I am coding !

// 引用类型的属性被所有实例共享
parent.actions.push('sleep')
console.log(child.actions) // [ 'eat', 'reading', 'sleep' ]

// 每次创建对象都会创建一遍方法, 造成内存浪费
let child2 = createEnhanceObj(parent)
console.log(child.coding === child2.coding) // false

从后面2个事例中可以看出,之前有的问题,还是有

那么我们再把组合式和寄生式写法融合就成了下面更好的寄生组合式继承

寄生组合式继承

看这个方法前,我建议先回顾下组合继承, 既然融合,需要深知两者特性

function Parent (name, actions) {
    this.name = name;
    this.actions = actions;
}

Parent.prototype.play = function () {
  console.log(`${this.name} is playing`)
}

function Child () {
  Parent.apply(this, arguments);
}

// 上面其实跟组合继承一模一样
// 组合继承下面有调用了一次构造函数像这样:
// Child.prototype = new Parent();
// =====================================
// 而现在是用寄生方式来写, 保证原型链上下文

// 其实就是 Object.create 核心实现代码
function myCreateObj(obj) {
  // 内部新建一个构造函数 F
  function F() {}
  // 将构造函数的prototype属性指向传入的对象
  F.prototype = obj;
  // 新建一构造函数的实例并返回
  return new F();
}

function createEnhanceObj(child, parent) {
    let prototype = myCreateObj(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

createEnhanceObj(Child, Parent);

let child1 = new Child('k', ['eat', 'sleep', 'coding']);

console.log(child1.name); // k
console.log(child1.actions); // ['eat', 'sleep', 'coding']

let child2 = new Child('cd', ['eat', 'sleep']);

console.log(child2.name); // cd
console.log(child2.actions); // ['eat', 'sleep']
child2.play(); // cd is playing

console.log(child1.play === child2.play) // true

console.log(child1 instanceof Parent) // true

其中有个 Object.create 的核心实现方式是关键, 所以我们可以简化

function Parent (name, actions) {
    this.name = name;
    this.actions = actions;
}

Parent.prototype.play = function () {
  console.log(`${this.name} is playing`)
}

function Child () {
  Parent.apply(this, arguments);
}

function createEnhanceObj(Parent, Child){
    // 设置 Child.prototype 的原型是 Parent.prototype
    Child.prototype = Object.create(Parent.prototype);
    // 保持 constructor 指向的一致性
    Child.prototype.constructor = Child;
}

// 或者可以这样写,把父类原型赋值给子类,并将构造函数设置为子类
// function createEnhanceObj(Parent, Child){
//   Child.prototype = Object.create(Parent.prototype, {
//     constructor: {
//       value: Child,
//       enumerable: false,
//       writable: true,
//       configurable: true
//     }
//   })
// }

createEnhanceObj(Parent, Child)



let child1 = new Child('k', ['eat', 'sleep', 'coding']);

console.log(child1.name); // k
console.log(child1.actions); // ['eat', 'sleep', 'coding']

let child2 = new Child('cd', ['eat', 'sleep']);

console.log(child2.name); // cd
console.log(child2.actions); // ['eat', 'sleep']
child2.play(); // cd is playing

console.log(child1.play === child2.play) // true

console.log(child1 instanceof Parent) // true

我们可以看出寄生组合式继承的优势

  1. 只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性

  2. 保证了原型链上下文不变。 因此,还能够正常使用 instanceofisPrototypeOf

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。在大多数Js框架中都是用这个作为继承方案。

今天到这,明天重点讲语法糖 ES6 class/extends 的一些用法。

另外向大家着重推荐下另一个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列 记得点赞哈

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友 Or 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考