前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【前端词典】继承

【前端词典】继承

作者头像
小生方勤
发布2019-06-01 11:00:45
5980
发布2019-06-01 11:00:45
举报
文章被收录于专栏:前端词典

前言

继承于我们前端来说绝对是非常熟悉也必须熟悉的一个高频必懂知识点。熟悉到只要是面试一定会有关于继承的问题;而且源码中继承的使用也随处可见。

可依旧有很多前端对继承的实现和应用没有一个整体的把握。追其原因无非有二:

  1. ECMAScript 继承的实现方法区别于其他基于类的实现继承的面向对象(Object Oriented)语言。
  2. 工作中即使对如何实现继承一知半解,也一点都不耽误写逻辑代码。

无论由于哪一个原因,建议请尽快弄懂继承的实现和应用,否则你可能会如同你的表情包一样——流下了没有技术的泪水。

接下来我会尽我所能讲清楚继承这个概念,并结合相关经典图文做辅助解释。

在讲 ECMAScript 继承的概念之前,我先说下原型的概念。

类与原型

讲 ECMAScript 继承的概念之前,我先说下类的概念。(如果接触过 Java 或者是 C++ 的话,我们就知道 Java(C++)的继承都是基于类的继承)。

类: 是面向对象(Object Oriented)语言实现信息封装的基础,称为类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。 类: 是描述了一种代码的组织结构形式,一种在软件中对真实世界中问题领域的建模方法。

类的概念这里我就不再扩展,感兴趣的同学可以自行查阅书籍。接下来我们重点讲讲原型以及原型链

原型

JavaScript 这门语言没有类的概念,所以 JavaScript 并非是基于类的继承,而是基于原型的继承。(主要是借鉴 Self 语言原型( prototype)继承机制)。

代码语言:javascript
复制
注意:ES6 中的 class 关键字和 OO 语言中的类的概念是不同的,下面我会讲到。ES6 的 class 其内部同样是基于原型实现的继承。

JavaScript 摒弃转而使用原型作为实现继承的基础,是因为基于原型的继承相比基于的继承上在概念上更为简单。首先我们明确一点,存在的目的是为了实例化对象,而 JavaScript 可以直接通过对象字面量语法轻松的创建对象。

每一个函数,都有一个 prototype 属性。 所有通过函数 new 出来的对象,这个对象都有一个 __proto__ 指向这个函数的 prototype。 当你想要使用一个对象(或者一个数组)的某个功能时:如果该对象本身具有这个功能,则直接使用;如果该对象本身没有这个功能,则去 __proto__ 中找。

1. prototype [显式原型]

prototype 是一个显式的原型属性,只有函数才拥有该属性。 每一个函数在创建之后都会拥有一个名为 prototype 的属性,这个属性指向函数的原型对象。( 通过 Function.prototype.bind 方法构造出来的函数是个例外,它没有 prototype 属性 )。

prototype 是一个指针,指向的是一个对象。比如 Array.prototype 指向的就是 Array 这个函数的原型对象。

在控制台中打印 console.log(Array.prototype) 里面有很多方法。这些方法都以事先内置在 JavaScript 中,直接调用即可。上面我标红了两个特别的属性 constructor__proto__。这两个属性接下来我都会讲。

我们现在写一个 functionnoWork(){} 函数。

当我写了一个 noWork 这个方法的时候,它自动创建了一个 prototype 指针属性(指向原型对象)。而这个被指向的原型对象自动获得了一个 constructor (构造函数)。细心的同学一定发现了: constructor 指向的是 noWork

代码语言:javascript
复制
noWork.prototype.constructor === noWork     // true
 // 一个函数的原型对象的构造函数是这个函数本身
代码语言:javascript
复制
tips: 图中打印的 Array 的显式原型对象中的这些方法你都知道吗?要知道数组也是非常重要的一部分哦 ~  咳咳咳,这是考试重点。
2. __proto__[隐式原型]

prototype 理解起来不难, __proto__ 理解起来就会比 prototype 稍微复杂一点。不过当你理解的时候你会发现,这个过程真的很有趣。下面我们就讲讲 __proto__

代码语言:javascript
复制
其实这个属性指向了 `[[prototype]]`,但是 `[[prototype]]`  是内部属性,我们并不能访问到,所以使用 `__proto__` 来访问。

我先给个有点绕的定义:

