面向对象编程:继承、封装、多态。

对象的继承:A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。

在经典的面向对象语言中,您可能倾向于定义类对象,然后您可以简单地定义哪些类继承哪些类(参考C++ inheritance里的一些简单的例子),JavaScript使用了另一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承

回顾《再谈javascriptjs原型与原型链及继承相关问题

什么是原型语言

  1. 只有对象,没有类;对象继承对象,而不是类继承类。

  2. “原型对象”是核心概念。原型对象是新对象的模板,它将自身的属性共享给新对象。一个对象不但可以享有自己创建时和运行时定义的属性,而且可以享有原型对象的属性。

  3. 每一个对象都有自己的原型对象,所有对象构成一个树状的层级系统。root节点的顶层对象是一个语言原生的对象,只有它没有原型对象,其他所有对象都直接或间接继承它的属性。

原型语言创建有两个步骤

  1. 使用”原型对象”作为”模板”生成新对象 :这个步骤是必要的,这是每个对象出生的唯一方式。以原型为模板创建对象,这也是”原型”(prototype)的原意。

  2. 初始化内部属性 :这一步骤不是必要的。通俗点说,就是,对”复制品”不满意,我们可以”再加工”,使之获得不同于”模板”的”个性”。

所以在JavaScript的世界里,万物皆对象这个概念从一而终。

JavaScript里面没有类这个概念,es6中class虽然很像类,但实际上只是es5上语法糖而已

function People(name){
  //属性
  this.name  = name || Annie
  //实例方法
  this.sleep=function(){
    console.log(this.name + '正在睡觉')
  }
}
//原型方法
People.prototype.eat = function(food){
  console.log(this.name + '正在吃:' + food);
}

JavaScript的基础方式,首推的就是原型继承

原型链继承

父类的实例作为子类的原型

function Woman(){ 
    this.name= "SubType"; // 子类属性
}
// 如果此处有Woman的原型对象上的方法,由于原型重定向,下面的代码会覆盖此方法
Woman.prototype= new People();// 重写原型对象,代之以一个新类型的实例
// 这里实例化一个 People时, 实际上执行了两步
// 1,新创建的对象复制了父类构造函数内的所有属性及方法
// 2,并将原型 __proto__ 指向了父类的原型对象
Woman.prototype.name = 'haixia';//子原型的属性
Woman.prototype.name = ()=>{};//子原型方法
let womanObj = new Woman();

原型链继承优点

  • 父类新增原型方法/原型属性,子类都能访问到

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例

  • 简单易于实现

原型链继承缺点:

  1. 可以在子类中增加实例属性,如果要新增加原型属性和方法需要在new 父类构造函数的后面

  2. 无法实现多继承

  3. 来自原型对象的所有属性被所有实例共享,子类可以重写父类原型上的方法

  4. 创建子类实例时,不能向父类构造函数中传参数

解释原型重定向: Woman.prototype= new People();

  • 自己开辟的堆内存中没有constructor属性,导致类的原型构造函数缺失(解决:自己手动在堆内存中增加constructor属性)

  • 当原型重定向后,浏览器默认开辟的那个原型堆内存会被释放掉,如果之前已经存储了一些方法或者属性,这些东西都会丢失(所以:内置类的原型不允许重定向到自己开辟的堆内存,因为内置类原型上自带很多属性方法,重定向后都没了,这样是不被允许的)

原型继承经典笔试题

function Parent () {
  this.a = 1;
  this.b = [1, 2, this.a];
  this.c = {demo: 5};
  this.show = function () {
    console.log(this.a + ' ' + this.c.demo + ':' + this.b + '\n');
  };
} function Child () {
  this.a = 2;
  this.change = function () {
    this.b.push(this.a);
    this.a = this.b.length;
    this.c.demo = this.a++;
  };
} Child.prototype = new Parent();
var parent = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.a = 11;
child2.a = 12;
child1.change();
child2.change();
parent.show();
child1.show();
child2.show();

思考原型公用问题

经典继承

function extendObj(obj) {
  if (Object.create) {
    return Object.create(obj)
  } else {
    function F () { } 
    F.prototype = obj; 
    return new F()
  }
}
var obj = { name: "smd", age: 26, sayHi: function () { } }
var newObj = createObj(obj)
/* Extend Function */  
function extend(subClass,superClass){  
    var Func = function(){} ;  
    Func.prototype = superClass.prototype ;  
    subClass.prototype = new Func() ;  
    subClass.prototype.constructor = subClass ;  
} ;

