【Javascript】- 面向对象

615 阅读11分钟

Javascript 这门语言非常与众不同,它好像没有什么特别强的规定,这导致它异常灵活,但也很奇怪。在 Java 或者 C++ 中,实例化一个对象是很简单的事情:定义一个 类,然后 new 一个类。但是在Javascript中,没有类这个东西,所以光是对象的实例化就有好多种方式,对象的继承更是如此。

值得一提的是:每种方式都与众不同,每种方式都有自己独到之处。用的好就能把Javascript面向对象编程玩儿的出神入化;所有的这些方法,都有必要去了解,去实践,甚至精通!

虽然目的只是实例化对象和继承对象,但这并不是“回字的4种写法”

对象实例化

字面量模式

// 定义
let Person = {
    name:'Jack'
    age:25,
    sayName:function(){
        console.log('my name is'+this.name)
    }
}
// 实例化
Person.sayName();

优点: 书写方便,定义后马上就能实例化,适合用来存储数据

缺点: 复用性差,所以也称为“单例模式”

工厂模式

所谓工厂模式就是定义一个函数(工厂),外界输入参数(原材料),然后运行这个工厂函数(生产),返回创建好的对象(产品)

// 定义
function CreatePerson(name,age){
    let obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.eatFood = function(){
        console.log('I am eating food'); 
    }
    return obj;
}

// 实例化
let Jack = CreatePerson('Jack',25)

// Jack就是createPerson类型的实例,可是却返回了 false
console.log(Jack instanceof createPerson) // false

**优点:**简单明了,一看就懂

缺点: 无法识别自定义的对象类型,因为该对象是通过函数来构造的

构造函数模式

// 定义
function Person(name,age){
    this.name = name;
    this.age = age;
    this.eatFood:function(){
        console.log('I am eating food')
    }
}

// 实例化
let Jack = new Person('Jack',25)

优点: 根据传参来构造对象,这样的对象具有较好的独立性

缺点: 每实例化一个对象,就会重复的构造出对象中包含的方法,这些方法功能相同,重复创建会浪费内存

原型模式

// 定义
const Person = function (){};
Person.prototype = {
    constructor:Person,
    name: 'Jack',
    age:25,
    eatFood:function(){
        console.log('I am eating food'); 
    }
}

// 实例化
let Jack = new Person();

优点: 只需构造一个原型,其余实例化的对象都引用自这个原型对象,节省内存

缺点: 如果修改了原型对象的属性或方法,那么其余所有实例对象的属性或方法都会被更改;而且原型不能像函数一样接收参数,这样的对象没有独立性可言

原型+构造函数模式

// 定义
function Person(name,age){
    // 需要自定义的的属性和方法放到构造函数中
    this.name = name;
    this.age = age;
}

Person.prototype = {
    // 公共的属性和方法放到原型中
    constructor:Person,
    eatFood:function(){
        console.log('I am eating food'); 
    }
}

// 实例化
let Jack = new Person('Jack',25);

优点: 综合了构造函数模式和原型模式的优点,实例化的对象独立性比较高,也能很好的复用

动态原型模式

function Person(name, age) {
    this.name = name
    this.age = age
    
    //定义方法前,要先检查是否已存在同名变量
    if (typeof this.say != "function") {
        // 不能使用对象字面量写法
        Person.prototype.say = function () {
            console.log(this.name)
        }
    }
}

const person = new Person("Jack", 29)

person.say()

**优点:**将属性和方法的定义都封装在构造函数中,这样代码可读性较高;可以确定其准确的类型

**缺点:**不能使用对象的字面量写法来给 prototype 赋值,如果定义的方法多,写法比较繁杂

最推荐的实例化对象写法

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

// 构造函数的原型对象
Person.prototype = {
    constructor: Person,
	gender: 'Male',
    say: function(){
        console.log(this.name, this.age, this.gender)
	}
}
		
const person1 = new Person('Ethan', 28)
person1.say()

对于ES5来说,这是目前最推荐的实例化对象的方法;个人也觉得简单易懂,大部分场景下,都可以使用这种方式来创建对象

对象的继承

原型链继承

原型链继承的原理就是:将父类型的实例赋值给子类型的原型,这样子类型就能继承父类型的所有属性和方法

function Father() {
  this.role = 'Father'
  this.msg = 'this message is from Father'
}

Father.prototype.say = function () {
  console.log(this.role)
}

function Child() {
  this.role = 'Child'
  this.age = 20
}

// 继承:将子对象的原型对象设置为父对象的实例
Child.prototype = new Father()

