面向对象编程

Mr.ZhaoAbout 12 min

首先,我们要明确,面向对象不是语法,是一个思想,是一种编程思想

1. 编程思想

1.1 面向过程

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了

  • 优点:性能比面向对象高,适合跟硬件联系很紧密的东西
  • 缺点:没有面向对象易维护、易复用、易扩展

1.2 面向对象

面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工

面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目

面向对象的特性:封装性、继承性、多态性

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
  • 缺点:性能比面向过程低

2. 创建对象的方式

因为面向对象就是一个找到对象的过程,所以我们先要了解如何创建一个对象

2.1 使用构造函数创建对象

构造函数是专门用于创建对象的函数,如果一个函数使用 new 关键字调用,那么这个函数就是构造函数

JS 给我们内置了一个 Object 构造函数,这个构造函数就是用来创造对象的,当构造函数和new 关键字连用的时候,就可以为我们创造出一个对象,因为 JS 是一个动态的语言,那么我们就可以动态的向对象中添加成员了

// 就能得到一个空对象
var o1 = new Object() 

// 正常操作对象
o1.name = 'Jack'
o1.age = 18
o1.gender = '男'

2.2 使用字面量创建对象

直接使用字面量的形式,也就是直接写 {},可以在写的时候就添加好成员,也可以动态的添加

// 字面量方式创建对象
var o1 = {
  name: 'Jack',
  age: 18,
  gender: '男'
}

// 再来一个
var o2 = {}
o2.name = 'Rose'
o2.age = 20
o2.gender = '女'

2.3 使用工厂函数创建对象

先书写一个工厂函数,这个工厂函数里面可以创造出一个对象,并且给对象添加一些属性,还能把对象返回

使用这个工厂函数创造对象

// 1. 先创建一个工厂函数
function createObj() {
  // 手动创建一个对象
  var obj = new Object()

  // 手动的向对象中添加成员
  obj.name = 'Jack'
  obj.age = 18
  obj.gender = '男'

  // 手动返回一个对象
  return obj
}

// 2. 使用这个工厂函数创建对象
var o1 = createObj()
var o2 = createObj()

2.4 使用自定义构造函数创建对象

工厂函数需要经历三个步骤:

  1. 手动创建对象
  2. 手动添加成员
  3. 手动返回对象

构造函数会比工厂函数简单一些:

  1. 自动创建对象
  2. 手动添加成员
  3. 自动返回对象

先书写一个构造函数,在构造函数内向对象添加一些成员,使用这个构造函数创造一个对象(和 new 连用),构造函数可以创建对象,并且创建一个带有属性和方法的对象

面向对象就是要想办法找到一个有属性和方法的对象,面向对象就是我们自己制造 构造函数 的过程

// 1. 先创造一个构造函数
function Person(name, gender) {
  this.age = 18
  this.name = name
  this.gender = gender
}

// 2. 使用构造函数创建对象
var p1 = new Person('Jack', 'man')
var p2 = new Person('Rose', 'woman')

3. 构造函数详解

我们了解了对象的创建方式,我们的面向对象就是要么能直接得到一个对象,要么就弄出一个能创造对象的东西,我们自己创造对象。我们的构造函数就能创造对象,所以接下来我们就详细聊聊构造函数

3.1 构造函数的基本使用

  • 和普通函数一样,只不过调用的时候要和 new 连用,不然就是一个普通函数调用

    function Person() {}
    var o1 = new Person()  // 能得到一个空对象
    var o2 = Person()      // 什么也得不到,这个就是普通函数调用
    

    注意:不写 new 的时候就是普通函数调用,没有创造对象的能力

  • 习惯将首字母大写

    function person() {}
    var o1 = new person() // 能得到一个对象
    
    function Person() {}
    var o2 = new Person() // 能得到一个对象
    

    注意: 首字母不大写,只要和 new 连用,就有创造对象的能力

  • 当调用的时候如果不需要传递参数可以不写 (),建议都写上

    function Person() {}
    var o1 = new Person()  // 能得到一个空对象
    var o2 = new Person    // 能得到一个空对象 
    

    注意: 如果不需要传递参数,那么可以不写 (),如果传递参数就必须写

  • 构造函数内部的 this,由于和 new 连用的关系,是指向当前实例对象的

    function Person() {
      console.log(this)
    }
    var o1 = new Person()  // 本次调用的时候,this => o1
    var o2 = new Person()  // 本次调用的时候,this => o2
    

    注意: 每次 new 的时候,函数内部的 this 都是指向当前这次的实例化对象

  • 因为构造函数会自动返回一个对象,所以构造函数内部不要写 return

    • 你如果 return 一个基本数据类型,那么写了没有意义
    • 如果你 return 一个引用数据类型,那么构造函数本身的意义就没有了

