面向对象编程

730 阅读14分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

面向对象编程

什么是对象

JS中我们可以理解成对象是一个容器,封装了属性和方法

  • 属性:对象的状态
  • 方法:对象的行为

在实际开发中,对象是一个抽象的概念,可简单理解为数据(属性)和一些功能(方法)的集合

ES262中,对象定义为无序属性的集合,其属性可以是基本值、对象或者函数等

面向对象编程

将真实世界中各种复杂的关系,抽象为一个个对象,然后由对象之间的分工和合作,完成对真实世界的模拟

面向对象和面向过程

  • 面向过程就是亲力亲为,事无巨细我们都要考虑清楚,一旦中间某个环节没有考虑到都可能会发生错误,需要我们面面俱到,步步紧跟,有条不紊
  • 面向对象即使找一个对象,通过对象指挥得到结果
  • 面向对象是将执行者转变为指挥者
  • 面向对象不是面向过程的替代 ,而是面向过程的封装

特性

封装性:把所有面向过程的编程封装到对象中

继承性:

多态性 (抽象):传入不同的参数可能会输出不同的结果

总结

在面向对象编程思想中,每个对象都是功能中心 ,具有明确的分工,可以完成接收信息、处理数据、发出信息等任务

因此,面向对象编程具有灵活性、代码可复用性、容易维护和开发,比起以一系列函数或者指令组成的传统面向过程编程,更适合多人合作的大型软件项目

面向对象和面向过程

//面向过程编程
// var std1 = {
//     name: '张三',
//     chengji: 98
//   },
//   std2 = {
//     name: '李四',
//     chengji: 70
//   };

// function go(student) {
//   console.log('姓名:' + student.name + ' 成绩:' + student.chengji);
// }

// go(std1);
// go(std2);


//面向对象编程
function Student(name, chengji) {
  this.name = name;
  this.chengji = chengji;
  this.go = function () {
    console.log('姓名:' + this.name + ' 成绩:' + this.chengji);
  }
}

var std1 = new Student('张三', 98),
  std2 = new Student('李四', 70);

std1.go();
std2.go();

面向对象编程设计思想

  • 抽象一个Class构造函数
  • 根据Class构造函数创建实例(instance)
  • 只会实例(instance)得到结果

创建对象的方式

  • new Object()构造函数: 很复杂,我们需要分别定义它的各种属性和方法,而且需要多个对象需要书写重复代码
  • 对象字面量 = {}:也比较复杂,我们如果需要创建多个对象,需要书写很多重复性代码
  • 工厂函数:简单,我们可以很方便地创造对象,但创造出来的对象不是很具象,不能有效分类,因为这些对象都是属于Obiect创造的,我们不能知道生成的这个对象是通过那个构造函数构造的,而且总是需要return出来
  • 自定义构造函数:简单,当对象生成后可以得知是由哪个构造函数生成的,我们可以使用this.constructor查看是由哪个构造函数生成的

判断对象具体的构造函数类型还是需要使用instanceof来判断,因为this.constructor是可以更改的,可能会有所改变

构造函数和实例之间的关系

  • 构造函数是一个抽象模版,是根据具体事物抽象出来的
  • 实例对象就是根据这个模版(构造函数)创建出来的具体对象(根据传入参数不同,可能会形成不同的对象)
  • 每一个实力对象都可以通过constructor属性指向创建该对象的构造函数(constructor是实例属性的说法不是很严谨,具体的后面说)
  • 可以通过constructor属性判断实例和某个构造函数之间的关系(这种方法不是很严谨,还是推荐使用instanceof方法进行判断,后面会解释)

静态成员和实例成员

使用构造函数创造对象时,可以给构造函数和创建的实例添加属性和方法,这些属性和方法叫做成员

实例成员:在构造函数内部,通过this.xxx创建的成员,在创建对象之后只能通过实例才能调用,不能直接通过构造函数调用

静态成员:添加给函数自身的成员,排除全部通过this创造的成员,只能由构造函数调用,不能通过实例对象调用

function Perope(name, age) {
  //实例成员:只有实例对象可以调用
  this.name = name;
  this.age = age;
  this.sayName = function () {
    console.log(this.name);
  }
}
//静态成员,只有构造函数自身可以调用
Perope.sayHello = function () {
  console.log('hello');
}

//创建实例
var p1 = new Perope('张三', 19);

