文章目录

  • 继承
    • 简介
    • 1、原型链继承
      • 默认原型
      • 判断原型与实例间是否为继承关系
      • 原型继承中的方法
      • 原型链的破坏
      • 原型继承的问题
    • 2、盗用构造函数继承
      • 简介
      • 盗用构造函数继承的问题
    • 3、组合继承
      • 简介
      • 实现
    • 4、原型式继承
      • 方式一:自定义一个函数实现
      • 方式二:使用Object.create()方法实现
      • 优缺点
    • 5、寄生式继承
    • 6、寄生组合继承*(最完美继承)

继承

简介

继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承
前者继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

通过原型链实现继承的主要思想是:通过原型对象继承多个引用类型的属性和方法。

====复习下构造函数、原型对象、实例对象的关系:每个构造函数都对应一个原型对象,通过prototype属性指向这个原型对象,这个原型对象中有一个属性construct指回构造函数,实例对象中有一个内部指针指向原型对象。
====复习下啥是原型链:当某一个构造函数对应的原型对象是另一个类型的实例,这就意味着这个原型对象本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数,这样就在实例和原型之间构成了一条原型链。

1、原型链继承

看如下例题,帮助理解原型链与原型继承

//构造函数:构造父类Animal
function Animal(){this.name = "animal";
}//在Animal原型对象中定义方法
Animal.prototype.getAnimalName = function() {console.log(this.name + 'getAnimalName');
}//构造函数,构造Dog
function Dog(){this.name = 'dog';
}//让Dog继承Animal
Dog.prototype = new Animal();
/*** 上面这串代码的意思可以理解为将Animal的实例对象赋值给Dog的原型对象。如此就达到了继承的效果。*//*** 不建议使用未被承认的_proto_属性* Dog.prototype.__proto__ === Animal.prototype* 因为双下划线的属性是js中的内部属性,各个浏览器兼容性不一,* 不建议直接操作属性,ES6中提供了操作属性的方法可以实现。*/// 在使用原型链继承的时候,要在继承之后再去原型对象上定义自己所需的属性和方法
Dog.prototype.getDogName = function(){console.log(this.name + 'getDogName');
}//创建Dog实例对象
var dog1 = new Dog();
dog1.getAnimalName(); //调用继承自Animal中的方法
dog1.getDogName(); //调用自身的方法

分析:

如图所示:
(1)这里实现继承的关键是Dog的实例对象中的_proto_属性指向Dog原型对象,而这个Dog原型对象又是Animal原型对象的实例,也就是说Dog原型对象中的_proto_属性指向Animal.prototype原型对象,从而使得Dog实例对象间接的指向了Animal.prototype原型对象,这样就构成了一条原型链。在这个原型链中,Dog的实例不仅能够从Dog原型对象中继承属性和方法,还能从Animal原型对象中继承其拥有的属性和方法。

(2)并且此时Dog原型对象中的constructor指针重新指向了Animal构造函数,所以Dog实例对象中的constructor属性也指向了Animal构造函数,想要让constructor指回Dog构造函数,则需要修改Dog.prototype.constructor。

(3)如果想要读取实例上的属性或方法时,首先会在实例上搜索这个属性或方法,若没有找到,则会接着搜索实例的原型,这就是原型搜索机制。在通过原型链实现继承之后,搜索就可以继续向上搜索原型的原型,直到找到这个属性或方法。例如,这里调用dog1.getAnimalName()经过了三步搜索:首先找Dog实例对象有没有getAnimalName()方法,没找到,紧接着找Dog的原型对象有没有这个方法,发现也没有,继续向上找,找Animal的原型对象,哎,找到了该方法,开始调用。

默认原型

实际上,原型链中还有一环。默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype。这也是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。

判断原型与实例间是否为继承关系

原型与实例的关系可以通过两种方式来确定。

第一种方式是使用 instanceof 操作符,如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true。如下例所示:

//instanceof运算符用于检测构造函数的 `prototype` 属性是否出现在某个实例对象的原型链上。
console.log(d1 instanceof Object);  //true
console.log(d1 instanceof Animal);  //true
console.log(d1 instanceof Dog);     //true

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

console.log(Object.prototype.isPrototypeOf(d1)); // true
console.log(Animal.prototype.isPrototypeOf(d1)); // true
console.log(Dog.prototype.isPrototypeOf(d1)); // true

