Javascript面向对象三大特性(封装性、继承性、多态性)详解及创建对象的各种方法

时间:2023-02-24 17:42:52

Javascript基于对象的三大特征和C++,Java面向对象的三大特征一样,都是封装(encapsulation)、继承(inheritance )和多态(polymorphism )。只不过实现的方式不同,其基本概念是差不多的。其实除三大特征之外,还有一个常见的特征叫做抽象(abstract),这也就是我们在一些书上有时候会看到面向对象四大特征的原因了。


一、封装性


    封装就是把抽象出来的数据和对数据的操作封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(成员方法),才能对数据进行操作。

  JS封装只有两种状态,一种是公开的,一种是私有的。
   案例:

function Person(name,sal){
this.name=name; //公开
var sal=sal; //私有
this.showInfo=function(){ //公开
window.alert(this.name+" "+sal);
}
function showInfo2(){ //把函数私有化
window.alert("你好"+this.name+" "+sal);
}
}
var p1 = new Person('Cece', 20, 10000);
window.alert(p1.name
+ " is " +p1.age); //Cece is undefined
p1.showInfo();//Cece 20
p1.showInfo2();//VM302:1 Uncaught TypeError: p1.showInfo2 is not a function(…)

构造函数方式与原型方式给对象添加方法的区别:

//1.通过构造函数方式给对象添加方法
function Dog(name){
this.name=name;
this.shout=function(){
window.alert(
"小狗尖叫"+this.name);
}
}
var dog1=new Dog("aa");
var dog2=new Dog("bb");
if(dog1.shout==dog2.shout){
window.alert(
"相等");
}
else{
window.alert(
"不相等");
}

//会输出“不相等”
//2.通过原型方式给对象添加方法
function Dog(name){
this.name=name;
}
Dog.prototype.shout
=function(){
window.alert(
"小狗尖叫"+this.name);
}
var dog1=new Dog("aa");
var dog2=new Dog("bb");
if(dog1.shout==dog2.shout){
window.alert(
"相等");
}
else{
window.alert(
"不相等");
}

//会输出“相等”

说明通过构造函数来分配成员方法,给每个对象分配一份独立的代码。这样的弊端就是如果对象实例有很多,那函数的资源占用就会很大,而且有可能造成内存泄漏。

而原型法是大家共享同一份代码,就不会有那种弊端。

因此,通过构造函数添加成员方法和通过原型法添加成员方法的区别:

1.通过原型法分配的函数是所有对象共享的;

2.通过原型法分配的属性是独立的;(如果你不修改属性,他们是共享)

3.如果希望所有的对象使用同一个函数,最好使用原型法添加方法,这样比较节省内存。

 

特别强调:我们前面学习的通过prototype给所有的对象添加方法,但是这种方式不能去访问类的私有变量和方法。案例:

function Person(){
this.name="Cece";
var age=18;
this.abc=function(){ //公开
window.alert("abc");
}
function abc2(){ //私有
window.alert("abc");
}
}
Person.prototype.fun1
=function(){
window.alert(
this.name);//Cece
//window.alert(age);//Uncaught ReferenceError: age is not defined(…)
//abc2(); //Uncaught ReferenceError: abc2 is not defined(…)
this.abc(); //abc
}
var p1=new Person();
p1.fun1();

 

二、继承性


继承可以解决代码复用,让编程更加靠近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过继承父类中的属性和方法。
JS中实现继承的方式:

1.类继承:


(1)对象冒充
案例:

//1.把子类*有的属性和方法抽取出,定义一个父类Stu 
function Stu(name, age){
this.name = name;
this.age = age;
this.show = function(){
window.alert(
this.name + " " + this.age);
}
}
function MidStu(name, age) {
this.stu = Stu;
// 通过对象冒充来实现继承的
// 对象冒充的意思就是获取那个类的所有成员,因为js是谁调用那个成员就是谁的,这样MidStu就有了Stu的成员了
this.stu(name, age);
this.payFee = function(){
window.alert(
"缴费" + money * 0.8);
}
}
function Pupil(name, age) {
this.stu = Stu;
// 通过对象冒充来实现继承的
this.stu(name, age);
this.payFee = function(){
window.alert(
"缴费" + money * 0.5);
}
}

var midStu = new MidStu("zs", 13);
midStu.show();
var pupil = new Pupil("ls", 10);
pupil.show();