// 尝试调用实例成员
console.log(p1.name); //张三
console.log(p1.age); //19
//构造函数无法调用
console.log(Perope.name); //Perope(这里实际不能调用,但是构造函数自带一个name属性,所以会输出Perope)
console.log(Perope.age); //undefined()不能调用

//尝试调用静态成员
Perope.sayHello(); //hello
//实力对象无法调用
p1.sayHello(); //报错

总结:我们可以给构造函数创建成员,只有在构造函数内部使用this.xxx创建的成员有且仅能由实例调用,而使用构造函数名.xxx创建的成员实例是无法使用的,只能由构造函数调用

构造函数的问题

浪费内存

如果构造函数内部有多个相同的方法或者属性,假如我们要创建多个对象,那么每个对象都会有这些相同的属性和对象,我们会发现这些方法和属性都是独立群在的(在内存中),所以说如果有大量的实例对象,那么就会非常消耗内存

// 构造函数
function Perope(name, age) {
  //实例成员:只有实例对象可以调用
  this.name = name;
  this.age = age;
  this.sayName = function () {
    console.log(this.name);
  }
}
//创建实例
var p1 = new Perope('a', 19);
var p2 = new Perope('b', 20);
//验证p1和p2的sayname方法是否相同
//结果显示flase,那么这两个对象的方法会在内存中占用两块内存,如果实例比较多,同样的方法会在内存中占用很多内存,导致内存浪费
console.log(p1.sayName === p2.sayName);  //flase

解决方案

  • 方案1:把方法写在全局的函数中,然后在构造函数内部把方法指向这些函数。
    • 缺点:如果我们的方法很多,我们就需要在全局创建大量函数,可能会出现一些问题
  • 方案2:把函数封装在一个对象内部,在构造函数内部调用这个对象的方法
    • 缺点:我们还是想让这些属性和方法归于我们实例对象自己,这样比较简洁,也便于管理

原型对象

使用原型对象可以很好的避免前面构造函数造成的内存浪费的问题

prototype原型对象

在任何函数内部都有一个pertotype属性,这个属性的属性值是一个对象

  • 可以在原型对象上添加方法或者属性

  • 构造函数的prototype默认都有一个cinstructor属性,指向原型对象所在的函数(构造函数)

  • 同构构造函数创建的实例内部会包含一个指针(_proto_),这个指针指向生成市里的构造函数内部的prototype

  • 实例对象可以直接访问原型对象的成员(属性和方法)

    rdo4ns.png

在工作中,我们不会使用_proto_调用方法或者属性,我们会省略_proto_直接使用实例.方法/属性调用,因为_proto_不是一个标准的属性,是浏览器根据语法自动生成的

构造函数浪费内存解决方案-原型对象

  • js规定,每个构造函数都有一个prototype属性,指向构造函数的原型对象
  • 实例对象可以直接或者间接的继承原型对象上面的方法或者属性

因此,我们可以把所有实例对象需要的共享属性和方法定义在原型上

// 构造函数
function Perope(name, age) {
  this.name = name;
  this.age = age;
}

//把所有实例共享的方法或者属性添加给原型对象
//给原型添加sayName方法
Perope.prototype.sayName = function () {
  //这里面的this谁调用指向谁
  console.log(this.name);
}
//给原型添加sayAge方法
Perope.prototype.sayAge = function () {
  //这里面的this谁调用指向谁
  console.log(this.age);
}

//创建两个实例对象
var p1 = new Perope('a', 12),
  p2 = new Perope('b', 20);

//执行实例对象的方法,这里省略_proto_
p1.sayAge();
p2.sayName();

//判断一下p1和p2的sayName是不是同一个引用
console.log(p1.sayName === p2.sayName); //true

在工作中,我们可以把所有实例对象私有的属性或者方法放到构造函数上,把公共的方法或者属性添加到原型上

原型链

为什么实例对象可以调用原型上的属性和方法?

rwiknI.png

当我们调用实例对象的某个方法或者属性时,会优先在实例对象内部寻找,如果对象实例中没有这个方法或者属性,会向上寻找构造函数的原型上的方法或者属性,如果还没有再向上寻找Object构造函数原型上的方法或者属性。

// 构造函数
function Perope(name, age) {
  this.name = name;
  this.age = age;
  //在构造函数上创建一个属性和一个方法
  this.num = 2;
  this.sayName = function () {
    console.log('hello');
  }
}