原型继承中的方法

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

//构造父类Animal
function Animal() {this.name = 'animal_';
}
//在Animal原型对象中添加getAnimalName方法
Animal.prototype.getAnimalName = function () {console.log(this.name + 'getAnimalName');
}
// 创建Animal的实例
var a1 = new Animal();
//实例会继承原型对象中的方法,所以a1可以调用getAnimalName()方法
a1.getAnimalName(); //animal_getAnimalName//构造子类Dog
function Dog() {this.name = 'dog_';
}
//让子类继承父类,也就是让Dog的prototype属性指向Animal的实例对象,以此达到继承的目的
Dog.prototype = new Animal();//给子类的原型对象中添加getDogName()方法
Dog.prototype.getDogName = function () {console.log(this.name + 'getDogName');
}
// 子类中再添加一个getAnimalName()方法,会覆盖父类同名的方法
Dog.prototype.getAnimalName = function () {console.log('我覆盖了父类的方法');
}//创建Dog实例对象
var d1 = new Dog();
d1.getAnimalName(); // 我覆盖了父类的方法
d1.getDogName(); //dog_getDogName

在上面的代码中,getDogName()方法是只属于Dog对象的方法,父类Animal中没有;而getAnimalName()方法是原型链上已经存在的方法,所以在子类Dog中再次定义的时候会覆盖父类中的getAnimalName()方法。需要注意的是,上述两个方法都是在把原型赋值为 Animal 的实例之后定义的,也就是在完成Dog.prototype = new Animal();这句代码时定义的,就是实现子类继承父类后再定义的。

原型链的破坏

以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

如下:

function Animal() {this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {console.log(this.name);
};
function Dog() {this.name = 'dog';
}
// 继承
Dog.prototype = new Animal();//字面量方式再次创建原型对象,原来的原型就被覆盖了,这就成为另一个原型了。
Dog.prototype = {getDogName() {console.log(this.name);},someOtherMethod() {return false;}
};var d1 = new Dog();
d1.getAnimalName(); // 出错!

在这段代码中,子类的原型在被赋值为 Animal 的实例后,又被一个对象字面量覆盖了。覆盖后的原型是一个 Object 的实例,而不再是 Animal 的实例。因此之前的原型链就断了。Dog和 Animal 之间也没有关系了。

原型继承的问题

原型链虽然是实现继承的强大工具,但它也有问题。
第一个主要问题是当原型对象中包含引用值时,引用值会被所有实例对象共享(这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因),在使用原型实现继承的时候,子类的原型实际上变成了父类的实例,这意味着子类原先的实例属性就变成了父类中的原型属性。
第二个主要问题是子类型在实例化时不能给父类型的构造函数传参。我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。

看如下例题:

//构造父类Animal,该构造方法中含有引用类型的属性categories
function Animal(name,age) {this.name = namethis.age = agethis.categories = ["cat", "rabbit"];
}//构造子类Dog
function Dog(color) {this.color = color}// Dog 继承 Animal
Dog.prototype = new Animal();//创建一个Dog实例,同时尝试往父类传参
var d1 = new Dog("白色");
console.log(d1.color);  // "白色"
console.log(d1.name); // undefined 因为没有在创建子类实例时传参数,所以为undefined
console.log(d1.age); // undefined 因为没有在创建子类实例时传参数,所以为undefinedconsole.log(d1.hasOwnProperty('color')) // true // 自身的属性
console.log(d1.hasOwnProperty('name')); // false // 这个是父类中有但子类实例却没有继承下来的属性
console.log(d1.hasOwnProperty('age')); // false // 这个是父类中有但子类实例却没有继承下来的属性//Dog实例继承了父类Animal中的属性categories,所以可以给categories添加元素
d1.categories.push("dog");
console.log(d1.categories); // [ 'cat', 'rabbit', 'dog' ]//再创建一个Dog实例
var d2 = new Dog();
// 可以发现categories属性共享
console.log(d2.categories); // [ 'cat', 'rabbit', 'dog' ]

分析:
在这个例题中,Animal构造函数定义了一个 categorys 属性,其中包含一个数组(引用值)。每个Animal 的实例都会有自己的 categorys 属性,包含自己的数组。但当 Dog 通过原型继承Animal 后,Dog.prototype变成了 Animal 的一个实例,因而也获得了自己的 categorys属性。最终结果是,Dog 的所有实例都会共享这个 categorys 属性。从这里的修改d1.categories会影响到d2.categories可以看出来。

2、盗用构造函数继承

简介

为了解决原型对象中包含引用类型数据导致引用值被所有对象共享的继承问题而产生的,这种方式也可以解决子类构造函数不能向父类构造函数传参的问题。
其基本思路是在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和 call()方法以新创建的对象为上下文执行构造函数。

  • 解决原型包含引用值造成的问题

具体操作如下:

function Animal() {this.categorys = ["cat", "rabbit"];
}function Dog() {// 继承 Animal Animal.call(this);
}//创建Dog实例
var d1 = new Dog();d1.categorys.push("dog");
console.log(d1.categorys); // [ 'cat', 'rabbit', 'dog' ]
var d2 = new Dog();
console.log(d2.categorys); // [ 'cat', 'rabbit']

我们分析下这两句代码function Dog(){Animal.call(this);}var d1 = new Dog();
在创建实例时,d1调用Dog构造函数,构造内部的this的值指向的是d1(在函数中谁用this调用属性或方法,this就代表哪个对象),Animal.call(this)也就是相当于Animal.call(d1);,也就是d1.Animal();。在d1调用Animal方法时,Animal内部的this就指向了d1实例对象。那么Animal内部this上的所有属性和方法都被拷贝到了d1上,因此,每个实例都具有自己的categories副本,并且互不影响。

  • 解决传参问题
    相比于使用原型链,经典继承函数的一个优点就是可以在子类构造函数中向父类构造函数传参。
function Animal(name) {this.name = name;
}function Dog() {// 继承 Animal 并传参Animal.call(this, "zhangsan");// 实例属性this.age = 29;
}var d = new Dog();
console.log(d.name); // zhangsan
console.log(d.age); // 29

传递过程如下:
在 Dog构造函数中调用 Animal 构造函数时传入一个参数,实际上会在 Dog 的实例上定义 name 属性。Animal 构造函数接收来自Dog构造函数传过来的参数 name,然后将它赋值给this指向的属性。
为确保 Animal 构造函数不会覆盖 Dog 定义的属性,可以在调用父类构造函数之后再给子类实例添加额外的属性。

盗用构造函数继承的问题

1、方法无法复用:由于在子类构造函数中创建父类实例对象时,每次都会重新调用一次父类构造函数,因此父类中的方法无法被复用,每个子类对象都会拥有一份独立的副本,从而导致内存空间的浪费。

2、无法继承父类原型对象上的属性和方法:由于在盗用构造函数继承中,子类并没有直接继承父类原型对象上的属性和方法,而是通过调用父类构造函数来创建一份与父类完全独立的副本。这意味着如果父类原型对象上新增或修改了某个属性或方法,子类并不会感知到这些变化,从而导致子类与父类之间的差异性增加。

3、不支持多继承:盗用构造函数继承的实现方式是基于调用父类构造函数来实现的,因此它无法同时继承多个父类的属性和方法。这在需要实现多重继承的场景下就显得非常不便。


总结一下盗用构造函数继承的特点:
1.创建的实例并不是父类的实例,只是子类的实例。
2.没有拼接原型链,不能使用instanceof。因为子类的实例只继承了父类的实例属性/方法,没有继承父类原型对象中的属性/方法。
3.每个子类的实例都持有父类实例方法的副本,浪费内存,影响性能,而且无法实现父类的实例方法的复用。


3、组合继承

简介

组合继承综合了原型链和经典继承函数,将两者的优点集中了起来。
基本的思路是使用原型链继承原型上的属性和方法,而通过经典继承函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

实现

//父类Animal构造函数
function Animal(name) {this.name = name;this.categories = ["cat", "rabbit"];
}//在父类原型对象中定义方法
Animal.prototype.sayName = function () {console.log(this.name);
};//子类Dog构造函数,通过经典继承函数继承父类实例中的属性
function Dog(name, age) {// 继承属性Animal.call(this, name); // 第一次调用父类构造函数this.age = age;
}//实现继承
Dog.prototype = new Animal(); // 第二次调用父类构造函数//在子类原型对象中定义独有的方法
Dog.prototype.sayAge = function () {console.log(this.age);
};//创建子类实例对象并传参
var d1 = new Dog("zhangsan", 29);
//给d1实例对象的categories数组类型属性添加元素(不会影响到d2对象)
d1.categories.push("dog");
console.log(d1.categories); // [ 'cat', 'rabbit', 'dog' ]
//调用d1中的独有方法sayAge()以及从父类中继承过来的方法sayName();
d1.sayName(); // zhangsan
d1.sayAge(); // 29 //创建第二个Dog对象并传参
var d2 = new Dog("lisi", 27);
console.log(d2.categories); // [ 'cat', 'rabbit' ]
d2.sayName(); // lisi
d2.sayAge(); // 27

分析:
主要还是围绕这句话通过使用原型链继承原型上的属性和方法,并且通过经典继承函数继承实例属性。
在这个例子中,Animal 构造函数定义了两个属性,name 和 categorys,而它的原型上也定义了一个方法叫 sayName()。Dog 构造函数调用了 Animal 构造函数,传入了 name 参数,然后又定义了自己的属性 age。此外,Dog.prototype 也被赋值为 Animal 的实例。原型赋值之后,又在这个原型上添加了新方法 sayAge()。这样,就可以创建两个 Dog 实例,让这两个实例都有自己的属性,包括 categorys,同时还共享相同的方法。

组合继承弥补了原型链和经典继承函数的不足。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

缺点是每次创建子类实例时都会调用两次父类构造函数,创建了两次父类实例对象,浪费内存

4、原型式继承

方式一:自定义一个函数实现

在Object()函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的新实例。

本质是Ojbect()对传入其中的对象执行了一次浅复制

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

方式二:使用Object.create()方法实现

这个方法接收两个参数:一是用作新对象原型的对象,还有一个是为新对象定义额外属性的对象。
在传入一个参数的情况下,这个方法于object()方法的作用一致。
在传入第二个参数的情况下,指定的任何属性都会覆盖原型对象上的同名属性。

var person = {name:'xiaolizi',colors:['red','green','blue']
}
// 传入一个参数
var person1 = Object.create(person)// 传入两个参数,第二个参数会覆盖原来的属性值
var person2 = Object.create(person,{name:{value : 'jack'}
})console.log(person1.name);
console.log(person2.name);
console.log(person1.colors);
console.log(person2.colors);
console.log(person === person1); // false 证明person1是一个新对象

优缺点

原型式继承的优点在于它比较简单、灵活,可以快速地创建对象,并从现有对象那里继承属性和方法。同时,由于原型式继承本质上是基于引用共享的机制,因此可以有效地节省内存空间。

但原型式继承也存在一些缺点,例如它会导致对象之间的关联性变得相对模糊,还容易出现同名属性和方法的覆盖问题,这些都需要在设计和开发过程中加以注意和处理。

5、寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象

// 父类对象
let Father = {name:'ll',hobbies:['tennis','music'],getName:function(){console.log(this.name)}
}// 寄生式继承
function createSon(obj,age){let newSon = Object.create(obj)// 强化属性newSon.age = age;// 强化方法newSon.getAge = function(){console.log(this.age);}return newSon
}let son1 = createSon(Father,12)
console.log(son1);
son1.getName();
son1.getAge();

缺点是
(1)做不到函数复用
(2)对象识别难度大:不能够使用 instanceof 操作符来确定对象类型,因为它只能测试原型链中的构造函数。
(3)增加复杂性:通过寄生式继承创建对象时,必须记住做了什么,并确保不会在对象间混淆引用。这增加了复杂性,降低了可读性和可维护性。

6、寄生组合继承*(最完美继承)

使用盗用构造函数继承父类中的属性,将子类原型作为父类原型的属性,实现原型链继承,会解决2次调用父类函数以及复用率的问题

// 父类构造方法
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);this.age = age;
}
// 将子类原型设置到父类原型上(这个是ES6之后添加上的方法)
Object.setPrototypeOf(SubType.prototype, SuperType.prototype)SubType.prototype.sayAge = function () {console.log(this.age);
}const instance = new SubType('xiaohong', 18)
console.log(instance);