newObj继承了obj的属性和方法,但是同样出现了共享父类中引用类型属性的问题

实例继承(原型式继承)

function Wonman(name){
  let instance = new People();
  instance.name = name || 'wangxiaoxia';
  return instance;
}
let wonmanObj = new Wonman(); // 父类
function People (name) {
  this.colors = ["red", "blue", "green"];
  this.name = name; // 父类属性
}
People.prototype.sayName = function () { // 父类原型方法
  return this.name;
}; /** 第一步 */
// 子类,通过 call 继承父类的实例属性和方法,不能继承原型属性/方法
function Woman (name, subName) {
  People.call(this, name); // 调用 People 的构造函数,并向其传参 
  this.subName = subName;
} /** 第二步 */
// 解决 call 无法继承父类原型属性/方法的问题
// Object.create 方法接受传入一个作为新创建对象的原型的对象,创建一个拥有指定原型和若干个指定属性的对象
// 通过这种方法指定的任何属性都会覆盖原型对象上的同名属性
Woman.prototype = Object.create(People.prototype, {
  constructor: { // 注意指定 Woman.prototype.constructor = Woman
    value: Woman,
    enumerable: false,
    writable: true,
    configurable: true
  },
  run : {
    value: function(){ // override
      People.prototype.run.apply(this, arguments);
      // call super
      // ...
    },
    enumerable: true,
    configurable: true,
    writable: true
  }
}) /** 第三步 */
// 最后:解决 Woman.prototype.constructor === People 的问题
// 这里,在上一步已经指定,这里不需要再操作
//Woman.prototype.constructor = Woman; var instance = new Woman('An', 'sistenAn')

实例继承优点:

  • 不限制调用方式

  • 简单,易实现

实例继承缺点:

  • 不能多次继承

拷贝继承

function Wonman (name) {
  let instance = new People();
  for (var p in instance) {
    Wonman.prototype[p] = instance[p];
  }
  Wonman.prototype.name=name||'Tom'
} let wonmanObj = new Wonman();

特点:

  • 支持多继承

缺点:

  • 效率较低,内存占用高(因为要拷贝父类的属性)

  • 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

对象冒充继承

function Woman(name, age) {
  //3行关键代码 此三行用于获取父类的成员及方法
  //用子类的this去冒充父类的this,实现继承
  //父类People中的this.name、sleep,分别成为了子类的成员、子类的方法
  this.method = People;
  //接收子类的参数 传给父类
  this.method(name);
  //删除父类
  delete this.method;   //此后的this均指子类
  this.age = age;
  this.sayWorld = function() {
    alert(age);
  }
}

因为对象冒充的留下,才有call apply的兴起

借用构造函数继承(伪造对象、经典继承)

复制父类的实例属性给子类

  • 函数只不过是在特定环境中执行代码的对象,所以这里使用 apply/call 来实现。

  • 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Woman(name){
 //继承了People,子类的this传给父类
  People.call(this); //People.call(this,'zhoulujun'); 
  this.name = name || 'andy'
}
let womanObj = new Woman();

通过这种调用,把父类构造函数的this指向为子类实例化对象引用,从而导致父类执行的时候父类里面的属性都会被挂载到子类的实例上去。

但是通过这种方式,父类原型上的东西是没法继承的,因此函数复用也就无从谈起

Woman无法继承Parent的原型对象,并没有真正的实现继承(部分继承)

借用构造函数继承优点:

  1. 解决了子类构造函数向父类构造函数中传递参数

  2. 解决了子类实例共享父类引用属性的问题

  3. 可以实现多继承(call或者apply多个父类)

借用构造函数继承缺点:

  1. 方法都在构造函数中定义,无法复用

  2. 不能继承原型属性/方法,只能继承父类的实例属性和方法

组合式继承

调用父类构造函数,继承父类的属性,通过将父类实例作为子类原型,实现函数复用

function Woman(name,age){
  People.call(this,name,age)
}
Woman.prototype = new People();
Woman.prototype.constructor = Woman;
let wonmanObj = new Woman(ren,27);
wonmanObj.eat();

缺点:

  • 由于调用了两次父类,所以产生了两份实例

    • 第一次是 Woman.prototype = new People()

    • 第二次是 在实例化的时候,People.call(this,name,age)

