ES5 继承

SegmentFault

共 6890字,需浏览 14分钟

 ·

2020-10-16 00:05

作者:assassin cike

来源:SegmentFault 思否社区




首先要明白两点:


一、非方法属性每个子类实例需要独立
二、方法属性每个子类实例需要共享


为什么?


如果非方法属性为引用类型,且非方法属性共享,在一个实例中改变,其他实例中就会做出改变,这样每个实例就会相互影响,而方法属性一般是不需要进行改变的,只是对方法调用。


  • 方法跟属性分别可以定义在构造函数内部跟prototype上。

  • 继承的目的是子类继承父类的方法跟属性。

  • 代码主要来自于红宝书4




基于原型链的继承


每个函数都有个prototype属性,每个对象都有__proto__属性(在chrome中表现如此,prototype也是如此) 如图,属性的查找会从当前层级依次向原型链上查找,直到查找到原型链的顶端null,具体可参考:

js proto(https://www.jianshu.com/p/cd26f07df9ba)




既然属性的查找是按照原型链向上查找的,且继承就是继承父类的属性跟方法,那么就可以利用这个特性,进行继承。


function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property;
};
function SubType() {
  this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true 可以正确调用父类的方法,拿到父类的属性


原型虽然实现了继承,但是还是有缺点的


劣势:


1. 子类或者父类的属性为引用类型时,改变一个实例的引用类型属性,其他实例的该引用类型属性也会发生改变,这样其实例就会相互污染了。


function SuperType() {
  this.colors = ["red""blue""green"];
}

function SubType() {} // 继承SuperType
SubType.prototype = new SuperType();

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black";

let instance2 = new SubType();
console.log(instance2.colors);
// "red,blue,green,black";


为什么非方法属性不写在prototype上?


因为prototype上的属性的共享的,在一个实例上改了该属性,其他实例的该属性也会被改掉。


为什么方法不写在构造函数内部?


  • 方法写在子类内部:每次实例化构造函数,方法都是新的;方法只是用来调用,不需要修改,所以实例共享就行了。

  • 方法写在父类内部:不同的子类继承父类都需要实例化父类;方法只是用来调用,不需要做修改,所以实例共享就行了,包括子类实例。如果子类需要修改父类方法,直接在子类中定义相同方法名,进行覆盖就行了。


2. 子类在实例化时不能给父类的构造函数传参,因为父类的实例化是在前面,而不是构造函数调用的时候。




盗用构造函数


为了解决父类中属性为引用类型导致子类实例化后,引用属性共享的问题,跟父类构造函数无法传参的问题。引入了“盗用构造函数“方式实现继承。思路是在子类构造函数中调用父类构造函数。


  • 不同实例的引用属性不会相互影响

function SuperType() {
  this.colors = ["red""blue""green"];
}
function SubType() {
  // 继承SuperType
  SuperType.call(this);
}

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black";

let instance2 = new SubType();
console.log(instance2.colors);
// "red,blue,green";

instance1 instance2两个实例就不会相互影响。

  • 可以为父类构造函数传参

function SuperType(name) {
  this.name = name;
}
function SubType(name) {
  // 继承SuperType并传参
  SuperType.call(this, name);
  // 实例属性
  this.age = 29;
}
let instance = new SubType("geek");
console.log(instance.name); // "geek";
console.log(instance.age); // 29

动态传递参数到父类构造函数

劣势:

  • 定义在父类prototype上的方法,子类无法继承

function SuperType(name) {
  this.name = name;
}
SuperType.prototype.say = function () {
  console.info("hello");
};

function SubType(name) {
  // 继承SuperType并传参
  SuperType.call(this, name);
  // 实例属性
  this.age = 29;
}

let instance = new SubType("geek");
console.log(instance.name); // "geek";
console.log(instance.age); // 29
instance.say() // 获取不到该函数

通过 new 实例化后,实例才能拿到prototype上的方法,a.__proto__===Animal.prototype,所以instance.say()不存在

  • 定义在父类构造函数中方法无法共享

每次实例化子类,都会调用父类构造函数,其内部定义的方法都是新的,占用了不必要的内存,没有实现方法的共享。



组合继承


组合继承兼顾原型链继承跟盗用构造函数的优点,这样既可以把方法定义在原型上以实现重用,又可以看让每个实力都有自己的属性。

function SuperType(name) {
  this.name = name;
  this.colors = ["red""blue""green"];
}
SuperType.prototype.sayName = function () {
  console.log(this.name);
};
function SubType(name, age) {
  // 继承属性,绑定上下文为SubType的实例
  SuperType.call(this, name);
  this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
  console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29

let instance2 = new SubType("Greg", 27);
console.log(instance2.colors);
// "red,blue,green";
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27

  • 可以传递参数到父类构造函数

  • 两个实例中的引用类型不会相互影响

  • 实例可以调用父类的方法,且实现方法的共享

  • 组合继承也保留了 instanceof 操作符和isPrototypeOf() 方法识别合成对象的能力。






劣势:

SuperType会被调用两次,SubType实例跟原型链上都有name跟colors属性。



原型式继承


不定义构造函数通过原型实现对象之前的继承。

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

返回新对象,让其原型指向O

let person = {
  name: "Nicholas",
  friends: ["Shelby""Court""Van"],
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);
// "Shelby,Court,Van,Rob,Barbie";

父对象中的引用属性会在子对象中共享,导致相互污染。

ES5引入了Object.create()规范了原型式继承。

let person = {
  name: "Nicholas",
  friends: ["Shelby""Court""Van"],
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);
// "Shelby,Court,Van,Rob,Barbi

使用Object.create后anotherPerson.__proto__===person成立,所以anotherPerson可以拿到person的属性,但是同样存在父对象属性共享的问题,改了父对象的属性,其他的子对象都跟着改变。

劣势:

父对象的引用类型会在实例中共享,这样就会相互污染。



寄生式继承


寄生式继承跟原型式继承很类似,用工厂函数在对返回的新对象包一层,给新对象赋值一些属性

工厂函数的定义:

function createAnother(original) {
  let clone = object(original);
  // 通过调用函数创建一个新对象;

  clone.sayHi = function () {
    // 以某种方式增强这个对象;
    console.log("hi");
  };
  return clone; // 返回这个对象
}

使用:

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

定义在新对象上的sayHi方法,每次调用新对象都是新的,无法实现共享。

劣势:

  • 父对象的引用类型会在实例中共享,这样就会相互污染。

  • 方法无法实现共享





寄生式组合继承


上面提到,组合继承的缺点就是父类构造函数会被调用两次,一次是在子类的构造函数中,另一次在创建子类原型时调用。继承就是要继承父类的属性跟方法,组合继承实现了这个目标,但是怎么避免重复调用父类构造函数。

先看下组合继承:

function SuperType(name) {
  this.name = name;
  this.colors = ["red""blue""green"];
}

SuperType.prototype.sayName = function () {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name); // 第二次调用,将父类的属性绑定到子类的实例中
  SuperType();
  this.age = age;
}

SubType.prototype = new SuperType();
// 第一次调用SuperType();

SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
  console.log(this.age);
};




