javascript面向对象与原型

ECMAScript有两种开发模式:1.函数式(过程化)2.面向对象(OOP)

面向对象的语言有一个标志,那就是类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但是,ECMAScript没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。

一、创建对象

创建一个对象,然后给这个对象新建属性和方法。

var laber = new Object();                     //创建一个Object对象
laber.name = 'momo';                          //创建一个name属性并赋值
laber.age = 28;                               //创建一个age属性并赋值
laber.run = function () {                     //创建一个run()方法并返回值
    return this.name + this.age + '运行中...';
};
alert(laber.run());                           //输出属性和方法的值

上面创建了一个对象,并且创建属性和方法,在run()方法里的this,就是代表laber对象本身。这种是JavaScript创建对象最基本的方法,但有个缺点,想创建一个类似的对象,就会产生大量的代码。

var laber2 = laber;                        //得到laber的引用
laber2.name = 'xiaoxiao';                        //直接改变了name属性
alert(laber2.run());                        //用laber.run()发现name也改变了

var laber2= new Object();
laber2.name = 'xiaobo';
laber2.age = 28;
laber2.run = function () {
    return this.name + this.age + '运行中...';
};
alert(laber2.run());                        //这样才避免和laber混淆,从而保持独立

为了解决多个类似对象声明的问题,我们可以使用一种叫做工厂模式的方法,这种方法就是为了解决实例化对象产生大量重复的问题。

function createObject(name, age) {        //集中实例化的函数
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.run = function () {
        return this.name + this.age + '运行中...';
    };
    return obj;
}

var laber1 = createObject('momo', 28);        //第一个实例
var laber2 = createObject('xiaoxiao', 27);        //第二个实例
alert(laber1.run());
alert(laber2.run());                        //保持独立

工厂模式解决了重复实例化的问题,但还有一个问题,那就是识别问题,因为根本无法搞清楚他们到底是哪个对象的实例。

ECMAScript可以采用构造函数(构造方法)可用来创建特定的对象。类型于Object对象。

function Laber(name, age) {                //构造函数模式
    this.name = name;
    this.age = age;
    this.run = function () {
        return this.name + this.age + '运行中...';
    };
}

var laber1 = new Laber('momo', 28);            //new Laber()即可
var laber2 = new Laber('xiaobo', 27);
alert(laber1 .run());
alert(laber1 instanceof Laber);                //很清晰的识别他从属于Laber

使用构造函数的方法,即解决了重复实例化的问题,又解决了对象识别的问题,但问题是,这里并没有new Object(),为什么可以实例化Box(),这个是哪里来的呢?

使用了构造函数的方法,和使用工厂模式的方法他们不同之处如下:

1.构造函数方法没有显示的创建对象(new Object())

2.直接将属性和方法赋值给this对象;

3.没有renturn语句。

构造函数的方法有一些规范:

1.函数名和实例化构造名相同且大写,(PS:非强制,但这么写有助于区分构造函数和普通函数)

2.通过构造函数创建对象,必须使用new运算符。

既然通过构造函数可以创建对象,那么这个对象是哪里来的,new Object()在什么地方执行了?执行的过程如下:

1.当使用了构造函数,并且new 构造函数(),那么就后台执行了new Object()

2.将构造函数的作用域给新对象,(new Object()创建出的对象),而函数体内的this就代表new Object()出来的对象。

3.执行构造函数内的代码;

4.返回新对象(后台直接返回)

关于this的使用,this其实就是代表当前作用域对象的引用。如果在全局范围this就代表window对象,如果在构造函数体内,就代表当前的构造函数所声明的对象。

var laber= 2;
alert(this.laber);                            //全局,代表window

构造函数和普通函数的唯一区别,就是他们调用的方式不同。只不过,构造函数也是函数,必须用new运算符来调用,否则就是普通函数。

var laber= new Laber('momo', 28);            //构造模式调用
alert(laber.run());

Laber('momo', 28);                            //普通模式调用,无效

var o = new Object();                    
Laber.call(o, 'xiaobo', 27)                    //对象冒充调用
alert(o.run());            

二.原型

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法。逻辑上可以这么理解:prototype通过调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。

function Laber() {}                            //声明一个构造函数

Laber.prototype.name = 'momo';                    //在原型里添加属性
Laber.prototype.age = 28;                    
Laber.prototype.run = function () {                //在原型里添加方法
    return this.name + this.age + '运行中...';
};

