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

224 阅读4分钟

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

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

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

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

27. 各类继承方式的手写(上)

问题是什么

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

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

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

上篇我们主要讲

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

原型链继承

function Parent() {
    this.name = 'ParentName';
    this.actions = ['eat', 'sleep'];
}

function Child() {}

// 我们把 Child 的 prototype 属性设置为 Parent 的一个实例
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 原型链继承的继承方式都要修改子类构造函数的指向,
// 否则子类实例的构造函数会指向父类构造函数。

let child1 = new Child()

// 从原型链继承了父类的属性
console.log(child1.name) // ParentName
console.log(child1.actions) // [ 'eat', 'sleep' ]

存在的问题

  1. 引用类型的属性被所有实例共享
  2. 在创建 Child 的实例时,不能向 Parent 传参
function Parent() {
    this.name = 'ParentName';
    this.actions = ['eat', 'sleep'];
}

function Child() {}

// 我们把 Child 的 prototype 属性设置为 Parent 的一个实例
Child.prototype = new Parent();
Child.prototype.constructor = Child;

let child1 = new Child()
// 引用类型属性修改
child1.actions.push('coding');

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

// 接下来 Child 创建的对象就共享相同属性
let child2 = new Child()
console.log(child2.actions) // [ 'eat', 'sleep', 'coding' ]

那么如何避免这两个问题呢,我们来看下面的构造函数继承。

构造函数继承

function Parent (name, age) {
  this.name = name;
  this.age = age;
  this.actions = ['eat', 'sleep'];
  this.play = function () {}
}

// function Child (name, age) {
//     Parent.call(this, name, age);
// }
// 或者用 arguments 更简单
function Child() {
  Parent.apply(this, arguments);
}

const child1 = new Child('hh', 25);
child1.actions.push('coding')

const child2 = new Child('cd', 1);

console.log(child1.name) // hh
console.log(child1.age)  // 25
console.log(child1.actions)  // [ 'eat', 'sleep', 'coding' ]
console.log(child2.name) // cd
console.log(child2.age)  // 1
console.log(child2.actions)  // [ 'eat', 'sleep' ]

console.log(child1.play === child2.play); // false

关注几个点:

  • 其实核心就是通过 Parent.call/apply 继承了父类的属性
  • 引用类型的属性没有被所有实例共享
  • 而且可以传参给 Parent

但注意最后一行 child1.play === child2.playfalse 说明了 play 这个方法占用了两份内存空间。或者说,这个相同的方法被复制了一份, 而不是引用的同一个方法。 原因是我们用到构造函数,而方法都在构造函数中定义每次创建实例都会创建一遍方法, 造成内存浪费。

那么我们就需要继续改进,融合上面两种继承方式,组合继承出现

组合继承

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

// 直接在原型链上挂方法,就避免了内存浪费
Parent.prototype.play = function () {
  console.log(`${this.name} is playing`)
}

function Child (name, age, actions) {
  // 注意这是第一次调用构造函数
  Parent.call(this, name, actions);
  this.age = age;
}

// 这是第二次调用构造函数
Child.prototype = new Parent();
Child.prototype.constructor = Child;

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

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

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

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

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

注意

  • child1.play === child2.play 说明方法指向一处内存
  • 结合了上面两种继承的优势

那么组合继承这么好用是不是没缺点了, 我们思考下, 其实我代码已经标出,调用了2次构造函数,我们该如何优化

今天就到这,明天继续接下来的几种继承,且听下回分解

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

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

参考