JavaScript面向对象与原型

标签: javascript  面向对象

面向对象

创建对象

var box = new Object();
//创建一个name属性并赋值
box.name = 'CAI';
box.age = 22;
box.run = function(){
    return this.name + this.age + " running..."
};

console.log("box.run():" + box.run());

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

var box2 = box; //得到box的引用
box2.name = "Jack";

console.log("after change name box2:" + box2.run());
console.log("after change name box:" + box.run());  //用 box.run()发现 name 也改变了

这里写图片描述

工厂模式

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

function createObject(name,age){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.run = function(){
        return this.name + this.age + "running...";
    };
    return obj;
}
var box1 = createObject('Lee',100);
var box2 = createObject('Jack',200);
console.log("use createObject(): " + box1.run());
console.log("use createObject(): " + box2.run());

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

//无法搞清楚他们到底是哪个对象的实例
console.log(typeof box1);//object
console.log(box1 instanceof Object);//true

这里写图片描述

构造函数

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

//构造函数模式
function Box(name,age){
    this.name = name;
    this.age = age;
    this.run = function(){
        return this.name + this.age + "running..";
    };
}

var box1 = new Box('Lee',111);
var box2 = new Box("Cai",222);

console.log("use new Box():" + box1.run());
console.log("use new Box():" + box2.run());
console.log(box1 instanceof Box);   //true,能识别清楚它是那个对象的实例

这里写图片描述

构造方法和工厂模式的区别

使用构造函数的方法,即解决了重复实例化的问题,又解决了对象识别的问题,但问题 是,这里并没有 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 其实就是代表当前作用域对象的引用。如果在全局范围 this 就代 表 window 对象,如果在构造函数体内,就代表当前的构造函数所声明的对象。

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

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

var box=new Box('Lee', 100); //构造模式调用 
alert(box.run());
Box('Lee',20); //普通模式调用,无效
var o=new Object(); 
Box.call(o,'Jack', 200) //对象冒充调用 
alert(o.run());

探讨构造函数内部的方法(或函数)的问题,首先看下两个实例化后的属性或方法是否相等。

var box1= new Box('Lee',100); //传递一致 
var box2= new Box('Lee',100); //同上
alert(box1.name == box2.name); //true,属性的值相等 alert(box1.run == box2.run); //false,方法其实也是一种引用地址 alert(box1.run()== box2.run()); //true,方法的值相等,因为传参一致

可以把构造函数里的方法(或函数)用 new Function()方法来代替,得到一样的效果,更加证明,他们最终判断的是引用地址,唯一性。

function Box(name, age){ //new Function()唯一性 
    this.name =name; 
    this.age= age; 
    this.run=new Function("return this.name +this.age +'运行中...'");
 }

我们可以通过构造函数外面绑定同一个函数的方法来保证引用地址的一致性,但这种做 法没什么必要,只是加深学习了解:

function Box(name, age){
     this.name =name; 
     this.run = run; 
 }
function run(){ //通过外面调用,保证引用地址一致 
    return this.name + this.age+'运行中...';
}

虽然使用了全局的函数 run()来解决了保证引用地址一致的问题,但这种方式又带来了 一个新的问题,全局中的 this 在对象调用的时候是 Box 本身,而当作普通函数调用的时候, this 又代表 window

原型

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

function Box(){ //声明一个构造函数
}
Box.prototype.name = "Lee"; //在原型里添加属性 
Box.prototype.age = 100;
Box.prototype.run = function(){//在原型里添加方法 
    return this.name + this.age + "running..";
};

//比较一下原型内的方法地址是否一致: 
var box1 = new Box();
var box2 = new Box();
console.log(box1.run == box2.run);//true 方法的引用地址保持一致

为了更进一步了解构造函数的声明方式和原型模式的声明方式,我们通过图示来了解一 下:
这里写图片描述

这里写图片描述

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

PS:IE 浏览器在脚本访问__proto__会不能识别,火狐和谷歌浏览器及其他某些浏览器 均能识别。虽然可以输出,但无法获取内部信息。
alert(box1.__proto__); //[objectObject]

prototype和__proto__的区别

http://www.cnblogs.com/shuiyi/p/5305435.html这个博主写的很详细,他画的图很好的解释了两者的区别。感谢该博主@水乙
这里写图片描述

这里写图片描述
任何对象都是Object

这里写图片描述

这里写图片描述

这里写图片描述

var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}(即构造器function A 的原型对象)
console.log(a.__proto__.__proto__); //Object {}(即构造器function Object 的原型对象)
console.log(a.__proto__.__proto__.__proto__); //null

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