比较一下原型内的方法地址是否一致:

var laber1 = new Box();
var laber2 = new Box();
alert(laber1.run == laber2.run);                    //true,方法的引用地址保持一致

在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。__proto__属性是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性constructor。通过这两个属性,就可以访问到原型里的属性和方法了。

判断一个对象是否指向了该构造函数的原型对象,可以使用isPrototypeOf()方法来测试。

alert(Laber.prototype.isPrototypeOf(laber));        //只要实例化对象,即都会指向

原型模式的执行流程:

1.先查找构造函数实例里的属性或方法,如果有,立刻返回;

如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;

虽然我们可以通过对象实例访问保存在原型中的值,但却不能访问通过对象实例重写原型中的值。

var laber1 = new Laber();
alert(laber1 .name);                            //momo,原型里的值
laber1.name = 'xiaobo';
alert(laber1.name);                            //xiaobo,就近原则,

var laber2 = new Laber();                        
alert(laber2 .name);                            //momo,原型里的值,没有被laber1修改

如果想要box1也能在后面继续访问到原型里的值,可以把构造函数里的属性删除即可。

delete laber1.name;                            //删除属性
alert(laber1.name);    

如何判断属性是在构造函数的实例里,还是在原型里?可以使用hasOwnProperty()函数来验证:

alert(laber1.hasOwnProperty('name'));            //实例里有返回true,否则返回false

in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。

alert('name' in laber);                        //true,存在实例中或原型中

我们可以通过hasOwnProperty()方法检测属性是否存在实例中,也可以通过in来判断实例或原型中是否存在属性。那么结合这两种方法,可以判断原型中是否存在属性。

function isProperty(object, property) {            //判断原型中是否存在属性
    return !object.hasOwnProperty(property) && (property in object);
}

var laber = new Laber();
alert(isProperty(laber, 'name'))                    //true,如果原型有

为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式:

function Laber() {};
Laber.prototype = {                        //使用字面量的方式
    name : 'momo',
    age : 28,
    run : function () {
        return this.name + this.age + '运行中...';
    }
};

使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同,但还是有一些区别,字面量创建的方式使用constructor属性不会指向实例,而会指向Object,构造函数创建的方式则相反。

var laber = new Laber();
alert(laber instanceof Laber);
alert(laber instanceof Object);
alert(laber.constructor == Laber);                //字面量方式,返回false,否则,true
alert(laber.constructor == Object);                //字面量方式,返回true,否则,false

如果想让字面量方式的constructor指向实例对象,那么可以这么做:

Laber.prototype = {
    constructor : Laber,                        //直接强制指向即可
};

字面量方式为什么constructor会指向Object?因为Laber.prototype={};这种写法其实就是创建了一个新对象。而每创建一个函数,就会同时创建它prototype,这个对象也会自动获取constructor属性。所以,新对象的constructor重写了Laber原来的constructor,因此会指向新对象,那个新对象没有指定构造函数,那么就默认为Object

原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型。

function Laber() {};

Laber.prototype = {                            //原型被重写了
    constructor : Laber,
    name : 'momo',
    age : 28,
    run : function () {
        return this.name + this.age + '运行中...';
    }
};

Laber.prototype = {
    age = 27
};

var laber = new Laber();                        //在这里声明
alert(laber.run());                            //laber只是最初声明的原型

原型对象不仅仅可以在自定义对象的情况下使用,而ECMAScript内置的引用类型都可以使用这种方式,并且内置的引用类型本身也使用了原型。

原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。

原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题:

function Laber() {};
Laber.prototype = {
    constructor : Laber,
    name : 'momo',
    age : 28,
    family : ['父亲', '母亲', '弟弟'],            //添加了一个数组属性
    run : function () {
        return this.name + this.age + this.family;
    }
};

var laber1 = new Laber();
laber1.family.push('哥哥');                    //在实例中添加'哥哥'
alert(laber1.run());

var laber2 = new Laber();
alert(laber2.run());                            //共享带来的麻烦,也有'哥哥'了

数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化出的数据需要保留自己的特性,而不能共享。

为了解决构造传参和共享问题,可以组合构造函数+原型模式

