回顾总结一
总结一链接: JavaScript 之 类与对象 + 面向对象及其案例 —— 总结一.
在总结一中我们曾得出结论:
- 类:是把许多相同事物或者类似事物综合在一起统归为一个类;
- 对象:某个类特指的实例;
- 面向对象:面向着一个实例一个具象,去想,要怎么做;
- 面向对象的思想:也就是我们人类的思维去想去做;
- 创建对象的方式
- 面向对象的案例
接下来,就让我们进入本文的主题:继承
继承
继承分为原型继承、借用函数(call
或者 apply
)完成继承以及组合继承;另外,在ES6中新增了关键字 extends
和 super
来解决继承问题;
原型继承
既然是原型继承,那么什么是原型呢?本文将以通俗易懂的方法来解释:
原型
定义:对象有一个 __proto__
属性指向 prototype 属性,而这个 __proto__
属性的属性值就是原型,而这个属性值也是一个对象,所以也叫做原型对象。
作用:当访问一个对象的属性或方法的时候,先在该对象本身的属性和方法中查找,找到了就获取并访问,如果对象本身不具有这个属性或方法,就会访问到原型。
注:原型上的属性和方法被多个实例对象所共享。
示例:
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.eat = function(){
console.log(this.name + "吃方便面");
}
var a = new Person('张三', 20);
console.log(a);
解释:如代码所示,定义了一个 Person
类,该类有 name
以及 age
两个属性,以及 prototype
属性的 eat
方法,那么在创建对象并打印该对象的时候可以发现 name
以及 age
两个属性是直接展现在创建的a
对象中的,而 eat
方法则是在 __proto__
里面的
那么,如果访问原型的时候发现也不具有这个方法或者属性呢?
通过原型我们已经知道原型也是对象,既然对象有这么一个 __proto__
属性,那么原型就应该也有 __proto__
属性了,所以如果访问原型的时候发现也不具有这个方法或者属性的时候,就会通过原型去访问原型的原型,以此类推,一直到找到需要访问的方法或者属性为止,如果找不到,则为null;而这个不断访问原型的原型的链式结构就叫做原型链。所以原型继承也可以叫做原型链继承。
然后,讲到这里到底什么是原型继承呢?
定义:其实,原型继承就是简单的把该类(父类)new出来的对象赋值给另外一个类(子类)的原型,
解释:通过前面对原型的了解我们已经知道了将某个对象或者方法赋值给一个类的原型,那么该类就具有了赋值的对象以及方法;
示例:
function Person(name, age){
// 定义父类
this.name = name;
this.age = age;
}
Person.prototype.eat = function(){
// 父类的原型
console.log(this.name + "能吃");
}
function Students(id){
// 定义子类
this.id = id;
}
Students.prototype = new Person('张三', 20); // 将父类 new 出来的对象赋值给子类的原型 ==> 继承(原型继承)
var a = new Students(001);
console.log(a);
从上图中我们可以发现:子类 Students
的原型的值为Person
也就是父类,点开之后发现父类的方法和属性也在,那么也就有了父类 Person
的属性和方法;这就是原型继承,这一条原型的原型( __proto__
的 __proto__
)的链式结构,也就是原型链。
缺点:其实,我们从代码中可以看出,原型继承的缺点很明显,明显在哪儿? 将父类 new 出来的对象赋值给子类的原型这就已经把子类继承父类的属性写“死”了,创建子类的对象时无法对其属性再进行改变了。
那么如何解决这个问题呢?借用函数(call 或者 apply方法)的继承方法也由此而出……
借用函数(call 或者 apply方法)的继承
借用函数!望文知意,我们可以知道这个继承的方法是借用了call
方法或者 apply
方法来完成继承。
下面就让我们看下借用call
方法的方法完成继承是如何继承的:
// 借用函数完成继承
function Person(name, age){
// 定义父类
this.name = name;
this.age = age;
this.run = function(){
console.log(this.name + "说:生命不息,奋斗不止!");
}
}
Person.prototype.eat = function(){
// 父类的原型
console.log(this.name + "吃方便面");
}
function Students(name, age){
// 定义子类
Person.call(this, name, age); // 借用call函数继承父类的属性和方法
}
Students.prototype.study = function(){
console.log(this.name + '在' + this.age + '岁的时候攻读硕士了');
}
var a = new Students('张三', 20);
console.log(a);
a.study();
a.run();
a.eat();
注: 借用call
方法或者apply
方法完成继承的写法是一样的,只需把需要用到call
方法的地方换成 apply
方法,对应的参数换成arguments;那么有小伙伴们就要问了,为什么换成apply
方法后要把参数换成arguments?其实,这和这两种方法有关,call
方法是分别接受参数;而apply
方法则是接受数组形式的参数。详见W3C中对这call
方法以及apply
方法的官方解释.
那么借用apply
方法代码如下:
// 借用函数完成继承
function Person(name, age){
// 定义父类
this.name = name;
this.age = age;
this.run = function(){
console.log(this.name + "说:生命不息,奋斗不止!");
}
}
Person.prototype.eat = function(){
// 父类的原型
console.log(this.name + "吃方便面");
}
function Students(){
// 定义子类
Person.apply(this, arguments); // 借用apply函数继承父类的属性和方法
}
Students.prototype.study = function(){
console.log(this.name + '在' + this.age + '岁的时候攻读硕士了');
}
var a = new Students('张三', 20);
console.log(a);
a.study();
a.run();
从打印结果我们可以看出借用函数的方法来完成继承解决了原型继承遗留下来的问题(创建子对象的时候可以重新传入新的参数),但是随之而来的是另外一个问题:子类能继承父类的方法和属性,但是有局限性,局限在于子类并没有把定义在父类原型上的方法继承下来。
那么在这两种继承方法的催生下,组合继承就出现了。
组合继承
综上所述,我们可以发现:既然原型继承能够继承父类原型的方法,借用函数继承又能在解决原型继承的缺点的前提下继承父类的属性,是不是可以考虑将两种方法组合起来呢?也就是说直接原型继承继承父类原型上的方法,借用函数继承就继承父类的属性呢?答案当然是可以的。
具体实现如下:
// 组合继承
function Person(name, age){
// 定义父类
this.name = name;
this.age = age;
this.run = function(){
console.log(this.name + "说:生命不息,奋斗不止!");
}
}
Person.prototype.eat = function(){
// 父类的原型
console.log(this.name + "吃方便面");
}
function Students(name, age){
// 定义子类
Person.call(this, name, age); // 借用call函数继承父类的属性
}
Students.prototype = new Person(); // 原型继承父类原型定义的方法
Students.prototype.study = function(){
console.log(this.name + '在' + this.age + '岁的时候攻读硕士了');
}
var a = new Students('张三', 20);
console.log(a);
a.study();
a.run();
a.eat();
如图所示,组合继承刚好把原型继承与借用函数继承的缺点完美解决了。
ES6的继承
上面讲的几种继承方法都是ES5的继承方法,那么发展到ES6时,ES6提供了关键字 extends
和 super
来解决继承问题。
首先,我们要了解ES6是如何定义类的:
class Person{
// 定义类
constructor(name, age){
// 定义属性
this.name = name;
this.age = age;
}
run(){
// 定义方法
console.log(this.name + "说:生命不息,奋斗不止!");
}
}
接下来,还是回到我们的主题:继承;
那么在ES6中又如何利用关键字 extends
和 super
来解决继承问题呢?
具体实现代码如下:
// ES6的继承
class Person{
// 定义父类
constructor(name, age){
// 定义属性
this.name = name;
this.age = age;
}
run(){
// 定义方法
console.log(this.name + "说:生命不息,奋斗不止!");
}
}
class Students extends Person{
// 定义子类
constructor(name, age){
super(name, age); // super()必须写在constructor构造函数函数体的第一行,不然会报错。
}
}
var a = new Students('张三', 20);
console.log(a);
a.run();
注:super()必须写在constructor构造函数函数体的第一行,不然会报错。
从上图的打印结果我们可以发现:其实,在ES6中所提供的关键字 extends
和 super
来解决继承的问题的根本上还是和ES5的继承类似(组合继承,也就是原型继承+借用函数继承),只是在ES6中封装好了这么一个方法来更加有效的解决关于继承的问题而已。
好的,就此,本文所表述的关于类的继承就告一段落了。如文中有哪里写的不对或者理解有误,还望大神们不吝指出,谢谢!