大话原型链

539 阅读6分钟

Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.

本人有丰富的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.

欢迎来到小五随笔系列大话原型链.

前言

提起它就头痛,永远在似懂非懂间徘徊,这就是笔者对原型链的感受;而突然有一天你无意间发觉它是 怎么来的 的时候,你会发现,呦,原型链不过如此。

本文为链表在 JavaScript 中的实际应用,如若想回忆链表,其传送门如下:小五的算法系列 - 链表,各位看官根据自身需求自取。

此文以笔者愚见所著,如若有误,欢迎留言指正。

other34.gif

何为原型链

原型,其本质不过一个链表罢了,不信你看 👇

8.png

🐳 __proto__即为链表的 next 指针,链表尾部指针指向 null

原型链,不断寻找其构造函数的原型 (prototypeprototype) 的过程,不信你看 👇

class Person {};
let person = new Person();

9.png

🐳 __proto__ -> constructor.prototype

👺 原型链本质就是链表,__proto__即为next指针,指向其构造函数的原型

原型链的查找即为链表的查找,如 instanceof

原型链的添加即为链表的添加,如 new

链表的操作就是在拨弄指针,原型链也一样,知道如何拨弄指针,原型链也就通了;

小试牛刀 (1).gif

🐢 坊间有一传闻,叫万物皆对象;就像下图,无论千变万化,链表的尾端始终为 Object.prototype

12.png

我们可以将 Object.prototype 看作万物的起源,代号 001

11.png

obj__proto__指向其构造函数 Object 的原型 001

🚶 我们沿着原型链继续向前走,走到红框的位置,以 str -> String.prototype -> Object.prototype -> null 为例,图示如下

15.png

这里的str无论是基本类型还是实例类型均为上图流程,什么,你说 instanceof ?,先卖个关子,后文见分晓。

let str1 = 'apple';
str1 instanceof Object // false

let str2 = new String('apple');
str2 instanceof Object // true

说完实例,我们来说说其构造函数,也就是类;不同的类各司其职,一同完善我们的 JavaScript 世界;

这些被直接创造的类,其 __proto__ 均指向 Function.prototype,我们也为其取个代号为 002

16.png

可能有人不理解 Object.__proto__ === Function.prototypeFunction.__proto__ === Function.prototype,我们这么去想,__proto__ 指向的是其构造函数的原型,而所有的类都是通过 Function 构造的,包括 ObjectFunction

“new” 一个对象

👀 “new” 会发生什么呢? 不妨一起来看下

先创建一个 Person

function Person (name) {
  this.name = name;
  this.action = function () {
    console.log('打 🏓️ 乒乓球');
  }
}

let person = new Person('黄刀小五');

17.png

new 的过程就类似于在链表头部插入元素, __proto__ 指向其构造函数的原型。

function new (P, ...args) {
  let obj = {};
  obj.__proto__ = P.prototype;
  P.call(obj, ...args); // 用于改变this指向, 使其指向P
  return obj;
}

let person = new(Person, '黄刀小五'); // 输出同上

Object.create()

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

这么直白的描述,照着实现就好了

Object.Create = function (obj) {
  let newObj = {};
  newObj.__proto__ = obj;
  return newObj;
}

作为一名视力良好的公民,可以发现,Object.create() 创造出来的对象,其 __proto__ 指向其入参 obj,而非 Object.prototype,其链式结构如下:

createObj -> obj -> Object.prototype -> null

谈谈继承

千万不要被下面这一堆奇怪的名字吓到,它们只是大招的分解动作而已,跟随它们一步一步探索继承的演变过程吧。

我们来建一个 Teacher 类,并构建一个 Student 类来继承它。

function Teacher (name) {
  this.name = name;
  this.info = {};
  this.action = function () {
    console.log('做练习题');
  }
};

🐳 原型链继承

18.png

原型链继承的思想就是改写 Student 的原型,使其等于 Teacher 的实例,以达到继承的效果,代码如下:

function Student () {};
Student.prototype = new Teacher();

