JavaScript之继承

这段时间主要学习了JavaScript的知识,不得不感慨js语言的强大与灵活。此次主要是对JavaScript中的继承进行学习记录与总结,但免不了涉及到面向对象编程、封装以及多态的相应知识。接下来我将先介绍什么是面向对象编程以及为什么要采用面向对象来进行编程,接着介绍一下面向对象中的重要特性——封装,然后介绍js实现继承的六种方法,最后简单介绍一下js实现多态的方式。此篇博文主要目的是想将此次的学习过程记录下来,以便进行后续的深入学习,如果在理解上有偏差的话,还请大家不吝赐教~

面向对象编程

什么是面向对象编程

面向对象编程通俗来说,就是将你的需求抽象成一个对象,然后针对这个对象分析其特征(属性)和动作(方法)。
在Java语言中通过class关键字就可以很方便地声明一个对象,但在JavaScript语言中因为没有通过class实现类封装的方法,所以通常是通过一些特性模仿实现的,但这也带来了极高的灵活性,使我们编写的代码更自由。

为什么要引入面向对象的思想

由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护,代码复用性强,能够在很大程度上促进团队的协同合作。

前置要点

在JavaScript中创建一个类很容易,首先声明一个函数保存在一个变量中,然后在函数(类)的内部通过对this变量添加属性或者方法来实现对类添加属性或者方法,例如:

1
2
3
4
5
var Book = function(id, bookName, price){
this.id = id;
this.bookName = bookName;
this.price = price;
}

因为类是一个对象,有一个原型prototype用于指向其继承的属性和方法,所以也可以通过在类的原型上添加属性或方法。但通过this定义的属性和方法是该对象自身拥有的属性和方法,而prototype指向的是其继承的属性和方法

1
2
3
Book.prototype.display = function(){
//展示这本书
}


1
2
3
4
5
Book.prototype = {
display: function(){
//展示这本书
}
}

但两种方式不要混用。
当创建一个函数或者对象时都会为其创建一个原型对象prototype,在prototype中又会像创建this一样创建一个constructor属性,指向拥有整个原型对象的函数或对象,也就是🌰中的Book对象。

封装

封装(Encapsulation)是面向对象方法的重要特性,就是把对象的属性和方法结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。

  • 由于JavaScript的函数级作用域,声明在函数内部的变量以及方法在外界是访问不到的,通过此特性即可创建类的私有变量以及私有方法
  • 在函数内部通过this创建的属性和方法,在类创建对象时,每个对象自身都拥有一份并且可以在外部访问到,因此可以通过this创建对象的公有属性公有方法
  • 通过this创建的方法,不但可以访问这些对象的公有属性和公有方法,还可以访问到其私有属性和私有方法,所以又称这些方法为特权方法
  • 类外面通过点语法定义的属性以及方法被称为类的静态公有属性类的静态公有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var Book = function(id, name, price){
// 私有属性
var num = 1;
// 私有方法
function checkId(){}
// 特权方法
this.getName = function(){};
this.getPrice = function(){};
this.setName = function(){};
this.setPrice = function(){};
// 对象公有属性
this.id = id;
// 对象公有方法
this.copy = function(){
console.log(num)
};
// 构造器
this.setName(name);
this.setPrice(price);
}
// 类静态公有属性(对象不能访问)
Book.isChinese = true;

// 类静态公有方法(对象不能访问)
Book.resetTime = function(){
console.log('new time');
}

测试代码:

1
2
3
4
5
6
7
8
var book = new Book(11, 'JavaScript 设计模式', 50);
console.log(book.num);
book.copy();
console.log(book.id);
console.log(book.isJSBook);
console.log(book.isChinese);
console.log(Book.isChinese);
Book.resetTime();

运行结果为:
encapsulate

继承

继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。
JavaScript中并没有实现继承的现有机制,接下来我将会介绍JavaScript中实现继承的几种方法。

方法一: 类式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 类式继承
function SuperClass(){
this.superValue = true;
this.book = ['JavaScrpt', 'html', 'css'];
}

SuperClass.prototype.getSuperValue = function(){
return this.superValue;
}

function SubClass(){
this.subValue = false;
}

// 继承父类
SubClass.prototype = new SuperClass();

//继承之后再添加子类公有方法
SubClass.prototype.getSubValue = function(){
return this.subValue;
}