__proto__ 指向了创建该对象的构造函数的显式原型。

我们现在还是使用 noWork 这个例子来说。我们发现 noWork 原型对象中还有另一个属性 __proto__

我们先打印这个属性:

我们发现这个 __proto__ 指向的是 Object.prototype

我听到有人在问为什么?

  1. 因为这个 __proto__.constructor 指向的是 Object
  2. 我们知道:一个函数的原型对象的构造函数是这个函数本身
  3. 所以这个 __proto__.constructor 指向的是 Object.prototype.constructor
  4. 进而 __proto__ 指向的是 Object.prototype
代码语言:javascript
复制
至于为什么是指向 Object? 因为所有的引用类型默认都是继承 Object 。

作用

  1. 显式原型:用来实现基于原型的继承与属性的共享。
  2. 隐式原型:构成原型链,同样用于实现基于原型的继承。 举个例子,当我们使用 noWork 这个对象中的 toString() 属性时,在 noWork 中找不到,就会沿着 __proto__ 依次查找。
3. new 操作符

当我们使用 new 操作符时,生成的实例对象拥有了 __proto__属性。即在 new 的过程中,新对象被添加了 __proto__ 并且链接到构造函数的原型上。

new 的过程

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

Function.__proto__===Function.prototype

难道这代表着 Function 自己产生了自己? 要说明这个问题我们先从 Object 说起。

我们知道所有对象都可以通过原型链最终找到 Object.prototype ,虽然 Object.prototype 也是一个对象,但是这个对象却不是 Object 创造的,而是引擎自己创建了 Object.prototype 所以可以这样说:

所有实例都是对象,但是对象不一定都是实例。

接下来我们来看 Function.prototype 这个特殊的对象:

打印这个对象,会发现这个对象其实是一个函数。我们知道函数都是通过 newFunction() 生成的,难道 Function.prototype 也是通过 newFunction() 产生的吗?这个函数也是引擎自己创建的。

首先引擎创建了 Object.prototype ,然后创建了 Function.prototype ,并且通过 __proto__ 将两者联系了起来。

这就是为什么 Function.prototype.bind() 没有 prototype 属性。因为 Function.prototype 是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype 属性。

对于为什么 Function.__proto__ 会等于 Function.prototype ? 我看到的一个解释是这样的: 其他所有的构造函数都可以通过原型链找到 Function.prototype ,并且 functionFunction() 本质也是一个函数,为了不产生混乱就将 functionFunction()__proto__ 联系到了 Function.prototype 上。

继承的几种方式

JavaScript继承是非常重要的一个概念。我们有必要去了解,请大家多指教。

目的:简化代码逻辑和结构,实现代码重用

接下来我们一起学习下 8 种 JavaScript 实现继承的方法。

推荐组合继承(四)、寄生组合式继承(七)、ES6 继承(八)

一、原型链法(使用原型)

基本思想是利用原型让一个引用类型继承另一个引用类型的方法和实例。

代码如下
代码语言:javascript
复制
function staff(){   this.company = 'ABC';}staff.prototype.companyName = function(){  return this.company; }function employee(name,profession){  this.employeeName = name;  this.profession = profession;}// 继承 staffemployee.prototype = new staff();// 将这个对象的 constructor 手动改成 employee,否则还会是 staffemployee.prototype.constructor = employee;// 不使用对象字面量方式创建原型方法,会重写原型链employee.prototype.showInfo = function(){  return this.employeeName + "'s profession is " + this.profession;}let instance = new employee('Andy','front-end');
// 测试 console.log(instance.companyName()); // ABCconsole.log(instance.showInfo());    // "Andy's profession is front-end"// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instance.hasOwnProperty('employeeName'))     // trueconsole.log(instance.hasOwnProperty('company'))          // false// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(employee.prototype.isPrototypeOf(instance)); // trueconsole.log(staff.prototype.isPrototypeOf(instance));    // trueconsole.log(Object.prototype.isPrototypeOf(instance));   // true
存在的问题

原型链实现继承最大的问题是:

当原型中存在引用类型值时,实例可以修改其值。

代码语言:javascript
复制
function staff(){   this.test = [1,2,3,4];}function employee(name,profession){  this.employeeName = name;  this.profession = profession;}employee.prototype = new staff();let instanceOne = new employee();let instanceTwo = new employee();instanceOne.test.push(5);console.log(instanceTwo.test); // [1, 2, 3, 4, 5]

