笔记:面向对象编程&原型及原型链

493 阅读5分钟

面向对象编程

一、什么是面向对象编程
面向对象编程是指一种思想,经常被拿来和面向过程编程比较。

面向过程:分析解决问题需要的步骤,编写对应的函数实现每个步骤,依次调用函数。

面向对象:把构成问题的事物,拆解为各个对象,目的不是不为了实现某个具体步骤,是为了描述这个事物在当前问题中的各种行为。

面向对象编程的特点
1、封装:让使用对象的人不考虑内部实现,只考虑功能使用,把内部代码保护起来,只留出一些api供使用方使用。
2、继承:为了代码复用,从父类上继承一些方法和属性,子类也有一些自己的属性
3、多态:不同对象作用于同一操作产生的不同结果,多态的思想实际上是把想做什么和谁去做分开了
比如下棋的过程

什么时候适合使用面向对象思想:在比较复杂的问题时,或参与方很多的时候,可以很好的简化问题,更好的拓展和维护

二、JS中的面向对象

1、对象包含属性和方法

2、创建对象的方式

普通方式

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

工厂方式
缺点:无法识别对象类型,比如 Player 的类型只是 Object

function createdObject () {
    let newObj = new object()
    newObj.color = 'black'
    newObj.start = function(){}
    return newObj;
}

构造函数方式
缺点:this 添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属性和方法都会在内存中复制一份,这样就会造成内存的浪费。
优点:改变某个对象的属性和方法,不会影响到其他对象

function Obj(color) {
  this.color = color;
  this.start = function () {};
}
const whitePlayer = new Obj("white");
const blackPlayer = new Obj("black");

原型方式
缺点:this 添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属性和方法都会在内存中复制一份,这样就会造成内存的浪费。
优点:start只占一份的内存

function Obj(color) {
  this.color = color;
}
Obj.prototype.start = function () {};
const whitePlayer = new Obj("white");
const blackPlayer = new Obj("black");

静态方式
是绑定在构造函数上的属性方法,需要通过构造函数访问

function Obj(color) {
  this.color = color;
  if(!Obj.pageNo){
      Obj.pageNo = 0
  }
  Obj.pageNo++
}
const whitePlayer = new Obj("white");
const blackPlayer = new Obj("black");

原型及原型链

1、怎么找到原型对象

function Player(color) {
  this.color = color;
}

Player.prototype.start = function () {
  console.log(color + "下棋");
};

const white = new Player("white");
const black = new Player("black");

console.log(black.__proto__);
console.log(Object.getPrototypeOf(black)); //可以通过Object.getPrototypeOf来获取__proto__
console.log(Player.prototype);
//black.__proto__ == Player.prototype
console.log(Player.__proto__); 

prototype1.png

2、new关键字做了什么?

1、一个继承自Player.prototype的新对象,p1/p2被创建
2、p1._prop_===Player.prototype,p1._prop_指向Player.prototype
3、将this指向新创建的p1,p2
4、返回新对象
   4.1 如果构造函数没有显式返回值,则返回 this
   4.2 如果构造函数有显式返回值,是基本类型,比如 number,string,boolean, 那么还是返回 this
   4.3 如果构造函数有显式返回值,是对象类型,比如{ a: 1 }, 则返回这个对象{ a: 1 }
// 1. 用new Object() 的方式新建了一个对象 obj
// 2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
// 3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
// 4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
// 5. 返回 obj
function objectFactory() {
  let obj = {};
  let Constructor = [].shift.call(arguments);
  obj.__proto__ = Constructor.prototype;
  let ret = Constructor.apply(obj, arguments);
  return typeof ret === "object" ? ret : obj;
}

3、原型链是什么?

当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的 __proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再次在构造函数的prototype的 __proto__中查找,这样一层一层的向上查找就会形成一个链式结构,我们称为原型链。

function Player() {}

Player.prototype.name = 'xxx';

const p1 = new Player();

p1.name = 'p1';

console.log(p1.name);  //p1

delete p1.name;

console.log(p1.name); //xxx

delete Player.prototype.name;

console.log(p1.name); //undefined

继承

1、原型链继承

function parent(){
    this.name='parentName'
}
parent.prototype.getName = function(){
    console.log(this.name)
}
function child(){}

// const Child = new child()
// Child._proto_ = child.prorotype
// child.prorotype._proto == parent.prorotype
child.prototype = new parent()
child.prototype.constructor = child;
const obj = new child()
obj.getName()

隐含的问题:

1、如果有属性是引用类型,一旦某个实例修改了这个属性,那么所有的实例都会受影响
2、创建child 实例的时候无法传参

2、构造函数继承

想办法把parent上的属性和方法,添加\复制到child上,而不是都存在原型对象\parent上,防止实例被共享

function Parent(name, color) {
    this.name = name;
    this.color = color;
    this.actions = ['sing', 'jump', 'rap'];
    this.eat = function () {}
}
//Parent.apply(this);解决原型链继承的第一个问题
function Child(id) {
    Parent.apply(this, Array.prototype.slice.call(arguments,1));
    this.id = id
}

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

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

隐含的问题:

1、属性或方法被继承的话,只能在构造函数内定义,如果方法在构造函数内定义,每次创建实例都会创建一遍方法,多占一块内存
2、创建child 实例的时候无法传参

3、组合继承

原型继承实现了基本继承,属性和方法存储在prototype,子类可以直接调用,但引用类型的属性会被所有实例共享且不能传参;构造函数继承首先解决了原型链继承的两个问题,但是构造删除内多次创建方法,占用内存

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

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

function Child(id) {
    Parent.apply(this, Array.from(arguments).slice(1)); // 第一次调用构造函数
    this.id = id
}

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

const c1 = new Child(1,'c1', ['eat']);
const c2 = new Child(2,'c2', ['run']);
c1.getName()
c2.getName()

隐含的问题:

1、调用了两次构造函数

3、寄生继承

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

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

function Child(id) {
    Parent.apply(this, Array.from(arguments).slice(1)); // 第一次调用构造函数
    this.id = id
}

//Child.prototype = new Parent(); // 第二次调用构造函数
// es5
// let TempFunction = function () {};
// TempFunction.prototype = Parent.prototype;
// Child.prototype = new TempFunction();
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

const c1 = new Child(1,'c1', ['eat']);
const c2 = new Child(2,'c2', ['run']);
c1.getName()
c2.getName()