再说面向对象(2)

304 阅读7分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

一、创建对象的模式

在上一篇文章中我从一个更深的层次介绍了对象(注:建议阅读 再说js中的面向对象(一)),了解了对象属性的属性。那么今天我们开启第二个阶段,面向对象的封装。封装是真正意义上的面向对象实际功能的实现。

在上一篇文章中,我们说过面向对象最大的优势就在于降低代码的非必要重复,提高代码的可维护性,降低服务器的压力。

面向对象有很多种封装方式,这里我们只探讨常用的,具有代表性的。如单例模式、工厂模式、构造函数模式以及基于构造函数的原型模式。

1.1 单例模式

所谓单例模式就是把描述同一个事物(同一个效果或者方法)的属性和方法放在一个对象当中。

单例模式作用是起到了分组的作用,这样一来,不同事物之间的属性或者方法即使名字相同(属性名和方法名),也不会互相感染,种分组编写代码的方式称为单例模式。单例模式是项目开发过程中最为常用的开发方式,也是实现模块化开发的最简便的方式。我们来看看单例模式:

例如我们要实现一个编辑的方法:

var edit={
   copy:function(source,target){},
   delete:function(target){},
   back:function(){}
};

这些方法只需要定义一次,当我们需要使用edit中的方法时,我们只需要edit.copy(a,b);edit.delte(c)…即可,我们不需要重复定义copy和delete方法,这样便降低了代码的耦合度,而且在Node和Angular中,自定义模块导出时也常用单例模式。单例模式结合js高阶编程技巧中的惰性思想可以十分方便的封装一些小的方法库; 这里简单示例:

例如我们要培养FE:

var tools=(function(){
   var isStandarBrowser = "getComputedStyle" in window;

    function jsonParse(jsonStr) {
        return "JSON" in window ? JSON.parse(jsonStr) : eval("(" + jsonStr + ")");
    }

    function listToAry(likeAry) {
        try {
            return Array.prototype.slice.call(likeAry, 0);
        } catch (e) {
            var ary = [];
            for (var i = 0; i < likeAry.length; i++) {
            ary.push(likeAry[i]);
        }
    }
        return ary;
    }

    function getRandom(n, m) {
        n = Number(n);
        m = Number(m);
        if (isNaN(n) || isNaN(m)) {
            return Math.random();
        }
        if (n > m) {
            var tem = m;
            m = n;
            n = tem;
        }
        return Math.round(Math.random() * (m - n) + n);
    }
       return {
          getRandom: getRandom,
          listToAry: listToAry,
          jsonParse:jsonParse
      }
 })();

1.2 工厂模式

单例模式虽然有了命名空间,但是他是一个写死的对象,如果我们要创建一个包含方法的单例,单例就做不到了。所以工厂模式应运而生。不同于单例模式,工厂模式是把实现一个事物的代码封装到一个函数中,每当我们需要实现这个事物时,只需执行这个函数即可;示例:

  function developeFE(student,skill){
       var objFE={};
       objFE.name=student;
       objFE.skill=skill;
        return objFE;
  };
     frontEngineer=developeFE(Binary, angular);//将Binary培养成一个会angular的前端工程师
     console.log(frontEngineer);//{name:Binary,skill:angular}
  • 构造函数模式:工厂模式虽然解决了创建多个相似的对象的问题,但是却没有解决对象识别的问题,(即我们不知道对象是哪个类的实例);这时候构造函数模式应运而生;构造函数是自定义一个类,并且通过new 运算符创建这个自定义类的实例;并且实现了实例识别,通过new A创建的实例,是A类的实例,通过new B创建的实例是B类的实例;【这里有必要强调一下,js中的类都是函数数据类型的,只不过他通过new执行是成为了一个类,但是他是一个函数的本质从未改变】;示例
  function DevelopFE(stu,skill){
   this.name=stu;
   this.skill=skill;
   this.saySkill=function(){alert(this.skill)}
  };
  
  var FE=new DevelopFE(Binary,nodejs);//FE是DevelopFE的一个实例;
  console.log(FE);//{name:Binary,skill:nodejs, saySkill: function(){alert(this.skill)};