鉴于此问题:所以我们在实践中会少单独使用原型链实现继承。

小结
  1. 基于构造函数和原型链
  2. 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
  3. 通过 isPrototypeOf() 方法来确定原型和实例的关系
  4. 在实例中可以修改原型中引用类型的值

二.仅继承父构造函数的原型对象

此方法和方法一区别就是将:

代码语言:javascript
复制
employee.prototype = new staff();

改成:

代码语言:javascript
复制
Employee.prototype = Person.prototype;
优点
  1. 构建继承关系时不需要新建对象实例
  2. 由于公用一个原型对象,所以在访问对象的时候不需要遍历原型链,效率自然就高
缺点
  1. 和方法一相同,子对象的修改会影响父对象。
小结
  1. 基于构造函数,没有使用原型链
  2. 子对象和父对象公用一个原型对象

三、借用构造函数法

此方法可以解决原型中引用类型值被修改的问题

代码语言:javascript
复制
function staff(){   this.test = [1,2,3];}staff.prototype.companyName = function(){  return this.company; }function employee(name,profession){  staff.call(this);      this.employeeName = name;  this.profession = profession;}// 不使用对象字面量方式创建原型方法,会重写原型链employee.prototype.showInfo = function(){  return this.employeeName + "'s profession is " + this.profession;}let instanceOne = new employee('Andy','front-end');let instanceTwo = new employee('Mick','after-end');instanceOne.test.push(4);// 测试 console.log(instanceTwo.test);    // [1,2,3]// console.log(instanceOne.companyName()); // 报错// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instanceOne.hasOwnProperty('test'))          // true// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(staff.prototype.isPrototypeOf(instanceOne));    // false

从上面的结果可以看出:

  1. 借用构造函数法可以解决原型中引用类型值被修改的问题
  2. 可是 instanceOnestaff 已经没有原型链的关系了
缺点
  1. 只能继承父对象的实例属性和方法,不能继承父对象原型属性和方法
  2. 无法实现函数复用,每个子对象都有父对象实例的副本,性能欠优

四、组合继承(推荐)

指的是将原型链技术和借用构造函数技术结合起来,二者皆取其长处的一种经典继承方式。

