侧边栏壁纸
博主头像
M酷博主等级

一帆风顺 ⛵️⛵️⛵️

  • 累计撰写 42 篇文章
  • 累计创建 37 个标签
  • 累计收到 351 条评论

目 录CONTENT

文章目录

面向对象编程及继承

M酷
2020-12-28 / 3 评论 / 8 点赞 / 1,258 阅读 / 8,540 字 / 正在检测是否收录...

简介

⾯向对象是⼀种编程思想,经常被拿来和⾯向过程比较。
面向过程关注的重点是动作,是分析出解决问题需要的步骤,然后编写函数实现每个步骤,最后依次调用函数。
⾯向对象关注的重点是对象,是把构成问题的事物拆解为各个对象,⽽拆解出对象的目的也不是为了实现某个步骤,⽽是为了描述这个事物在当前问题中的各种行为。

特点:

  1. 封装:让使⽤对象的人不考虑内部实现,只考虑功能使用,把内部的代码保护起来,只留出⼀些 api 接口供⽤户使用
  2. 继承:就是为了代码的复⽤,从⽗类上继承出一些⽅法和属性,⼦类也有⾃己的⼀些属性
  3. 多态:是不同对象作⽤于同⼀操作产⽣不同的效果。多态的思想实际上是把“想做什么”和“谁去做“分开

什么时候使⽤⾯向对象?

对于比较复杂的问题或者参与⽅较多的时候,⾯向对象的编程思想可以很好的简化问题,并且能够更好的扩展和维护。
对于比较简单的问题,⾯向对象和面向过程其实差异并不明显。

对象包含什么?

方法、属性

⼀些内置对象

Object Array Date Function RegExp

创建对象

1. new Object()

每个新对象都要重新写⼀遍 color 和 start 的赋值

const Player = new Object(); 
Player.color = "white"; 
Player.start = function () {
    console.log("white下棋"); 
};

2. 对象字面量

new Object()和对象字面量的方法在使用同一接口创建多个对象时,会产生大量重复代码,为了解决此问题,工厂模式被开发。

const Player = {
  color:'white',
  start: function () {
    console.log("white下棋");
  };
}

3. 工厂模式

工厂模式解决了重复实例化多个对象的问题,但没有解决对象识别的问题(无法识别对象的类型,因为全部都是Object,不像Date、Array等,本例中,得到的都是Player对象,对象的类型都是Object,因此出现了构造函数模式)。

function createObject() {
  const Player = new Object();
  Player.color = "white";
  Player.start = function () {
    console.log("white下棋");
  };
  return Player;
}

4. 构造函数/实例

通过 this 添加的属性和⽅法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属性和⽅法都会在内存中复制⼀份,这样就会造成内存的浪费。
但是这样创建的好处是即使改变了某⼀个对象的属性或⽅法,不会影响其他的对象

function Player(color) {
  this.color = color;
  this.start = function () {
    console.log(color + "下棋");
  };
}
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");

Tips. 怎么看函数是不是在内存中创建了多次呢?
我们可以直接比较 whitePlayer.start === blackPlayer.start // 输出 false

5. 原型

通过原型继承的⽅法并不是⾃身的,我们要在原型链上⼀层⼀层的查找,这样创建的好处是只在内存中创建⼀次,实例化的对象都会指向这个 prototype 对象。

function Player(color) {
  this.color = color;
}
Player.prototype.start = function () {
  console.log(color + "下棋");
};
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");

6. 混合模式(构造函数模式+原型模式)

构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性

function Person(name,age,family){
    this.name = name;
    this.age = age;
    this.family = family;
}

Person.prototype = {
    constructor: Person,  // 每个函数都有prototype属性,指向该函数原型对象,原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针
    say: function(){
        alert(this.name);
    }
}

var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
console.log(person1);
var person2 = new Person("wangwu",21,["lida","lier","lisi"]);
console.log(person2);

可以看出,混合模式共享着对相同方法的引用,又保证了每个实例有自己的私有属性。最大限度的节省了内存。

