美文网首页
javascript面向对象

javascript面向对象

作者: 陶小乐xy | 来源:发表于2017-08-01 16:41 被阅读0次

    一、封装

    在es6之前,javascript是没有类(class)的概念的,但是又确实是一种基于对象的语言,所以通常情况下我们都是把属性和方法封装成一个对象,可以直接从对象生成一个实例对象,下面我们来看看具体实现的方法。

    1、生成实例对象的原始模式

    假设我们把人看作是一个原型对象,他有名字和年龄两个属性。

    var People = {
       name:'',
       age:''
    }
    

    现在我们根据这个原型对象生成两个实例

    var people1 = {};
      people.name = 'people1';
      people.age = 12;
    
    var people2 = {};
      people.name = 'people2';
      people.age = 13;
    

    这个就是最原始的封装,把两个属性封装在一个对象里面。但是这样的写法有两个缺点,一是生成实例太多,写起来就很麻烦。二是实例与原型之间看不出来有任何联系。

    2、原始模式的改进

    写一个函数解决代码重复的问题

    function People(name,age){
      return {
        name:name,
        age:age
        }
      }
    

    然后生成实例对象,等于是在调用函数:

    var people1 = People('people1',12);
    var people2 = People('people2',13);
    

    但是这种方法问题是,people1和people2之间没有什么联系,看不出来是同一个原型的实例。

    3、构造函数的模式

    为了解决从原型对象生成实例的问题javascript提供了一个构造函数,使用new运算符就能生成实例,并且this变量会绑定到实例对象上
    比如:上面人的原型对象现在就可以这样写了。

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

    现在就可以生成实例对象了:

    var people1 = new ('people1',12);
    var people2 = new('people2',13);
    console.log(people1.name); // people1
    

    这时people1和people2会自动含有一个constructor属性,指向它们的构造函数。

    console.log(people1.constructor == People) // true
    console.log(people2.constructor == People) // true
    

    4、构造函数模式的问题

    构造函数很好用,但是存在一个消耗内存的问题。
    下面为People对象添加一个不变的属性height,那么People原型对象就变成这样:

    function People (name,age){
      this.name = name;
      this.age = age;
      this.height = 170;
      }
    

    还是采用同样的方法生成实例:

    var people1 = new ('people1',12);
    var people2 = new('people2',13);
    console.log(people1.height); // 170
    

    表面上看没什么问题,但是实际上这样做有一个很大的弊端。那就是对于每一个实例对象,height属性都是一样的内容,每生成一个实例都会为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。

    console.log(people1.height == people2.height) // false
    

    能不能让height属性在内存中只生成一次,然后所有实例都指向那个内存地址呢?答案是可以的。

    5、Prototype模式

    javascript规定,每一个构造函数都有prototype属性,指向另外一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
    这就意味着,我们可以把那些不变的属性和方法直接定义在prototype对象上。

    function People (name,age){
      this.name = name;
      this.age = age;
      }
    People.prototype.height = 170;
    People.prototype.say = function(){console.log('hello)};
    

    生成实例。

    var people1 = new ('people1',12);
    var people2 = new ('people2',13);
    console.log(people1.height); // 170
    console.log(people1.say()); // hello
    

    这时所有实例的height属性和say方法,其实都是一个内存地址,指向prototype对象,因此就提高了运行效率。

    console.log(people1.height == people2.height) // true
    

    6 、Prototype模式的验证方法

    为了配合prototype属性,javascript定义了一些方法辅助我们使用它们。

    6.1、isPrototypeOf()

    这个方法用来判断,某个prototype对象和某个实例对象之间的关系

    console.log(People.prototype.isPrototypeOf(people1)); //true
    console.log(People.prototype.isPrototypeOf(people2)); //true
    

    6.2、 hasOwnProperty()

    每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。

    console.log(people1.hasOwnProperty("name")); // true
    console.lot(people1.hasOwnProperty("height")); // false
    

    二、构造函数的继承

    我们已经知道了如何封装属性和方法,以及如何从原型对象上生成实例,
    但是如何实现对象之间的继承呢?
    比如现在有一个‘人’对象的构造函数。

    function People (){
      this.name = 'people';
    }
    

    还有一个‘中国人’对象的构造函数。

    function Chinese (height) {
      this.height = height ;
    }
    

    怎么才能使‘中国人’继承‘人’呢?

    1.构造函数绑定

    第一种方法也是最简单的办法,使用apply或者call方法,将父对象的构造函数绑定在子对象上。

    function Chinese (height) {
      People.call(this, arguments);
      this.height = height ;
    }
    var chinese1 = new Chinese();
    console.log(chinese.name); // people
    

    2、prototype模式

    这种方法更常见,使用prototype属性
    如果‘中国人’的prototype对象指向People实例,那么所有的‘中国人’的实例,就能继承People了。

    Chinese.prototype = new People();
    Chinese.prototype.constructor = Chinese;
    var chinese1 = new Chinese();
    console.log(chinese.name); // people
    

    代码的第一行,我们将Chinese的prototype对象指向一个People实例。

    Chinese.prototype = new People();
    

    它相当于完全删除了prototype对象原先的值,然后赋了一个新的值。第二行代码又是在干什么呢?

    Chinese.prototype.constructor = Chinese;
    

    原来任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有‘Chinese.prototype = new People()’这一句话,Chinese.prototype.constructor是指向Chinese的,现在是指向People的。

    console.log(Chinese.prototype.constructor == People); // true
    

    一次在加上Chinese.prototype.constructor = Chinese这句以后,Chinese.prototype.constructor指向了Chinese;

    console.log(Chinese.prototype.constructor == Chinese); // true
    

    更重要的是每一个实例也有constructor属性,默认调用prototype对象的constructor属性。

    console.log(chinese1.constructor == Chinese.prototype.constructor); // true
    

    因此在运行‘Chinese.prototype = new People();’之后,chinese1.constructor也指向了People。

    console.log(chinese1.constructor == People); //true
    

    这显然会造成继承链的混乱,people1明明是People生成的,因此我们必须手动纠正,将Chinese.prototype对象的constructor值改为Chinese。这是很重要的一点,编程时一定要遵守。

    3、直接继承prototype

    第三种方法是对第二种方法的改进。在People对象中,不变的属性可以直接写入People.prototype。所以我们可以让Chinese跳过People直接继承People.prototype.
    实现方式如下:

    function People(){};
    PeoPle.prototype.name = 'people';
    

    然后将将Chinese的prototype对象指向People.prototype,这样就完成了继承。

    Chinese.prototype = Chinese.prototype;
    Chinese.prototype.constructor = Chinese;
    var chinese1 = new Chinese();
    console.log(chinese.name); // people
    

    与前一种方法相比,这样做的优点是效率高(不用执行和建立People实例),就比较省内存,缺点就是Chinese.prototype和People.prototype都指向了同一个对象,那么任何对Chinese.prototype的修改也同时反映到了People.prototype上。
    所以上面的这一段代码是有问题的。

    Chinese.prototype.constructor = Chinese;
    

    这一句话实际上也把People.prototype对象的constructor属性也改了。

    console.log(People.prototype.constructor ); // Chinese
    

    4、利用空对象作为中介

    由于直接继承prototype存在上述的缺点,所以我们可以用一个空对象作为中介。

    var F = function(){};
    F. prototype = People.prototype;
    Chinese.prototype = new F();
    Chinese.prototype.constructor = Chinese;
    

    F 是一个空对象,几乎不占内存。这时修改Cat的prototype对象就不会影响到People的prototype对象。

    console.log(Chinese.prototype.constructor ); // Chinese
    

    我们将上面的方法封装成一个方法,便于调用。

    function extend(Child,Parent){
      var F = function(){};
      F. prototype = Parent.prototype;
      Chind.prototype = new F();
      Child.prototype.constructor = Chind;
      Child.uber = Parent.prototype;  
    }
    

    使用方式如下

    extend(Chinese,People);
    var chinese1 = new Chinese();
    console.log(chinese.name); // people
    

    另外说明一点,函数最后一行

      Child.uber = Parent.prototype;  
    

    意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

    5、拷贝继承

    上面采用prototype对象实现继承,我们可以换一种思路,纯粹采用拷贝方法实现继承。简单说如果把父对象的所以属性和方法拷贝进子对象,也能实现继承。

    function extend2(Child,Parent){
      var p = Parent.prototype;
      var c = Child.prototype;
      for(var i in p){
      c[i] = p[i];
      c.uber = p;
      }
    }
    

    这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。
    使用的时候,这样写:

    extend(Chinese,People)
    var chinese1 = new Chinese();
    console.log(chinese.name); // people
    

    三、非构造函数的继承

    前面提到的都是构造函数实现的继承,那么不使用构造函数,怎么能实现继承呢?

    1、什么是非构造函数的继承

    比如我有个对象叫“中国人”。

    var Chinese = {
      nation:'中国'
    }
    

    还有一个对象叫“医生”。

    var Doctor = {
      career:'医生'
    }
    

    那要怎样才能让“医生”去继承“中国人”,也就是说,要怎样才能去生成一个中国医生。这两个都是普通对象不是构造函数,无法用构造函数的方式实现继承。

    2、object()方法

    function object(o){
      function F (){}
      F.prototype = o;
      returen new F();
    }
    

    这个object函数只做一件事,就是把子对象的prototype指向父对象,从而使得子对象与父对象连在一起。使用的时候先在父对象的基础上生成子对象:

    var Doctor = object(Chinese);
    

    在加上子对象本身的属性

    Doctor.career = '医生';
    

    这时子对象已经继承了父对象的属性了。

    console.log(Doctor.nation); // 中国
    

    3、浅拷贝

    除了使用‘prototype链’以外,我们可以把父对象的属性全部拷贝给子对象,也能实现继承。
    请看下面这个函数 :

    function extendCopy(p){
      var c = {};
      for (var i in p){
        c[i] = p[i]
        }
      c.uber = p;
      return c;
    }
    

    使用的时候,这样写:

    var Doctor = extendCopy(Chinese);
    Doctor.career = '医生';
    console.log(Doctor.nation); // 中国
    

    但是这样拷贝有一个问题。那就是如果父对象的属性等于数组或是另外一个对象,那么实际是,子对象获取的只是一个内存地址,而不是真正的拷贝,因此存在父对象被篡改的可能。
    请看,现在给Chinese添加一个‘出生地’属性,它是一个数组。

    Chinese.birthPlaces = ['成都','上海','北京'];
    

    通过extendCopy()函数,Doctor继承了Chinese。

    var Doctor = extendCopy(Chinese);
    

    然后我们为Doctor的出生地添加一个城市:

    Doctor.birthPlaces.push('厦门');
    

    发生了什么事?Chinese的‘出生地’也被改掉了!

    console.log(Doctor.birthPlaces); // '成都','上海','北京','厦门';
    console.log(Chinese.birthPlaces); // '成都','上海','北京','厦门';
    

    所以,extendCopy()函数只是拷贝基本类型的数据,我们把这种拷贝叫做浅拷贝。

    4、深拷贝

    所谓深拷贝,就是能够实现真正意义上的数组和对象的拷贝。实现并不难,只要递归调用浅拷贝就行了。

    function deepCopy(p,c){
      var c = c || {};
      for(var i in p){
        if(typeof p[i] == 'object'{
          c[i] = (p[i].constructor == Array) ? [] : {};
          deep(p[i],c[i]);
        }else { 
          c[i] = p[i];
        }
      }
      return c;
    }
    

    使用的时候这样写:

    var Dcotor = deepCopy(Chinese);
    

    现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:

    Chinese.birthPlaces = ['成都','上海','北京'];
    Doctor.birthPlaces.push('厦门');
    

    这时父对象就不受影响了。

    console.log(Doctor.birthPlaces);// '成都','上海','北京','厦门';
    console.log(Chinese.birthPlaces); // '成都','上海','北京';
    
    

    相关文章

      网友评论

          本文标题:javascript面向对象

          本文链接:https://www.haomeiwen.com/subject/hxejlxtx.html