1. 首页

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

引言

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

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

 js继承,各种继承的优缺点(原型链继承,组合继承,寄生组合继承). 2016-05-24 ... 缺点:两次调用父类构造函数

原型链

第一次是在创建子类原型的时候,第二次是在子类构造函数内部. 子类继承父类的 ...关于js的六种继承方式及其优缺点

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

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

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

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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);

整理《javascript高级程序设计》中继承的方法以及优缺点

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

比如调用 foo.valueOf() 会发生什么?

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

原型链继承和原型式继承有一样的优缺点,构造函数继承与寄生式

prototype__proto__

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

这次我们再深入一点,原型链的构建是依赖于 prototype 还是 proto 呢?

优点:解决了原型链继承和构造函数继承的缺点缺点:调用了两次Fu的构造 ... JS继承实现的几种方式及其优缺点

https://kenneth-kin-lum.blogspot.com/2012/10/javascripts-pseudo-classical.html
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 的原型链上。

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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

面向对象的JavaScript 编程,指向ClassA 的实例,其原型链如图1 所示,这样ClassB 便拥有 ... 因此可以结合二者的优点,采用混合的方式模拟继承

instanceof 原理及实现

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

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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,代码如下。

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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 原型上的属性和方法。

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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

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

  • 1、多个实例对引用类型的操作会被篡改
  • 2、子类型的原型上的 constructor 属性被重写了
  • 3、给子类型原型添加属性和方法必须在替换原型之后
  • 4、创建子类型实例时无法向父类型的构造函数传参

问题 1

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

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

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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; 

JavaScript 七大继承全解析

问题 3

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

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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)。

// JS中文网 – 前端进阶资源分享 www.javascriptc.com
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

继承作为基本功和面试必考点,必须要熟练掌握才行。小公司可能仅让你手写继承(一般写 寄生组合式继承 即可),大厂就得要求你全面分析各个继承的优缺点了。这篇文章深入浅出,让你全面了解 JavaScript 继承及其优缺点,以在寒冬中立于不败之地

其他继承方案

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

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

扩展题

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

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

参考答案:点击查看

小结

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

参考

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,是给开发者用的 Hacker News,技术文章由为你筛选出最优质的干货 www.javascriptc.com

往期目录:

  1. JS深入之理解JavaScript 中的执行上下文和执行栈
  2. JS深入之执行上下文栈和变量对象
  3. JS深入之执行上下文栈和变量对象
  4. JS深入之带你走进内存机制
  5. JS深入之4类常见内存泄漏及如何避免
  6. JS深入浅出图解作用域链和闭包
  7. JS深入之从作用域链理解闭包
  8. JS深入之闭包面试题解
  9. JS深入之5种this绑定全面解析
  10. JS深入之重新认识箭头函数的this
  11. JS深入之深度解析 call 和 apply 原理
  12. JS深入之深度解析bind原理、使用场景及模拟实现
  13. JS深入之深度解析 new 原理及模拟实现
  14. JS深入之详细解析赋值、浅拷贝和深拷贝的区别
  15. JS深入之Object.assign 原理及其实现
  16. JS深入之面试题之如何实现一个深拷贝
  17. JS深入之Lodash是如何实现深拷贝的
  18. JS深入之重新认识构造函数、原型和原型链

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文来源于网络,其版权属原作者所有,如有侵权,请与小编联系,谢谢!

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:【进阶19期】图解原型链及其继承优缺点

链接:https://www.javascriptc.com/465.html

原文链接:https://github.com/yygmind/blog/issues/34

« Js中文网周刊第01期
【进阶18期】重新认识构造函数、原型和原型链»
Flutter 中文教程资源

相关推荐

QR code