聊聊面向对象

181 阅读4分钟

什么是面向对象?

面向对象的语言,其特质是有“类”这个概念。通过类,可以创建任意多个具有相同属性和方法的对象。这里要注意的是:方法存储于堆内存中,这多个对象的方法,其地址都指向相同的堆内存。这也就是面向对象的目的:通过对象调用方法,实现代码复用,尽量少的占用内存。

构造函数和“类”

JS实现创建任意多个相同属性和方法的对象,需要借助new操作符。没有对象,我们new一个嘛。这里需要了解的是new操作符做了什么?

new做了下面的事情:

  1. 创建一个空对象,将对象的原型链指向构造器的原型
  2. 将构造函数的this绑定为上面创建的对象
  3. 执行构造函数
  4. 返回新对象

思路有了,接下来手写一下,好记性不如烂笔头嘛

function create() {  let obj = {}   let Con = [].shift.call(arguments) // 取出create调用时的第一个参数,应该是构造器  obj.__proto__ = Con.prototype // 将对象的原型链指向构造器的原型  let ret = Con.apply(obj, arguments) // 调用构造器函数,更改其this  return ret instanceof Object ? ret : obj // 若构造器本身返回一个对象,那返回执行结果}function Person(name) {  this.name = name;}Person.prototype = {  logName () {    console.log(this.name);  }}var p = create(Person, 'fe');p.logName()console.log(p)

依据代码,可以知道的是:当调用类或者构造器生成实例时,对象通过Con函数,为自身添加name属性,而其logName方法,则是通过原型链__proto__寻找的。所以通过new创建的多个实例对象,其中logName方法都是通过__proto__去寻找到对应堆内存。实现了代码复用和减少内存占用!

继承

继承的目的:想要使用某一个类,但是这个类上的方法和属性又不能完全满足需求,需要进一步拓展。

假设父类是A,子类是B。那么接下来后续不再赘述

原型链

B要继承A的属性和方法,我们可以令B的原型指向通过A创建的实例。那么B就获得了A的属性和方法。具体我们看代码,不写代码就是耍流氓!

function Parent () {  this.friends = ['jerry', 'bob', 'tom']}Parent.prototype = {  getFriends () {    return this.friends  }}function Child (name) {  this.name = name}Child.prototype = new Parent()let child = new Child('fff')child.friends.push('ggg')console.log(child.friends);

如上所示,原型链模式的继承是有弊端的:所有的子类继承后,都会获得引用类型的属性friends,那么子类的friends属性就变得不安全,任一子类修改,其他子类的friends属性值也随之变化,这都是因为大家原型链指向了同一处!!!

借用构造函数

这个思想是:在B的构造器中调用A,并绑定this,代码如下

function Parent () {  this.friends = ['jerry', 'bob', 'tom']}Parent.prototype = {  getFriends () {    return this.friends  }}function Child (name) {  Parent.call(this)}let child = new Child('fff')child.friends.push('ggg')console.log(child.friends);

注意:在B的构造器中,调用A函数。生成B实例时访问不到A原型上的方法,没法实现函数的复用

组合继承:

这个方法是基于原型链继承和借用构造函数继承两个方法之上实现的。其思想是:在B中继承属性,在原型上指向A的实例。那么此时B的实例既有自己的属性,还可以继承到A的方法

function Parent () {  this.friends = ['jerry', 'bob', 'tom']}Parent.prototype = {  getFriends () {    return this.friends  }}function Child (name) {  Parent.call(this)}Child.prototype = new Parent() // 继承到方法,且friends不会被访问到let child = new Child('fff')child.friends.push('ggg')console.log(child.friends, child.getFriends());

如上,其缺陷在于A调用了2次

原型式继承

这个继承方式,借助与原型可以基于已有对象,通过构造函数创建新对象。代码如下

function create (o) {  function F () {}  F.prototype = o  return new F()}let person = {  name: 'john',  friends: ['jerry', 'tom'],  getFriends: function () {    return this.friends  }}let p1 = create(person)p1.friends.push('fff')let p2 = create(person)console.log(p2.getFriends()); // [ 'jerry', 'tom', 'fff' ]

如上,其缺点显而易见,引用型数据,又是共享

寄生式继承

这个继承类似于原型式继承,一切也是在函数中进行的

let person = {  name: 'john',  friends: ['jerry', 'tom'],  getFriends: function () {    return this.friends  }}function create (original) {  let clone = Object.create(original) // 创建一个对象  clone.firstFriend = function () { // 增强这个对象    return clone.friends[0]  }  return clone}let p1 = create(person)p1.friends.push('xxx')let p2 = create(person)console.log(p1.friends, p2.friends);

这种继承可以增强自身,但是缺陷也明显,做不到代码复用(都是新创建了函数),然后引用数据不安全

寄生组合式继承

顾名思义,这个继承方式融合了组合继承和寄生继承。其思想是:通过基于A原型,创建一个对象,然后增强对象,更改其constructor。然后将原型赋值给B的原型。然后在B中调用A。如此既继承了方法,实现代码复用,同时又拥有了自己的属性,完美!

function inherit (sup, sub) {  let prototype = Object.create(sup.prototype)  prototype.constructor = sub  sub.prototype = prototype} function Sup () {  this.name = 'xxx'}Sup.prototype.logName = function () {  console.log(this.name);}function Sub () {  Sup.call(this)}inherit(Sup, Sub)let sub1 = new Sub()sub1.logName()

这种继承方式,对于引用型数据的处理最为完美!

写文章的目的是为了自己理解,写出来不但加深记忆,而且理解也更深刻