//把所有实例共享的方法或者属性添加给原型对象
//给原型添加sayName方法
Perope.prototype.sayName = function () {
  //这里面的this谁调用指向谁
  console.log(this.name);
}
//给原型添加sayAge方法
Perope.prototype.sayAge = function () {
  //这里面的this谁调用指向谁
  console.log(this.age);
}
//在原型上添加一个构造函数内部就有的属性
Perope.prototype.num = 1;

//创建实例对象
var p1 = new Perope('a', 12);
//调用sayName方法,发现执行结果为hello,说明执行的是实例对象本身的sayName方法
p1.sayName();
//同上,也是调用了实例对象本身
console.log(p1.num);

//我们调用实例上没有的方法sayAge,可以发现会可以执行,说明调用了原型上的方法
p1.sayAge();

//我们可以看出来
//当实例对象上和原型对象上有同样的方法的时候,会优先调用实例的方法,不会调用原型的方法
//当我们调用实例上没有的方法的时候,会向上寻找调用原型上的方法

查找机制

每当代码读取某个对象的属性时,会执行一次查找

  1. 首先搜索实例对象本身,如果在实例中查找到了相应的属性,会返回属性值
  2. 如果搜索不到,则继续搜索指针指向的原型对象,在原型对象中查找,如果找到了,返回属性值

实例对象读写原型对象成员

读取:

  1. 自身查找,找到返回
  2. 找不到找原型,找到返回
  3. 如果找到原型链末端还没找到,返回undefined

写入

  • 值类型成员写入(实例.值类型成员 = xxx):
    • 会屏蔽掉原型对象,也就是说无法修改原型对象上的属性或者方法,只能添加或者修改自身
  • 引用类型成员写入(实例.引用类型成员 = xxx):同上
  • 复杂类型成员修改(实例.成员.xx = xxx):
    • 现在自身寻找成员,找到修改自身,找不到找原型链,找到则修改原型链上的值,找不到报错
// 构造函数
function Perope(name, age) {
  this.name = name;
  this.age = age;
}

//给原型添加sayName方法
Perope.prototype.sayName = function () {
  //这里面的this谁调用指向谁
  console.log(this.name);
}
//给原型添加sayAge方法
Perope.prototype.sayAge = function () {
  //这里面的this谁调用指向谁
  console.log(this.age);
}
//原型上添加一个对象
Perope.prototype.me = {
  num: 1
};
//原型上添加一个值属性
Perope.prototype.sex = 'nan';

//创建一个实例
var p1 = new Perope('zhangsan', 19);

//试图修改原型上的方法
//修改引用
p1.sayAge = function () {
  console.log('1111');
}
//修改值
p1.sex = 'nv';
//修改复杂类型成员
p1.me.num = 2;

//打印实例
//我们发现,p1上面新增了sayAge和sex属性,并且属性值和我们试图修改的一样,说明我们修改值或者引用无法修改原型上的
//同时我们发现,原型上p1.me.num的值变成了2,那么证明我们修改复杂类型成员却是可以成功的,并且也不会在实例上新建属性
console.log(p1);

更简单的原型语法

使用对象字面量{} 重新赋值的方式书写,即构造函数.prototype = {}

但要注意一个问题,这样直接修改会导致constructor丢失,我们打印会发现指向了Object,所以我们需要手动将constuctor指向正确的构造函数

// 构造函数
function Perope(name, age) {
  this.name = name;
  this.age = age;
}

// //给原型添加sayName方法
// Perope.prototype.sayName = function () {
//   //这里面的this谁调用指向谁
//   console.log(this.name);
// }
// //原型上添加一个值属性
// Perope.prototype.sex = 'nan';


//上面添加原型方法的优化写法
Perope.prototype = {
  //手动添加constructor并指向对应构造函数
  constructor: Perope,
  //直接在对象内部添加属性和方法
  sayName: function () {
    console.log(this.name);
  },
  sex: 'nan'
}
var p1 = new Perope('zhangsan', 1);
console.log(p1);

原型对象使用建议

在定义构造函数时,可以根据成员的功能不同,分别进行设置:

  • 私有成员:一般是非函数成员,放在构造函数内部
  • 共享成员:一般是函数,放在原型中

如果重置了prototype记得修正constructor的指向

内置构造函数的原型对象

所有函数都有prototype属性对象,js中内置的构造函数也有prototype原型对象属性:

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • String.prototype
  • Number.prototype
  • .........

