知识整理(二)重学面向对象编程、原型、原型链、继承

169 阅读11分钟

面向对象

什么是面向对象?

  • 面向对象是一种思想,经常被拿来和面向过程比较

  • 面向过程: 关注的重点是动词,是分析出解决问题需要的步骤,然后编写对应的函数实现每一个步骤,依次调用函数.

  • 面向对象: 对象关注的重点是主谓,是把构成问题的事物拆解为各个对象,而拆解出对象的⽬的也不是为了实现某个步骤,⽽是为了描述这个事物在当前问题中的各种⾏为。

一. 面向对象的三大特点:

  1. 封装: 让使用对象的人不考虑内部实现,只考虑功能的使用,把内部的代码保护起来,只留出一些api供使用方使用;
  • 类:封装对象的属性和行为
  • 方法:封装具体逻辑功能
  • 访问封装:访问修饰封装无非就是对其访问权限进行封装
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    getInfo() {
        let { name, age } = this;
        return { name, age }
    }
    static test() {
        console.log('hello');
    }
}
let lisi = new Person('zhangsan',18);
lisi.test(); // 报错test不是一个方法
lisi.getInfo();// { name: ’zhangsan‘, age: ’18‘ }
Person.test(); // hello
  • 在person中抽出的公共属性有name,age, 公共方法有getInfo,test,然而getInfo与test所不同的是test使用了static修饰符,改变为静态方法,test只属于person这个类,然而getInfo方法则是属于实例的.这里使用了static对test方法进行了访问权限的封装.
  1. 继承: 为了代码服用,从父类上继承出一些方法和属性,子类也有自己的一些属性
  class Person{
      constructor(name) {
          this.name = name;
      }
      getName() {
          console.log(this.name);
      }
      static test() {
          console.log('hello');
      }
  }
 class Qianduan extends Person {
     constructor(name) {
         super(name);
     }
     work() {
         console.log('做前端工作');
     }
 }
 
 let qianduan = new Qianduan(‘zhangsan’);
 Qianduan.getName();
 Qianduan.work();
 // Qianduan.test(); // Qianduan.test is not a function
  • 从上面的例子可以看出继承父类的静态方法,只会继承父类的公有属性和方法,
  • 子类继承之后既拥有了getName方法,同样也拥有了自己的work方法
  1. 多态:是不同对象作用与同一操作产生不同的结果,多态的思想实际上是把‘想做什么’和‘谁去做’分开,
  • 字面意思也就是多种状态,允许将子类类型的指针赋值给父类类型的指针;

  • 多态的表现形式重写于重载

    1. 重写,子类可继承父类中的方法,而不需要重新编写相同的方法,但是有的时候子类并不想原封不动的继承父类的方法,而是想做一定的修改,这就需要采用的方法重写,方法重写又称方法覆盖;
class Person {
    constructor(name) {
        this.name = name;
    }
    test() {
        console.log('hello');
    }
    getName() {
        console.log(this.name);
    }
}
class Qianduan extends Person {
    constructor(name) {
        super(name);
    }
    test() {
        console.log(`hello, 我的名字是${this.name},我是做前端工作的`);
    }
}
const person = new Person('zhangsan');
const qianduan = new Qianduan(‘lisi’);
person.test();// hello
qianduan.test(); // hello,我的名字是lisi,我是做前端工作的
person.getName();// zhangsan
qianduan.getName(); // lisi
  • 什么是重载:重载就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数或者方法之间,互相称为重载函数或者方法,
class Person {
    constructor(arg) {
        let obj = null;
        switch (typeof arg) {
            case 'string':
                obj= new StringPerson(arg);
                break;
            case 'object':
                obj = new ObjPerson();
                break;
            case ‘number’:
                obj = new NumberPerson();
                break;
        }
        return obj;
    }
}
class ObjPerson {
    constructor() {
        console.log('ObjPerson');
    }
}
class StringPerson {
    constructor() {
        console.log('StringPerson');
    }
}
class NumberPerson {
    constructor() {
        console.log('NumberPerson');
    }
}

new Person({}); // ObjPerson
new Person('123456'); // StringPerson
new Person(987654); // NumberPerson

举个例子:比如下棋的过程.

