JavaScript创建对象、原型与继承

      JavaScript是一门极其灵活的语言,烂七八糟的设计是它最大的优点。不同于其他严格类型的语言例如java,学习曲线比较友好。JavaScript个人感觉上手基本不用费劲,要想上高度那就是一个悲催而且毁三观的故事。特别是有面向对象语言基础的人来说,JavaScript真像一个噩梦。JavaScript更加的零碎,封装的不是很好。你必须理清脉络深入理解了,才能写出来高大上的优雅的代码。在下尽量的用简练易懂的语言,简单的阐述一下我对JavaScript面向对象的一点粗浅的理解。

1,要想面向对象先得创建对象

a,原始模式

var object = new Object();
object.name="huazi";
object.age="22";
object.sayHi = function(){
       console.log("hi"+this.name);
}

object.sayHi(); //hi huazi

首先通过一个名称为object的对象,为其添加name和age属性以及一个sayHi的方法。this.name将会被解析成object.name。这种方式的缺点显而易见:使用同一个接口(Object)创建对象,产生大量的冗余代码。所以一般不会这么使用。

b,工厂模式

function createObject(name,age){
      var obj = new Object();
      obj.name=name;
      obj.age=age;

      obj.sayHi=function(){
            console.log("hi "+this.name);
      }
      return obj;
}