在内置构造函数对象原型上添加方法

  1. 不允许使用对象字面量的方式直接更改原型对象,因为js内置会被保护起来,不允许直接更改
  2. 可以使用打点滴调用的方法添加方法,但是我们工作中一般是不允许在内置对象上添加方法的

案例:随机方块

需求:在一个舞台内,让10个小方块随机改变位置,刷新后小方块颜色可辨,不刷新小方块颜色不变

  • 这里我们采用面向对象编程,把每个小方块都想象成一个对象,那我们就需要自定义构造函数,使用构造函数构造小方块,小方块内部应该有可以把自己渲染到html结构的方法,还有可以修改自身位置的方法
  • 另外,我们还需要一个工具函数集合,可以写在一个对象内部,对象哪需要有能够生成随机数和随机颜色的的方法;
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 引入css样式 -->
  <link rel="stylesheet" href="./css/index.css">
</head>

<body>
  <!-- 创建容纳小方块的舞台 -->
  <div id="box" class="box"></div>
  <!-- 引入js代码 -->
  <script src="./js/tools.js"></script>
  <script src="./js/block.js"></script>
  <script src="./js/index.js"></script>
</body>

</html>
/* 清空样式 */
* {
  margin: 0;
  padding: 0;
}

/* 设置舞台定位,宽高、背景色 */
.box {
  position: relative;
  width: 800px;
  height: 800px;
  background: #ccc;
}

/* 设置舞台内部小方块绝对定位 */
.box span {
  position: absolute;
}
// 获取元素
var box = document.getElementById('box');
//创建一个数组,容纳创建的实例对象
var blocks = [];
// 循环10次,创建小方块对象
for (let i = 0; i < 10; i++) {
  //创建实例对象
  var me = new Block(box, {
    // 传入随机颜色参数
    backgroundColor: Tools.getColor()
  });
  // 执行创建的实例对象的渲染方法
  me.toHtml();
  //把创建的实例对象添加进数组
  blocks.push(me);
}
//遍历数组里面的所有实例对象
for (let i = 0; i < blocks.length; i++) {
  //创建定时器,每1秒一次,执行实例对象中的随机位置方法
  setInterval(function () {
    blocks[i].chagePosition();
  }, 1000)
}
// 创建构造函数,构建小方块
//参数1:小方块的舞台元素
//参数2:小方块的样式属性对象
function Block(father, obj) {
  //避免用户没有传入boj,用来做备案
  var obj = obj || {};
  //创建所有小方块需要的属性
  this.width = obj.width || 20;
  this.height = obj.heigth || 20;
  this.backgroundColor = obj.backgroundColor || 'red';
  this.top = obj.top || 0;
  this.left = obj.left || 0;
  //创建舞台属性,获取舞台(这里必须获取,否则会显示未定义)
  this.father = father;
}

//原型对象修改
Block.prototype = {
  //手动指向Block构造函数
  constructor: Block,
  //添加原型方法:渲染
  toHtml: function () {
    //创建元素节点,并把节点传入构造函数,以供其他方法使用
    this.me = document.createElement('span');
    //修改元素style行内样式
    this.me.style.width = this.width + 'px';
    this.me.style.height = this.height + 'px';
    this.me.style.backgroundColor = this.backgroundColor;
    this.me.style.top = this.top + 'px';
    this.me.style.left = this.left + 'px';
    //将小方块插入到父元素底部
    this.father.appendChild(this.me);
  },
  //原型方法:随机位置
  chagePosition: function () {
    //修改自己的left属性(使用Tools随机数(1-舞台宽度/小方块宽度 - 1)*小方块宽度),为了避免小方块重合
    this.left = Tools.getNumber(0, this.father.clientWidth / this.width - 1) * this.width;
    //同上,修改top值
    this.top = Tools.getNumber(0, this.father.clientHeight / this.height - 1) * this.height;
    //修改小方块定位属性
    this.me.style.top = this.top + 'px';
    this.me.style.left = this.left + 'px';
  }
}
//创建工具对象,防止随机数方法
var Tools = {
  //对象方法:获取指定两个数之间的随机数,包括这两个数
  getNumber: function (min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1) + min);
  },
  //返回随机颜色值
  getColor: function () {
    return 'rgb(' + this.getNumber(0, 255) + ',' + this.getNumber(0, 255) + ',' + this.getNumber(0, 255) + ')';
  }
}