面向过程编程思想中

开局-> ⽩方下棋 -> 棋盘展示 -> 检查胜负 -> ⿊方下棋 ->棋盘展示 ->检查 ->胜负 ->循环

⽤代码表示可能是⼀连串函数的调⽤

init();

whitePlay(); // ⾥面实现一遍下棋的操作

repaint(); // 棋盘展示

check();

blackPlay(); // 再单独实现一遍下棋的操作

repaint(); // 棋盘展示

check();

⾯向对象是这样的:棋盘.开局 ->选⼿.下棋-> 棋盘.重新展示 ->棋盘.检查胜负 ->选⼿.下棋 ->棋盘.重新展示 ->棋盘.检查胜负

⽤用代码表示可能是这样的

constcheckerBoard = new CheckerBoard(); // CheckerBoard 类内部封账了棋盘的操作,⽐比 如初始化棋盘,检查胜负关系等

const whitePlayer = new Player(‘white’); // Player 类内部封装了各种玩家的操作,⽐比如等待, 落棋,悔棋

const blackPlayer = new Player(‘black’);
whitePlayer.start(); // start ⽅法的结束,内部封装了或者通过事件发布触发 
checkerBoard.repaint(), checkerBoard.check()的调用
blackPlayer.start(); 

你只需要调用 new 一个 player, 然后调⽤用 start ⽅法,也就是说我们只需要关注行为,⽽不需 要知道内部到底做了什么。

⽽且如果要加⼀些新功能,⽐如悔棋,⽐如再加一个玩家,⾯面向对象都很好扩展。

在上⾯的例子中,面向对象的特性是怎么表现出来的呢?

  • 封装:Player, CheckerBoard 类,使用的时候并不需要知道内部实现了什么,只需要考虑暴露 出的 api 的使用
  • 继承:whitePlayer 和 blackPlayer 都继承自 Player,都可以直接使⽤ Player 的各种⽅法和属性
  • 多态:whitePlayer.start() 和 blackPlayer.start() 下棋的颜⾊色分别是⽩色和黑⾊

什么时候适合使⽤面向对象

二. 创建对象js中的面向对象

  • 对象包括方法、属性、一些内置对象(Object、Array、Date、Function、RegExp)
1.普通方式
  • 以上面的下棋例子为例,每一个新对象都要重新写一遍color和start的赋值
const Player = new Object();
Player.color = "white";
Player.start = function () {
console.log("white下棋"); };

// 或者工厂模式,这2种方法都无法识别对象类型.比如 Player 的类型只是 Object
function createObject() {
    const Player = new Object();
    Player.color = "white";
    Player.start = function () {
    console.log("white下棋"); };
    return Player;
}
2.构造函数/实例
  • 通过 this 添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属 性和⽅法都会在内存中复制⼀份,这样就会造成内存的浪费。但是这样创建的好处是即使改变了某⼀个对象的属性或方法,不会影响其他的对象;
function Player(color) {
    this.color = color;
    this.start = function () {
    console.log(color + "下棋"); };
}
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
3.原型
  • 通过原型继承的⽅方法并不不是⾃自身的,我们要在原型链上⼀一层⼀一层的查找,这样创建的好处是只 在内存中创建⼀一次,实例例化的对象都会指向这个 prototype 对象。
function Player(color) {
    this.color = color;
}
Player.prototype.start = function () {
    console.log(color + "下棋"); 
};
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
4.静态属性
  • 是绑定在构造函数上的属性⽅方法,需要通过构造函数访问
function Player(color) {
    this.color = color;
    if (!Player.total) {
      Player.total = 0;
    }
    Player.total++;
}
let p1 = new Player("white");
console.log(Player.total); // 1
let p2 = new Player("black");
console.log(Player.total); // 2

原型及原型链

在原型在添加属性或者方法有什么好处?
  • 如果不通过原型的方式,每⽣成一个新对象,都会在内存中新开辟一块存储空间,当对象变多之后,性能会变得很差。
Person.prtotype.x = function() {};
Person.prtotype.y = function() {};
Person.prtotype.z = function() {};

// 上面的这种性能很差,可以下面的为例子
Person.prtotype = {
    x: function() {
        console.log('x');
    },
    y: function() {
        console.log('y');
    },
    z: funciton() {
        console.log('z');
    }
}

