前端全栈工程化开发专题 — 面向对象专题:类的继承、封装、多态

209 阅读12分钟

1、什么是面向对象(封装、多态)

原型和原型链(prototype)

类继承

  • 继承、封装、多态
  • 原型继承
  • call继承
  • 寄生组合继承
  • ES6中的类及类的继承

关于js中面向对象的理解

面向对象编程(oop)

它是一种编程思想(Object Oriented Programming),我们的编程或者学习其实是按照 类、实例来完成的

学习类的 继承、封装、多态

什么叫面向对象oop思想:

它是一种编程思想,基于类和实例来完成学习和开发的一种思想,对于js编程语言来说有很多的内置类,平时开发的时候操作内置类的实例来完成的,而这个实例所具备的属性和方法都是这个内置类提供的

我们也可以通过构造函数方式来创造自定义类和自定义类的实例,不管创建什么类,最终都会操作这个类的实例,实例继承了父类私有的属性和父类原型上公有的属性、方法,

这里面就涉及到构造函数、原型和原型链这些所有东西其实都是面向对象,

这些就是面向对象思想,而掌握面向对象思想我们需要掌握关于类的继承(call继承:父类私有变子类私有,原型继承:父类的私有和公有都变成子类公有,object.Creat()解决:创建一个空对象,让父类原型作为子类原型)、封装(低耦合高内聚)和多态(重载重写), 回答的时候尽可能把有关于面向对象的知识点都抛出来。(原型,原型链,构造函数)

封装

把实现一个功能的代码封装到一个函数中(一个类中),以后再想实现这个功能,只需要执行函数方法即可,不需要再重复的编写代码

低耦合,高内聚: 减少页面中的冗余代码,提高代码的重复使用率

多态

一个类(函数)的多种形态:重载、重写

[重载]

后台JAVA、C#等编程语言中,对于重载的概念:方法名相同参数不同,叫做方法的重载

//java
public void sum(int num1,int num2){
    // code

}

public void sum(int num1){
    // code
    
}

public void sum(int num1,string str){
    // code
    
}

sum(12,23)
sum(12)
sum(12,'呵呵')

js中没有类似于后台严格意义上的重载:js中如果方法名相同了,最后只能保留一个(和实参没关系的)

js中的重载:同一个方法,通过传递实参的不同(arguments)我们完成不同的功能,我们把这个也可以理解为重载

function sum(num1,num2){
    return num1+num2;
}
function sum(){
    // var ary =[].slice.call(arguments);//类数组转化成数组
    var ary = Array.prototype.slice.call(arguments);
    return eval(ary.join('+'));
}
sum(10,20);
sum(10,20,30);//不管哪一次执行的都是第二个sum 
// 因为在全局作用域的变量提升阶段,先声明加定义了一个sum,后面定义的sum把前面声明定义的sum替换掉了

[重写]

不管是后台语言还是js都有重写:子类重写父类的方法

2、原型继承

原型继承、call继承、寄生组合继承、ES6中的类及类的继承

类的继承

什么是继承?

子类继承父类中的一些属性和方法

原型继承

让子类的原型指向父类的实例

Children.prototype = new Parent()

function Parent(){
    this.x = 100;
}
Parent.prototype.getX = function () {
    console.log(this.x);
};
function Children() {
    this.y = 200;
}
Children.prototype = new Parent()//让子类的原型指向父类的一个实例  最好都在扩展子类原型方法之前完成
Children.prototype.constructor = Children;//给子类的原型重新手动设置constructor属性值
Children.prototype.getY = function () {
    console.log(this.y);
};
var child = new Children();
console.log(child.y);//构造函数中的this就是当前这个(Children)类的实例(child),this.y相当于给child这个实例加了一个私有属性y=200
child.getY();//Children私有里面没有getY,通过__proto__这个属性指向所属类的原型(Children.prototype),原型上有getY()这个方法


// 想让child用getX这个方法,child属于Children的一个实例,Children中没有getX,让Children继承Parent中的getX方法,Children的实例child就可以使用getX这个方法了
// 原型继承:Children.prototype = new Parent()//让子类的原型指向父类的一个实例

为什么这样做就可以实现原型继承了呢?原型继承图解

[细节]

1、我们首先让子类的原型指向父类的实例,然后再向子类原型上扩展方法,防止提前增加方法,等原型重新指向后,之前在子类原型上扩展的方法都没用了(子类原型已经指向新的空间地址了)

2、让子类原型重新指向父类实例,子类原型上原有的constructor就没有了,为了保证构造函数的完整性,我们最好给子类的原型重新手动设置constructor属性值:Children.prototype.constructor = Children;

