javascript-ES5-继承的实现

ES6的类继承其实是对ES5的较复杂的继承过程的封装。这里讲一下我对ES5继承的实现过程的学习和理解。

如果有问题 , 请大佬指出也可以一起交流。

继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。

前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

原型链

就是实例有一个指针指向其构造函数的原型 ,而原型是对象 , 也就是原型也是构造函数实例化来的 。 那原型也有一个指针指向一个原型 。 如此就构成了实例与原型的一个原型链。

以下代码构成了一条原型链 , 其实最上层的是 Object。

// 最上层构造函数
function up() {
  this.up = 'up'
}
// up 的原型修改
up.prototype.upFun = function() {
  console.log('upFun');
}

// 中间层构造函数
function cen() {
  this.cen = 'cen'
}
// 实例化 cen 原型
cen.prototype = new up()
Object.defineProperty(cen.prototype, 'constructor', { value: cen })
cen.prototype.cenFun = function() {
  console.log('cenFun');
}

// 实例化对象
let down = new cen()

// 是否继承了 up 和 down 的 属性和方法
console.log(down.up); // up
console.log(down.cen); // cen
down.upFun() // upFun
down.cenFun() // cenFun

// 检验原型链是否形成
console.log(down.__proto__ === cen.prototype); // true
console.log(down.__proto__.constructor === cen); // ture
console.log(down.__proto__.__proto__ === up.prototype); // true

// 类型检测
console.log(down instanceof up);  // true
console.log(down instanceof cen);  // true
默认原型

实际上,原型链中还有一环。默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。
在这里插入图片描述

原型与继承关系

原型与实例的关系可以通过两种方式来确定。第一种方式是使用 instanceof 操作符,如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true。

确定这种关系的第二种方式是使用 isPrototypeOf()方法。原型链中的每个原型都可以调用这个方法,如下例所示,只要原型链中包含这个原型,这个方法就返回 true.

原型.isPrototypeOf(实例)

console.log(Object.prototype.isPrototypeOf(up)); // true
console.log(up.prototype.isPrototypeOf(cen.prototype)); // true
console.log(cen.prototype.isPrototypeOf(down));  // true
console.log(Object.prototype.isPrototypeOf(down));  // true
关于方法

子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上。

如果实例增加了新方法或属性 , 那么原型或者是构造函数是无法访问到的

而如果是原型增加了新方法或属性 , 那么构造函数和实例都可以访问到

**如果实例的方法与原型方法 或属性相同名, 那么实例会会覆盖原型的 , 在实例访问时先访问实例自身的属性和方法 ,如果没有再去访问原型 , 最后访问到 NULL了 就undefined **

原型链的问题

原型链虽然是实现继承的强大工具,但它也有问题。主要问题出现在原型中包含引用值的时候。前面在谈到原型的问题时也提到过,原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。

也就是说 : 原型是 Object 的实例 , 而 构造函数是原型里的 一个属性 。 即 constructor 属性 。

盗用构造函数

为了解决原型包含引用值导致的继承问题,一种叫作“盗用构造函数”(constructor stealing)的技术在开发社区流行起来(对象伪装”或“经典继承)

基本思路 : 通过 调用 call() 或 apply() 方法以新创建的对象为上下文执行构造函数

function Fclass() {
  this.arr = [1, 2, 3]
}

function Sclass() {
  Fclass.call(this);
}
let d1 = new Sclass()
let d2 = new Sclass()
console.log(d1.arr); // [ 1, 2, 3 ]
console.log(d2.arr); // [ 1, 2, 3 ]
console.log(d1.arr === d2.arr); // false
传递参数

相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。

function Fclass(v) {
  this.name = v
}

function Sclass(v) {
  Fclass.call(this,v);
}
let d = new Sclass('n')
console.log(d.name)  // n
盗用构造函数的问题

盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。

组合继承

组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。

基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

原型式继承

2006 年,Douglas Crockford 写了一篇文章:《JavaScript 中的原型式继承》(“Prototypal Inheritance in JavaScript”)。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法。他的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}
let shareObj = {
  name: 'shareObj',
  arr: ['a', 'b', 'c']
}
let one = object(shareObj)
let second = object(shareObj)
console.log(one.name === second.name);  // true
one.arr.push('c')
console.log(second.arr);  // [ 'a', 'b', 'c', 'c' ]