初学JavaScript:原型继承/盗用构造函数继承/组合继承/寄生式继承/原型式继承/寄生组合式继承相关推荐

  1. JavaScript中实现继承的方法(深入学习原型链、盗用构造函数、组合继承、原型式继承、寄生式继承、寄生式组合继承)

    一.原型链 原型链的基本思想就是通过原型继承多个引用类型的属性和方法. 构造函数.原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型. 若原型是另 ...

  2. JS原型继承,盗用构造函数,组合继承,原型式继承

    继承 ECMA-262 把原型链定义为 ECMAScript 的主要继承方式.其基本思想就是通过原型继承多个引用类型的属性和方法. 原型链 重温一下构造函数.原型和实例的关系:每个构造函数都有一个原型 ...

  3. JS 继承(类式 与 原型式)

    1. /* -- 类式继承 -- */ //先声明一个超类 function Person(name) { this.name = name; } //给这个超类的原型对象上添加方法 getName ...

  4. JS中对象的四种继承方式:class继承、原型链继承、构造函数继承、组合继承(构造函数和原型链继承的结合)

    前言 才发现之前没有对JavaScript中的继承做过总结,不过看得到是不少,接下来就对这几种继承方式做一下总结. class继承 class继承是ES6引入的标准的继承方式. ES6引入了class ...

  5. 记录--JS精粹,原型链继承和构造函数继承的 “毛病”

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 先从面向对象讲起,本瓜认为:面向对象编程,它的最大能力就是:复用! 咱常说,面向对象三大特点,封装.继承.多态. 这三个特点,以" ...

  6. 深入浅出理解Javascript原型概念以及继承机制(转)

    在Javascript语言中,原型是一个经常被讨论到但是有非常让初学者不解的概念.那么,到底该怎么去给原型定义呢?不急,在了解是什么之前,我们不妨先来看下为什么. Javascript最开始是网景公司 ...

  7. 详细解析JavaScript中的继承(包括组合继承和寄生式继承)

    继承:相信很多学习过Java等面向对象语言的同学,都会接触过继承,它们实现继承的主要方式是接口继承和实现继承.但由于JavaScript函数没有签名,所以无法实现接口继承.ECMAScript支持实现 ...

  8. JavaScript 原型链和继承面试题

    JavaScript 原型链和继承问题 JavaScript 中没有类的概念的,主要通过原型链来实现继承.通常情况下,继承意味着复制操作,然而 JavaScript默认并不会复制对象的属性,相反,Ja ...

  9. JavaScript原型继承详细解读

    目录 1.构造函数的简单介绍 2.构造函数的缺点 3.prototype属性的作用 4.原型链(prototype chains) 5.constructor属性 5.1:constructor属性的 ...