优点:

  • 函数可以复用

  • 不存在引用属性问题

  • 可以继承属性和方法,并且可以继承原型的属性和方法

寄生组合继承

通过寄生的方式来修复组合式继承的不足,完美的实现继承

function Woman(name,age){
  //继承父类属性
  People.call(this,name,age)
}
//继承父类方法,可以简化为:Woman.prototype = Object.create(People.prototype);
(function(){
  // 创建空类
  function Super (){};
  Super.prototype = People.prototype;
  //父类的实例作为子类的原型
  Woman.prototype = new Super();
})();
//修复构造函数指向问题
Woman.prototype.constructor = Woman;
let womanObj = new Woman();

其实还是有两次执行

es6继承

//class 相当于es5中构造函数
//class中定义方法时,前后不能加function,全部定义在class的protopyte属性中
//class中定义的所有方法是不可枚举的
//class中只能定义方法,不能定义对象,变量等
//class和方法内默认都是严格模式
//es5中constructor为隐式属性
class People{
  constructor(name='wang',age='27'){
    this.name = name;
    this.age = age;
  }
  eat(){
    console.log(`${this.name} ${this.age} eat food`)
  }
}
// 继承父类属性,super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
// 子类必须在constructor方法中调用super方法,否则新建实例时会报错。如果子类没有定义constructor方法,这个方法会被默认添加,不管有没有显式定义,任何一个子类都有constructor方法。
class Woman extends People{ 
   constructor(name = 'ren',age = '27'){ 
     super(name, age); 
   } 
    eat(){ 
     //继承父类方法
      super.eat() 
    } 

let wonmanObj=new Woman('xiaoxiami'); 
wonmanObj.eat();

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。

ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

/**
 * 继承
 * @param {*} subClass 子类
 * @param {*} superClass 父类
 */
function _inherits(subClass, superClass) {
  // 类型检测
  if (!superClass || typeof superClass !== "function" ) {
    throw new TypeError("Super expression must either be null or a function, not " +typeof superClass);
  }
  /**
   * Object.create 接受两个参数
   * 指定原型创建对象
   * @param {*} 目标原型
   * @param {*} 添加属性
   */
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass, // subClass.prototype.constructor 指向 subClass
      enumerable: false, // constructor 不可枚举
      writable: true,
      configurable: true
    }
  });   /**
   * Object.setPrototypeOf 方法
   * 设置子类的 __proto__ 属性指向父类
   * @param {*} 子类
   * @param {*} 父类
   */
  if (superClass) {
    // 设置子类的__proto__ 让 Child 能访问父类静态属性
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass);
  }
}

参考文章:

JavaScript深入之继承的多种方式和优缺点 #16 https://github.com/mqyqingfeng/Blog/issues/16

JavaScript 中的继承 https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Inheritance

JavaScript常见的六种继承方式 https://segmentfault.com/a/1190000016708006

js继承的几种方式 https://zhuanlan.zhihu.com/p/37735247

深入浅出js实现继承的7种方式 https://cloud.tencent.com/developer/article/1536957

前端面试必备之JS继承方式总结 https://www.imooc.com/article/20162

转载本站文章《JavaScript继承的实现方式:原型语言对象继承对象原理剖析》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2015_0520_8494.html