1.3 构造函数中的注意事项

  1. 构造函数区别于工厂模式,首先我们不需要自己创建空对象,第二也不需要我们手动return,第三,向实例添加属性使用this关键字;通过this关键字添加的属性,默认都是enumerable(可枚举)的;
  2. 构造函数创建一个实例,必须使用new操作符;使用new操作符实际是经历了四步: 2.1 创建一个新对象; 2.2 将this指向这个新对象 2.3 执行构造函数中的代码 2.4 将新对象返回
  3. 构造函数中的this是当前构造函数的实例,这是实例识别的实现原理;
  4. 构造函数中许可以随意return,如果return一个基本数据类型的值,实例不会改变,如果return一个引用数据类型,实例将会被返回值所替代;
  5. 使用this关键字添加给实例的属性是实例的私有属性,是受命名空间的保护的,与其他实例(含当前类的实例)无关;检测私有属性的方法obj.hasOwnProperty(key);返回值是布尔值,true表示是该实例的私有属性,false则不是;

1.4 原型模式

原型模式:构造函数在工作中也很常用,构造函数很完美,也很方便,但是他也不是没有缺点的,比如说在前面构造函数的例子中,每一个FE都应该具备saySkill的方法,所以这应该是一个共用的方法,没有必要每个实例身上都有一份,这说明一个问题呢?说明构造函数模式虽然解决了工厂模式的实例识别的问题,但是并没有实现面向对象中公有方法共用;这时候基于构造函数的原型模式诞生了;其实,从逻辑上来讲,我们也应该能想到:既然要方法共用,那就不应该放在实例上,应该在构造函数身上找一个公共的地方来存放这些方法,而这个公共的地方需满足实例能访问,构造函数可以访问的要求;这个地方就是原型prototype,原型定义在构造函数身上,构造函数可以通过 . 访问,实例通过__proto__访问;

如果想理解原型模式及原型链,请将下面的段落熟读5遍; 每一个函数数据类型(普通函数、类),都有一个天生自带的属性prototype(原型,下称原型),并且他是一个对象数据类型的【公共的地方,定义在构造函数上】;

并且在prototype上天生自带constructor(构造函数)属性,constructor的值是当前原型的类本身;

每一个对象数据类型(原型,实例,普通对象)都有一个__proto__属性,该属性的值是一个指针,指向该实例(对象)所属类的prototype。【实例也可以找得到】 示例:

function DevelopFE(stu,skill){
   this.name=stu;
   this.skill=skill;
};
DevelopeFE.prototype.saySkill=function(){alert(this.skill)};

原型的逻辑就是这样,但是实例又是怎样访问原型的呢?这里就引入了“原型链”,原型链和作用域链很相似,都是链式查找的;

二、原型链机制

当我们调用实例的方法或者读取实例的属性时,机制会现在私有属性中查找,如果找到就读取调用;如果找不到,会读取 __proto__ 的属性值,通过 __proto__ 指向去实例所属类的原型上查找,找到就读取调用,如果还没找到,就通过这个类的__proto__指向的原型找,一直找到Object上,Objcet是所有类的基类,通俗说就是找到头了。因Object是基类,他没有__proto__。这里很多人可能很疑惑,类怎么也有__proto__,请大家不要忘记,在js中大到DOM、BOM小到key都是一个对象,所谓js中万物皆对象;

三、原型模式细节问题

  1. 在IE中为避免我们修改原型上的属性或者方法,__proto__ 被禁用,如果我们想修改,只能用 .prototype 的方式来修改;

  2. constructor 问题,在我们手动更换类的原型的时候,constructor 指向也可能改变,示例

var obj={
   this.sayExprience=function(years){alert(years)}
}
console.log(DevelopeFE.prototype.contructor)// DevelopeFE
DevelopeFE. prototype=obj;
console.log(DevelopeFE.prototype.contructor)// Object

这并不是我们想要的,如果此时我们用 constructor 检测数据类型,这时结果就会出现偏差;所以如果更好原型,记得修改constructor指向。