var instance = new SubClass();
console.log(instance.getSubValue());
console.log(instance.getSuperValue());

class-inherit

类式继承中实现继承的主要方法是SubClass.prototype = new SuperClass();SubClass的原型被赋予了SuperClass的实例,因而SubClass的原型继承了SuperClass。
类式继承存在两个缺点:

  • 由于子类通过其原型prototype对父类实例化,继承了父类。所以说父类中的公有属性要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的公有属性就会直接影响到其他子类。
    1
    2
    3
    4
    5
    var instance1 = new SubClass();
    var instance2 = new SubClass();
    console.log(instance2.book);
    instance1.book.push('设计模式');
    console.log(instance2.book);

class-dis

  • 由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。

因为类式继承存在的两个缺点,所以引出了第二种继承方法——构造函数继承

方法二: 构造函数继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 构造函数继承
function SuperClass(id){
this.id = id;
this.books = ['JavaScrpt', 'html', 'css'];
}

SuperClass.prototype.showBooks = function(){
console.log(this.books);
}

function SubClass(id){
// 构造函数继承,使用call来更改函数的作用环境
SuperClass.call(this, id);
}

var instance1 = new SubClass(10);
var instance2 = new SubClass(20);

instance1.books.push("Java");
console.log(instance1.id);
console.log(instance1.books);
console.log(instance2.id)
console.log(instance2.books);

construct-inherit

SuperClass.call(this, id);这条语句是构造函数继承的精华,call方法可以更改函数的执行环境,因此在子类中,对SuperClass调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然就继承了父类的公有属性。
由于构造函数继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承。

1
instance1.showBooks();

constructor-dis

因而引出了第三种继承方法——组合继承

方法三: 组合继承

组合继承是将类式继承构造函数继承组合起来实现继承的一种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 组合继承
function SuperClass(name){
this.name = name;
this.books = ['JavaScrpt', 'html', 'css'];
}

SuperClass.prototype= {
getName: function(){
console.log(this.name);
},
showBooks: function(){
console.log(this.books);
}
}

function SubClass(name, time){
// 调用父类构造函数
SuperClass.call(this, name);

this.time = time;
}

SubClass.prototype = new SuperClass();

SubClass.prototype.getTime = function(){
console.log(this.time);
}

var instance1 = new SubClass('js book', 2010);// 调用父类构造函数
instance1.books.push("Java");
instance1.showBooks();
instance1.getName();
instance1.getTime();

var instance2 = new SubClass('css book', 2021);
instance2.showBooks();
instance2.getName();
instance2.getTime();

combine-inherit

组合继承解决了类式继承和构造函数继承存在的问题,但仍然不是完美的继承方法,因为在组合继承中调用了两次父类构造函数,在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类构造函数。

最完美的继承方法是——寄生组合式继承,在学习寄生组合式继承之前,我们需要了解原型式继承、寄生式继承方法的实现。

方法四: 原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 原型式继承
function inheritObject(o){
// 声明一个过渡对象
function F(){};
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡对象的实例,该实例的原型继承了父对象
return new F();
}

var book = {
name: "js book",
alikeBook: ["css book", "html book"]
}
var newBook = inheritObject(book);
newBook.name = "ajax book";
newBook.alikeBook.push("xml book");

var otherBook = inheritObject(book);
otherBook.name = "flash book";
otherBook.alikeBook.push("as book");

// 存在同类式继承一样的问题:共享属性
console.log(newBook.name);
console.log(newBook.alikeBook);
console.log(otherBook.name);
console.log(otherBook.alikeBook);
console.log(book.name);
console.log(book.alikeBook);

prototype-inherit

原型式继承引入一个过渡对象,通过过渡对象的原型继承父对象,返回过渡对象的实例来实现继承。
原型式继承是对类式继承的一次封装,所以类式继承的问题在原型式继承中也会出现。

方法五: 寄生式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 寄生式继承——对原型继承的第二次封装
function inheritObject(o){
// 声明一个过渡对象
function F(){};
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡对象的实例,该实例的原型继承了父对象
return new F();
}

var book = {
name: "js book",
alikeBook: ["css book", "html book"]
}