最新文章

  1. 深入Java虚拟机——类型装载、连接(转)
  2. 个人电脑详细的安全设置方法之一
  3. 控制用户输入字符的个数
  4. Spine 2D animation for games
  5. java aspectj_Java:AspectJ的异常翻译
  6. 南安职业中专学校计算机专业,南安职专:国家级重点职业中专学校
  7. 素筛打表(输出小于n最大素数)
  8. Google、Baidu
  9. kafka从入门到精通:Java设置全局变量传值
  10. matlab 全局变量(global)数据类型报错问题
  11. 我是SPI,我让框架更加优雅了!
  12. 项目管理的七个工作法则
  13. js怎样判断是不是整数
  14. eclipse Strut2环境搭建
  15. Xmind用例导入到TAPD的方案(附代码)
  16. 解决 Macbook 连接蓝牙鼠标卡顿、飘的现象
  17. Android滑动浮层(滑动布局中使其中子布局一个浮动)
  18. ACP slave interface 学习
  19. 武汉微软认证考点及考试流程 与 微软认证考试流程
  20. IT人必看!2018年上半年云栖大会300份干货PPT免费开放!最前沿的技术都在这了!...

热门文章

  1. Linux cpuidle framework(1)_概述和软件架构 -- wowo
  2. 2019年度中国锂离子电池出口百强榜发布
  3. 成都地铁,生活一脉--成都进入轨道时代
  4. 流程化项目管理咨询师刘俊平介绍
  5. 入门图像处理与图像识别的知识框架
  6. linux fseek函数用法详解
  7. web项目创建桌面快捷键
  8. 年初立的flag 华为智慧城市生态来“交卷”了
  9. 视频监控边缘分析盒 yolov5
  10. mysql varchar 与text_mysql的varchar与text对比