[原理]

原型继承,并不是把父类的属性和方法copy一份给子类,而是让子类的原型和父类的原型之间搭建一个连接的桥梁,以后子类(或者子类的实例),可以通过原型链的查找机制,找到父类原型上的方法,从而调取这些方法使用即可

new Parent()是创建Parent这个类的实例,实例继承了Parent的私有属性(私有的直接拿过来了)和公有方法(公有的通过原型链查找),让子类的原型指向父类的这个实例(Children.prototype = new Parent()),也是子类的原型或子类是实例通过原型链查找机制一直往上找,而不是copy,getX还是在parent上。

[特征]

子类不仅可以继承父类原型上的公有属性和方法,而且父类提供给实例的那些私有的属性方法,也被子类继承了(存放在子类原型上,作为子类公有的属性和方法)

由于重写机制,子类可以通过原型链找到父类的原型,给父类增加新的属性和方法,对父类的其它子类也会有影响。(因为原型继承是让父类提供的私有属性或者方法作为子类的公有的属性和方法去继承的,所以当其中一个子类通过原型链修改父类时,其它子类都会受到影响)

这种方式ie不兼容,禁止使用__proto__,就是为了防止子类把父类重写

3、call继承

把父类私有的变成子类私有的

在子类的构造体中,把父类做普通方法执行,让父类方法中的this指向子类的实例


function Parent(){
    this.x = 100;
}
Parent.prototype.getX = function () {
    console.log(this.x);
};
function Children() {
    // this:child 子类的实例
    Parent.call(this);//让Parent执行,方法中的this依然是子类的实例(在父类构造体中写的this.xxx=xxx 都相当于在给子类的实例增加一些私有的属性和方法)  
    this.y = 200;
}
var child = new Children();

[原理]

把父类构造体中私有的属性和方法,原封不动复制了一份给子类的实例(继承完成后,子类和父类是没有关系的)

[细节]

我们一般把call继承放在子类构造体中的第一行,也就是创建子类实例的时候,进来的第一件事情就是先继承,然后再给实例赋值自己私有的(好处:自己的可以把继承过来的结果替换掉)

但是call继承不能继承父类公有的属性和方法

4、寄生组合式继承

最终想要的是能把父类私有的变成子类私有,父类公有变成子类公有,所以就有了寄生组合式继承

先了解Object:内置类

Object:内置类,所有对象数据类型内置基类,所有对象数据类型值都是object这个内置基类的一个实例。既可以作为函数也可以作为对象

console.dir(Object)

Object.create([obj]):创建一个空对象(实例),把[obj]作为新创建对象的原型

写成代码:

var obj = {name:'呵呵'};
var newObj = Object.create(obj);//创建一个空对象(实例)=> newObj, 让newObj.__proto__指向obj  但是这个obj不是空的
//所以通过newObj.__proto__会继承到这个obj的公有属性
//Object.create()不是原型上的方法,是把Object本身当成一个普通对象提供的方法,原型上的方法Object.prototype.xxx

newObj.__proto__===obj//把obj作为新创建对象的原型 true

这个功能对我们寄生组合式继承有什么作用呢?

我们发现之前在做原型继承的时候,我们让children这个子类的原型指向父类的一个实例new Parent()(如上 2、原型继承中的图),父类实例有个特点,父类实例不仅仅继承父类共有的还继承了父类私有的,而把父类实例new Parent()作为子类children这个原型,相当于子类children这个原型里面也继承了父类私有和公有的, 而我们只想让children这个子类的原型继承父类公有的,不继承父类私有的。

所以我们让children的原型不再指向父类的实例new Parent(),而是通过Object.create()这个方法的特点去解决

function Parent() {
    this.x = 100;
}
Parent.prototype.getX = function () {
    console.log(this.x);
};
var obj = Object.create(Parent.prototype);//通过Object.create()创建的obj是空对象  Parent.prototype不是空对象  让obj的__proto__指向Parent.prototype去继承Parent这个类的原型上的方法(原型上方法属性是公有的)
console.dir(obj);//obj的__proto__指向Parent.prototype,把Parent.prototype作为obj的原型
//而Parent的实例的__proto__才会指向Parent.prototype 所以console.dir(obj instanceof Parent)是true
console.dir(obj instanceof Parent);

//这样写的好处:让新创建的实例obj指向Parent.prototype,但是这新创建的实例obj并不会继承这个Parent的私有属性
// 因为Object.create(Parent.prototype)是创建一个空对象,dir(obj)输出依然是一个空对象不会继承私有的,只会找到父类公有的

