【进阶5-2期】图解原型链及其继承优缺点

更新:谢谢你们的支持,最近折腾了一个博客官网出来,方便你们系统阅读,后续会有更多内容和更多优化,猛戳这里查看javascript

------ 如下是正文 ------html

引言

上篇文章介绍了构造函数、原型和原型链的关系,而且说明了 prototype[[Prototype]]__proto__ 之间的区别,今天这篇文章用图解的方式向你们介绍原型链及其继承方案,在介绍原型链继承的过程当中讲解原型链运做机制以及属性遮蔽等知识。前端

建议阅读上篇文章后再来阅读本文,连接:【进阶5-1期】从新认识构造函数、原型和原型链java

有什么想法或者意见均可以在评论区留言。下图是本文的思惟导图,高清思惟导图和更多文章请看个人 Githubwebpack

5-2

原型链

48185513-25833c00-e370-11e8-9939-678da278704d

上篇文章中咱们介绍了原型链的概念,即每一个对象拥有一个原型对象,经过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这种关系被称为原型链(prototype chain)。git

根据规范不建议直接使用 __proto__,推荐使用 Object.getPrototypeOf(),不过为了行文方便逻辑清晰,下面都以 __proto__ 代替。github

注意上面的说法,原型上的方法和属性被 继承 到新对象中,并非被复制到新对象,咱们看下面这个例子。web

// 木易杨
function Foo(name) {
	this.name = name;
}
Foo.prototype.getName = function() {
  	return this.name;
}
Foo.prototype.length = 3;
let foo = new Foo('muyiy'); // 至关于 foo.__proto__ = Foo.prototype
console.dir(foo);
复制代码

image-20190406105351100

原型上的属性和方法定义在 prototype 对象上,而非对象实例自己。当访问一个对象的属性 / 方法时,它不只仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。面试

好比调用 foo.valueOf() 会发生什么?算法

  • 首先检查 foo 对象是否具备可用的 valueOf() 方法。
  • 若是没有,则检查 foo 对象的原型对象(即 Foo.prototype)是否具备可用的 valueof() 方法。
  • 若是没有,则检查 Foo.prototype 所指向的对象的原型对象(即 Object.prototype)是否具备可用的 valueOf() 方法。这里有这个方法,因而该方法被调用。

image-20190407165429484

prototype__proto__

上篇文章介绍了 prototype__proto__ 的区别,其中原型对象 prototype 是构造函数的属性,__proto__ 是每一个实例上都有的属性,这两个并不同,但 foo.__proto__Foo.prototype 指向同一个对象。

此次咱们再深刻一点,原型链的构建是依赖于 prototype 仍是 __proto__ 呢?

kenneth-kin-lum.blogspot.com/2012/10/jav…

Foo.prototype 中的 prototype 并无构建成一条原型链,其只是指向原型链中的某一处。原型链的构建依赖于 __proto__,如上图经过 foo.__proto__ 指向 Foo.prototypefoo.__proto__.__proto__ 指向 Bichon.prototype,如此一层一层最终连接到 null

能够这么理解 Foo,我是一个 constructor,我也是一个 function,我身上有着 prototype 的 reference,只要随时调用 foo = new Foo(),我就会将 foo.__proto__ 指向到个人 prototype 对象。

不要使用 Bar.prototype = Foo,由于这不会执行 Foo 的原型,而是指向函数 Foo。 所以原型链将会回溯到 Function.prototype 而不是 Foo.prototype,所以 method 方法将不会在 Bar 的原型链上。

// 木易杨
function Foo() {
  	return 'foo';
}
Foo.prototype.method = function() {
  	return 'method';
}
function Bar() {
  	return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函数
let bar = new Bar();
console.dir(bar);

bar.method(); // Uncaught TypeError: bar.method is not a function
复制代码

image-20190404190228096

instanceof 原理及实现

instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

// 木易杨
function C(){} 
function D(){} 

var o = new C();

o instanceof C; // true,由于 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,由于 D.prototype 不在 o 的原型链上
复制代码

instanceof 原理就是一层一层查找 __proto__,若是和 constructor.prototype 相等则返回 true,若是一直没有查找成功则返回 false。

instance.[__proto__...] === instance.constructor.prototype
复制代码

知道了原理后咱们来实现 instanceof,代码以下。

// 木易杨
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
   var O = R.prototype;// 取 R 的显示原型
   L = L.__proto__;// 取 L 的隐式原型
   while (true) { 
       // Object.prototype.__proto__ === null
       if (L === null) 
         return false; 
       if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true 
         return true; 
       L = L.__proto__; 
   } 
}

// 测试
function C(){} 
function D(){} 

var o = new C();

instance_of(o, C); // true
instance_of(o, D); // false
复制代码

