谈谈javascript中的面向对象和继承

写在前面

本文章适合帮助前端开发者理解面向对象编程,并且了解javascript中对于初始化对象和实现继承的几种方式和各自的优缺点。同时也简单介绍了在ES6中实现类与继承的方式。
文章也是对《JavaScript高级程序设计(第3版)》中 第6章面向对象的程序设计 与《ES6标准入门(第3版)》(阮一峰著)第19,20章的读书总结,同时也加入了一些自己的想法和总结,作以记录。

1 如何理解面向对象

1.1 如何定义面向对象

面向对象语言都有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。

面向对象三大特性:

  • 封装
    通过封装函数,直接调用,无需关心函数的实现过程
  • 继承
    可以继承父类的属性和方法,并添加自己的属性和方法。
    继承的好处是可以增加代码的可重用性和拓展性。在想要拓展功能的时候不必重写整个对象,只需继承父类然后再写新的属性,方法就好了。
  • 多态
    多态性简单的说就是能够去重写父类的方法,即父类的方法不能满足子类的需求,所以在子类中对父类方法进行重写,以满足子类对象的需求。
1.2 javascript中的面向对象

javascript中没有类的概念,ECMA-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。

严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。

在javascript中,通过原型链实现继承。

1.3 面向对象解决了什么问题

面向对象解决了传统开发过程中可维护性,重用性差的问题。

2 如何创建对象

在javascript 中,可以通过Object 构造函数或对象字面量来创建单个对象。

var person = new Object()
person.name = 'skye'
person.sayName = function() {
    console.log('my name is ' + this.name)
}
person.sayName()  // my name is skye

缺点:每次创建具有相同属性和方法的对象,都要重新写代码,导致产生大量的重复代码,并且可维护性差

2.1 工厂模式
// 工厂模式可以理解为在一个函数内生产一个对象,最后输出对象。这种模式抽象了创建具体对象的过程。
function createPerson(name) {
    var obj = new Object()
    obj.name = name
    obj.sayName = function() {
        console.log('my name is ' + this.name)
    }
    return obj
}

优点:解决了创建多个相似对象的问题。
缺点:没有解决对象识别的问题(即不知道这个对象是什么类型的)。

2.2 构造函数模式
function Person(name) {
    this.name = name
    this.sayName = function() {
        console.log('my name is ' + this.name)
    }
}

由于ECMAScript 中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。
上述的sayName()的方法也可以写成:

this.sayName = new Function(" console.log('my name is ' + this.name) ")

优点:解决了对象识别的问题。
缺点:使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,这样会导致占用大量内存。由于有this的存在,我们可以使多个实例共享一个方法。

2.3 原型模式
// 要理解原型模式,就要理解原型链
function Person() {}
Person.prototype.name = 'skye'
Person.prototype.sayName = function() {
    console.log('my name is ' + this.name)
}

优点:使用原型模式的好处是可以让所有对象实例共享它所包含的属性和方法
缺点:所有对象实例共享它所包含的属性,导致一个实例修改了引用类型的值,另一个实例的引用类型的值也会被修改,因为它们指向的是同一个地址。

2.4 组合使用构造函数模式和原型模式
function Person(name) {
    this.name = name
    this.hobbies = ['music', 'sport']
}
Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log('my name is ' + this.name)
    }
}

优点:相对于原型模式,最大限度地节省了内存,并且集合了构造函数模式的优点,支持向构造函数传递参数。

2.5 动态原型模式
function Person(name) {
    this.name = name
    if(typeof this.sayName != 'function') {
        Person.prototype.sayName = function() {
            console.log('my name is ' + this.name)
        }
    }
}

作用:动态原型模式主要为了解决独立的构造函数和原型,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下)。

2.6 寄生构造函数模式
function Person(name) {
    var obj = new Object()
    obj.name = name
    obj.sayName = function() {
        console.log('my name is ' + this.name)
    }
    return obj
}

缺点:这个模式除了方法名为首字母大写,即可以用new创建实例外,实际上与工厂模式并没有什么差别。所以同工厂模式一样,具有无法识别对象的问题
优点:这种模式可以在特殊的情况下用来为对象创建构造函数,即为不方便更改的构造函数添加新方法
例如:

function SpecialArray() {
    // 创建数组
    var values = new Array()
    // 添加值
    values.push.apply(values, arguments)
    // 添加方法
    values.toPipedString = function () {
        return this.join("|")
    };
    // 返回数组
    return values
}
var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString())  // "red|blue|green"

这个方法为数组添加了一个新的toPipedString()方法,而可能其他新创建的数组不需要这个方法,则可以用这种模式创建。

2.7 稳妥构造函数模式
function Person(name) {
    var obj = new Object()
    obj.sayName = function() {
        console.log('my name is ' + name)
    }
    return obj
}
var person = Person('skye')
person.sayName()  // my name is skye

稳妥对象没有公共属性,而且其方法也不引用 this 的对象
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:
1、新创建对象的实例方法不引用 this
2、不使用 new 操作符调用构造函数

3 ES5实现继承

3.1 原型链继承