由图可见s的原型链上依然有name跟colors属性。这也是不需要的。怎么解决这两个问题?

父类的属性是需要的,父类的原型上的方法是需要的,重复的父类属性不需要,由上图可见重复的父类属性是由于实例化父类给子类原型造成的,我们不去实例化父类,而是将父类的原型传递给子类的原型就行了,结合原型式继承特点可以做到

function SuperType(name) {
  this.name = name;
  this.colors = ["red""blue""green"];
}

SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name); // 将父类的属性绑定到SubType实例中
  this.age = age;
}

SubType.prototype = Object.create(SuperType.prototype);
// 将子类的prototype关联到父类的prototype上

SubType.prototype.sayAge = function () {
  console.log(this.age);
};

使用Object.create解决了父类构造函数调用两次,父类属性重复的问题,但是子类constructor并没有出现在原型链中



下面做出改造:

SubType.prototype = Object.create(SuperType.prototype, {
  constructor: {
    value: SubType, // 修正 constructor 指向
    writable: true,
    configurable: true,
  },
});


SuperType的constructor出现了,其实constructor并没什么用,只是个约定罢了,参考贺老的解释JavaScript 中对象的 constructor 属性的作用是什么?(https://www.zhihu.com/question/19951896/answer/13457869)

instanceof操作符和 isPrototypeOf() 方法正常有效。寄生式组合继承可以
算是引用类型继承的最佳模式



点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流。

- END -

浏览 17
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报