代码语言:javascript
复制
function staff(){   this.company = "ABC";      this.test = [1,2,3];}staff.prototype.companyName = function(){  return this.company; }function employee(name,profession){  // 继承属性  staff.call(this);      this.employeeName = name;  this.profession = profession;}// 继承方法employee.prototype = new staff();employee.prototype.constructor = employee;employee.prototype.showInfo = function(){  return this.employeeName + "'s profession is " + this.profession;}
let instanceOne = new employee('Andy','front-end');let instanceTwo = new employee('Mick','after-end');instanceOne.test.push(4);// 测试 console.log(instanceTwo.test);    // [1,2,3]console.log(instanceOne.companyName()); // ABC// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instanceOne.hasOwnProperty('test'))          // true// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(staff.prototype.isPrototypeOf(instanceOne));    // true
优点
  1. 可以复用原型上定义的方法
  2. 可以保证每个函数有自己的属性,可以解决原型中引用类型值被修改的问题
缺点
  1. staff 会被调用 2 次:第 1 次是 employee.prototype=newstaff();,第 2 次是调用 staff.call(this)

五、原型式继承 - Object.create()

利用一个临时性的构造函数(空对象)作为中介,将某个对象直接赋值给构造函数的原型。

代码语言:javascript
复制
function object(obj){  function F(){}  F.prototype = obj;  return new F();}

本质上 object() 对传入其中的对象执行了一次浅复制,将构造函数 F 的原型直接指向传入的对象。

代码语言:javascript
复制
var employee = {  test: [1,2,3]}
let instanceOne = object(employee);let instanceTwo = object(employee);// 测试 instanceOne.test.push(4);console.log(instanceTwo.test); // [1, 2, 3, 4]
缺点
  1. 原型中引用类型值会被修改
  2. 无法传递参数

另,ES5 中存在 Object.create() 的方法规范化了原型式继承,能够代替 object 方法。

六、寄生式继承

要点:在原型式继承的基础上,通过封装继承过程的函数增强对象,返回对象

代码语言:javascript
复制
function createAnother(original){  var clone = object(original); // 通过调用 object() 函数创建一个新对象  clone.sayHi = function(){  // 以某种方式来增强对象    alert("hi");  };  return clone; // 返回这个对象}

createAnother 函数的主要作用是为构造函数新增属性和方法,以增强函数。

缺点(同原型式继承):
  1. 原型中引用类型值会被修改
  2. 无法传递参数

七、寄生组合式继承(推荐)

该方法主要是解决组合继承调用两次超类构造函数的问题。

代码语言:javascript
复制
function inheritPrototype(sub, super){  var prototype = Object.create(super.prototype); // 创建对象,父原型的副本  prototype.constructor = sub;                    // 增强对象  sub.prototype = prototype;                      // 指定对象,赋给子的原型}
function staff(){   this.company = "ABC";      this.test = [1,2,3];}staff.prototype.companyName = function(){  return this.company; }function employee(name,profession){  staff.call(this, name);  this.employeeName = name;  this.profession = profession;}
// 将父类原型指向子类inheritPrototype(employee,staff)let instanceOne = new employee("Andy", "A");let instanceTwo = new employee("Rose", "B");instanceOne.test.push(4);// 测试 console.log(instanceTwo.test);            // [1,2,3]console.log(instanceOne.companyName());   // ABC// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instanceOne.hasOwnProperty('test'))           // true// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(staff.prototype.isPrototypeOf(instanceOne));  // true
代码语言:javascript
复制
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式,

八、Class 的继承(推荐)

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

代码语言:javascript
复制
class staff {   constructor(){    this.company = "ABC";        this.test = [1,2,3];  }  companyName(){    return this.company;   }}class employee extends staff {  constructor(name,profession){    super();    this.employeeName = name;    this.profession = profession;  }}
// 将父类原型指向子类let instanceOne = new employee("Andy", "A");let instanceTwo = new employee("Rose", "B");instanceOne.test.push(4);// 测试 console.log(instanceTwo.test);    // [1,2,3]console.log(instanceOne.companyName()); // ABC// 通过 Object.getPrototypeOf() 方法可以用来从子类上获取父类console.log(Object.getPrototypeOf(employee) === staff)// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性console.log(instanceOne.hasOwnProperty('test'))          // true// 通过 isPrototypeOf() 方法来确定原型和实例的关系console.log(staff.prototype.isPrototypeOf(instanceOne));    // true

super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。

  1. 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。
  2. 只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。
代码语言:javascript
复制
`super` 虽然代表了父类 `A` 的构造函数,但是返回的是子类 `B` 的实例,即` super` 内部的 `this ` 指的是 `B`,因此 `super()` 在这里相当于
A.prototype.constructor.call(this)
ES5 和 ES6 实现继承的区别

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

extends 继承核心代码(寄生组合式继承)
代码语言:javascript
复制
function _inherits(subType, superType) {  subType.prototype = Object.create(superType && superType.prototype, {    constructor: {      value: subType,      enumerable: false,      writable: true,      configurable: true    }  });  if (superType) {    Object.setPrototypeOf     ? Object.setPrototypeOf(subType, superType)     : subType.__proto__ = superType;  }}

由此可以看出:

  1. 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
  2. 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。
代码语言:javascript
复制
另:ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。

以上八种继承方式是比较常见的继承方式,倘若了解了这些方式的机制,在以后的面试中原型链与继承的问题也就不在话下了。

参考

  1. 《JavaScript 高级程序设计》
  2. http://es6.ruanyifeng.com/#docs/class-extends
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小生方勤 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 类与原型
      • 原型
        • 1. prototype [显式原型]
        • 2. __proto__[隐式原型]
        • 3. new 操作符
    • 继承的几种方式
      • 一、原型链法(使用原型)
        • 代码如下
        • 存在的问题
        • 小结
      • 二.仅继承父构造函数的原型对象
        • 优点
        • 缺点
        • 小结
      • 三、借用构造函数法
        • 缺点
      • 四、组合继承(推荐)
        • 优点
        • 缺点
      • 五、原型式继承 - Object.create()
        • 缺点
      • 六、寄生式继承
        • 缺点(同原型式继承):
      • 七、寄生组合式继承(推荐)
        • 八、Class 的继承(推荐)
          • ES5 和 ES6 实现继承的区别
          • extends 继承核心代码(寄生组合式继承)
      • 参考
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档