利用原型让一个引用类型继承另一个引用类型的属性和方法。

// 原型链继承
function Parent() {
    this.property = true
    this.hobbies = ['code', 'music', 'read']
}
Parent.prototype.getParentValue = function() {
    console.log(this.property) 
}

function Child() {
    this.childProperty = false
}
Child.prototype.getChildValue = function() {
    console.log(this.childProperty)
}
// 实现继承
Child.prototype = new Parent()

原型链实现的本质是 重写原型对象,即Child的原型指向了Parent的原型。(导致instance.constructor现在指向的是Parent)
缺点:
1、引用类型值被所有实例共享;
2、在创建子类型的实例时,不能向超类型的构造函数中传递参数。

3.2 借用构造函数

在子类型构造函数的内部调用超类型构造函数。

// 构造函数继承
function Parent(name) {
    this.name = name
    this.hobbies = ['code', 'music', 'read']
    this.sayHello = function() {
        console.log('i am saying hello...')
    }
}
Parent.prototype.sayName = function() {
    console.log('my name is ' + this.name)
}

function Child(name) {
	// 实现继承
    Parent.call(this, name)
    this.age = 18
}

优点:
1、引用类型值不会被所有实例共享
2、可以向父类传参
缺点:只能继承构造函数内的方法,不能继承原型链的方法。方法都定义在构造函数中,每次都要实例化一个函数对象,占用大量内存。

3.3 组合继承

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

// 组合继承
function Parent(name) {
    this.name = name
    this.hobbies = ['code', 'music', 'read']
}
Parent.prototype.sayName = function() {
    console.log('my name is ' + this.name)
}

function Child(name, age) {
    Parent.call(this, name)  // 改变this指向,继承实例属性,第二次调用Parent()
    this.age = age
}
// 继承原型链的属性和方法
Child.prototype = new Parent()  // 第一次调用Parent()
Child.prototype.constructor = Child
Child.prototype.sayAge = function() {
    console.log('my age is ' + this.age)
}

缺点:无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

3.4 原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

// 原型式继承
function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}

缺点:包含引用类型的属性始终都会共享相应的值

3.5 寄生式继承

寄生式继承:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

// 寄生式继承
function createAnother(original) {
    let clone = Object.create(original)
    clone.sayHi = function() {
        console.log('i am saying hi...')
    }
    return clone
}

在这个例子中,createAnother() 函数接收了一个参数,也就是将要作为新对象基础的对象,即被继承的对象。然后,把这个对象(original)返回给新的对象 clone,再为 clone 对象添加一个新方法 sayHi(),最后返回 clone 对象。
Object.create可以理解为 做了一次浅复制
Object.create不是必须的,任何能够返回新对象的函数都适用于此模式。
缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率。

3.6 寄生组合式继承

寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

// 寄生组合式继承
function inheritPrototype(child, parent) {  // 继承方法
    let prototype = Object.create(parent.prototype)
    prototype.constructor = child  // 改变constructor指向
    child.prototype = prototype
}

function Parent(name) {
    this.name = name
    this.job = 'programmer'
}
Parent.prototype.sayName = function() {
    console.log('my name is ' + this.name)
}

function Child(name, age) {
    Parent.call(this, name)  // 继承属性
    this.age = age
}
inheritPrototype(Child, Parent)
Child.prototype.sayAge = function() {
    console.log('my age is ' + this.age)
}

优点:寄生组合式继承的好处是 不必为了指定子类型的原型而调用超类型的构造函数 ,因为我们需要的只是超类型原型的一个副本而已。

4 ES6中实现类和继承

4.1 代码示例
class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }

    sayName () {
        console.log(`my name is ${this.name}`)
    }

    sayAge () {
        console.log(`my age is ${this.age}`)
    }
}

class Student extends Person {
    constructor(name, age, num) {
        // 由于子类的this是继承于父类的this,所以子类必须在constructor方法中调用super方法
        // 相当于Student.propotype.constructor.call(this)
        super(name, age)
        // 对子类的this再加工
        this.num = num
    }

    sayNum () {
        console.log(`my num is ${this.num}`)
    }
}
4.2 class介绍
  1. ES6中利用 class 定义类,constructor ()方法作为构造方法,this关键字代表实例对象。
  2. 类的所有方法都定义在类的prototype上
  3. ES6中创建类的实例必须使用new,而ES5中可以直接调用类,无需使用new,而此时类的属性挂载到window上
  4. ES6中的类不存在变量提升
  5. 可以用 Object.getPrototypeOf() 方法来判断一个类是否继承了另一个类
继承

ES6中使用extends关键字实现继承

5 ES5与ES6实现继承的区别

1、ES5的继承实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.call(this));
2、ES6的继承实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

写在最后

对于如何理解面向对象,网上有好多解释。关于面向对象的特性和各种封装、继承模式,在本文章中也做了教科书式的总结。而我对于面向对象的理解,就是抽象化。在遇到一个需求时,不要首先想着如何实现它,而是先将它抽象化,从更深层的去理解它,最后从这个抽象化的过程中完成它。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值