上面就是一个完整的实现了原型链继承的演示,其中最重要的一步就是:将 Child 的原型指定为 Father 的实例。

通过原型链,子类型可以继承到父类型的所有属性和方法,子类型还可以重写继承而来的方法或属性、添加自身的方法或属性

// 定义自身的方法
Child.prototype.hi = function () {
  console.log(this.msg)
}

const child1 = new Child()

// 调用从父类型继承而来的方法
child1.say() // 'Father'

// 调用自身定义的方法,访问继承而来的属性
child1.hi() // 'this message is from Father'

注意:

  • 子类型可以重写父类型中的方法,或者添加自定义的方法。但是,这些操作一定要放在替换原型的语句之后

  • 子类型在自身的原型对象上,定义方法或属性时,不能使用对象字面量写法,因为这样会导致 prototype 被重写,继承也会无效

    Child.prototype = {
    	hi: function () {
      		console.log(this.msg)
    	}
    }
    
    const child1 = new Child()
    
    child1.say() // Uncaught TypeError: child1.say is not a function
    

    上面的代码之所以会报错,是因为:使用对象字面量写法来给 prototype 赋值,相当于重写了 prototype 对象,也会导致前面的Child.prototype = new Father()这条语句等于没写

使用原型链来实现继承很简单,只需要把子类型的原型对象指向父类型的实例对象,但是它存在着一个不容忽视的问题:只要一个子类型修改了继承来的复合类型的属性,其余子类型中的复合类型属性也会被修改

要知道,所有实例对象都会共享原型对象的属性和方法,父类型实例中的属性和方法全都注入了子类型的原型中,原型一旦发生改变,所有的实例对象毫无疑问也会随之而变。

为了更清楚的解释,请看下面的代码:

function Father() {
    this.list = [1, 2, 3]
}

function Child() { }

Child.prototype = new Father()

Child.prototype.say = function () {
  console.log(this.list)
}

const child1 = new Child()
child1.say() // [1, 2, 3]

const child2 = new Child()
child2.list.push(4)

// child1 就受到了影响
child1.say() // [1, 2, 3, 4]

最后一行打印出 child1 打印出来的 list 继承自 Father,它应当是 [1, 2, 3],但是打印的结果却是 [1, 2, 3, 4] ? 这是因为 child2 修改了 list 属性,而 list 存在于 Father 的构造函数上(Child 的原型上),所以会造成 “牵一发而动全身” 的结果。这样的话,通过原型链继承而来的对象就毫无独立性可言了

借用构造函数继承

借用构造函数可以解决原型链继承中复合类型属性的问题。它的原理是:在子类型的构造函数中,调用父类型的构造函数

function Father(msg) {
  this.msg = msg
  this.list = [1, 2, 3]
}

function Child() {
  // 继承
  Father.call(this)
}

Child.prototype.say = function () {
  console.log(this.list)
}

现在我们实例化两个对象,来看看这次复合类型的属性还会不会受影响

const child1 = new Child()

const child2 = new Child()
child2.list.push(4)

child1.say() // [1, 2, 3]
child2.say() // [1, 2, 3, 4]

好了,现在多个对象实例的复合类型属性各自独立,互不干扰

而且,借用构造函数模式的本质是通过 call 或 apply 在子类型的构造函数中调用父类型的构造函数,所以子类型可以向父类型传参:

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

function Child() {
  // 继承
  Father.call(this, 'Jack', 29)
}

Child.prototype.say = function () {
  console.log(this.name, this.age)
}

const child = new Child()
child.say() // Jack 29

组合继承

原型链继承的优点就是属性和方法都定义在原型对象上,实例对象都能共享原型中的属性和方法;缺点则是实例对象都共享同一个原型,没有独立性可言。借用构造函数继承的优点则是实例的复合类型属性可以不受影响,因为方法和属性都定义在构造函数上;但是缺点也因此而生,属性和方法会被每个每个对象复制一份,没有复用性可言。

于是,原型和构造函数这对“好兄弟”再一次合作,产生了组合继承方式

组合继承结合了原型链和借用构造函数的优点,通过原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样既保证了子类型的独立性又保证了父类型原型上方法的复用

function Father() {
    // 父类型构造函数中的属性
    this.msg = 'Hello World'
}

// 父类型原型上的方法
Father.prototype.say = function () {
    console.log(this.name, this.age)
}

function Child(name, age) {
    // 继承父类型构造函数中的属性
    Father.call(this)
    
    this.name = name
    this.age = age
}