静态属性

是绑定在构造函数上的属性方法,需要通过构造函数访问,比如我们想看⼀下一共创建了多少个玩家的实例

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

原型及原型链

在原型上添加属性/方法有什么好处?
如果不通过原型的方式,每生成⼀个新对象,都会在内存中新开辟⼀块存储空间,当对象变多之后,性能会变得很差。
Player.prototype.xx = function () {};
这种方式向原型对象添加属性或者方法的话,⼜显得⾮常麻烦。所以我们可以这样写,可以通过Object.getPrototypeOf()获取对象的原型

Player.prototype = {
  start: function () {
    console.log("下棋");
  },
  revert: function () {
    console.log("悔悔棋");
  },
};

new关键字做了什么?

  1. 创建一个新对象
  2. 将新对象的__proto__指向构造函数的prototype >> 为了访问构造函数原型上的属性&方法
  3. 将构造函数的this指向新对象的this >> 为了访问构造函数的自身属性&方法
  4. 返回新对象
    • 如果构造函数没有显式返回值或返回基本类型,⽐如 number,string,boolean, 返回 this
    • 如果构造函数有显式返回值,是对象类型,比如{ a: 1 }, 则返回这个对象{ a: 1 }
    // 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;
}

原型链是什么?

当读取实例属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,⼀直找到最顶层Object为止,如果还没有就返回undefined。
这样⼀条通过 proto 和 prototype 去连接的链条,就是原型链。

继承

1、原型链继承

function Parent() {
  this.name = "parentName";
}
Parent.prototype.getName = function () {
  console.log(this.name);
};
function Child() {}
// Parent的实例同时包含实例属性⽅法和原型属性⽅法,所以把new Parent()赋值给 Child.prototype。
// 如果仅Child.prototype = Parent.prototype,那么Child只能调用getName,无法调用.name
// 当Child.prototype = new Parent()后, 如果new Child()得到⼀个实例对象child,那么 // child.__proto__ === Child.prototype;
// Child.prototype.__proto__ === Parent.prototype
// 也就意味着在访问child对象的属性时,如果在child上找不到,就会去Child.prototype去找,如果还找不到,就会去Parent.prototype中去找,从而实现了继承。
Child.prototype = new Parent();
// 因为constructor属性是包含在prototype⾥的,上⾯重新赋值了了prototype,所以会导致Child的constructor指向[Function: Parent],
// 有的时候使用child1.constructor判断类型的时候就会出问题
// 为了保证类型正确,我们需要将Child.prototype.constructor 指向他原本的构造函数Child
Child.prototype.constructor = Child;
var child1 = new Child();
child1.getName(); // parentName

问题:

  1. 如果有属性是引用类型,⼀旦某个实例修改了这个属性,所有实例都会受影响
  2. 创建 Child 实例时,不能传参
function Parent() {
  this.actions = ["eat", "run"];
}
function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child();
const child2 = new Child();
child1.actions.pop();
console.log(child1.actions); // ['eat']
console.log(child2.actions); // ['eat']

2、构造函数继承

针对问题 1. 我们可以使⽤用 call 来复制一遍 Parent 上的操作

function Parent() {
  this.actions = ["eat", "run"];
  this.name = "parentName";
}
function Child() {
  Parent.call(this);
}
const child1 = new Child();
const child2 = new Child();
child1.actions.pop();
console.log(child1.actions); // ['eat']
console.log(child1.actions); // ['eat', 'run']

针对问题 2. 我们应该怎么传参呢?

function Parent(name, actions) {
  this.actions = actions;
  this.name = name;
}
function Child(id, name, actions) {
  Parent.call(this, name); // 如果想直接传多个参数, 可以Parent.apply(this, Array.from(arguments).slice(1));
  this.id = id;
}
const child1 = new Child(1, "c1", ["eat"]);
const child2 = new Child(2, "c2", ["sing", "jump", "rap"]);
console.log(child1.name); // { actions: [ 'eat' ], name: 'c1', id: 1 } console.log(child2.name); // { actions: [ 'sing', 'jump', 'rap' ], name: 'c2', id: 2 }