function Laber(name, age) {                    //不共享的使用构造函数
    this.name = name;
    this.age = age;
    this. family = ['父亲', '母亲', '弟弟'];
};
Laber.prototype = {                            //共享的使用原型模式
    constructor : Laber,
    run : function () {
        return this.name + this.age + this.family;
    }
};

这种混合模式很好的解决了传参和引用共享的大难题。是创建对象比较好的方法。

原型模式,不管你是否调用了原型中的共享方法,它都会初始化原型中的方法,并且在声明一个对象时,构造函数+原型部分让人感觉又很怪异,最好就是把构造函数和原型封装到一起。为了解决这个问题,我们可以使用动态原型模式

function Laber(name ,age) {                    //将所有信息封装到函数体内
    this.name = name;
    this.age = age;
    
    if (typeof this.run != 'function') {            //仅在第一次调用的初始化
        Laber.prototype.run = function () {
            return this.name + this.age + '运行中...';
        };
    }
}

var laber = new Laber('momo', 28);
alert(laber.run());

当第一次调用构造函数时,run()方法发现不存在,然后初始化原型。当第二次调用,就不会初始化,并且第二次创建新对象,原型也不会再初始化了。这样及得到了封装,又实现了原型方法共享,并且属性都保持独立。

if (typeof this.run != 'function') {
        alert('第一次初始化');                    //测试用
        Laber.prototype.run = function () {
            return this.name + this.age + '运行中...';
        };
    }

var laber1 = new Laber('momo', 28);                //第一次创建对象
alert(laber1.run());                            //第一次调用
alert(laber1.run());                            //第二次调用

var laber2 = new Laber('xiaobo', 27);                //第二次创建对象
alert(laber2.run());
alert(laber2.run());

三.继承

继承是面向对象中一个比较核心的概念。其他正统面向对象语言都会用两种方式实现继承:一个是接口实现,一个是继承。而ECMAScript只支持继承,不支持接口实现,而实现继承的方式依靠原型链完成。

function Laber() {                            //Box构造
    this.name = 'momo';
}

function Luke() {                            //Luke构造
    this.age = 100;
}

Luke.prototype = new Laber();                    //Luke继承了Laber,通过原型,形成链条

var luke = new Luke();
alert(luke.age);
alert(luke.name);                            //得到被继承的属性

function Table() {                            //Table构造
this.level = 'AAAAA';
}                            

Table.prototype = new Luke();                //继续原型链继承

var table = new Table();
alert(table.name);                            //继承了Laber和Luke

以上原型链继承还缺少一环,那就是Obejct,所有的构造函数都继承自Obejct。而继承Object是自动完成的,并不需要程序员手动继承。

JavaScript里,被继承的函数称为超类型(父类,基类也行,其他语言叫法),继承的函数称为子类型(子类,派生类)。继承也有之前问题,比如字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。

为了解决引用共享和超类型无法传参的问题,我们采用一种叫借用构造函数的技术,或者成为对象冒充(伪造对象、经典继承)的技术来解决这两种问题。

借用构造函数虽然解决了刚才两种问题,但没有原型,复用则无从谈起。所以,我们需要原型链+借用构造函数的模式,这种模式成为组合继承

function Laber(age) {
    this.name = ['momo', 'xiaobo', 'Hello']
    this.age = age;
}

Laber.prototype.run = function () {                
    return this.name + this.age;
};

function Luke(age) {
    Laber.call(this, age);                        //对象冒充
}

Luke.prototype = new Laber();                    //原型链继承

var luke = new Luke(100);
alert(luke.run());

还有一种继承模式叫做:原型式继承;这种继承借助原型并基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function obj(o) {                            //传递一个字面量函数
    function F() {}                        //创建一个构造函数
    F.prototype = o;                        //把字面量函数赋值给构造函数的原型
    return new F();                        //最终返回出实例化的构造函数
}

var laber = {                                //字面量对象
    name : 'Lee',
    arr : ['哥哥','弟弟']
};

var laber1 = obj(laber);                        //传递
alert(laber1.name);
laber1.name = 'momo';
alert(laber1.name);

alert(laber1.arr);
laber1.arr.push('父母');
alert(laber1.arr);

var laber2 = obj(box);                        //传递
alert(laber2.name);
alert(laber2.arr);                            //引用类型共享了 

 

posted @ 2018-11-19 14:32  莫小波  阅读(216)  评论(0)    收藏  举报