JS数据类型之Object(三)继承

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

  • 接口继承:只继承方法的签名。
  • 实现继承:继承实际的方法。
    js中用到的是实现继承,主要是通过原型链实现。

原型链:【了解】

  • 原型:所有的JavaScript对象至少继承于一个对象,被继承的对象被称为原型。
  • 原型链:多个原型之间有继承关系,组成的链条叫做原型链。
  • 构造函数、原型、实例的关系:每个构造函数都有一个原型对象prototype,原型对象上有一个属性指回构造函数constructor,实例有一个内部指针指向原型__proto__

获取对象原型

方法一:Object.getPrototypeOf()方法返回指定对象的原型。
方法二:实例化对象.__proto__

Object.getPrototypeOf(object)

  • 参数
    obj:要返回其原型的对象
  • 返回值
    给定对象的原型,如果没有继承属性,则返回null

设置或修改对象原型

方法一:Object.create()创建对象的时候指定原型.
Object.create(proto, propertiesObject):

  • 参数:
    proto:新创建对象的原型对象
    propertiesObject:可选
  • 返回值
    一个新对象,带着指定的原型对象的属性。

方法二:Objcet.setPrototypeOf()
Objcet.setPrototypeOf(obj, prototype)

  • 参数:
    obj:设置其原型对象
    prototype:对象的新原型(对象或null)
  • 过程:
    如果对象的原型被修改成不可扩展(通过Object.isExtensible())的,则抛出TypeError异常。
    如果prototype新原型参数不是对象或努力了,则什么也不做。
    否则,该方法修改对象的原型。
    通过此方法设置原型比较消耗资源。

方法三:Reflect.setPrototypeOf()
Reflect.setPrototypeOf(target, prototype)

  • 参数:
    target:设置其原型对象
    prototype:对象的新原型(对象或null)
  • 返回值:
    返回一个boolean类型表明是否原型已经设置成功。
  • 过程:
    如果参数target不是Object,或者prototype既不是对象,也不是null,则抛出TypeError异常。

Object原型与属性相关的方法

方法 作用 参数 返回值
obj.hasOwnProperty(prop) 判断属性是否在某个对象自身上 {不含原型链} prop:要检测的属性 true:有
false:无
prop in obj 判断属性是否在某个对象上 {含原型链} true:有
false:无
Object.getPrototypeOf(obj) 获取实例obj的原型对象 obj:实例化对象 返回原型对象
prototypeObj.isPrototypeOf(obj) 测试一个对象是否在另一个对象的原型链上 object:在该对象的原型链上搜寻 true:在
false:不
instanceof操作符 检测构造函数的prototype属性是否会出现在某个实例对象的原型链上 操作符,不是函数 true:在
false:不在
obj.propertyIsEnumerable(prop) 判断属性名是否可枚举 prop需要测试的属性 true:枚举
false:非枚举

原型链

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

// 在原型上添加方法
SuperType.prototype.getProperty = function() {
  return this.property;
}

function SubType() {
  this.subproperty = false;
}

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

SubType.prototype.getSubValue = function() {
  return this.subproperty;
}

let a = new SubType();

console.log(a.getProperty()); // true
以上代码的原型链.png
  • 默认原型:默认情况下,所有引用类型都继承自Object,这个是通过原型链实现的。
  • 原型与继承的关系:
    1、通过instanceof操作符进行判断实例的原型链上是否有相应的构造函数。
    2、调用原型中的提供的方法isPrototypeOf()方法,判断该原型是否是此实例的原型。
    3、操作原型时,必须在原型赋值之后。

原型和原型链的缺点:
1、原型或原型链中包含引用类型的值时会在全部实例中共享,任意一个实例操作共享属性时,会影响其他属性。
2、子类型实例化的时候,不能给父类传递参数


盗用构造函数【了解】

原理:在子类构造函数中使用apply()、call()调用父类的构造函数。解决原型链中的问题。

示例

function SuperType(name) {
  this.name = name;
  this.colors = ["blue", "yellow", "white"];
}

function SubType(name) {
  SuperType.call(this, name)
  this.age = 18;
}

let example1 = new SubType("张三");
example1.colors.push("red")

let example2 = new SubType("李四");

console.log(example1.name) // 张三
console.log(example1.colors) //  ["blue", "yellow", "white", "red"]
console.log(example2.name) // 李四
console.log(example2.colors) //  ["blue", "yellow", "white"]

SuperType.call(this, name)盗用构造函数的实现。


盗用构造函数的缺点:
1、和构造函数类型,必须在构造函数中定义方法,且函数不能重用。
2、创建的对象没有原型链,子类也不能访问父类原型上定义的方法。


组合继承【佳】

思想:

将原型链和盗用构造函数两个的优点进行结合:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。

示例

function SuperType(name) {
  this.name = name;
  this.colors = ["blue", "yellow", "white"];
}

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