怎么找到 Player 的原型对象?

function Player(color) {
    this.color = color;
}
Player.prototype.start = function () {
    console.log(color + "下棋"); };
    const whitePlayer = new Player("white");
    const blackPlayer = new Player("black");
    console.log(blackPlayer.__proto__); // Player {} console.log(Object.getPrototypeOf(blackPlayer)); // Player {},可以通过 Object.getPrototypeOf来获取__proto__
    console.log(Player.prototype); // Player {} console.log(Player.__proto__); // [Function]

可以看下prototype原型的流程图:

image.png

new关键字做了什么东西

  1. ⼀个继承⾃自 Player.prototype 的新对象 whitePlayer 被创建

  2. whitePlayer.proto 指向 Player.prototype,即 whitePlayer.proto = Player.prototype 3. 将 this 指向新创建的对象 whitePlayer

  3. 返回新对象

    4.1 如果构造函数没有显式返回值,则返回 this

    4.2 如果构造函数有显式返回值,是基本类型,⽐如 number,string,boolean, 那么还是返回 this

    4.3 如果构造函数有显式返回值,是对象类型,⽐如{ a: 1 }, 则返回这个对象{ a: 1 }

实现一个new函数

// 1. ⽤new Object() 的⽅式新建了一个对象 obj
// 2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第⼀个参数
// 3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
// 4. 使⽤用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的 属性
// 5. 返回 obj

function objectFactory() {
    let obj = new Object();
    let Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    let ret = Constructor.apply(obj, arguments);
    return typeof ret === "object" ? ret : obj;
}

原型链又是什么呢?

  • 我们都知道当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
 function Player() {}
 
 Player.prototype.name = ’lisi‘;
 
 var p1 = new Player();
 p1.name = ’zhangsan‘;
 // 查找p1对象中的name属性,因为上面添加了name,所以会输出“zhangsan”
 console.log(p1.name); // zhangsan
 
 delete p1.name;
 // 删除了p1.name,然后查找p1发现没有name属性,就会从p1的原型p1.__proto__中去找,也就是 Player.prototype,然后找到了name,输出"zhangsan"
console.log(p1.name); // zhangsan

如果我们在Player.prototype 中找不到name属性,就会去Player.prototype.proto中寻找.也就是{}

Object.prototype.name = ‘root’;

function Player() {}
Player.prototype.name = "lisi";

var p1 = new Player();
p1.name = "zhansan";
// 查找p1对象中的name属性,因为上面添加了name,所以会输出“zhansan”
console.log(p1.name); // zhansan
delete p1.name;
// 删除了p1.name,然后查找p1发现没有name属性,就会从p1的原型p1.__proto__中去找,也就是 Player.prototype,然后找到了name,输出"lisi"
console.log(p1.name); // lisi
delete Player.prototype.name;
console.log(p1.name);

这样通过proto和prototype去连接的对象的链条,就是原型链

继承

一.原型链继承

  • 每个构造函数都有一个原型对象,原型的对象都包括了一个指向构造函数的指针,而每个实例都包含了一个原型对象的指针,继承的本质就是复制,又可以是重写原型对象来代替一个新类型的实例;
function Parent() {
    this.name = 'ParentName';
    this.actions = ['sing', 'jump', 'rap'];
}

function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

const c1 = new Child();
c1.actions.push('basketball');
console.log(c1.actions); //[ 'sing', 'jump', 'rap', 'basketball' ]
const c2 = new Child();
console.log(c2.actions); // [ 'sing', 'jump', 'rap' ]

二. 借用构造函数继承

  • 在子类型构造函数的内部调用超类构造函数,通过使用call()和apply()方法可以在新创建的对象上执行构造函数。
function Parent(name, color) {
    this.name = name;
    this.color = color;
    this.actions = ['sing', 'jump', 'rap'];
    this.eat = function () {}
}

function Child() {
    Parent.apply(this, arguments);
}

const c1 = new Child('c1', 'red');
const c2 = new Child('c2', 'white');

console.log(c1.eat === c2.eat);

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