(2)通过call或者apply实现
案例:

//1.把子类*有的属性和方法抽取出,定义一个父类Stu 
function Stu(name,age){
//window.alert("确实被调用.");
this.name=name;
this.age=age;
this.show=function(){
window.alert(
this.name+"年龄是="+this.age);
}
}
//2.通过call或者apply来继承父类的属性的方法
function MidStu(name,age){
//这里这样理解: 通过call修改了Stu构造函数的this指向,
//让它指向了调用者本身.
Stu.call(this,name,age);
//如果用apply实现,则可以
//Stu.apply(this,[name,age]); //说明传入的参数是 数组方式
//可以写MidStu自己的方法.
this.pay=function(fee){
window.alert(
"你的学费是"+fee*0.8);
}
}
function Pupil(name,age){
Stu.call(
this,name,age);//当我们创建Pupil对象实例,Stu的构造函数会被执行,当执行后,我们Pupil对象就获取从 Stu封装的属性和方法
//可以写Pupil自己的方法.
this.pay=function(fee){
window.alert(
"你的学费是"+fee*0.5);
}
}
//测试
var midstu=new MidStu("zs",15);
var pupil=new Pupil("ls",12);
midstu.show();
midstu.pay(
100);
pupil.show();
pupil.pay(
100);

2.原型继承

原型继承是js中最通用的继承方式,不用实例化对象,通过直接定义对象,并被其他对象引用,这样形成的一种继承关系,其中引用对象被称为原型对象。

function A(){  
this.color = 'red';
}
function B(){}
function C(){}
B.prototype
= new A();
C.prototype
= new B();
// 测试原型继承
var c = new C();
console.log(c.color);
// red

原型继承显得很简单,不需要每次构造都调用父类的构造函数,也不需要通过复制属性的方式就能快速实现继承。但它也存在一些缺点:

① 每个类型只有一个原型,所以不支持多重继承(即一个子类继承自多个父类)。
② 不能很好的支持多参数或动态参数的父类,显得不够灵活。

③ 占用内存多,每次继承都需要实例化一个父类,这样会存在内存占用过多的问题。

3.复制继承(知道就好)

复制继承就是利用for in 遍历对象成员,逐一复制给另一个对象。通过这种方式来实现继承。
function A(){  
this.color = 'red';
}
A.prototype.say
= function() {
console.log(
this.color);
}
var a = new A();
var b = {};
// 开始拷贝
for(var item in a) {
b[item]
= a[item];
}
// 开始测试
console.log(b.color); // red
b.say(); // red

封装后:

Function.prototype.extend = function(obj){  
for(item in obj){
this.constructor.prototype[item] = obj[item];
}
}
function A(){
this.color = 'green';
}
A.prototype.say
= function(){
console.log(
this.color);
}
// 测试
var b = function(){};
b.extend(
new A());
b.say();
// green

复制继承实际上是通过反射机制复制类对象中的可枚举属性和方法来模拟继承。这种可以实现多继承。但也有缺点:

① 由于是反射机制,不能继承非枚举类型的属性和方法。对于系统核心对象的只读方法和属性也无法继承。
② 执行效率差,这样的结构越庞大,低效就越明显。
③ 如果当前类型包含同名成员,这些成员会被父类的动态复制给覆盖。
④ 多重继承中,复制继承不能清晰描述父类和子类的相关性。
⑤ 在实例化后才能遍历成员,不够灵活,也不支持动态参数

⑥ 复制继承仅仅是简单的引用赋值,如果父类成员包含引用类型,那么也会带来很多副作用,如不安全,容易遭受污染等。

4.混合继承(构造+原型)

混合继承是把多种继承方式一起使用,发挥各个优势,来实现各种复杂的应用。
最常见的就是把类继承和原型继承一起使用。做法是将需要独立的属性方法放入构造函数中,而可以共享的部分则放入原型中,这样做可以最大限度节省内存而又保留对象实例的独立性。注意:
1、把方法写在原型中比写在构造函数中消耗的内存更小,因为在内存中一个类的原型只有一个,写在原型中的行为可以被所有实例共享,实例化的时候并不会在实例的内存中再复制一份
而写在类中的方法,实例化的时候会在每个实例中再复制一份,所以消耗的内存更高。
所以没有特殊原因,我们一般把属性写到类中,而行为写到原型中。
2、构造函数中定义的属性和方法要比原型中定义的属性和方法的优先级高,如果定义了同名称的属性和方法,构造函数中的将会覆盖原型中的。
function A(x,y){  
this.x = x;
this.y = y;
}
A.prototype.add
= function(){
return (this.x-0) + (this.y-0);
}
function B(x,y){
A.call(
this,x,y);
}
B.prototype
= new A();