// 这样子 one 和 second 都共享了 shareObj 的属性和方法 , 且是同地址的 。 
// 内部对 object() 函数进行封装 , 只需要调用它再传递一个共享对象即可 。 

ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了。

这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与上面的 object()方法效果相同

相当于 实例对象 = Object.crate(原型对象) , 但构造函数是没有的(内部隐藏起来了)

let third = Object.create(shareObj)
console.log(third.name); // shareObj
console.log(third.__proto__ === shareObj);  // true

第二个参数是给新对象的 , 它是一个对象 , 对象里面是描述属性的对象

let fourth = Object.create(shareObj, {
  age: {
    value: 1
  }
})
console.log(fourth.age); // 1
寄生式继承
function createAnother(original) {
  let clone = object(original); // 通过调用函数创建一个新对象 , 不一定是 object()[上面自己封装的一个方法] , 也可以是 Oject.create
  clone.sayHi = function() { // 以某种方式增强这个对象
    console.log("hi");
  };
  clone.arr = [1, 2, 3]
  return clone; // 返回这个对象
}

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"

let anotherPerson1 = createAnother(person);
console.log(anotherPerson1.sayHi === anotherPerson.sayHi); // false
console.log(anotherPerson1.arr === anotherPerson.arr); // false
console.log(anotherPerson1.friends === anotherPerson.friends); // true

有些类似构造函数模式(不依赖原型链) , 其中只要能将传入的对象里的方法和属性传给一个新的对象 , 然后返回新对象 。

也有些类似原型式继承 , 它多个一道工序 。 就是可以在外部传入对象作为共享的数据源(就是原型) , 在内部还添加了一些方法,如代码里的 sayHi() , 它是新的 , 不是共享的 。

寄生式组合继承

组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用(组合的原型模式),另一次是在子类构造函数中调用(组合的盗用构造函数方法)。

这也使父构造函数里的属性和方法会重建两次 。 一次是在子类实例化对象时 , 通过 call或 apply , 使实例化对象能够继承到父类里的属性和方法。

第二次是将子类的原型重写为父类的实例化对象 , 从而继承父类共用的方法等 。 但同时子类的原型也继承了父类构造函数内部的属性和方法。

而解决办法就是 寄生式组合继承

基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

(可以这样子理解 : 就是盗用构函数的方法 , 是为了继承父类里的属性和方法 , 而原型继承是为了继承父类的原型里的方法 , 那么就没有必要为了继承原型而重新调用父类去实例化子类的原型。 那么我只需要使用父类的原型即可 。 )

代码如下 :

// 寄生式继承原型 核心部分
function inheritPrototype(F, S) {
  // F的原型是作为 prototype 的构造函数的原型
  let prototype = Object.create(F.prototype)
    // constructor 指向子类
  prototype.constructor = S;
  // 继承父原型 , S.prototype.__proto__ === F.prototype
  S.prototype = prototype
}

// 创建父类
function Fclass() {
  this.fc = 'fc'
  this.arr = [1, 2, 3]
}
// 创建子类
function Sclass() {
  Fclass.call(this)
  this.sc = 'sc'
}

// 调用原型继承方法
inheritPrototype(Fclass, Sclass)

// 实例化对象
let ns = new Sclass
// 检验是否继承
console.log(ns.fc); // fc
console.log(ns.sc); // sc
// 检验构造函数与原型的关系是否混乱
console.log(Fclass.prototype.constructor === Fclass) // ture
console.log(Sclass.prototype.__proto__ === Fclass.prototype); // true
// 检验 instanceOf 等方法是否有效
console.log(ns instanceof Fclass);  // ture
console.log(Fclass.prototype.isPrototypeOf(Sclass.prototype)); // ture

// 关键在于  ,如何不调用 new Fclass 去实例化原型 , 又可以让子类的原型继承到父类的原型。object() 和 Object.create() 都可以

这个方法效率高原型链仍然保持不变,因此 instanceof 操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无糖的酸奶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值