【JS面试题】渐进式学习手写继承

232 阅读3分钟

在理解手写继承的原理之前必须要先学会三个API的实现原理

手写原型链相关API

这三个API和继承的实现以及原型链的指向息息相关,务必要先理解这几个API再去学习继承。

手写new操作符

function myNew(fn, ...args) {
    // 1.创建一个新对象
    const obj = {}
    // 2.新对象原型指向构造函数原型对象
    obj.__proto__ = Func.prototype
    // 3.将构建函数的this指向新对象
    let result = Func.apply(obj, args)
    // 4.根据返回值判断
    return result instanceof Object ? result : obj
}

还需要知道原型链的查找流程,如果不清楚可以去了解一下instanceof实现原理

手写instanceof

function myInstanceOf(obj, constructor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto) {
    if (proto === constructor.prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

手写Object.create

function myCreate(obj, props) {
  function F() {}
  F.prototype = obj;
  const newObj = new F();
  if (props) {
    Object.defineProperties(newObj, props);
  }
  return newObj;
}

原型链继承

使用原型链实现简单的继承。

function Parent() {
  this.name = 'parent1';
  this.play = [1, 2, 3]
}
function Child() {
  this.type = 'child1';
}
// new的过程Child.prototype.__proto__ = Parent.prototype; (1)
Child.prototype = new Parent();
console.log(new Child()); // {"type":"child2"}

// new的过程s1.__proto__ = Child.prototype 
// 通过(1)又可得 s1.__proto__.__proto__ == Parent.prototype
var s1 = new Child();
var s2 = new Child();

s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]

缺点:由以上输出可知,new的实例共享的同一个原型对象,内存空间是共享的。

构造函数继承

通过改变this指向将父类的属性和方法定义在自己身上。

function Parent() {
  this.name = 'parent2';
}

Parent.prototype.getName = function () {
  return this.name;
}

function Child() {
  // 修改this指向,
  Parent.call(this);
  this.type = 'child2'
}

let s1 = new Child();
let s2 = new Child();
console.log(s1.name); // parent2
s1.name = "change"
console.log(s1.name); // change
console.log(s2.name); // parent2 
console.log(s1.getName());  // 会报错

缺点:与父类失去了关联,没有继承原型属性和方法。

组合继承

将原型链继承和组合继承组合在一起。

function Parent() {
  this.name = 'parent3';
  this.play = [1, 2, 3];
}

Parent.prototype.getName = function () {
  return this.name;
}
function Child() {
  // 第二次调用 Parent()
  Parent.call(this);
  this.type = 'child3';
}

// 第一次调用 Parent()
Child.prototype = new Parent();
console.dir(Child3);
// 手动挂上构造器,指向自己的构造函数,在此之前自建的prototype没有constructor
Child.prototype.constructor = Child;
var s3 = new Child();
var s4 = new Child();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
s3.name = "change";
console.log(s3.getName()); // 输出'change'
console.log(s4.getName()); // 正常输出'parent3'

缺点:每次new Child都会执行两次Parent,造成了多构造一次的性能开销。

原型式继承

借助Object.create方法实现普通对象的继承。这里我们需要了解create的过程发生了什么

使用Object.create指向父类

let Parent = {
  name: "parent4",
  friends: ["p1", "p2", "p3"],
  getName: function () {
    return this.name;
  }
};

//通过create的流程使person1.__proto__指向了Parent本身(注意是本身而不是prototype)
// F.prototype = Parent4
// person1.__proto__ = F.prototype
let person1 = Object.create(Parent);
person1.name = "tom";
person1.friends.push("jerry");

let person2 = Object.create(Parent);
person2.friends.push("lucy");

console.log(person1.name); // tom
console.log(person1.name === person2.getName()); // true
console.log(person2.name); // parent4
console.log(person1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person2.friends); // ["p1", "p2", "p3","jerry","lucy"]

缺点:Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存。

寄生式继承

和原型式继承相同,只是把create部分放进函数中。

let parent = {
    name: "parent5",
    friends: ["p1", "p2", "p3"],
    getName: function() {
        return this.name;
    }
};

function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
        return this.friends;
    };
    return clone;
}

let person5 = clone(parent);

console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]

寄生组合式继承

function clone(parent, child) {
  // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
  // 与上述代码不同的是此处是child.prototype.__proto__指向parent.prototype(1)
  child.prototype = Object.create(parent.prototype);
  child.prototype.constructor = child;
}

function Parent() {
  this.name = 'parent6';
  this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
  return this.name;
}
function Child() {
  Parent.call(this);
  this.friends = 'child5';
}

clone(Parent, Child);

Child.prototype.getFriends = function () {
  return this.friends;
}

//new后person6.__proto__ == Child.prototype
//又因为(1)所以可以通过原型链,Child.prototype.__proto__查找parent.prototype
let person6 = new Child();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent}
console.log(person6.getName()); // parent
console.log(person6.getFriends()); // child5

感兴趣的也可以使用babel转换ES6代码,思路与寄生组合式继承基本相同。

总结

要真正学会继承得要学会

  1. new操作符的实现原理
  2. 原型链的思想(可以通过instanceof实现来理解)
  3. Object.create的实现原理

在以上的基础上理清原型的流动即可理解原型、原型链、继承这几个知识点。

参考

面试官:Javascript如何实现继承? | web前端面试 - 面试官系列 (vue3js.cn)