//判断一个对象是否指向了该构造函数的原型对象,
console.log(Box.prototype.isPrototypeOf(box1)); //true

原型模式的执行流程

1.先查找构造函数实例里的属性或方法,如果有,立刻返回;
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;

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

var box1 = new Box();
console.log(box1.name);//Lee
box1.name = 'Jack';
console.log(box1.name);//Jack

var box2 = new Box();
console.log(box2.name);//Lee

console.log(box1.hasOwnProperty('name'));//true 实例里有返回 true,否则返回 false
//如果想要 box1 也能在后面继续访问到原型里的值,
//可以把构造函数里的属性删除即可, 具体如下: 
delete box1.name;
console.log(box1.name);//Lee
//如何判断属性是在构造函数的实例里,还是在原型里?可以使用 hasOwnProperty()函数 来验证: 
console.log(box1.hasOwnProperty('name'));//false 实例里有返回 true,否则返回 false
/*

这里写图片描述

这里写图片描述

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

console.log('name' in box1);        //true

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

function isProperty(object,property){
    return !object.hasOwnProperty(property) && (property in object)
}

var box = new Box();
console.log(isProperty(box,'name'));        //true

原型字面量的创建方式

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

function Box(){

}

Box.prototype = {
    name : 'Lee',
    age : 100,
    run : function(){
        return this.name + this.age + "running..";
    }
};

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

var box = new Box();        
console.log(box instanceof Box);        //true
console.log(box instanceof Object);     //true
//字面量创建的方式使用 constructor 属性不会指向实例
//而会指向 Object,构造函数创建 的方式则相反。 
console.log(box.constructor == Box);        //false
console.log(box.constructor == Object);     //true

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

Box.prototype = {
    constructor : Box,
};

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

原型的重写

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

function Box(){

}

Box.prototype = {
    constructor : Box,
    name : "Lee",
    age : 100,
    run : function(){
        return this.name + this.age + "running..";
    }
};

var box = new Box();
console.log(box.run());

Box.prototype = {
    age : 200
};

var box = new Box();
console.log(box.run());

这里写图片描述

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

console.log(Array.prototype.sort);//sort 就是 Array 类型的原型方法
console.log(String.prototype.substring);

String.prototype.addstring = function(){
    return this + ",被添加了!"
};

console.log('Lee'.addstring());

这里写图片描述
PS:尽管给原生的内置引用类型添加方法使用起来特别方便,但我们不推荐使用这种 方法。因为它可能会导致命名冲突,不利于代码维护。

原型模式创建对象的缺点

原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺 点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。 原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性 也还可以。但如果属性包含引用类型,就存在一定的问题:

function Box(){

};


Box.prototype = {
    constructor : Box,
    name : 'Lee',
    age : 100,
    family : ['父亲','母亲','妹妹'],
    run : function(){
        return this.name + this.age + this.family;
    }
};


var box1 = new Box();
box1.family.push("哥哥");  //在实例中添加'哥哥' 
console.log(box1.run());

var box2 = new Box();
console.log(box2.run()); //共享带来的麻烦,也有'哥哥'了

这里写图片描述

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

构造函数+原型模式

function Box(name,age){ //不共享的使用构造函数 
    this.name = name;
    this.age = age;
    this.family = ['父亲','母亲','妹妹'];
};

Box.prototype = { //共享的使用原型模式 
    constructor : Box,
    run : function(){
        return this.name + this.age + this.family;  
    }
};


var box = new Box("CAI",100);
box.family.push("哥哥");
console.log(box.run());
var box1 = new Box("Jack",50);
console.log(box1.run());

console.log(box instanceof Box);
console.log(box.constructor == Box);
console.log(typeof box.run);

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

动态原型模式

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

function Box(name,age){ //将所有信息封装到函数体内 
    this.name = name;
    this.age = age;

    if(typeof this.run != 'function'){ //仅在第一次调用的初始化 
        Box.prototype.run = function(){
            return this.name + this.age + "running..";
        };
    }
}

var box = new Box("Lee",100);
console.log(box.run());

这里写图片描述

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

PS:使用动态原型模式,要注意一点,不可以再使用字面量的方式重写原型,因为会 切断实例和新原型之间的联系。

寄生构造函数

以上讲解了各种方式对象创建的方法,如果这几种方式都不能满足需求,可以使用一开 始那种模式:寄生构造函数。

function Box(name,age){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.run = function(){
        return this.name + this.age + "running";
    };
    return obj;
}

寄生构造函数,其实就是工厂模式+构造函数模式。这种模式比较通用,但不能确定对 象关系,所以,在可以使用之前所说的模式时,不建议使用此模式。 在什么情况下使用寄生构造函数比较合适呢?假设要创建一个具有额外方法的引用类 型。由于之前说明不建议直接 String.prototype.addstring,可以通过寄生构造的方式添加。

function myString(string){
    var str = new String(string);
    str.addstring = function(){
        return this + ",被添加了!"
    };
    return str;
}

var box = new myString("Lee"); //比直接在引用原型添加要繁琐好多 
console.log(box.addstring());

这里写图片描述

稳妥构造函数

在一些安全的环境中,比如禁止使用 this 和 new,这里的 this 是构造函数里不使用 this, 这里的 new 是在外部实例化构造函数时不使用 new。这种创建方式叫做稳妥构造函数。

function Box(name, age){
    var obj = new Object();
    obj.run = function(){
        return name + age + "running..";
    };
    return obj;
}

var box = Box("Lee",100);
console.log(box.run());

PS:稳妥构造函数和寄生类似。

继承

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

function Box(){
    this.name = "Lee";
}

function Desk(){
    this.age = 100;
}

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

var desk = new Desk();

console.log(desk.age);
console.log(desk.name); //得到被继承的属性

function Table(){
    this.level = 'AAAA';

}

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

var table = new Table();
console.log(table.name); //继承了 Box 和 Desk

Desk.prototype.age = 200;
console.log(desk.age);

console.log(table instanceof Object);   //true
console.log(desk instanceof Table); //false
console.log(table instanceof Desk); //true
console.log(table instanceof Box);  //true

这里写图片描述

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

对象冒充

在 JavaScript 里,被继承的函数称为超类型(父类,基类也行,其他语言叫法),继承的 函数称为子类型(子类,派生类)。继承也有之前问题,比如字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。 为了解决引用共享和超类型无法传参的问题,我们采用一种叫借用构造函数的技术,或者成为对象冒充(伪造对象、经典继承)的技术来解决这两种问题。

function Box(age){
    this.name = ['Lee','Jack','Hello'];
    this.age = age;
}


function Desk(age){
    Box.call(this,age); //对象冒充,给超类型传参
}

var desk = new Desk(200);
console.log(desk.age);
console.log(desk.name);
desk.name.push('AAA'); //添加的新数据,只给 desk 
console.log(desk.name);

组合继承

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

function Box(age){
    this.name = ['Lee','Jack','Hello'];
    this.age = age;
}

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

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

Desk.prototype = new Box();//原型链继承

var desk = new Desk(100);

原型式继承

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

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

}

var box = {     //字面量对象
    name : 'Lee',
    arr : ['哥哥','妹妹','姐姐']
}

var box1 = obj(box);    //传递
console.log(box1.name);

box1.name = 'Jack';
console.log(box1.name);

console.log(box1.arr);

box1.arr.push("父母");
console.log(box1.arr);

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

这里写图片描述

寄生式继承把原型式+工厂模式结合而来,目的是为了封装创建对象的过程。

function create(o){ //封装创建过程 
var f = obj(o);
     f.run = function(){
      return this.arr; //同样,会共享引用
     }; 
     return f; 
}


组合式继承是 JavaScript 最常用的继承模式;但,组合式继承也有一点小问题,就是超 类型在使用过程中会被调用两次:一次是创建子类型的时候,另一次是在子类型构造函数的内部。

function Box(name){
    this.name = name;
    this.arr ['哥哥','妹妹','父母'];
}

Box.prototype.run = function(){
    return this.name;
}

function Desk(name,age){
    Box.call(this,name); //第二次调用 Box 
    this.age= age;

}

Desk.prototype = new Box(); //第一次调用 Box

以上代码是之前的组合继承,那么寄生组合继承,解决了两次调用的问题。

function obj(o){ 
    function F() {}
    F.prototype = o;
    return new F();
}

function create(box,desk){ 
    var f = obj(box.prototype); 
    f.constructor = desk; 
    desk.prototype = f; 
}
function Box(name) { 
    this.name = name; 
    this.arr = ['哥哥','妹妹','父母']; 
}
Box.prototype.run = function(){ 
    return this.name; 
};
function Desk(name,age) { 
    Box.call(this,name); 
    this.age = age;
}
inPrototype(Box,Desk); //通过这里实现继承

var desk = new Desk('Lee',100); 
desk.arr.push('姐姐'); 
alert(desk.arr); 
alert(desk.run()); //只共享了方法
var desk2 = new Desk('Jack', 200);
alert(desk2.arr); //引用问题解决
版权声明:本文为cckevincyh原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/cckevincyh/article/details/77496446