// 测试
var b = new B(2,1);
console.log(b.x);
// 2
console.log(b.add()); // 3

5.多重继承

继承一般包括单向继承和多向继承,单向继承模式较为简单,每个子类有且仅有一个超类,多重继承是一个比较复杂的继承模式。一个子类可拥有多个超类。JavaScript原型继承不支持多重继承,但可通过混合模式来实现多重继承。下面让类C来继承类A和类B:
function A(x){  
this.x = x;
}
A.prototype.hi
= function(){
console.log(
'hi');
}
function B(y){
this.y = y;
}
B.prototype.hello
= function(){
console.log(
'hello');
}
// 给Function增加extend方法
Function.prototype.extend = function(obj) {
for(var item in obj) {
this.constructor.prototype[item] = obj[item];
}
}
// 在类C内部实现继承
function C(x,y){
A.call(
this,x);
B.call(
this,y);
};
C.extend(
new A(1));
C.extend(
new B(2));

// 通过复制继承后,C变成了一个对象,不再是构造函数了,可以直接调用
C.hi(); // hi
C.hello(); // hello
console.log(C.x); // 1
console.log(C.y); // 2

在js中实现类继承,需要设置3点:

① 在子类构造函数结构体内,使用函数call()调用父类构造函数,把子类的参数传递给调用函数如上面的例子:A.call(this,x) 这样子类可以继承父类的所有属性和方法。
② 在子类和父类之间建立原型链,如上例:B.prototype = new A() 为了实现类的继承必须保证他们原型链上的上下级关系。即设置子类的prototype 属性指向父类的一个实例即可。
③ 恢复子类原型对象的构造函数, 如上例:B.prototype.constructor = B
在类继承中,call() 和 apply() 方法被频繁使用,它们之间的功能和用法都是相同的,唯一区别就是第2个参数类型不同。如果深入,参考:http://blog.csdn.net/tyro_java/article/details/51020720
类的构造函数中的成员,一般称之为本地成员,本地成员继承可以用call 和 apply。而类的原型成员就是类的原型中的成员。

 

关于继承更多知识参考:面向对象在javascript中的三大特征之继承

 

三、多态性


JS的函数重载
这个是多态的基础,在之前的Javascript入门已经说过了,JS函数不支持多态,但是事实上JS函数是无态的,支持任意长度,类型的参数列表。如果同时定义了多个同名函数,则以最后一个函数为准。
案例1:js不支持重载

/*****************说明js不支持重载*****/
function Person(){
this.test1=function (a,b){
window.alert(
'function (a,b)');
}
this.test1=function (a){
window.alert(
'function (a)');
}
}
var p1=new Person();
//js中不支持重载.
//
但是这不会报错,js会默认是最后同名一个函数,可以看做是后面的把前面的覆盖了。
p1.test1("a","b");
p1.test1(
"a");

案例2:js如何实现重载

//js怎么实现重载.通过判断参数的个数来实现重载 
function Person(){
this.test1=function (){
if(arguments.length==1){
this.show1(arguments[0]);
}
else if(arguments.length==2){
this.show2(arguments[0],arguments[1]);
}
else if(arguments.length==3){
this.show3(arguments[0],arguments[1],arguments[2]);
}
}
this.show1=function(a){
window.alert(
"show1()被调用"+a);
}
this.show2=function(a,b){
window.alert(
"show2()被调用"+"--"+a+"--"+b);
}
function show3(a,b,c){
window.alert(
"show3()被调用");
}
}
var p1=new Person();
//js中不支持重载.
p1.test1("a","b");
p1.test1(
"a");

1、多态基本概念
多态是指一个引用(类型)在不同情况下的多种状态。也可以理解成:多态是指通过指向父类的引用,来调用在不同子类中实现的方法。
案例:

// Master类 
function Master(name){
this.nam=name;
//方法[给动物喂食物]
}
//原型法添加成员函数
Master.prototype.feed=function (animal,food){
window.alert(
"给"+animal.name+" 喂"+ food.name);
}
function Food(name){
this.name=name;
}
//鱼类
function Fish(name){
this.food=Food;
this.food(name);
}
//骨头
function Bone(name){
this.food=Food;
this.food(name);
}
function Peach(name){
this.food=Food;
this.food(name);
}
//动物类
function Animal(name){
this.name=name;
}
//猫猫
function Cat(name){
this.animal=Animal;
this.animal(name);
}
//狗狗
function Dog(name){
this.animal=Animal;
this.animal(name);
}
//猴子
function Monkey(name){
this.animal=Animal;
this.animal(name);
}
var cat=new Cat("猫");
var fish=new Fish("鱼");

var dog=new Dog("狗");
var bone=new Bone("骨头");

var monkey=new Monkey("猴");
var peach=new Peach("桃");

//创建一个主人
var master=new Master("zs");
master.feed(dog,bone);
master.feed(cat,fish);
master.feed(monkey,peach);

多态利于代码的维护和扩展,当我们需要使用同一类树上的对象时,只需要传入不同的参数就行了,而不需要再new 一个对象。

以上就是Javascript基于对象三大特性。

 

附录:js中创建对象的各种方法(现在最常用的方法是组合模式)。

1)原始模式

//1.原始模式,对象字面量方式
var person = {
name:
'Jack',
age:
18,
sayName:
function () { alert(this.name); }
};

//1.原始模式,Object构造函数方式
var person = new Object();
person.name
= 'Jack';
person.age
= 18;
person.sayName
= function () {
alert(
this.name);
};

显然,当我们要创建批量的person1、person2……时,每次都要敲很多代码,资深copypaster都吃不消!然后就有了批量生产的工厂模式。

2)工厂模式

//2.工厂模式,定义一个函数创建对象
function creatPerson (name, age) {
var temp = new Object();
person.name
= name;
person.age
= age;
person.sayName
= function () {
alert(
this.name);
};
return temp;
}

工厂模式就是批量化生产,简单调用就可以进入造人模式。指定姓名年龄就可以造一堆小宝宝啦,解放双手。但是由于是工厂暗箱操作的,所以你不能识别这个对象到底是什么类型(instanceof 测试为 Object),另外每次造人时都要创建一个独立的temp对象,代码臃肿。

3)构造函数

//3.构造函数模式,为对象定义一个构造函数
function Person (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
alert(
this.name);
};
}
var p1 = new Person('Jack', 18); //创建一个p1对象
Person('Jack', 18); //属性方法都给window对象,window.name='Jack',window.sayName()会输出Jack

构造函数与C++、JAVA中类的构造函数类似,易于理解,另外Person可以作为类型识别(instanceof 测试为 Person 、Object)。但是所有实例依然是独立的,不同实例的方法其实是不同的函数。这里把函数两个字忘了吧,把sayName当做一个对象就好理解了,就是说张三的 sayName 和李四的 sayName是不同的存在,但显然我们期望的是共用一个 sayName 以节省内存。

4)原型模式

//4.原型模式,直接定义prototype属性
function Person () {}
Person.prototype.name
= 'Jack';
Person.prototype.age
= 18;
Person.prototype.sayName
= function () { alert(this.name); };

//4.原型模式,字面量定义方式
function Person () {}
Person.prototype
= {
name:
'Jack',
age:
18,
sayName:
function () { alert(this.name); }
};
var p1 = new Person(); //name='Jack'
var p2 = new Person(); //name='Jack'

这里需要注意的是原型属性和方法的共享,即所有实例中都只是引用原型中的属性方法,任何一个地方产生的改动会引起其他实例的变化。

5)混合模式(构造+原型)

//5. 原型构造组合模式,
function Person (name, age) {
this.name = name;
this.age = age;
}

Person.prototype
= {
hobby: [
'running','football'];
sayName:
function () { alert(this.name); },
sayAge:
function () { alert(this.age); }
};

var p1 = new Person('Jack', 20);
//p1:'Jack',20; __proto__: ['running','football'],sayName,sayAge

var p2 = new Person('Mark', 18);
//p1:'Mark',18;__proto__: ['running','football'],sayName,sayAge

做法是将需要独立的属性方法放入构造函数中,而可以共享的部分则放入原型中,这样做可以最大限度节省内存而又保留对象实例的独立性。

 

参考:Javascript基于对象三大特性(封装性、继承性、多态性)

   简单粗暴地理解js原型链--js面向对象编程