一、面向对象编程基础
JavaScript有几个包含在其核心中的对象;例如,Math、Object、Array、以及String等对象。JavaScript用函数作为类,定义一个类就是定义一个函数
在下例中,我们首先定义名为Person的类,然后创建两个实例(person1和person2)。
function Person() {}
var person1 = new Person();
var person2 = new Person();
构造函数内部没有显式的创建对象,直接将属性和方法赋给this对象,没有return语句。函数名是以大写字母开头,要创建对象的新实例,必须使用new
检测对象类型的时候,用instanceof比constructor靠谱
alert(person1.constructor==Person);//true
alert(person2 instanceof Person);//true
alert(person2 instanceof Object);//true
属性是包含在类中的变量;每个对象实例都有这些属性。属性应设置在类(函数)的原型(prototype)属性中,以便继承正常工作。
对象有两种属性:数据属性和访问器属性。
数据属性——包含一个数据值的位置,有四个描述其行为的特性:
Configurable:表示能否通过delete删除属性从而定义属性特性,默认值为true
Enumerable:表示能否通过for-in循环返回属性,默认true
Writable:表示能否修改属性的值,默认true
Value:包含这个属性的数据值,默认undefined
访问器属性——不包含数据值,包含一对getter和setter,也有四个特性:
Configurable:表示能否通过delete删除属性从而定义属性特性,默认值为true
Enumerable:表示能否通过for-in循环返回属性,默认true
Get:读取函数时调用的函数,默认undefined
Set:写入函数时调用的函数,默认undefined
访问器属性不能直接定义,必须使用Object.defineProperty()
二、创建对象自定义类型
创建自定义类型的最常见的方式就是组合使用构造函数模式与原型模式。构造函数用于定义实例属性(可作为副本修改),原型模式用于定义方法
这种混合模式还支持向构造函数传递参数。
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
this.friends=["Shelly","Court"];
}
Person.prototype={
constructor:Person,
sayName:function(){
alert(this.name);
}
}
var person1=new Person ("Nico",29,"Software Engineer");
var person2=new Person("Mike",27,"Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelly,Court,Van"
alert(person2.friends); //"Shelly,Court"
alert(person1.friends===person2.friends);//false
alert(person1.sayName===person2.sayName); //true
三、JavaScript面向对象三大特征
1.封装
封装就是把抽象出来的属性和对属性的操作封装在一起,属性被保护在内部,程序的其它部分只有通过被授权的操作(函数),才能对属性进行操作!
<script type="text/javascript">
/*定义一个Person类*/
function Person(_name,_age,_salary){
//Person类的公开属性,类的公开属性的定义方式是:”this.属性名“
this.Name=_name;
//Person类的私有属性,类的私有属性的定义方式是:”var 属性名“
var Age=_age;
var Salary=_salary;
//定义Person类的公开方法(特权方法),类的公开方法的定义方式是:”this.functionName=function(){.....}“
this.Show=function(){
alert("Age="+Age+"\t"+"Salary="+Salary);//在公开方法里面访问类的私有属性是允许的
}
/*定义Person类的私有方法(内部方法),类的私有方法的定义方式是:”function functionName(){.....}“,
或者 var functionName=function(){....} */
function privateFn(){
alert("我是Person类的私有函数privateFn");
}
var privateFn2=function(){
alert("我是Person类的私有函数privateFn2");
}
}
/*通过prototype给可以类的所有对象添加公共(public)方法,但是这种方式定义的方法不能去访问类的私有属性和私有方法*/
Person.prototype.Fn=function(){
alert("访问公共属性this.Name="+this.Name);//访问公共属性,OK的
//alert("访问私有属性Aag="+Age);//访问私有属性,这里会报错“Age未定义”
//privateFn();//调用私有方法,这里会报错“缺少对象
}
var p1 = new Person("孤傲苍狼",24,2300);
alert("p1.Name="+p1.Name);//访问公有属性,这是可以正常访问的
alert("p1.Age="+p1.Age+"\t"+"p1.Salary="+p1.Salary);//不能使用类的对象去直接访问类私有属性,这是访问不了的,结果都是undefined
p1.Show();//调用类的公共函数,这次允许的
p1.Fn();//调用类的公共函数,这次允许的
//alert("p1.privateFn():"+p1.privateFn()+" p1.privateFn2():"+p1.privateFn2());
//不能使用类的对象去调用类的私有方法,这里会报错”对象不支持此属性或者方法“
</script>
2.继承
Js的继承在很多书里面细致的分了很多种类型和实现方式,大体上就是两种:对象冒充、原型方式。这两种方式各有优点和缺陷,这里我先列举出来,
再从底层分析区别:
(一)对象冒充
<script type="text/javascript">
/*定义Stu类*/
function Stu(name,age){
this.Name=name;
this.Age=age;
this.Show=function(){
window.alert("我的名字是:"+this.Name+",今年:"+this.Age);
}
this.SayHello = function(){
window.alert("Hello,"+this.Name);
}
}
/*定义MidStu类*/
function MidStu(name,age){
this.stu=Stu;//MidStu类继承Stu类
this.stu(name,age);//JS中实际上是通过对象冒充来实现继承的,这句话不能少,因为JS是动态语言,如果不执行,则不能实现继承效果
//在子类MidStu中重写父类Stu的SayHello方法
this.SayHello=function(){
alert("你好,"+this.Name);
}
}
var midStu = new MidStu("孤傲苍狼",24);//创建一个MidStu类实例对象
alert("访问继承下来的属性Name和Age,midStu.Name="+midStu.Name+",midStu.Name="+midStu.Age);//访问继承下来的属性
midStu.Show();//调用从父类Stu继承下来的Show方法
midStu.SayHello();//调用从父类Stu继承下来的SayHello方法,SayHello()在子类中进行了重写,这里调用的是重写过后的SayHello()方法
</script>
当构造对象MidStu的时候,调用stu相当于启动Stu的构造函数,注意这里的上下文环境中的this对象是MidStu的实例,所以在执行Stu构造函数脚本时,
所有Stu的变量和方法都会赋值给this所指的对象,即MidStu的实例,这样子就达到MidStu继承了Stu的属性方法的目的。
对象冒充继承就是这么一回事,它可以实现多重继承,只要重复做这一套赋值的流程就可以了。不过目前真正大规模使用得并不多,为什么呢?因为它有
一个明显的性能缺陷,这就要说道OO的概念了,我们说对象是成员+成员方法的集合,构造对象实例的时候,这些实例只需要拥有各自的成员变量就可以了,
成员方法只是一段对变量操作的可执行文本区域而已,这段区域不用为每个实例而复制一份,所有的实例都可以共享。现在回到Js利用对象冒充模拟的继承里,
所有的成员方法都是针对this而创建的,也就是所所有的实例都会拥有一份成员方法的副本,这是对内存资源的一种极度浪费。其它的缺陷比如说对象冒充
无法继承prototype域的变量和方法就不用提了,笔者认为前一个致命缺陷就已经足够。不过,我们还是需要理解它,特别是父类的属性和方法是如何
继承下来的原理,对于理解Js继承很重要。
(二)原型方式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针指向一个对象,而这个对象的用途是包含可以由特定类型的所有
实例共享的属性和方法。简单来说,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有
对象实例共享它所包含的属性和方法。
第二种继承方式是原型方式,所谓原型方式的继承,是指利用了prototype或者说以某种方式覆盖了prototype,从而达到属性方法复制的目的。其实现
方式有很多中,可能不同框架多少会有一点区别,但是我们把握住原理,就不会有任何不理解的地方了。看一个例子(某一种实现):
function Person(){
this.name = “Mike”;
this.sayGoodbye = function(){alert(“GoodBye!”);};
}
Person.prototype.sayHello = function(){alert(”Hello!”);};
function Student(){}
Student.prototype = new Person();
关键是对最后一句Student原型属性赋值为Person类构造的对象,这里解释一下父类的属性和方法是如何copy到子类上的。Js对象在读取某个对象属性的时
候,总是先查看自身域的属性列表,如果有就返回,即这个属性就会屏蔽原型对象中保存的同名属性;否则去读取prototype域(每个
对象共享构造对象的类的prototype域所有属性和方法),如果找到就返回,由于prototype可以指向别的对象,所以Js
解释器会递归的去查找prototype域指向对象的prototype域,直到prototype为本身,查找变成了一种循环
这样看来,最后一句发生的效果就是将父类所有属性和方法连接到子类的prototype域上,这样子类就继承了父类所有的属性和方法,包括name、
sayGoodbye和sayHello。这里与其把最后一句看成一种赋值,不如理解成一种指向关系更好一点。这种原型继承的缺陷也相当明显,就是继承时父类
的构造函数时不能带参数,因为对子类prototype域的修改是在声明子类对象之后才能进行,用子类构造函数的参数去初始化父类属性是无法实现的
个人总结的一种综合方式:
function Person(name){
this.name = name;
}
Person.prototype.sayHello = function(){alert(this.name+“say Hello!”);};
function Student(name,id){
Person.call(this,name);
this.id = id;
}
Student.prototype = new Person();
Student.prototype.show = function(){
alert(“Name is:”+ this.name+” and Id is:”+this.id);
}
总结就是利用对象冒充机制的call方法把父类的属性给抓取下来,而成员方法尽量写进被所有对象实例共享的prototype域中,以防止方法副本重复
创建。然后子类继承父类prototype域来抓取下来所有的方法。
3.多态性
四:确定原型和实例的关系
第一种方式:使用instanceof操作符测试实例与原型链中出现的构造函数
第二种方式:使用isPrototypeOf()方法,只要原型链中出现过的原型,都可以说是该原型链多派生的实例的原型
alert(intune instanceof SubType); //true
alert(SubType.prortotype.isPrototypeOf(intune)); //true