阅读 246
浅谈js面向对象和继承

浅谈js面向对象和继承

前言

传统的面向对象的语言都是通过class来定义一个类的,但是在es6之前,js中并没有class关键字,那么它是怎么实现面向对象的呢?带着这些疑问,咱们来看看js实现面向对象的曲折之路吧。

构造函数

javascript的函数分为普通函数箭头函数,而普通函数除了执行其功能以外,还能当作构造函数使用。

  function Person(){}
复制代码

在这里我们定义了一个函数,我们使用new关键字来执行它,如下

  const wife = new Person();
  console.log(wife)
复制代码

此时我们就生成了一个wife对象,它是Person的实例,我们来打印一下我们新建的这个wife,打印结果如下:

image.png

此时的wife对象只有有个半透明(不可枚举)的__proto__属性,那如果我在新建wife对象的时候想给她设置年龄,姓名,三围呢?

其实很简单,因为它本身就是一个构造函数,所以我们只需要把我们想要设置的属性当成参数传入即可,代码如下

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

      let wife = new Person(16,'如花','30,150,180');
      
复制代码

此时我们的wife对象包含了 name,age,sanwei image.png

ps 上面的this指向分为两种情况

  1. 如果我们的构造函数没有返回值或者返回值不是对象,那么this指向我们实例化对象。
  2. 如果我们构造函数返回了一个对象,那么this指向我们返回的找个对象。

其中最常用的是第一种情况,因为通常情况下我们不会给构造函数定义返回值。

prototype

上面我们对Person定义了一些属性,但是如果我们要定义方法呢?比如说我们想定义一个kiss方法,应该怎么做呢?直接定义在构造函数中显然不合理,因为每次new 的时候我们都重新去定义了。鉴于这种清况,js对普通函数给出了一个prototype属性,我们只需要将方法定义在函数的prototype属性上即可。 所以我们最终可以这样写

      Person.prototype.kiss = function() {
        console.log('kiss' + this.name);
      };
      let wife = new Person(16, '如花', '30,150,180');
      let wife2 = new Person(20, '凤姐', '30,180,200');
      wife.kiss();
      wife2.kiss();
      console.log(wife);
      console.log(wife2);
复制代码

image.png

__proto__

我们通过打印大wife和小wife没有发现有定义的kiss方法呀,此时你只需要将__proto__属性点开看看,如下:

image.png

这不就是我们定义的kiss方法嘛。

通过以上的实验,我们发现当我们访问一个对象上的属性和方法时,如果对象本身上面没得,那么它会去__proto__上面查找,比如我们上面的例子,wife对象上本身是没有kiss方法的,所以它会去__proto__上面查找,最终找到了了kiss方法,如果__proto__上面还没有呢,那么他会继续沿着__proto__属性往上查找,直到找到或者__proto__为null为止,这个就是原型链。

细心的同学会发现Person.prototype里面有一个constructor属性,那么它是干嘛得呢?接下来我们就来说说它。

constructor

其实从上面打印的结果来看,我们会发现,constructor指向的是构造函数也就是Person,我们可以论证一下。

image.png

我们可以看到,最终得输出是true。

小结

通过上面的讲解,我想大部分同学都对prototype__proto__,constructor,都有了一个比较深的认识,我大概通过一张图来小结一下,如下

未命名文件 (1).png

  1. 普通函数可以当作构造函数使用,使用new关键字可以实例化对象,例如let wife = new Person
  2. 构造函数Person本身有一个prototype引用,可以将类方法定义在上面,上面有个constructor引用指向构造函数Person本身,即Person.prototype.constructor===Person
  3. 实例对象有一个__proto__属性,指向的是其构造函数的prototype,也就是说wife.__proto__===Person.prototype

继承

说起面向对象那不得不说到继承,我们都知道java中实现继承用到了extends关键字,但是js中没有extends(es5)那么js又是怎么实现继承的呢。我们想一想继承的本质是什么,就是只要儿子继承了老子,那么老子有的儿子都有。

下面咱们看看下面的例子。

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

Parent.prototype.say = function () {
    console.log('identity' + this.identity)
}

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

Child.prototype.say = function () {
    console.log('identity' + this.identity)
}

Child.prototype.spendMoney = function () {
    console.log('我要买买买')
}
复制代码

例子中定义了两个构造函数,很明显,子构造函数中除了spendMoney这个方法不能继承以外,其他的属性和方法都可以派生至父构造函数。说句题外话,如果老子有1yi,那么儿子真没必要再去挣那1yi了,道理事这个道理,可是放在js代码中我们应该怎么去实现呢?

第一步,我们先干掉child上父构造函数中已有的属性方法,毕竟咱有家族企业,砸可以继承。家族没有的,咱再生产。

//子构造函数
function Child(name, age, identity) {
  
}
Child.prototype.spendMoney = function () {
    console.log('我要买买买')
}
复制代码

第二步:咱们先考虑怎么继承父构造函数的属性吧

仔细观察父构造函数在添加属性值的时候是使用的this.xxx = ''(this.name = name),我们只需要把这个this改成子类的实例就好,那么子类的实例不久拿到这些属性了吗

于是我们可以像下面这样写

//子构造函数
function Child(name, age, identity) {
   this.Parent = Parent;
   this.Parent(name,age,identity)
   delete this.Parent;
}
let child1 = new Child('longhang',12,'儿子')
复制代码

我们打印一下child1,结果如下

image.png

完美,整理一下思路

1 我们首先让子类的parent属性指向了父类的构造函数 this.Parent = Parent;

2 子类通过this.Parent调用构造函数,这样Parent中的this就是指向子类的实例; 3 删除this.Parent这个指针。

其实我们有个更优雅的方式,使用call或者apply,之所以用上面的方式来实现是想让小伙伴们知道call或者apply的原理。

//子构造函数
function Child(name, age, identity) {
   Parent.call(this,name,age,identity)
}
let child1 = new Child('longhang',12,'儿子')
复制代码

我们打印一下child1,结果如下

image.png

第三步:继承原型上的属性和方法

上面我们谈了继承实例上面的属性,现在我们来谈谈继承原型上的属性和方法,比如,我们仙子啊要继承say方法。

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

Parent.prototype.say = function () {
    console.log('identity' + this.identity)
}

//子构造函数
function Child(name, age, identity) {
    Parent.call(this, name, age, identity)
}
复制代码

如上,我们现在要继承Parent原型上面的say方法 可能有同学会说很简单,直接让Child.prototype = Parent.prototype 如下图所示

image.png

这样确实可以让child的实例通过__proto__访问到say方法,但是这样有一个问题,当我们现在想给Child添加一个结婚的方法,Parent也能调用结婚的方法,那老妈还不发飙么。所以这种方式是有问题的,但是我们只需要稍微改一改,就可以了,如下。

image.png

实际上就是让我们Child的原型指向一个空对象,但是空对象的__proto__指向Parent的原型对象,是不是很简单,下面我们用代码实现一下,以下是继承部分所有的代码。

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

Parent.prototype.say = function () {
    console.log('identity' + this.identity)
}

//子构造函数
function Child(name, age, identity) {
    Parent.call(this, name, age, identity)
}
//创建空对象,并让空对象的__proto__指向Parent的原型
let tempObj = Object.create(Parent.prototype);
//Child的原型指向这个空对象
Child.prototype = tempObj;
复制代码

小结

1 继承实际上就是让子类拥有父类的一切

2 实例属性的继承方式是调用父类的构造函数实现继承

3 原型上的属性和方法的继承是首先建立一个__proto__指向父类原型空对象,然后再让子类的原型指向这个空对象

文章分类
前端
文章标签