var obj = createObject("huazi",22);
console.log(obj.sayHi(); //hi huazi

此种方式创建对象叫做工厂模式,你给我属性,我给你个包含这些属性和方法的对象.这种模式创建的对象也有很大的问题:得到的对象属于什么类型的是不确定的.instanceof发现只能匹配到Object,不能匹配到createObject。

c,构造器模式

function createObject(name,age){
    this.name=name;
    this.age=age;
    this.sayHi=function(){
        console.log("hi "+this.name);
    }
}
var obj = new createObject("huazi",22);
obj.sayHi(); //hi huazi
obj instanceof createObject;//true

构造器模式创建对象的过程分为四步:1,创建一个Object类型对象.2,将执行环境交给这个对象(this指向).3,执行构造很熟.4,返回对象.此种方式解决了类型不确定的问题.但是缺点是,在每次创建对象的过程中,都会重新创建类如sayHi的对象.而这明显是不必要的.修改如下:

function createObject(name,age){
    this.name=name;
    this.age=age;
    this.sayHi=sayHi;
}
function sayHi(){
    console.log("hi "+this.name);
}
var obj = new createObject("huazi",22);
obj.sayHi(); //hi huazi
obj instanceof createObject;//true

很简单,只需要把函数放到全局作用域即可。可是问题又来了,全局作用域中的函数只能被某一个对象调用。这在逻辑上实在有点牵强。更严重的情况是,往往我们需要定义很多方法来实现一个对象。所以就会出现大量的全局函数,并且全局函数不能在其他对象上使用。

d,原型模式

function createObject(){};
createObject.prototype.name = "huazi";
createObject.prototype.age=22;
createObject.prototype.sayHi=function(){
    console.log("hi "+this.name);
}

var obj1 = new createObject();
var obj2 = new createObject();
obj1.sayHi(); //hi huazi
obj1.sayHi===obj2.sayHi; //true

好了,现在好像解决了刚才的问题,把属性和方法都加到了原型中。这样就不会出现全局属性和重复函数对象了。这种模式的缺点也显而易见:构造不能传参,也就是说所有对象将长的一模一样。还有就是内存共享的问题。属性要是引用类型比如Array那么就热闹了。牵一发动全身。

e,组合模式(构造+原型)

function createObject(name,age){
    this.name=name;
    this.age=age;
    this.array = [1,2,3];
}
createObject.prototype.sayHi = function(){
    console.log("hi "+this.name);
}

var obj1 = new createObject("huazi",22);
var obj2 = new createObject("huazi",22);
obj1.sayHi(); //hi huazi
obj1.array===obj2.array;//false

除了此种写法之外,在给原型加方法的时候还可以使用字面量的方式添加。但是需要注意的是使用字面量添加等于重写了prototype了,所以需要显示的申明,constructor的指向。

createObject.prototype={
    constructor:createObject,
    sayHi:function(){
        console.log("hi "+this.name);
    }
}

还有需要注意的一点是,在使用字面量之前不能创建对象。前面说过此种方式等于是重写了prototype。所以之前创建的对象实例,不会更新新的属性和方法。为啥?自行脑补一下。

f,动态原型模式

function createObject(name,age){
    this.name=name;
    this.age=age;
    if(typeof this.sayHi != "function"){  //不存在的情况下添加方法
        createObject.prototype.sayHi = function(){
            console.log("hi "+this.name);
        }
    }
}

var obj1 = new createObject("huazi",22);
var obj2 = new createObject("saint",22);
obj1.sayHi(); //hi huazi
obj2.sayHi(); //hi saint

这种方式就算比较完美的了。但是还还需要注意,在构造内部不能使用字面量的方式去添加原型属性。回溯到构造创建对象过程,我们知道第一步就已经创建好对象了。所以使用字面量也会发生意外的事情。

g,寄生构造模式和稳妥模式

寄生构造模式的思路是:创建一个函数,这个函数的职责就是封装一个对象所需要的所有属性和方法,然后返回对象。从样子上来看,此种方式几乎与工厂模式无异。只是早创建对象的时候方式有些变化。就是使用new关键字来创建。

function createObject(name,age){
    var obj = new Object();
    obj.name=name;
    obj.age=age;
    obj.sayHi=function(){
        console.log("hi "+this.name);
    }
    return obj;
}
var o  = new createObject("huazi",22);
o.sayHi();

再回忆一下构造模式创建对象的最后一步,返回对象。寄生模式其实是覆盖了返回对象的那一步。同时此种模式也没有摆脱类型不能确定的问题。那么此种模式在什么时候可以用到呢?

function createObject(){
    var array = new Array();
    //Array构造中创建对象是使用push的
    array.push.apply(array,arguments);
    //添加新的方法
    array.toPipeString=function(){
        return this.join("|");
    }
    return array;
}
var o  = new createObject("huazi",22);
o.toPipeString(); //huazi|22

为Array添加了一个方法,同时没有修改Array的构造和原型。实际上就还对Array的二次包装,添加新的方法。这种方式比较靠谱。安全性也比较好,够封闭。

还有一种非常安全的创建对象模式叫做稳妥模式。与寄生模式非常的类似

function createObject(name,age){
    var obj = new Object();
    obj.sayHi=function(){
        console.log("hi "+name);
    }
    return obj;
}
var o  = new createObject("huazi",22);
o.sayHi(); //hi huazi

可以发现,name值只能在sayHi中访问,在构造中包装的数据是绝对安全可靠的。这就有点private的意思了。sayHi是暴露给外部的接口。和寄生模式一样采用稳妥模式创建的对象,类型是无法确定的。

2,原型

       无论什么时候只要是创建了一个新函数,随之就会根据一组特定的规则为该函数创建一个prototype属性。在默认情况下prototype属性会自动获得一个constructor(构造函数)属性,这个函数非常的特殊。包含一个指向prototype属相所在函数的指针。

function createObject(name,age){}
var o  = new createObject("huazi",22);
console.log(createObject.prototype.constructor);//function createObject(name,age) {}
console.log(createObject.prototype)//createObject(name,age) {}
//判断对象o是否是原型对象的派生。
console.log(createObject.prototype.isPrototypeOf(o));//true

      实际上是这样的。每个对象都会有一个_proto_属性,这个属性指向的是函数原型createObject.prototype。而crateObject.prototype中存在一个constructor属性,此属性指向了createObject构造函数。等于指来指去,指出了一个回路。isPrototypeOf函数的参数是函数对象实例,作用是判断该实例的归属,是否是该原型对象的派生。使用hasOwnProperty()可以检测一个方法是存在于原型中还是存在于实例中,当然前提是确定可以访问这个方法或者这个方法存在才能确定。使用delete操作符可以删除掉实例中的方法或者属性。在原型中的属性方法默认是不能被delete的。还有一个坑就是delete不存在的属性或者方法也会返回true所以在使用delete的时候需要小心一些。

      在给prototype添加属性之前创建了一个对象,那么这个对象是否可以引用新添加的原型上的属性呢?答案是可以的。在访问属性的时候首先会查看实例对象是否存在此属性,不然就去原型找。而_proto_就是指向原型对象的。没错是指向,所以不管何时更新原型属性都是ok的。

3,继承

      ECMAScript中描述了原型链的概念,并说明原型链将作为实现继承的主要方法。原型链顾名思义,就是讲原型链接起来实现继承。子类的prototype指向父类的实例对象就是很简单的一种实现。

function superClass(){
    this.name="huazi";
}
superClass.prototype.getName=function(){
    console.log(this.name);
}
function childClass(){
    this.name="bob";
}
childClass.prototype = new superClass();

childClass.prototype.getName = function(){
    console.log(this.name+"v");
}

var instance = new childClass();
//重写了父类方法和属性
instance.getName();    //bobv
console.log(instance instanceof Object); //true
console.log(instance instanceof childClass);//true
console.log(instance instanceof superClass);//true

console.log(Object.prototype.isPrototypeOf(instance));//true
console.log(superClass.prototype.isPrototypeOf(instance));//true
console.log(childClass.prototype.isPrototypeOf(instance));//true

和其他的情况一样,一旦使用到prototype,就不建议使用字面量的方式为原型添加属性了。脑补即可。

同时此种不加区分的继承方式,任然保留内存共享的问题(引用类型属性的问题)。同原型模式创建对象的问题是一样的。当然解决这个问题的办法和解决创建对象时的方法也是一样的,那就是混合使用构造和原型来实现继承。

function superClass(name){
    this.name=name;
    this.number=[1,2,4];
}
superClass.prototype.getName=function(){
    console.log(this.name);
}
function childClass(name,age){
    //继承属性
    superClass.call(this,name);
    this.age=age;
}
childClass.prototype = new superClass();

childClass.prototype.getName = function(){
    console.log(this.name+"v");
}

var instance = new childClass("huazi",22);
//重写了父类方法和属性
instance.getName();    //huaziv
instance.number.push("1");
instance.number===new childClass().number;  //false
instance.number//[1,2,4,'1']

此种方法的原则就是属性靠构造,方法靠原型。可以简单地这么理解一下。此种模式也有个问题就是效率没有达到极致,因为多次调用了父类构造。还有一个让人不舒服的地方就是原型和实例上都包括有name和age属性。这是不能接受的。

最后放一个大招,叫做寄生组合模式。其实也就是避免了上面这个方式的缺点,即绕过父类构造来继承父类原型。

/**
 * 1,找到父类原型对象
 * 2,修改构造的指向
 * 3,父类原型 
 */
function inheritPrototype(superClass,childClass){
    var _prototype = new Object(superClass.prototype);
    _prototype.constructor = childClass;
    childClass.prototype=_prototype;
}

function superClass(name){
    this.name=name;
    this.number=[1,2,4];
}
superClass.prototype.getName=function(){
    console.log(this.name);
}
function childClass(name,age){
    //继承属性
    superClass.call(this,name);
    this.age=age;
}
//不同点就再这
inheritPrototype(superClass,childClass);

childClass.prototype.getName = function(){
    console.log(this.name+"v");
}

var instance = new childClass("huazi",22);
//重写了父类方法和属性
instance.getName();    //huaziv
instance.number.push("1");
instance.number===new childClass().number;  //false
instance.number//[1,2,4,'1']

恶心死我了,终于写完了!

原文地址:https://www.cnblogs.com/huaziWEB/p/4491994.html