原型链继承

原型链继承的本质是重写原型对象,代之以一个新类型的实例。以下代码,新原型 Cat 不只有 new Animal() 实例上的所有属性和方法,而且因为指向了 Animal 原型,因此还继承了Animal 原型上的属性和方法。

// 木易杨
function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}

// 这里是关键,建立 Animal 的实例,并将该实例赋值给 Cat.prototype
// 至关于 Cat.prototype.__proto__ = Animal.prototype
Cat.prototype = new Animal(); 

var instance = new Cat();
instance.value = 'cat'; // 建立 instance 的自身属性 value
console.log(instance.run()); // cat is runing
复制代码

原型链继承方案有如下缺点:

  • 一、多个实例对引用类型的操做会被篡改
  • 二、子类型的原型上的 constructor 属性被重写了
  • 三、给子类型原型添加属性和方法必须在替换原型以后
  • 四、建立子类型实例时没法向父类型的构造函数传参

问题 1

原型链继承方案中,原型实际上会变成另外一个类型的实例,以下代码,Cat.prototype 变成了 Animal 的一个实例,因此 Animal 的实例属性 names 就变成了 Cat.prototype 的属性。

而原型属性上的引用类型值会被全部实例共享,因此多个实例对引用类型的操做会被篡改。以下代码,改变了 instance1.names 后影响了 instance2

// 木易杨
function Animal(){
  this.names = ["cat", "dog"];
}
function Cat(){}

Cat.prototype = new Animal();

var instance1 = new Cat();
instance1.names.push("tiger");
console.log(instance1.names); // ["cat", "dog", "tiger"]

var instance2 = new Cat(); 
console.log(instance2.names); // ["cat", "dog", "tiger"]
复制代码

问题 2

子类型原型上的 constructor 属性被重写了,执行 Cat.prototype = new Animal() 后原型被覆盖,Cat.prototype 上丢失了 constructor 属性, Cat.prototype 指向了 Animal.prototype,而 Animal.prototype.constructor 指向了 Animal,因此 Cat.prototype.constructor 指向了 Animal

Cat.prototype = new Animal(); 
Cat.prototype.constructor === Animal
// true
复制代码

image-20190407153437908

解决办法就是重写 Cat.prototype.constructor 属性,指向本身的构造函数 Cat

// 木易杨
function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}
Cat.prototype = new Animal(); 

// 新增,重写 Cat.prototype 的 constructor 属性,指向本身的构造函数 Cat
Cat.prototype.constructor = Cat; 
复制代码

image-20190407164839128

问题 3

给子类型原型添加属性和方法必须在替换原型以后,缘由在第二点已经解释过了,由于子类型的原型会被覆盖。

// 木易杨
function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}
Cat.prototype = new Animal(); 
Cat.prototype.constructor = Cat; 

// 新增
Cat.prototype.getValue = function() {
  return this.value;
}

var instance = new Cat();
instance.value = 'cat'; 
console.log(instance.getValue()); // cat
复制代码

属性遮蔽

改造上面的代码,在 Cat.prototype 上添加 run 方法,可是 Animal.prototype 上也有一个 run 方法,不过它不会被访问到,这种状况称为属性遮蔽 (property shadowing)。

// 木易杨
function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}
Cat.prototype = new Animal(); 
Cat.prototype.constructor = Cat; 

// 新增
Cat.prototype.run = function() {
  return 'cat cat cat';
}

var instance = new Cat();
instance.value = 'cat'; 
console.log(instance.run()); // cat cat cat
复制代码

那如何访问被遮蔽的属性呢?经过 __proto__ 调用原型链上的属性便可。

// 接上
console.log(instance.__proto__.__proto__.run()); // undefined is runing
复制代码

image-20190407162620611

其余继承方案

原型链继承方案有不少问题,实践中不多会单独使用,平常工做中使用 ES6 Class extends(模拟原型)继承方案便可,更多更详细的继承方案能够阅读我以前写的一篇文章,欢迎拍砖。

点击阅读:JavaScript 经常使用八种继承方案

扩展题

有如下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

参考答案:点击查看

小结

  • 每一个对象拥有一个原型对象,经过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这种关系被称为原型链
  • 当访问一个对象的属性 / 方法时,它不只仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。
  • 原型链的构建依赖于 __proto__,一层一层最终连接到 null
  • instanceof 原理就是一层一层查找 __proto__,若是和 constructor.prototype 相等则返回 true,若是一直没有查找成功则返回 false。
  • 原型链继承的本质是重写原型对象,代之以一个新类型的实例

参考

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 做用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总以下,内有优质前端资料,以为不错点个star。

github.com/yygmind/blo…

我是木易杨,公众号「高级前端进阶」做者,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

相关文章
相关标签/搜索