此时遇到个很尴尬的问题,Teacher 类是接收参数的,我该如何传递过去呢 ❓ 传递不了参数,此其罪1也。

Teacher 中有一 info 对象代表基本信息,我创建两个学生实例,如下,分别更改其 info 中的 age 会发生什么?

student1.info.age = 12;
student2.info.age = 13;

console.log(student1.info.age, student2.info.age); // 13 13

会指向的同一地址,值被改变,此其罪2也。

🐳 构造函数继承

21.png

核心思想就是在子类的构造函数中调用父类的构造函数,以达到继承父类的效果

function Student (...args) {
  Teacher.call(this, ...args);
}

我们在 Teacher 的原型上追加一个方法 eat

Teacher.prototype.eat = function () {
  console.log(`${this.name} eat apple`);
}

student 实例中调用下

const student = new Student('小五');
student.eat(); // student.eat is not a function

无法访问父类原型中的属性与方法,此其之罪也

🐳 组合继承

说白了就是上面两种不堪用呀,我们缝合一下,就成了组合继承。

function Student (...args) {
  Teacher.call(this, ...args);
}
Student.prototype = new Teacher();
Student.prototype.constructor = Student; // 重新赋值constructor, 否则其constructor为Teacher

看似完美,可以达到继承的效果;实则调用了两次父类构造函数,造成不必要的损耗

20.png

🐳 原型继承

一个浅拷贝,其本质就是一个Object.create(),我们换一种等同的写法实现下

function create (o) {
  let P = function () {};
  P.prototype = o;
  return new P();
}

缺点同原型链继承,不再赘述

🐳 寄生式继承

所谓寄生式就是复制而已,复制后进行扩展,我们在原型继承的基础上为其添加 study 方法。

function createStudent (o) {
  let clone = create(o);
  clone.study = function () {};
  return clone;
}

🐳 寄生组合式继承

接下来有请我们的终极boss闪亮登场,还记不记得上文组合继承的问题了,其会调用两次父类构造函数;那我们这次不直接指向 Teacher 的实例,而是复制一份 Teacher 的原型,完美解决上述问题。

function Student (...args) {
  Teacher.call(this, ...args);
}
Student.prototype = Object.create(Teacher.prototype);
Student.prototype.constructor = Student;

手写 instanceof

在讲原型链的时候说到,str instanceof Object -> false,因为在 instanceof 的实现上,非 object 类型均返回 false;若 object 类型,则为链表的查找;是不是 so easy,我们来一起实现下。

const instanceof = (L, P) => {
  if (typeof L !== 'object') return false;

  let current = L.__proto__;
  while (current) {
    if (current === P.prototype) return true;
    current = current.__proto__;
  }
  return false;
}

instanceof('str', String) // false
instanceof({}, Object) // true

小试牛刀

题目来源:2019 面试准备 - JS 原型与原型链2020 年我碰到的原型链的面试题

👺 No.1

Function.prototype.a = () => {
  console.log(1);
}
Object.prototype.b = () => {
  console.log(2);
}
function A() {};
const a = new A();

a.a();
a.b();
A.a();
A.b();

解析:

22.png

答案:

a.a(); // Error
a.b(); // 2
A.a(); // 1
A.b(); // 2

👺 No.2

var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
  n: 2,
  m: 3
}
var c = new A();

console.log(b.n);
console.log(b.m);

console.log(c.n);
console.log(c.m);

解析:

23.png

答案:

console.log(b.n); // 1
console.log(b.m); // undefined
console.log(c.n); // 2
console.log(c.m); // 3

👺 No.3

function A() {};
A.prototype.n = 0;
A.prototype.add = function () {
  this.n += 1;
}

a = new A();
b = new A();
a.add();
console.log(b.n);

解析:

执行 add() 时,沿原型链找到 n = 0this.n += 1,此时,a 获取属性 n,其值为1,b 同理

24.png

答案:

console.log(b.n); // 1

参考🔗链接

[manxisuo] JavaScript 世界万物诞生记

[木易杨说] JavaScript常用八种继承方案