通过Object.create()这个特征来继承父类公有方法

function Parent() {
    this.x = 100;
}
Parent.prototype.getX = function () {
    console.log(this.x);
};

function Children() {
    Parent.call(this);
    this.y = 200;
}
Children.prototype = Object.create(Parent.prototype);//通过Object.create()创建一个空对象,这个空对象的原型__proto__指向第一个参数Parent.prototype  把Parent.prototype作为Children.prototype
Children.prototype.constructor = Children;
Children.prototype.getY = function () {
    console.log(this.y);
};

寄生组合继承图解:

不考虑ie兼容问题:Children.prototype._proto_=Parent.prototype 是可以的

Children.prototype=Parent.prototype 也是可以的,但是这样:

constructor到底指向Children还是Parent?

当Children通过原型修改父类原型上的方法属性时,父类其他实例会受到影响

寄生组合继承完成了一个需求:子类公有的继承父类公有的(原型继承的变通),子类私有的继承父类私有的(call继承)

Object.create([obj]):创建一个空对象(实例),把[obj]作为新创建对象的原型

自己实现一个类似于object.create的方法

object.create在ie低版本不兼容,自己实现一个兼容

Object.myCreate = function myCreate(obj) {
    var Fn = new Function();//创建一个空实例fn
    Fn.prototype = obj;//让实例fn的原型指向obj
    return new Fn();//返回这个实例
};

var oo = {name: 'oo'};
Object.myCreate(oo)//创建一个空实例,把oo作为这个空实例的原型,由于实例指向类的原型,所以得创建一个类
//(才会有实例、原型)
console.dir(Object.myCreate(oo));

5、ES6中的类和继承

创建类

html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>珠峰培训</title>
</head>
<body>
<script src="js/1-5.js"></script>
</body>
</html>

js/1-5.js


// es5中定义一个类:
function Fn(){//es5定义一个类
    //=>给实例设置的私有属性
    this.xxx = 'xxx';
}
Fn.prototype //原型  原型上的方法相当于给实例增加公有属性和方法
Fn.xxx = function () {} // 作为普通对象增加属性方法  跟实例没关系  

// ==================================================================

// es6中定义一个类:
class Fn {
    constructor() {
        //=>constructor:Fn
        //=>这里面的 this.xxx=xxx 是给当前实例设置的私有属性
    }

    //=>getX & setX:这里设置的方法都放在Fn.prototype上(给实例提供的公有属性方法)
    getX() {}

    setX() {}
    
    ...
    
    //=>这写属性和方法都是把Fn当做普通对象设置的私有属性和方法,和实例没有任何的关系
    static property() {}
    
    ...
    
}
var f = new Fn();

打印出来看指向问题

继承

html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>珠峰培训</title>
</head>
<body>
<script src="js/1-6.js"></script>
</body>
</html>

js/1-6.js

class A {
    constructor() {
        this.x = 100;
    }

    getX() {
        console.log(this.x);
    }
}

class B extends A {//执行extends A会自动采用object.create()这种方式创建一个空对象,让B的原型是A的一个实例
    constructor() {
        // 进来第一句话必须写上super()
        super();//=>原理就是CALL继承(相当于把A的constructor执行的时候让A里面的this变成B的一个实例)  
        this.y = 200;
    }

    getY() {
        console.log(this.y);
    }
}

var b = new B();

es6的继承原理就是寄生组合式继承,把父类私有的变成子类私有,父类公有变成子类公有

6、for in循环遍历的一点细节问题 + ws配置快捷键

Object.prototype.hasPubProperty = function hasPubProperty() {//验证是不是公有属性

};

/*
 * for in
 *   不仅可以遍历当前对象(或者当前实例)所有的私有属性和方法,还可以把原型上自己创建的公共属性方法进行遍历
 *
 * for
 *   只会遍历私有的属性和方法(更多的是索引),自己在原型上扩展的方法不会被遍历出来
 */

var obj = {name: '珠峰培训', age: 8};
for (var key in obj) {
    // if (obj.hasOwnProperty(key)) { // 判断当前key这个属性是不是它的私有属性

    // }
    console.log(key);//如果不加判断key这个属性是不是它的私有属性,这里会输出name、age、hasPubProperty
}

// var ary = [12, 23, 34];
// for (var i = 0; i < ary.length; i++) {
//     console.log(ary[i]);//12、23、34
// }

// for (var key in ary) {
//     console.log(ary[key]);//12、23、34、f hasPubProperty
// }