问题

  1. 属性或方法想被继承的话,只能在构造函数中定义。而如果⽅法在构造函数内定义了,那每次创建实例都会创建⼀遍方法,多占一块内存。
function Parent(name, actions) {
  this.actions = actions;
  this.name = name;
  this.eat = function () {
    console.log(`${name} - eat`);
  };
}
function Child(id) {
  Parent.apply(this, Array.prototype.slice.call(arguments, 1));
  this.id = id;
}
const child1 = new Child(1, "c1", ["eat"]);
const child2 = new Child(2, "c2", ["sing", "jump", "rap"]);
console.log(child1.eat === child2.eat); // false

3、组合继承

通过原型链继承我们实现了基本的继承,⽅法存在 prototype 上,⼦类可以直接调用。但引⽤类型的属性会被所有实例共享,且不能传参。
通过构造函数继承,我们解决了上⾯2个问题:使用 call 在子构造函数内重复⼀遍属性和方法的创建,且支持传参了,但构造函数内部存在重复创建,占用内存过多。
所以我们将这两种⽅式结合起来,这就叫做组合继承。

function Parent(name, actions) {
  this.name = name;
  this.actions = actions;
}
Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`);
};
function Child(id) {
  Parent.apply(this, Array.from(arguments).slice(1));
  this.id = id;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child(1, "c1", ["hahahahahhah"]);
const child2 = new Child(2, "c2", ["xixixixixixx"]);
child1.eat(); // c1 - eat
child2.eat(); // c2 - eat console.log(child1.eat === child2.eat); // true

问题
调⽤了2次构造函数,做了重复的操作

Parent.apply(this,Array.from(arguments).slice(1)); 
Child.prototype = new Parent();

4、寄生组合式继承

针对调用2次构造函数的问题,我们可以考虑让 Child.prototype 间接访问到 Parent.prototype

function Parent(name, actions) {
  this.name = name;
  this.actions = actions;
}
Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`);
};
function Child(id) {
  Parent.apply(this, Array.from(arguments).slice(1));
  this.id = id;
}
// 模拟Object.create的效果
let TempFunction = function () {};
TempFunction.prototype = Parent.prototype;
Child.prototype = new TempFunction();
// 以上几行直接用Object.create的话,可写成Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child1 = new Child(1, "c1", ["hahahahahhah"]);
const child2 = new Child(2, "c2", ["xixixixixixx"]);

直接 Child.prototype = Parent.prototype 不⾏吗?
答:不⾏不行不行!!这样做的话,修改子构造函数的prototype会影响父构造函数的prototype

function Parent(name, actions) {
  this.name = name;
  this.actions = actions;
}
Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`);
};
function Child(id) {
  Parent.apply(this, Array.from(arguments).slice(1));
  this.id = id;
}
Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;
console.log(Parent.prototype, '之前'); // Child { eat: [Function], childEat: [Function] }
Child.prototype.childEat = function () {
  console.log(`childEat - ${this.name}`);
};
const child1 = new Child(1, "c1", ["hahahahahhah"]);
console.log(Parent.prototype, '之后'); // Child { eat: [Function], childEat: [Function] }

可以看到,在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变,这可不是我们想看到的。

5、class继承

ES6之后我们可以直接用class实现继承,要注意的是,子类必须在constructor方法中调用super方法,否则会报错。这是因为子类自己的this对象必须先通过父类的构造函数完成塑造,然后再加上子类自己的实例属性和方法。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面Parent.apply(this),它与ES6的继承机制完全不同。

在子类的构造函数中,只有调用super之后,才能使用this关键字,否则会报错

class Parent {
  constructor(x, y) {
    this.x = x; 
    this.y = y;
  }
}

class Child extends Parent {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y); // 必须先执行一下super
    this.color = color; // 正确
  }
}

扩展阅读:https://juejin.cn/post/6903333764991680525

8
广告 广告

评论区