首页 > 技术文章 > Js继承各模式总结

thewaytomakemiracle 2016-11-28 12:46 原文

  最近一直在反复看javascript高级程序设计,每一次翻看都有新的体悟。这一次来总结一下js对象继承的一些东西。

1.工厂模式

     工厂模式是一种古老的模式,所谓工厂,从字面上理解就是批量化生成相同的产品。比如某一个对象拥有属性a,b,c又拥有方法say。那么使用工厂模式可以轻松的构造多个相似的实例出来。下面写一个简单的工厂模式方法。

function factoryObj(name,age,job){
    var sObj = new Object();
    sObj.name = name;
    sObj.age= age;
    sObj.job= job;
    sObj.sayName = function{
       console.log(this.name);
    };
    return sObj;
}

var p1 = factoryObj("a",12,"worker");
var p2 = factoryObj("b",16,"coder");

  函数factoryObj接受三个参数已构建实例,里面包含了三个属性和一个方法。由于是用函数表达式创建的sayName,所以this指向的是函数调用对象即sObj。你可以无限的创建p实例,但是我们却无法识别实例的类型。因为我们创建的方法都是通过factoryObj实现的。

  那么有没有方法既可以创建相似的实例又可以识别实例的类型呢?

2.构造函数模式

  ES中的构造函数可以用来创造特定类型的对象,其实我们使用的Array就是一个特定类型的对象,我们可以清楚的知道这个实例的类型是Array。同样,ES中的构造函数也支持我们自定义一类对象。下面写一个简单的构造函数模式方法。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        console.log(this.name);  
    }
}

var p1 = new Person("a",12,"worker");
var p2 = new Person("b",16,"coder");
    

  在上面这个例子中,我们没有在person函数中显示的构造一个obj,反而将参数全部赋予this对象并且我们没有return语句。我们在创建p1,p2实例时使用了new操作符。用这种方式调用构造函数,实际上会经历四个步骤。1.创建一个新对象。2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)。3.执行构造函数中的代码(为新对象增添属性和方法)。4.返回新对象。

     所以,我们通过new操作符实际上实现了工厂模式方法中显示构造对象,赋予对象属性和方法,返回对象三个步骤。

     在上述例子中,p1和p2保存了一个constructor属性,该属性指向了Person函数。如何证明:我们只要console.log(p1.constructor == Person)即可获取答案true(对象的constructor属性是用来标示对象类型)。

  构造函数除了调用方法(使用new操作符)与其他函数不同,它本质上就是一个函数。任何函数只要通过new操作符调用,实际上它就作为了一个构造函数调用(经历4个步骤)。如何来理解,请看下面的代码。

//作为普通函数调用
Person("a",12,"worker");
window.sayName();//"a"

//在另一个函数的作用域中调用
var o = new Object();
Person.call(o,“a”,12,"worker");
o.sayName();  //"a"

  没有使用new操作符,所以this对象指向的是window,所以通过window来调用sayName获取了a。使用call则可改变其作用域为o,使用o来调用sayName就能获取a。

  构造函数模式很好用,但是它也有问题。因为每生成一个实例,其中的方法就要重复创建一次。p1和p2都有一个sayName,即便它们的功能都是一样的。(这两个sayName并不是同一个Function实例。alert(p1.sayNmae == p2.sayName) //false)。

  创建两个完成同样内容的函数没有必要,所以我们可以将函数的定义转移到构造函数外部。这样一来,虽然实现了方法没有重复创建只是通过this指针来指向一个全局作用域中方法的功能,但是实际上破坏了封装性。那么有没有其他的方法可以解决这个问题,原型模式应运而生。

3.原型模式

  我们创建的每一个函数都有一个prototype属性,这个属性是一个指针指向一个对象,这个对象就是所谓的原型。使用原型的好处就是可以让所有的对象实例都共享它所包含的属性和方法。下面写一个简单的原型模式方法。

function Person(){};
Person.prototype.name = "a";
Person.prototype.age= 12;
Person.prototype.job= "worker";
Person.prototype.sayName = function(){
   console.log(this.name); 
};

var p1 = new Person();
var p2 = new Person();

  如上所述,person的构造函数成为了一个空函数,我们将属性和方法都添加到了person的prototype上。新对象的属性和方法都是共享的。每当代码读取某个对象属性时,都会执行一次搜索,目标是具有给定标识符的属性。搜索从实例本身开始,如果找到了就返回,没找到则沿着prototype往上搜,即搜索原型。虽然我们可以通过实例访问保存在原型中的值,但是却不能通过实例改写原型中的值。如果我们在实例中重写了某个属性和方法,这个属性和方法就会屏蔽掉原型中的属性和方法,原因是因为搜索是从本身开始的,一旦搜索到了就直接返回该属性和方法,不会继续往上搜索。当然,如果我们使用delete操作,删除了实例中重写的属性和方法,那么我们可以继续获得原型中的属性和方法。检测某一个属性和方法是否是自己本身就有的,还是原型上的,js提供了hasOwnProperty这个方法来实现。

  原型模式的最大问题在于其在共享上的本质。由于省略了构造函数传递参数的方法,所以,所有的实例都是同样的模子,仿佛克隆出来的一样。这本身看上去没有什么太大的问题,但是一旦我们在属性中加入了引用类型就产生了bug。因为不同的实例通过同样的方式获取的引用类型,其实质是用指针指向了同一块堆内存,如果我们进行了一个实例的修改,另外一个实例也会进行修改。这样一来,我们就不能得到自己想要的结果。

 

推荐阅读