三. 组合继承

  • 将原型链和借用构造函数的技术组合在一块,从而发挥两者之长的一种继承模式。
// 原型链继承 + 构造函数继承
// 1. 引用类型被改变,所有实例共享
// 2. 无法传参
// 3. 多占用了内存空间

function Parent(name, actions) {
    this.name = name;
    this.actions = actions;
}

Parent.prototype.getName = function () {
    console.log(this.name + '调用了getName');
}

function Child() {
    Parent.apply(this, arguments); // 第一次调用构造函数
}

Child.prototype = new Parent(); // 第二次调用构造函数
Child.prototype.constructor = Child;

const c1 = new Child('c1', ['eat']);
const c2 = new Child('c2', ['run']);

四. 原型式继承

  • 借助原型可以基于已有的对象创建新对象,同时还不必须因此创建自定义的类型。
function CreateObj(o){
    function F(){}
    F.prototype = o;
    console.log(o.__proto__ === Object.prototype);
    console.log(F.prototype.constructor === Object); // true
    return new F();
}

var person = {
    name: 'xiaopao',
    friend: ['daisy','kelly']
}

var person1 = CreateObj(person);

// var person2 = CreateObj(person);

person1.name = 'person1';
// console.log(person2.name); // xiaopao
person1.friend.push('taylor');
// console.log(person2.friend); // ["daisy", "kelly", "taylor"]
// console.log(person); // {name: "xiaopao", friend: Array(3)}
person1.friend = ['lulu'];
// console.log(person1.friend); // ["lulu"]
// console.log(person.friend); //  ["daisy", "kelly", "taylor"]
// 注意: 这里修改了person1.name的值,person2.name的值并未改变,并不是因为person1和person2有独立的name值,而是person1.name='person1'是给person1添加了name值,并非修改了原型上的name值
// 因为我们找对象上的属性时,总是先找实例上对象,没有找到的话再去原型对象上的属性。实例对象和原型对象上如果有同名属性,总是先取实例对象上的值

缺点:

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

五.寄生式继承

  • 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正是它做了所有工作一样返回对象。
 var ob = {
    name: 'xiaopao',
    friends: ['lulu','huahua']
}

function CreateObj(o){
    function F(){};  // 创建一个构造函数F
    F.prototype = o;
    return new F();
}

// 上面CreateObj函数 在ECMAScript5 有了一新的规范写法,Object.create(ob) 效果是一样的 , 看下面代码
var ob1 = CreateObj(ob);
var ob2 = Object.create(ob);
console.log(ob1.name); // xiaopao
console.log(ob2.name); // xiaopao

function CreateChild(o){
    var newob = CreateObj(o); // 创建对象 或者用 var newob = Object.create(ob)
    newob.sayName = function(){ // 增强对象
        console.log(this.name);
    }
    return newob; // 指定对象
}

var p1 = CreateChild(ob);
p1.sayName(); // xiaopao 

缺点(同原型式继承):

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

六.寄生组合式继承

  • 通过寄生方式,去掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
// 原型链继承 + 构造函数继承
// 1. 引用类型被改变,所有实例共享
// 2. 无法传参
// 3. 多占用了内存空间

function Parent(name, actions) {
    this.name = name;
    this.actions = actions;
}

Parent.prototype.getName = function () {
    console.log(this.name + '调用了getName');
}

function Child() {
    Parent.apply(this, arguments);
}

// Child.prototype = Parent.prototype; // 一旦更改Child.prototype,Parent.prototype也会被修改。
Child.prototype = Object.create(Parent.prototype);
// Child.prototype = new Parent();
// let TempFunction = function () {};
// TempFunction.prototype = Parent.prototype;
// Child.prototype = new TempFunction();

Child.prototype.constructor = Child;



// super() Child

const c1 = new Child('c1', ['eat']);
const c2 = new Child('c2', ['run']);

七.ES6 class继承

class person {
    constructor(){
        this.kind="person"
     }
    eat(food){
        console.log(this.name+‘ ’+food);
    }
}

class student extends person{
     constructor(name){
         super();
         this.name=name;
     }
}

var martin = new student(‘martin’);
console.log(martin.kind); //person
martin.eat(’apple‘); //martin apple

参考文章 JavaScript常用八种继承方案