function SubType(name, age) {
  // 通过盗用改造函数继承属性
  SuperType.call(this, name)
  this.age = age;
}

// 通过原型链继承方法
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function() {
  console.log(this.age)
}
Object.defineProperty(SubType.prototype, "constructor", {
  value: SubType,
  enumerable: false,
})

let example1 = new SubType("张三", 18);
example1.colors.push("red")
console.log(example1.colors); // ["blue", "yellow", "white", "red"]
example1.sayName(); // 张三
example1.sayAge(); // 18
console.log(example1 instanceof SuperType) // true

let example2 = new SubType("李四", 19);
console.log(example2.colors); // ["blue", "yellow", "white"]
example2.sayName(); // 李四
example2.sayAge(); // 19
console.log(SuperType.prototype.isPrototypeOf(example2)) // true

组合继承的原型链如下:


组合继承的原型链.png

组合继承的优点:

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

组合继承的缺点:效率的问题。父类的构造函数始终会调用两次:一次是创建子类原型时[原型链],一次是调用子类构造函数时[盗用构造函数]。


原型式继承【了解】

思想

创建临时的构造函数,把传入的对象赋值给临时构造函数的原型,然后返回这个临时构造函数创建的对象。此过程中相当于对传入的对象进行一次浅复制

示例

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

let person = {
  name: "张三",
  age: 18,
  friends: ["1", "2", "3", "4"]
}

let person2 = object(person);
person2.name = "李四";
person2.age = 19;

person.friends.push("5");

console.log(person2.friends) // ["1", "2", "3", "4", "5"]

  • ES2015增加了Object.create(原型对象[, 额外属性及其属性描述符])此方法,将原型式继承概念化。

适用场景

1、不需要单独创建构造函数,但仍然需要在对象将共享信息。


缺点:与原型模式一样,无法传递参数;共享属性时,多个实例之间会受到影响。


寄生式继承【了解】

与原型式继承相似。

思想

寄生于构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

示例

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

function createAnother(original) {
  let clone = object(original);
  clone.sayHi = function() {
    console.log("Hi")
  }
  return clone;
}

let person = {
  name: "张三",
  age: 18,
  friends: ["1", "2", "3", "4"]
}

let person2 = createAnother(person);
person2.name = "李四";
person2.age = 19;

person.friends.push("5");

person2.sayHi(); // Hi
console.log(person2.friends) // ["1", "2", "3", "4", "5"]

适用场景

1、主要关注对象,不在乎类型和构造函数的场景。


缺点:
1、与原型模式一样,无法传递参数;共享属性时,多个实例之间会受到影响。
2、创建的函数难以复用。


寄生式组合继承 【最佳】

思路

解决组合模式中效率的问题,即父类的构造函数被调用了两次。
寄生式组合继承通过盗用构造函数继承属性,使用混合式原型链继承方法。
设置原型时,要对原型的constructor进行写回,同时保证constructor是不可枚举的【通过Object.defineProperty()方法实现】。

实例

function SuperType(name) {
  this.name = name;
  this.colors = ["blue", "yellow", "white"];
}

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

function SubType(name, age) {
  // 通过盗用构造函数继承属性
  SuperType.call(this, name)
  this.age = age;
}

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

function inhertPrototype(child, father) {
  let proto = object(father.prototype);
  Object.defineProperty(proto , "constructor", {
    value: child,
    enumerable: false,
  });
  child.prototype = proto ; 
}
// 通过原型链继承方法
inhertPrototype(SubType, SuperType);

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

let example1 = new SubType("张三", 18);
example1.colors.push("red")
console.log(example1.colors); // ["blue", "yellow", "white", "red"]
example1.sayName(); // 张三
example1.sayAge(); // 18
console.log(example1 instanceof SuperType) // true

let example2 = new SubType("李四", 19);
console.log(example2.colors); // ["blue", "yellow", "white"]
example2.sayName(); // 李四
example2.sayAge(); // 19
console.log(SuperType.prototype.isPrototypeOf(example2)) // true

寄生式组合继承只调用了一次SuperType的构造函数,避免了SubType.prototype中的不必要的也用不到的属性。

寄生式组合继承 VS 组合继承

寄生式组合继承。组合继承综合了。其中主要的区别就是寄生式继承和原型继承这两种方式。同时instanceof操作符和isPrototypeOf()方法正常有效。

寄生式组合继承 组合继承
思想 寄生式继承和盗用构造函数继承两种方式 原型继承和盗用构造函数继承两种方式
主要区别 直接到构造函数的原型上,将拿到的原型作为目标原型 通过构造函数创建一个对象,将这个对象作为目标原型
构造函数的使用 原型的构造函数使用了一次
1、盗用构造函数的时候
原型的构造函数使用了两次
1、盗用构造函数的时候
2、设置原型的时候
现象 原型上没有多余的、不用的属性 原型上有多余的属性
效率
更加符合类 稍微符合类
图片.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容