3.2 使用构造函数封装对象

我们在使用构造函数的时候,可以通过一些代码和内容来向当前的对象中添加一些内容

function Person() {
  this.name = 'Jack'
  this.age = 18
}

var o1 = new Person()
var o2 = new Person()

我们得到的两个对象里面都有自己的成员 nameage,我们在写构造函数的时候,是不是也可以添加一些方法进去呢?

function Person() {
  this.name = 'Jack'
  this.age = 18
  this.sayHi = function () {
    console.log('hello constructor')
  }
}

var o1 = new Person()
var o2 = new Person()

显然是可以的,我们的到的两个对象中都有 sayHi 这个函数,也都可以正常调用,但是这样好不好呢?缺点在哪里?

function Person() {
  this.name = 'Jack'
  this.age = 18
  this.sayHi = function () {
    console.log('hello constructor')
  }
}

// 第一次 new 的时候, Person 这个函数要执行一遍
// 执行一遍就会创造一个新的函数,并且把函数地址赋值给 this.sayHi
var o1 = new Person()

// 第二次 new 的时候, Person 这个函数要执行一遍
// 执行一遍就会创造一个新的函数,并且把函数地址赋值给 this.sayHi
var o2 = new Person()

这样的话,那么我们两个对象内的 sayHi 函数就是一个代码一模一样,功能一模一样,但是是两个空间函数,占用两个内存空间,也就是说 o1.sayHi 是一个地址,o2.sayHi 是一个地址,所以我们执行 console.log(o1 === o2.sayHi) 的到的结果是 false

缺点: 一模一样的函数出现了两次,占用了两个空间地址,存在内存浪费问题

怎么解决这个问题呢?就需要用到一个东西,叫做原型

4. 原型

原型的出现,就是为了解决构造函数的缺点,也就是给我们提供了一个给对象添加函数的方法,不然构造函数只能给对象添加属性,不能合理的添加函数

4.1 prototype

每一个函数天生自带一个成员,叫做 prototype,这个 prototype 属性可以由函数名来访问,它是一个对象,我们称为原型对象

function Person() {}

console.log(Person.prototype) // 是一个对象

即然是个对象,那么我们就可以向里面放入一些东西

function Person() {
    // 此处未定义任何方法
  }

  // 为构造函数的原型对象添加方法
  Person.prototype.sayHi = function () {
    console.log('Hi~');
  }
	
  var p1 = new Person();
  p1.sayHi(); // 输出结果为 Hi~

我们发现了一个叫做 prototype 的对象是和函数有关联的,并且可以向里面存储一些东西

重点: 在函数的 prototype 里面存储的内容,不是给函数使用的,是给函数的每一个实例化对象使用的

4.2 __proto__

每一个对象都天生自带一个成员,叫做 __proto__,是一个对象空间,我们称为对象原型,[[prototype]]__proto__ 意义相同

即然每一个对象都有,实例化对象也是对象,那么每一个实例化对象也有这个成员,这个 __proto__ 对象空间是给每一个对象使用的

当你访问一个对象中的成员的时候

  • 如果这个对象自己本身有这个成员,那么就会直接给你结果
  • 如果没有,就会去 __proto__ 这个对象空间里面找,里面有的话就给你结果

那么这个 __proto__ 又指向哪里呢?这个对象是由哪个构造函数 new 出来的,那么这个对象的 __proto__ 就指向这个构造函数的 prototype

function Person() {}

var p1 = new Person()

console.log(p1.__proto__ === Person.prototype) // true

我们发现实例化对象的 __proto__ 和所属的构造函数的 prototype 是一个对象空间,我们可以通过构造函数名称来向 prototype 中添加成员,对象在访问的时候自己没有,可以自动去自己的 __proto__ 中查找

那么,我们之前构造函数的缺点就可以解决了:我们可以把函数放在构造函数的 prototype 中,实例化对象访问的时候,自己没有,就会自动去 __proto__ 中找,那么也可以使用了

function Person() {}

Person.prototype.sayHi = function () {
  console.log('hello Person')
}

var p1 = new Person()
p1.sayHi()