// 二次封装 优点:对继承的对象进行了拓展
function createBook(obj){
var o = new inheritObject(obj);
// 拓展新对象
o.getName = function(){
console.log(this.name);
};
// 返回拓展后的新对象
return o;
}
var newBook = createBook(book);
var otherBook = createBook(book);
newBook.getName();
console.log(newBook.alikeBook);
newBook.alikeBook.push('Java');
console.log(otherBook.alikeBook);

parastic-inherit

寄生式继承是对原型式继承的第二次封装,并且在这第二次封装过程中对继承的对象进行了拓展。
仍然存在属性共享的问题。

方法六: 终极继承者——寄生组合式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 寄生组合式继承:寄生式继承+构造函数继承
function inheritObject(o){
// 声明一个过渡对象
function F(){};
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡对象的实例,该实例的原型继承了父对象
return new F();
}

// 处理的不是对象,而是类的原型
function inheritPrototype(subClass, superClass){
// 复制一份父类的原型副本保存在变量中
var p = new inheritObject(superClass.prototype);
// 修正因为重写子类类型导致子类的constructor属性被修改
p.constructor = subClass;
// 设置子类的原型
subClass.prototype = p;
}

function SuperClass(name){
this.name = name;
this.colors = ["red", "green", "yellow"]
}

SuperClass.prototype.getName = function(){
console.log(this.name);
}

function SubClass(name, time){
SuperClass.call(this, name);
this.time = time;
}
inheritObject(SubClass, SuperClass);

SubClass.prototype.getTime = function(){
console.log(this.time);
}

// Test
var instance1 = new SubClass('js book', 2014);
var instance2 = new SubClass('css book', 2020);
instance1.colors.push("grown");
console.log(instance1.colors);
console.log(instance2.colors);
instance1.getTime();
instance2.getTime();

parastic-combine

寄生组合式继承是将寄生式继承构造函数继承组合在一起的继承方法。

通过寄生式继承重新继承父类的原型。我们继承的仅仅是父类的原型,不再需要调用父类的构造函数。因为在构造函数继承中我们已经调用了父类的构造函数,所以我们需要的就是父类的原型对象的一个副本,而这个副本我们通过原型继承,即inheritObject()方法便可得到。因为对父类原型对象复制得到的复制对象p中的constructor指向的不是subClass子类对象,因此寄生式继承中要对复制对象p做一次增强,修复constructor指向不正确的问题,最后将得到的复制对象p赋值给子类的原型,这样,子类的原型就继承了父类的原型并且没有执行父类的构造函数。

寄生组合式继承是JavaScript实现继承的终极继承方式,也就是说,使用寄生组合式继承可以解决类式继承共享属性的问题、构造函数继承无法继承原型prototype上的属性的问题以及组合继承调用两次父类构造函数的问题。但是寄生组合式继承也比较不好理解,我已经在代码部分都写上了注释,供大家参考理解~

多继承

当前流行的用于继承单对象属性的extends方法,是通过对对象中属性的复制来实现的。
JavaScript也可以使用属性复制的方式来实现多继承:

1
2
3
4
5
6
7
8
9
10
11
12
// 多继承——对对象中的属性的一个复制过程
Object.prototype.mix = function(){
var i = 0,
len = arguments.length,
arg;
for(; i < len; i++){
arg = arguments[i];
for(var property in arg){
this[property] = arg[property];
}
}
}

多态

多态通俗来说就是同一个方法多种调用方式。在JavaScript中通过对传入的参数进行判断来实现多种调用方式。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 多态
function add(){
var arg = arguments,
len = arg.length;
switch(len){
case 0:
return 10;
case 1:
return 10 + arg[0];
case 2:
return arg[0] + arg[1];
}
}

// Test
console.log(add());
console.log(add(5));
console.log(add(1, 7));

polymorphism

总结

封装、继承多态是面向对象中的重要特性,从JavaScript的角度上实现这三个特性,让我感受到JavaScript的灵活与强大。因为本学期上了伟帅的设计模式的课,对设计模式颇有兴趣,接下来我将会学习JavaScript中的设计模式,并将学习过程记录下来~

本文标题:JavaScript之继承

文章作者:萌萌哒的邱邱邱邱

发布时间:2018年01月30日 - 19:01

最后更新:2018年02月06日 - 23:02

原始链接:https://qiuruolin.github.io/2018/01/30/js-herit/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------本文结束感谢您的阅读-------------
感谢您的支持