JavaScript继承的实现方式:原型语言对象继承对象原理剖析的更多相关文章

  1. 《JAVASCRIPT高级程序设计》根植于原型链的继承

    继承是面向对象的语言中,一个最为津津乐道并乐此不疲的话题之一.JAVASCRIPT中的继承,主要是依靠原型链来实现的.上一篇文章介绍过,JAVASCRIPT中,每一个对象都有一个prototype属性 ...

  2. Javascript函数、构造函数、原型、类和对象

    函数 函数是JavaScript中特殊的对象,对函数执行typeof运算会返回字符串"function",因为函数也是对象,他们可以拥有属性和方法. 静态方法 函数在JS中定义了类 ...

  3. JavaScript高级内容笔记:原型链、继承、执行上下文、作用域链、闭包

    最近在系统的学习JS深层次内容,并稍微整理了一下,作为备忘和后期复习,这里分享给大家,希望对大家有所帮助.如有错误请留言指正,tks. 了解这些问题,我先一步步来看,先从稍微浅显内容说起,然后引出这些 ...

  4. [转]深入javascript——原型链和继承

    在上一篇post中,介绍了原型的概念,了解到在javascript中构造函数.原型对象.实例三个好基友之间的关系:每一个构造函数都有一个“守护神”——原型对象,原型对象心里面也存着一个构造函数的“位置 ...

  5. js对象的几种创建方式和js实现继承的方式[转]

    一.js对象的创建方式 1. 使用Object构造函数来创建一个对象,下面代码创建了一个person对象,并用两种方式打印出了Name的属性值. var person = new Object(); ...

  6. JS继承的实现方式

    JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一.那么如何在JS中实现继承呢?让我们拭目以待. JS继承的实现方式 既然要实现继承,那么首先我们得有一个父类,代码如下: // 定义一个动物类 ...

  7. Java对象拷贝原理剖析及最佳实践

    作者:宁海翔 1 前言 对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po.Dto.Do.Vo各个表现层数据的转换,也存在于系统交互如序列化.反序列化. Java对象拷贝分为深拷贝和浅拷贝,目前 ...

  8. 详解JavaScript对象继承方式

    一.对象冒充 其原理如下:构造函数使用 this 关键字给所有属性和方法赋值(即采用类声明的构造函数方式).因为构造函数只是一个函数,所以可使 Parent 构造函数成为 Children 的方法,然 ...

  9. JavaScript 的对象继承方式,有几种写法?

    JavaScript 的对象继承方式,有几种写法? 一.对象冒充 其原理如下:构造函数使用 this 关键字给所有属性和方法赋值(即采用类声明的构造函数方式).因为构造函数只是一个函数,所以可使 Pa ...

  10. JavaScript对象继承方式

    一.对象冒充 其原理如下:构造函数使用 this 关键字给所有属性和方法赋值(即采用类声明的构造函数方式).因为构造函数只是一个函数,所以可使 Parent 构造函数 成为 Children 的方法, ...

随机推荐

  1. 使用Kali Linux进行主机发现实验

    主机发现 [实训目的] 掌握主机扫描的工作原理,学会使用ping等扫描工具,发现网络当中活跃的主机. [场景描述] 在虚拟机环境下配置4个虚拟系统"Win XP1" "W ...

  2. 推理(Inference)与预测(Prediction)

    在机器学习的背景下,很多人似乎混淆了这两个术语.这篇文章将试图澄清我们所说的这两个词是什么意思,每一个词在哪里有用,以及它们是如何应用的.在这里,我将举几个例子来直观地理解两者之间的区别. 推理和预测 ...

  3. websocket和ajax的区别(和http的区别)

    websocket和ajax的区别(和http的区别) https://segmentfault.com/a/1190000021741131 1. 本质不同 ajax,即异步JavaScript和X ...

  4. 解决Maven中90%的依赖(导包)问题

    今天给大家分享一个非常好用的技巧,这个技巧是一个组合技巧 是的,自从我开始接触了以spring为框架的项目学习后,这个maven导包老是出现问题,每次在这个上面花费好多时间,于是乎打算写一个秘籍出来. ...

  5. Typora + PicGo 快乐书写 Markdown 文档

    声明 以下提及的图床服务商跟本人无任何商业来往,你可以根据自己的需要选择其他更适合的服务商. 个人观点 这是一个服务付费的时代,相比于自己折腾.在价格适当,服务到位的情况下,我更倾向于选择商业服务.毕 ...

  6. window.onload 触发时机问题

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...

  7. SQL Server 2000 创建角色,登陆用户,安全用户,批量授予权限

    前言 我在2011年刚入门学习的时候,是从 SQL Server 2008 开始学的,再加上这些年较少接触 SQL Server 2000,因此对它不是很熟. 之前都是在 SQL Server 200 ...

  8. GitHub Desktop安装与使用教程

    一.安装 1.下载 下载地址 2.安装 下载之后GitHub Desktop是没有安装步骤的,而是开始安装之后,稍等片刻就安装成功了. 然后登陆个人GitHub账号就可以进行一下操作了. 二.新建仓库 ...

  9. K8s容器debug高级技巧

    使用 kubectl exec 执行指令 如果您在 Kubernetes 上运行软件,您会想要在某些时候去调试您所部署的软件的一些方面.对于习惯于使用虚拟机 (VMs) 的人来说能自然使用的一种简单的 ...

  10. Javascript Ajax总结——FormData类型

    XMLHttpRequest1级只是把已有的XHR对象的实现细节描述出来.XMLHttpRequest2级进一步发展了XHR.FormData类型FormData类型,为序列化表单以及创建以表单格式相 ...