面向对象程序设计
在程序员的眼里,万事万物皆对象(C语言排除在外),在JS当中,任何类型的数据,我们都可以看成对象,但并不是真正的对象类型
在我们平常的生活当中,我们看看到的人,动物,汽车等等物体,这些都是对象,只要是物体,它是一个对象,则必然会具备以下几种特殊
- 对象具备属性
- 对象具备方法
- 对象可以继承
在JS当中,对象用object来表示
如何创建一个对象
使用键值对来创建对象
本质上面就是封装一个对象,封装一个对象使用的就是{}
语法格式
var 对象名={
属性名1:属性值1,
属性名2:属性值2
}
属性名我们也叫键(key)或以可以叫属性变量,属性值我们叫value或属性的值,像这样通过key:value的方法来创建的对象,我们叫键值对创建对象
var stu1 = {
userName:"张三",
sex:"男"
}
stu1是我们创建的对象的名子,里面的userName与sex代表的是这个对象的属性
刚刚我们已经在对象里面定义了一些属性,我们还可以在对象里面定义方法,如下代码所示
var stu1 = {
userName:"张三",
sex:"男",
sayHello:function(){
document.writeln("大家好, 我是"+stu1.userName+",我的性别是:"+stu1.sex);
}
}
stu1.sayHello(); //大家好, 我是张三,我的性别是:男
说明:上面的代码,我们就已经定义好了一个方法sayHello,调用stu.sayHello()的方法以后,就会在页面打印出相关信息
这个时候,表面看起来,这个对象已经没有问题了,定义完成了,现在,我们继续深入分析
当我们把stu1这个对象名改变以后,我们发现在sayHello这个方法当中,它也用到了stu1,这个时候,sayHello里面的stu1它也要改变,这个的代码不严谨,非常麻烦,也很容易出错,这个时候,我们就要想一下改变方法
var stu0 = {
//this指代当前对象
userName:"张三",
sex:"男",
sayHello:function(){
document.writeln("大家好, 我是"+this.userName+",我的性别是:"+this.sex);
}
}
在上面的代码当中,我们在方法里面,使用了this关键字,这个关键字在这里,它指代的是当前的这一个对象
对象调用属性
-
通过打点的方法去调用
对象名.属性名
这种方式来获取里面的属性值,如stu1.userName
则可以拿到“张三”,stu1.sex
就可以拿到“男”stu1.userName; stu1.sex;
但是请各们要注意,如果在这里,这个属性它是一个数字或是以数字开头的属性则不行,则不能通过上面的方法去调用
-
使用中括号去调用属性
对象名[属性名]
这种方式去获取里面的属性值,如stu1["userName"]
则可以拿到“张三“,stu1["sex"]
则可以拿到”男“stu1["userName"]; stu1["sex"];
这一种对象调用属性方法它没有限制,可以拿数字(或数字开头)的属性,也可以拿普通的属性
对象调用方法
对象调用方法与对象调用属性是一样的,都是通过对象名.方法名()
或对象名["方法名()"]
来调用的,以此不做过多说明
什么是属性,什么是方法
属性可以用来描述特征,方法可以拿来调用
使用Object来创建对象
使用Object直接创建对象
在JavaScript内部,有一个内置的对象叫Object,这个对象用来创建对基本的对象,格式如下
var obj=new Object();
var obj={};
它与我们之前学习的数组的定义非常相似
var arr=new Array();
var arr=[];
直接new出来的Object它是一个空的对象,如果要向里面添加属性,可以直接添加,如下所示
var stu1=new Object();
stu1.userName="张三";
stu1.sex="男";
stu1.sayHello=function(){
document.writeln("大家好,我叫:"+this.userName);
}
注意:上面的
对象名.属性名
也可以换成对象名["属性名"]
的形式
使用Object定义对象属性
通过上面的Object我们可以创建对象,并且可以直接添加属性,但是这些属性都是最基本的,它没有经过相关的配置信息,如果想定义这些属性的详细信息,则应该使用Object.defineProperty()
这一个方法来定义属性
var stu1=new Object();
stu1.userName="张三";
//stu1.sex="男";
stu1.address="湖北省武汉市";
//上面,我们已经定义了三个属性,但是在这里,我如果想让当前的这个sex不能被改变(只读)
//花括号是对象
Object.defineProperty(stu1,"sex",{
value:"男",
writable:false, //只否可以改变
configurable:false, //是否可以删除这个属性
enumerable:false //这个属性是否可以遍历出来(for...in...) Object.keys()也不行
});
- value:代表当前这个属性的默认值
- writable:代表这个属性是否可以更改
- configurable:代表这个属性是否可以通过delete删除
- enumrable:代表这个属性是否可以通过for…in…和Object.keys()来遍历
上面的四个参数在定义对象属性的时候,是最基本的四个方面,同时还有两个也是定义对象的时候所属使用的,但是它们有另一种说法,叫访问器
定义对象属性的访问器
所谓的属性访问器,其实就是一个get方法与set方法,get方法代表的是取属性值的方法,set方法其实代表赋属性值的方法,这两个方法分别在对属性赋值的时候与取值的时候自动执行
var stu1=new Object();
stu1.userName="张三";
stu1.address="湖北省武汉市";
//stu1.sex="男";
//sex属性可以被更改,但是只能被改成男或者女,不能是其它值
Object.defineProperty(stu1,"sex",{
get:function(){
//取值的过程,它会自己执行
return this._sex;
},
set:function(v){
//赋值过程当中自己去执行的
//这一个地方的参数v代表的就是你要赋的值
if(v=="男"||v=="女"){
//向sex属性赋值了
this._sex=v;
}
}
});
说明:在上面的代码当中,我们定义了一个属性sex,这个属性在赋值的时候,其实是把值赋给了
_sex
这个属性,在取值的时候,取的也是_sex
这个属性,这个sex值并没有保存在自身
注意:一旦定义了get和set的属性访问器以后,就不能够再去定义value与writable这两个属性了
应用点:控制赋值的时候,数据有合法性
var person={}; //var person=new Object();
person.familyName="张";
person.lastName="三";
Object.defineProperty(person,"userName",{
get:function(){
return this.familyName+this.lastName;
}
//你没有 set就说明你没有 赋值过程
});
说明 :在上面的代码过程当中,我们定义了姓的属性,也定义了名的属性,同时,通过Object.defineProperty来定义userName,通过属性访问器返回的是
this.familyName+this.lastName
,也就是我们的性+名同时,在这里,我们没有定放set的方法 ,则说明属性userName只能取值,不能赋值
总结:一般情况下,我们不会通过Object.defineProperty来定义属性,但是,如果碰到一些特殊的要求,如控制数据的有效性,或者这个属性不可以更改,或者这个属性不可以删除,或这个属性不可以被遍历出来,则需要通过上面的方法来定义属性
使用Object定义多个属性
之前,我们已经使用Object.defineProperty
去定义某一个对象的属性,现在我们可以通过它的复数形式去一次性定义多个属性Object.defineProperties(),它的语法格式与上一个很相似,具体如下所示
假设现在我们要对对象obj去定义两个属性分别是userName
与sex
,按照以前的定义方式,则需要一个一个的定义
var obj={}; //var obj=new Object();
//假设是以前
Object.defineProperty(obj,"sex",{
value:"男",
writable:false,
configurable:false,
enumerable:false
});
Object.defineProperty(obj,"userName",{
value:"张三",
writable:true,
configurable:false,
enumerable:true
});
现在,我们可以 使用新的方式,一次性定义多个属性
var obj={}; //var obj=new Object();
//现在采用下面的方式,可以一次性定义多个属性
Object.defineProperties(obj,{
userName:{
//这个花括号里面,就是这个属性的特性
value:"张三",
writable:true,
configurable:false,
enumerable:true
},
sex:{
value:"男",
writable:false,
configurable:false,
enumerable:false
}
});
在上定面定义对象的时候,它的语法格式 ,我们可以总结如下
如果是**一次定义一个属性**,它的语法格式如下
Object.defineProperty(对象名,"属性名",{
//属性的特征
})
如果**一次定义多个属性**,它的语法格式如下
Object.defineProperties(对象名,{
属性名1:{
//对象特征
},
属性名2:{
//对象特征
}
})
对象的特征指的就是get/set或value/configurable/enumerable/writable
获取属性的描述(特征)
我们可以通过Object.getOwnPropertyDescriptor
来获取某一个对象的属性的描述特征,具体如下
var stu={
userName:"张三",
sex:"男"
}
Object.getOwnPropertyDescriptor(stu,"sex");
上面的Object.getOwnPropertyDescriptor(stu,"sex");
就是用来获取stu这个对象里面的sex这个属性的描述特征,结果如下图
应用点:主要用于检测某一个对象里面的某一些属性它的特性,例如,这个对象是否可遍历,这个对象的属性是否是只读的,或这个对象的这个属性是否可删除
使用工厂模式创建对象
当我们在创建多个相似对象的时候,如果我们使用普通的键值对方式去创建,这个时候,它会变得非常繁琐,不利于我们的开发效果,如下代码所示
/*
* 现在有一个需求,要将我们班学生 张三 李四 王五和冯六
* 这四位学生的 姓名 性别 爱好,地址记录下来
*/
var stu1={
userName:"张三",
sex:"男",
hobby:["看书","打球"],
address:"湖北省武汉市"
};
var stu2={
userName:"李四",
sex:"男",
hobby:["看书","睡觉"],
address:"湖北省荆州市"
};
var stu3={
userName:"王五",
sex:"男",
hobby:["睡觉","玩游戏"],
address:"湖北省黄冈市"
};
var stu4={
userName:"冯六",
sex:"男",
hobby:["写代码","购物"],
address:"湖北省武汉市"
};
var stu5={
userName:"其他",
sex:"女",
hobby:["学习","逛街"],
address:"湖北省"
}
上面的代码当中,我们创建了5个对象,但是这样代码非常多,很不方便,现在,我们可以把上面的代码转换成下面的方法
//第一步:通过一种叫工厂模式去快速创建对象
function Student(_userName,_sex,_hobby,_address){
var obj={
userName:_userName,
sex:_sex,
hobby:_hobby,
address:_address
};
return obj;
//Student就相当于一个工厂,这个工厂里面,它只干一件事情,就是帮我们生产
//对象
}
//得到的是学生
var stu1=Student("张三","男",["看书","睡觉"],"湖北省武汉市");
var stu2=Student("李四","男",["购物","看书"],"湖北省武汉市江夏区");
function Teacher(_userName,_sex,_address){
var obj={
userName:_userName,
sex:_sex,
address:_address,
sayHello:function(){
document.writeln("大家好, 我叫:"+this.username+",我的地址是:"+this.address);
}
}
return obj;
}
var teacher1=Teacher("张三","男","湖北省武汉市");
在上面的代码当中,我们把创建对象的方法封装成了一个对象,这个时候,当我们再次去创建对象的时候,就会变得非常容易
注意:上面的方法也有一个非常大的弊端,就是不能够检测出对象的类型(所有的对象通过instanceof Object得到的都是true),所以我们后期不会常使用这种方法
使用构造函数创建对象
现有函数类型:普通的函数,带参数的函数,函数表达式,回调函数,匿名函数
构造函数其实就是一个普通的函数,只在调用的时候,我们可以new一下这个方法
构造函数是通过new
关键字来调用的,它经过new调用以后,会返回一个对象
案例
//构造函数
function Student(_userName,_sex,_address){
//this代表当前对象
this.userName=_userName;
//如果性别有值,则赋值,没有值则给默认值“男”
this.sex=_sex||"男";
//如果地址有值,则赋值,没有值则给默认值"中国";
this.address=_address||"中国";
this.sayHello=function(){
document.writeln("大家好,我是:"+this.userName+",我的性别:"+this.sex+",我的地址:"+this.address);
}
}
function Teacher(){
}
var stu1=new Student("张三","男","湖北武汉市");
var stu2=new Student("李四");
var teacher1=new Teacher();
使用构造函数创建对象是以后创建对象的主流方式,它既省略了我们之前创建对象要输入属性名的繁琐,也可以分辨所创建对象的类型
构造函数与普通函数的区别
构造函数它也是一个函数
- 如果一个函数通过
方法名+()
来调用,我们就把它当成普通函数,如果通过new来调用,我们就把它看成是构造函数(任何构造函数都是普通的函数)【相同点】 - 当一个函数被作为构造函数new出来的时候,里面的this指向的是当前对象本身,如果这个函数被作为普通函数运行,里面的this指向的是全局的window(window可以理解为浏览器窗口)对象(在浏览器)【不同点】
通过下面的代码,我们可以具体去体现
function Student(){
this.userName="张三";
//document.writeln("张三,这是执行的方法");
console.log(this);
}
代码执行结果如下
遍历对象的属性
在JS里面,我们有两种方式得到一个对象的所有属性
通过Object.keys()
通过调用对象Object.keys()的方法,我们可以得到某一个对象所有的属性
Object.keys(对象);
这个时候,就会得到当前对象所有的属性名,返回的是一个数组
通过for…in…来获取对象的属性名
for(var i in obj){
//这个时候的i就是当前这个对象的属性名
console.log(i);
}
如果这个对象是数组,则返回的属性名是这个数组的索引(下标)
👉 问题:上在的两个方法有区别吗?
👊 区别:Object.keys
只是拿当前对象的所有属性,而for...in...
可以拿到父级对象的
判断属性是否存在
问题:怎么样判断某一个属性是否存在于当前这个对象?
function Person(_name,_sex,_age){
this.name=_name;
this.sex=_sex;
this.age=_age;
}
function Student(_sid,_name,_sex,_age){
this.sid=_sid;
}
//Student与Person形成了继承关系
Student.prototype=new Person("张三","女",18);
var s1=new Student("y123","张三","女",18);
第一种方式:使用对象的方法hasOwnProperty()
这个方法去完成
s1.hasOwnProperty("sid"); //true
s1.hasOwnProperty("sex"); //false
第二种方式:使用in
关键字去判断属性是否存在
"sid" in s1; //true;
"aaa" in s1; //false;
"sex" in s1; //true
👊 总结:hasOwnProperty()
只在当前对象去判断是否存在这个属性,而关键字in
会在父级元素里面去判断
对象与对象的关系
对象包含对象
案例1
第一次演变
//学生的信息
var stu0={
userName:"张三",
sex:"男",
userId:"Y102XX"
}
var stu1={
userName:"李四",
sex:"男",
userId:"Y102XX"
}
//班级的信息
var classRoom={
className:"项目",
address:"教室",
maxNum:48,
classId:"Y102",
students:[stu0,stu1]
};
在上面的代码当中,我们看到了一个对象classRoom,还看到了另二个对象stu0与stu1。也就是一个是班级对象,一个是学生对象,然后班级对象通过
students
这个属性包含了学生对象
上面的代码,我们还可以写成如下格式,这样写会更方便一些
第二次演变
var classRoom={
className:"项目",
address:"教室",
maxNum:48,
classId:"Y102",
students:[
{
userName:"张三",
sex:"男",
userId:"Y101"
},{
userName:"李四",
sex:"男",
userId:"Y102"
},{
userName:"王五",
sex:"女",
userId:"Y103"
}
]
};
说明:我们直接把学生的信息定义在了classRoom对象的students下面,这样做少了一步定义变量的步骤
上面的一咱方式还不是非常方便,我们每次去添加学生的时候,都需要去定义学生的对象,很麻烦,现在我们分析以后,可以进行如下简化了
第三次演变
//班级的信息
var classRoom={
className:"项目",
address:"教室",
maxNum:48,
classId:"Y101",
students:[
{
userName:"张三",
sex:"男",
userId:"Y23"
}
]
};
function Student(_userName,_sex,_userId){
this.userName=_userName;
this.sex=_sex;
this.userId=_userId;
}
/*
var s=new Student("张三","男","Y23");
classRoom.students.push(s);
var s2=new Student("冯六","男","Y22");
classRoom.students.push(s2);
*/
classRoom.students.push(new Student("王五","男","U123"));
最终,我们就可以直接调用
classRoom.students.push(new Student("王五","男","U123"));
向班级里面去添加学生信息
小提示: 上面的代码还不是最规范的代码,我们可以在上面的代码上面继续优化我们的代码结构
第四次演变
//班级的信息
var classRoom={
className:"项目",
address:"湖北省武汉市",
maxNum:48,
classId:"Y101",
students:[
{
userName:"张三",
sex:"男",
userId:"Y103"
}
],
//通过下面的方法再去添加学生
addStudent:function(stu){
if(stu instanceof Student){
this.students.push(stu);
}
else{
console.log("请添加学生");
//throw new Error("请添加学生");
}
}
};
function Student(_userName,_sex,_userId){
this.userName=_userName;
this.sex=_sex;
this.userId=_userId;
}
说明:上面的代码就解决了一个问题,当我们通过
addStudent
这个方法去添加学生的时候,它会验证一下你添加进去的这个参数是否真的是一个学生,如果不是,我就会给你一个错误的提示,如果是学生,我才真正的保存下来
对象的继承关系
对象的继承可以理解为现实生活当中的父子关系,例如,父亲的东西,如果没有特殊要求, 是会传给儿子的。儿子是可以使用父亲的东西
我们现在的学生,如果没钱了,就会给家里打电话,家里就把钱给大家,学生拿到钱以后,就开始花钱,但是,这个钱又不是学生的,而是家长的。为什么学生可以花家长的钱呢?因为它们有关第?他们有什么关系呢?父子关系!
在编程语言里,所有的对象都会默认继承Object对象
无论我们是通过构造函数创建的对象还是通过键值对创建的对象,都会默认继承Object,Object是所有对象最根据的类(在中国的角度里面,我们喜欢把它当成女娲,在西方的角度里面,喜欢当成上帝)
通过方法的call与apply继承
案例1
/**
* 创建学生与老师的对象
* 学生与老师都具备姓名 性别 地址三个属性
* 学生有一个学号的属性,老师有一个教职工号的属性
* 老师还有一个方法叫Teaching方法 学生有一个Study的方法
* 要求:使用合理的方式去创建构造函数,然后再去创建一个学生与一个老师
*/
//定义一个人的构造函数
function Person(_name,_sex,_address){
this.name=_name;
this.sex=_sex;
this.address=_address;
}
//定义学生的构造函数
function Student(_sid,_name,_sex,_address){
this.sid=_sid;
//Person(_name,_sex,_address);
Person.call(this,_name,_sex,_address);
this.Study=function(){
console.log("学生在听课");
}
}
//定义老师的构造函数
function Teacher(_tid,_name,_sex,_address){
this.tid=_tid;
Person.call(this,_name,_sex,_address);
this.Teaching=function(){
console.log("老师在教书");
}
}
var s=new Student("Y104","李四","女","湖北省武汉市");
var t=new Teacher("Y105","王五","男","湖北省武汉市");
说明:在上面的代码当中,我们通过call这样一个该来实现了两个构造函数之间的调用关系,构造函数完成调用关系以后,这个时候生成的对象就完成了它最初要求的继承关系
注意:call是function特有的一个方法,是专门为了实现对象的继承而诞生的一个方法,同时与该方法相似的还有另一个方法也可以实现对象的继承,这个方法就是apply
案例2
/*
* 假设有学生的老师两个对象,学生study与老师teaching各有不同的方法
* 老师和学生还有一些共同的方法 吃饭 睡觉
* 学生与老师都要保存姓名与性别,还要保存地址
* 请按照上面的要求,合理的编写构造器,并创建对象
*/
function Person(_name,_sex,_address){
this.name=_name;
this.sex=_sex;
this.address=_address;
this.Eat=function(){
console.log("吃饭");
}
this.Sleep=function(){
console.log("睡觉");
}
}
function Student(){
//Person.apply(this,[_name,_sex]);
Person.apply(this,arguments);
this.Study=function(){
console.log("学习");
}
}
function Teacher(){
//Person.apply(this,[_name,_sex]);
Person.apply(this,arguments);
//arguments所有参数的集合
//如果发现自己的参数与要继承的对象的构造函数的参数一样,则可以直接通过arguments传递过去
this.Teaching=function(){
console.log("教书");
}
}
var s=new Student("张三","女","湖北省武汉市");
var t=new Teacher("李四","男","湖北省武汉市");
注意:call与apply是一样的方法,都是可以实现继承,第一个参数也是一样的,都是this,但是从第二个参数开始,它们就不一样了,call需要我们一个一个的去传递参数,而apply可以把所有的参数放在一个数组,然后传递过去
而我们方法里面arugments这个变量变保存了我们所有的实参,所以当发现自己的参数与要继承的对象的构造函数的参数一样,则可以直接通过arguments传递过去
通过原型进行继承
案例1
要求在每个Student得到的对象上面添加一个sayHello的方法
function Student(_sid,_name){
this.sid=_sid;
this.name=_name;
//第一种方式
/*
this.sayHello=function(){
console.log("你好");
}
*/
}
var s1=new Student("Y102XX","张三");
var s2=new Student("Y102X2","李四");
/*
* 现在,我们得到了两个学生,两个学生都有各自的属性
* 每 一个学生添加一个方法 sayHello 在控制台打印你好
*/
//第二种方式 最笨的方式
// s1.sayHello=function(){
// console.log("你好");
// }
// s2.sayHello=function(){
// console.log("你好");
// }
//第三步:通过对象的__proto__这个属性
//这里的s1变量有一个不确定性
// s1.__proto__.sayHello=function(){
// console.log("你好,我是你父亲");
// }
//第四步
//s1.__proto__==Student.prototype
Student.prototype.sayHello=function(){
console.log("你好,我是你原型上面的父亲");
}
我们可以通过原型来分析对象的属性到底是属于自己还是父级对象的
//构造函数
function Student(_name,_sex){
this.name=_name;
this.sex=_sex;
}
//我们在构造方法的原型上去扩展了两个属性
Student.prototype.address="湖北省武汉市";
Student.prototype.className="Y102";
var s1=new Student("张三","男");
var s2=new Student("李四","女");
结果如下图所示
通过上面的图片,我们可以清楚的看到,name与sex这两个属性是当前对象的,而address与className是父级对象的
当我们要继承的属性非常多的时候,我们可以把这些属性再次的封装成一个对象,如下代码所示
Student.prototype={
address:"湖北省武汉市",
className:"Y101",
study:function(){
console.log("我要学习");
}
}
这个时候,我们在这里就看到了prototype直接赋值了一个对象
后期的代码当中,我们会直接通过构造函数的方法完成对象之间的继承
//构造函数
function Student(_name,_sex,_address){
this.name=_name;
this.sex=_sex;
}
//构造函数
function Person(_address){
this.address=_address;
this.sayHello=function(){
console.log("你好,我给你打招呼");
}
this.study=function(){
console.log("我要学习");
}
}
/*
var p=new Person();
Student.prototype=p;
*/
//思考:能否通过原型继承把参数传递到父级对象
Student.prototype=new Person();
var s1=new Student("张三","男","湖北省武汉市"); //湖北省武汉市
var s2=new Student("李四","女","湖南省长沙市"); //湖南省长沙市
思考:能否通过原型继承把参数传递到父级对象(可以)
下面的代码演示了如何通过原型继承的同时还把参数传递到父级对象
//构造函数
function Student(_sid,_name){
this.sid=_sid;
//我要把姓名传递到父极对象,同时还要实现原型继承
//Student.prototype=new Person(_name); 这一句 话是无效的
//s.__proto__=new Person(_name);
this.__proto__=new Person(_name);
//一旦在这里把this.__proto__重新赋值以后,它的值就不再等于Student.prototype
//这个影响在这里不大
}
function Person(_name){
this.name=_name;
this.sayHello=function(){
console.log("你好啊");
}
}
//原型继承
//Student.prototype=new Person("小哥哥");
//创建的对象
var s=new Student("Y110","刘六");
关于原型的继承,我们多半都使用在方法的继承上面,而不是通过prototype这种继承去实现普通属性的继承
案例2
通过原型去扩展某一类构造函数的方法,在这里,我们以数组为例
var arr=new Array("a","b","c","d","e");
var arr1=[1,2,3];
Array.prototype.remove=function(v){
//console.log("我添加了一个删除的方法"+",你现在要删除的元素是:"+v);
//写一些相应的代码,删除指定的元素
//console.log(this);
//这个地方的this指的是通过当前这个构造器所创建出来的对象
var index=this.indexOf(v); //得到当前这个元素的索引(下标)
if(index!=-1){
this.splice(index,1);
}
}
当我们在构造函数Array上面的prototype上添加了一个方法以后,这个时候,只要是通过Array所构造出来的对象就都会有一个方法remove了,这样,我们的数组再去删除一个元素的时候,就会变得非常简单了