p1 自己没有 sayHi 方法,就会去自己的 __proto__ 中查找,p1.__proto__ 就是 Person.prototype,我们又向 Person.prototype 中添加了 sayHi 方法,所以 p1.sayHi 就可以执行了

到这里,当我们实例化多个对象的时候,每个对象里面都没有方法,都是去所属的构造函数的 prototype 中查找,那么每一个对象使用的函数,其实都是同一个函数,那么就解决了我们构造函数的缺点

function Person() {}

Person.prototype.sayHi = function () {
  console.log('hello')
}

var p1 = new Person()
var p2 = new Person()

console.log(p1.sayHi === p2.sayHi)

p1Person 的一个实例,p2Person 的一个实例,也就是说 p1.__proto__p2.__proto__ 指向的都是 Person.prototype,当 p1 去调用 sayHi 方法的时候是去 Person.prototype 中找,当 p2 去调用 sayHi 方法的时候是去 Person.prototype 中找,那么两个实例化对象就是找到的一个方法,也是执行的一个方法

结论:当我们写构造函数的时候,属性我们直接写在构造函数体内方法我们写在原型对象上

4.3 constructor

proto里面有一个成员叫做 constructor,这个属性就是指向当前这个对象所属的构造函数

使用场景:如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了,此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数

function Star(name){
	this.name = name
}
Star.prototype = {
	sing:function(){console.log('唱歌')},
	dance:function(){console.log('跳舞')}
}
console.log(Star.prototype.constructor) // 指向Object
function Star(name){
	this.name = name
}
Star.prototype = {
	//手动利用constructor指回Star构造函数
	constructor:Star,
	sing:function(){console.log('唱歌')},
	dance:function(){console.log('跳舞')}
}
console.log(Star.prototype.constructor) // 指向Star

我们刚才聊过构造函数了,也聊了原型,那么问题出现了,我们说构造函数的 prototype 是一个对象,又说了每一个对象都天生自带一个 __proto__ 属性,那么 构造函数的 prototype 里面的 __proto__ 属性又指向哪里呢?

__proto__里面也有一个成员叫做 constructor,这个属性就是指向当前这个对象所属的构造函数

function Star() {}
const ldh = new Star()
//对象原型里面有constructor 指向构造函数 Star
console.log(ldh.__proto__.constructor === Star)

5. 原型链

5.1 链状结构

当一个对象我们不知道准确的是谁构造的时候,我们呢就把它看成 Object 的实例化对象,也就是说,我们的 构造函数 的 prototype 的 __proto__ 指向的是 Object.prototype

那么 Object.prototype 也是个对象,那么它的 __proto__ 又指向谁呢?因为 Object 是 JS 中的顶级构造函数,我们有一句话叫 万物皆对象,所以 Object.prototype 就到顶了,Object.prototype__proto__ 就是 null

JavaScript-面向对象编程01.png
JavaScript-面向对象编程01.png
    function Person() {
    }
    const xm = new Person()
    console.log(xm.__proto__ === Person.prototype) //true
    console.log(Person.prototype.__proto__ === Object.prototype) //true
    console.log(xm instanceof Person) //true
    console.log(xm instanceof Object) //true
    console.log(xm instanceof Array) //false
    console.log([1, 2, 3] instanceof Array) //true
    console.log(Array instanceof Object) //true

5.2 原型链的访问原则

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性
  2. 如果没有就查找它的原型(也就是 __proto__ 指向的 prototype 原型对象)
  3. 如果还没有就查找原型对象的原型(Object 的原型对象)
  4. 依此类推一直找到 Object 为止(null)
  5. __proto__ 对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
  6. 可以使用 instanceof 运算符来检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

6. 对象的赋值

到这里,我们就会觉得,如果是赋值的话,那么也会按照原型链的规则来,但是: 并不是!并不是!并不是! 重要的事情说三遍

赋值的时候,就是直接给对象自己本身赋值,如果原先有就是修改,原先没有就是添加,不会和 __proto__ 有关系

7. 继承

7.1 构造函数继承

function Student(name,age,classroom){
            Person.call(this,name,age)
            this.classroom = classroom
}

7.2 原型继承

Student.prototype = new Person()

7.3 组合继承

构造函数继承+原型继承

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

Person.prototype.say = function(){
    console.log("hello")
}

function Student(name,age,classroom){
    Person.call(this,name,age)
    this.classroom = classroom
}

Student.prototype = new Person()

var obj = new Student("kerwin",100,"1班")