// 继承父类型原型上的方法
Child.prototype = new Father()

// 子类型自定义的方法
Child.prototype.hi = function () {
    console.log(this.msg)
}

const child = new Child('Aelly', 18)

child.say() // 'Aelly', 18
child.hi() // 'Hello World'

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof 和isPrototypeOf() 也能够用于识别基于组合继承创建的对象

原型继承

Douglas Crockford (道爷)在2006年发明的继承方式,原理是:原型可以基于现有的对象而创建出新的对象,它实现起来非常简单!

function createObject(obj){
    function F(){}
    F.prototype = obj
    return new F()
}

上面代码的意思是,定义一个空的函数 F ,将 F 的原型指向传入的对象,然后对 F 执行 new 操作,最后返回 new 出来的对象。函数 createObject 对传入的对象,执行了一次浅复制,相当于,以传入的对象为模板,创建了一个一模一样的新对象,然后返回。

原型继承的设计目的是:当你只想以一个对象为模板刻画出另一个对象,然后别有他用,同时你又不想大费周章的用上构造函数的话,那么你就可以使用原型继承。

注意: 包含复合类型值的属性始终都会被子类型共享或修改,就像使用原型链继承一样

const Father = {
	name: 'Father',
	list: [1,2,3]
}

const child1 = createObject(Father)
const child2 = createObject(Father)

console.log(child1.list) // [1,2,3]

child2.list.push(4)

console.log(child1.list) // [1,2,3,4]

ES6 中提供了一个 Object.create() 方法专门用来实现原型继承

寄生式继承

上面的原型继承是将传入的对象“复制一份”,然后返回,需要在函数 createObject 外部添加自定义的方法和属性。而寄生式继承,虽然名字听起来怪怪的,但其实就是在原型继承的基础上,把属性和方法定义封装在了一个函数中,然后函数返回继承后的对象

function createBy(original){
    // 前面的 createObject 函数
    const clone = createObject(original)
    
    // 定义方法或属性
    clone.hi = function(){
        console.log(this.msg)
    }
    
    return clone
}

const Father = {msg: 'Hello', age: 29}

const child = createBy(Father)
child.hi() // 'Hello'

需要注意的是, createObject 函数不是必须的,只要是能够返回的一个新对象的函数,都是可以的

使用寄生式继承来为对象添加方法,会由于不能做到函数复用而降低效率——与构造函数模式类似。

寄生组合式继承

虽然组合继承是JavaScript最常用的继承模式,但是,它也存在缺点——任何情况下,都会调用两次父类型的构造函数;一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

function Father() {
    this.msg = 'Hello World'
}

Father.prototype.say = function () {
    console.log(this.name, this.age)
}

function Child(name, age) {
    // 第2次调用父类型的构造函数
    Father.call(this)
    
    this.name = name
    this.age = age
}

// 第1次调用父类型的构造函数
Child.prototype = new Father()

函数的功能是固定,每调用一次就执行一次操作,但是同一个函数执行两次就有点“多余”。具有“代码洁癖”的程序员们为了解决掉这个问题,又提出了寄生组合式继承

寄生组合式继承的原理是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是父类型原型的一个副本而已,那么就通过寄生式继承来继承父类型的原型,通过借用构造函数来继承父类型构造函数中的属性

function inherit(Child, Father) {
    // 1. 创建父类型原型的一个副本
    const p = Object.create(Father.prototype)
    
    // 2. 为副本添加constructor 属性,补上因重写原型而失去的默认的constructor 属性
    p.constructor = Child
    
    // 3. 将副本赋值给子类型的原型,从而实现继承
    Child.prototype = p
}

function Father() {
    this.list = [1, 2, 3]
}

function Child() {
    Father.call(this)
}

// 继承操作
inherit(Child, Father)

Child.prototype.hi = function () {
    console.log(this.msg)
}

const child1 = new Child()
const child2 = new Child()

child1.list.push(4)

child1.hi() // [1, 2, 3, 4]
child2.hi() // [1, 2, 3]

寄生组合式继承一种堪称完美的对象继承方式!通过代码你可以发现:

  • 只调用一次父类型构造函数
  • 子类型继承而来复合类型属性,不再受到实例的影响
  • 继承了父类型构造函数中的属性,实现了子类型的独立性;继承了父类型原型对象中的方法,实现了函数的复用

简直完美!!!

本文所有内容都是参考自《Javascript高级程序设计》,文中代码都是亲手实现,并加上了一些自己的理解

如有错误,欢迎指出!!