面向对象详解之JavaScript篇
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象详解之JavaScript篇相关的知识,希望对你有一定的参考价值。
【重点提前说:面向对象的思想很重要!】
最近开始接触学习后台的php语言,在接触到PHP中的面向对象相关思想之后,突然想到之前曾接触的JS中的面向对象思想,无奈记性太差,便去翻了翻资料,花了点时间梳理下以前接触过的OOP相关知识点,也希望在PHP的学习中能相互对比,加深理解。
接下来可要进入化冰之路-PHP篇了,过几天我将会再发一篇PHP中有关OOP的相关知识点梳理学习,希望大家放平心态,面向OOP,共同进步!
一、学习前,你该知道这些基础知识~
1、语言的分类:
通常情况下我么所涉及的计算机语言可大致分三类
①、面向机器:汇编语言。
②、面向过程:C语言
③、面向对象:Java、C++、PHP等。
2、区分理解面向对象/面向过程
很多的初学者在刚接触到面向对象这个概念时,总是感觉其定义很笼统抽象,也比较难理解,我们可以将其和面向过程比较理解记忆。
①、面向过程:专注于如何去解决一个问题的过程步骤,编程的特点是由一个个的函数去实现每一步的过程步骤,没有类和对象的概念。
②、面向对象:专注于由哪一个对象来解决这个问题,编程特点是出现了一个个的类,从类中拿到对象,由这个对象去解决具体问题
举个可能不是很恰当的例子来理解一下吧,相信很多的园友经历过家装吧,新买的房子交付了,就要考虑着手去装修了。现在摆在我们面前的有两种方法:一是交给家装公司来做,在你信得过的情况下,只要告诉他你想要得到什么效果,剩下的让他来做即可。二是你自己多费点功夫,去做设计图,去跑家装市场,去学习下如何在短期内称为一个合格的设计师和施工人员。
我们以此类比,第一种方法中我们只需要找个一家合适的家装公司(一个对象),即面向对象。在第二种方法中我们需要考虑很多解决问题的方法(函数),即面向过程。由此可见二者的区别: 对于调用者来说,面向过程需要调用者自己去实现各种函数。 而面向对象,只需要告诉调用者对象中具体方法的功能,不需要调用者去了解方法中的实现细节。
3、面向对象的三大特征 继承、封装、多态
注意:JS可以模拟实现继承和封装,但是不能模拟实现多态,故js是基于事件的,基于对象的语言。
4、类和对象的概念
(1)、类:一类具有相同特征(属性)和行为(方法)的集合;
例如:人类--->: 属性:身高、姓名、体重
方法:吃、喝、拉、撒
(2)、对象:从类中拿出具有确定属性值和方法的个体叫做对象:
例如:张三--->:身高:180cm 体重:70kg 方法:说话--->我叫张三
(3)、类和对象的关系:
类是抽象的,对象是具体的
类是对象的抽象化,对象是类的具体化;
当我们对人类的每一个属性都进行了具体的赋值,那么就可以说张三是由人类产生的对象.
5、创建一个类并实例化出对象
①、创建一个类(构造函数):类名必须使用大驼峰法则。即首字母大写;
function 类名 (属性1){
this.属性1 = 属性1;
this.方法 = function(){
方法中调用自身的属性,必须使用this.属性;
}
}
②、通过类,实例化(new关键字)出一个对象:
var obi = new 类名 (属性1 的实参);
obj.属性;调用属性
obj.方法();调用方法
③、注意事项:
>>>通过类名,new出一个对象的过程,叫做"类的实例化";
>>>类中的this会在实例化的时候,指向新new出的对象。
>>>所以,this.属性 this.方法 实际上是将属性和方法绑定在新new出的对象上;
>>>在类中访问自身的属性,必须使用this.属性调用。如果直接使用变量名则无法访问该属性值;
>>>类名必须使用大驼峰法则,注意与普通函数区分;
我们通过例子来看一下:
//创建一个类的步骤如下: //★①、创建一个类(构造函数) function Person (name,age) { this.name = name;//前一个是自定义函数中的属性,后一个是调用函数的形参,可用来传递实参; this.age = age;//在类中访问自身的属性,必须使用this.属性调用。 this.say = function (content) { alert("我叫"+this.name+"今年"+this.age+"岁了!我说了一句话"+content); } } // ★②、类的实例化: var calcifer = new Person("calcifer",23); calcifer.say("哈哈哈!");
/*上面的也可以写成:
var calcifer = new Person();
calcifer.name= "louver";
calcifer.age = 14;
calcifer.say("哈哈哈!");
需要注意的是,赋值必须放在函数调用之前,否则结果为undefined;
*/
运行后页面会自动弹窗输出:
6、类和对象的两个重要属性;
①、constructor:返回当前对象的构造函数;
calcifer.constructor 返回的是上面声明的类;
②、instanceof:A instanceof B 检测一个对象(A)是不是一个类(B)的一个实例;
calcifer instanceof Person;√ louver是函数Person的实例化;
calcifer instanceof Object;√ 所有对象都是Object的一个实例;
Person instanceof Object;√ 函数本身也是一个对象;
7、补充:狭义对象与广义对象
①、只有属性和方法,除此之外,没有任何其他的内容; var obj= { } var obj = new object(); ②、广义对象:除了用字面量声明的基本数据类型之外,JS中万物皆对象。 换句话说。只要是能添加属性和方法的变量,都可以称为对象。 eg: △ 使用字面量声明 var s = "111";//不是对象 s.name = "aaa"; console.log(typeof(s))//检测为string字符串类型; console.log(s.name) //undefined 字面量声明的字符串不是对象,不能添加属性; △ 使用关键字声明 var m = new String();// 是对象 m.name = "aaa"; console.log(typeof(m))//检测为Object类型,因为m可以添加属性及方法,而s不行。 console.log(m.name) //aaa 使用new声明的字符串是对象类型,可以添加属性和方法;
二、继承与实现继承的方式
1、什么叫做继承?
使用一个子类继承另一个父类,那么子类可以自动拥有父类中的所有属性和方法,这个过程叫做继承;
>>>继承的两方,发生在两个类之间;
2、使用call bind apply实现继承
首先我们先来了解一下call bind apply这三个函数,先看一下共同点:通过函数名调用这三个函数,可以强行将函数中的this指定为某一个对象;
它们的区别主要在于写法的不同:接受func的参数列表的形式不同,除此之外,功能上没有差别!
① call写法:func.call(func的this指向的obj,func参数1,func参2,....);
② apply写法:func.apply(func的this指向的obj,[func参数1,func参2,....]);//接收一个数组形式
③ bind写法:func.bind(func的this指向的obj),(func参数1,func参2,....);
接下来我们主要看一下如何使用这三个函数实现继承:(以call为例)
实现步骤:
① 定义父类:
function Parent (name){}
② 定义子类时,在子类中使用三个函数,调用父类,将父类函数中的this,指向为子类函数的this;
function Son (num,name){
this.num = num;
Person.call(this,name);
}
③ 实例化子类时,将自动继承父类属性
var s = new Son(12,"zhangsan");
代码示例如下:
function Person (name,age) { this.name =name; this.age =age; this.say = function () { alert("我叫"+this.name); } } this.study = function () { alert("我叫"+this.name+"我今年"+this.age+"岁了!我的学号是"+this.num); } Person.call(this,name,age); } var s = new Student(12,"calcifer",24); s.say(); s.study();
在浏览器中代码运行结果如下:
第一张效果图表示子类已经继承了父类中的say()方法
第二张图表示子类同时继承了父类中的属性:
2、使用for-in循环扩展Object实现继承
废话不多说,我们直奔主题,先讲一下我们这个方法实现继承的思路吧,这个方法的关键点其实在于通过for-in循环将父类对象的所有属性和方法,全部赋给子类对象。
当然即使不扩展Object,也能通过简单的循环实现操作;下面我们详细展示下步骤:
① 声明一个父类
function Parent (){}
声明一个子类
function Son (){}
② 通过prototype给Object类添加一个扩展方法:
Object.prototype.extend = function (parent) {
for(var i in parent){
this[i] = parent[i];
}
}
③ 分别拿到父类对象和子类对象:
var p = new Parent();
var s = Son();
④ 用子类对象调用扩展方法,实现从继承操作:
s.extend(p)
代码示例如下:
function Person(name,age){ this.name = name; this.age = age; this.say = function(){ alert("我叫"+this.name); } } function Student(no){ this.no = no; this.study = function(){ alert("我在学习!"); } } var p = new Person("张三",12); var s = new Student("1234567"); for (var i in p) { s[i] = p[i]; } console.log(s);
我们同样可以手写继承方法如下:
Object.prototype.extend1 = function(parent){ for(var i in parent){ this[i] = parent [i]; } } var p = new Person("张三",12); var s = new Student("1234567"); s.extend1(p); console.log(s);
两者最终的执行效果是一样的:在控制台上的输出如下所示:
我们可以看到实例化的子类对象s中同时具有了父类对象的属性以及方法,实现了继承。
3、使用原型实现继承
使用原型实现继承其实就是 将父类对象,赋值给子类的prototype,那么父类对象的属性和方法就会出现在子类的prototype中, 那么实例化子类时,子类的prototype又会到子类的__proto__中。
代码示例如下:
function Person (name,age) { this.name = name, this.age = age, this.say = function () { alert("这是"+this.name); } } function Student (num) { this.num = num, this.study = function () { alert("我在学习!"); } } Student.prototype = new Person("张三",14); var s = new Student("1234567"); s.say(); s.study(); console.log(s);
将上面代码运行,在浏览器的控制台我们可以看到被打印出来的子类对象:
通过观察我们可以发现,这种使用原型继承的方法的特点在于子类自身的所有属性,都是成员属性;父类继承过来的属性,都是原型属性,但是这种方法的缺点在于仍然无法通过一步实例化拿到完整的子类对象。
这一种方法里面涉及到了原型思想,考虑到很多的初学者没有接触到这一概念,下面我们详细的介绍一下原型与原型链以及原型属性及方法。
三、原型与原型链&&原型属性与原型方法方法
1、prototype:函数的原型对象
① 只有函数才有prototype,而且所有的函数必然有prototype!
② prototype本身也是一个对象!
③ prototype指向了当前函数所在的引用地址!
2、__proto__:对象的原型
① 只有对象才有__proto__,而且所有的对象必有__proto__;
② __proto__也是一个对象,所以也有自己的__proto__,顺着这条线向上找的顺序,就是原型链。
③ 数组都是对象,也都有自己的__proto__;
3、实例化一个类,拿到对象的原理:
实例化一个类的时候,实际上是将新对象的__proto__,指向构造函数所在的prototype;
也就是说:zhangsan.__proto__ ==Person.prototype √
4、所有对象的__proto__沿着原型链向上查找都将指向Object的prototype;
Object的prototype的原型,指向null;
【原型链的指向问题】
研究原型链的指向问题,就是要研究各种特殊对象的__proto__的指向问题。
1、通过构造函数,new出的对象。新对象的__proto__指向构造函数的prototype;
2、函数的__proto__,指向function()的prototype;
3、函数的prototype的__proto__指向object的prototype;
(直接使用{}字面声明,或使用new Object 拿到的对象的__proto__ 直接指向Object的prototype)
4、 Object的prototype的__proto__,指向null;
(Object作为一个特殊函数,它的__proto__指向function()的prototype;)
类中的属性与方法的声明方式
1、【成员属性和成员方法】
this.name = ""; this.func = function(){};
>>>属于实例化出的新对象,使用对象.属性调用;
2、【静态属性与静态方法】
Person.name = "" Person.func= function(){};
>>>属于类的,用类名.属性调用;
3、【私有属性和私有方法】
在构造函数中,使用var声明的变量称为私有属性;
在构造函数中,使用function声明的函数,称为私有方法;
4、【原型属性和原型方法】
Person.prototype.name = "";
Person.prototype.func = function(){};
>>>将属性或者方法写到类的prototype上,在实例化的时候,这些属性和方法就会进入到新对象的__proto__上,就可以使用对象名调用;
也就是说1、4可以使用对象名访问,2使用类名访问,3只能在函数内部使用
5、当访问 对象的属性或者方法时,会优先使用对象自身上的成员属性和成员方法,
如果没有找到就使用__proto__上面的原型属性和原型方法,如果仍然没有将继续沿着原型链查找,最后返回undefined;
6、我们习惯上,将属性写成成员属性,将方法定义为原型方法;
function Person (name) { this.name = name;//声明成员属性 } Person.prototype.say = function(){};
原因:
① 原型属性在定义之后不能改变,无法在实例化时进行赋值。所以属性不能使用原型属性。
但是方法,写完之后基本上不用再需要进行改变,所以,方法可以使用原型方法;
② 实例化出对象之后,属性全在对象上,方法全在原型上,结构清晰。
③ 使用for-in遍历对象时会将属性和方法全部打印出来。
而方法往往不需要展示,那么将方法写在原型上,就可以使用hasOwnProperty将原型方法过滤掉。
④ 方法写在prototype上,将更加节省内存;
⑤ 这是官方推荐的写法;
示例如下:
function Person (name) { this.name = name;//成员属性 var num = 1;//私有属性 } Person.cout = "60亿"//静态属性 Person.prototype.age =14;//原型属性 var zhangsan = new Person("张三"); console.log(zhangsan);
浏览器控制台打印如下:
四、JS中模拟实现封装
在上面我们讲完JS的继承以及继承实现的方式之后,接下来我们来看一下另一大特征----封装。
1、什么叫封装?
① 方法的封装: 将类内部的函数进行私有化处理,不对外提供调用接口,无法在类外部使用的方法,称为私有方法,即方法的封装。
② 属性的封装: 将类中的属性进行私有化处理,对外不能直接使用对象名访问(私有属性)。 同时,需要提供专门用于设置和读取私有属性的set/get方法,让外部使用我们提供的方法,对属性进行操作。 这就叫属性的封装。
在这里我们需要注意的地方是:封装不是拒绝访问,而是限制访问。 它要求调用者,必须使用我们提供的set/get方法进行属性的操作,而不是直接拒绝操作。因此,单纯的属性私有化,不能称为封装!必须要私有化之后,提供对应的set/get方法!!!
2、如何实现封装?
接下来我们以实例来看一下JS中如何模拟实现封装:
function Person(name,age1){ this.name = name; // this.age = age; var age = 0; this.setAge = function(ages){ if(ages>0 && ages<=120){ age = ages; }else{ alert("年龄赋值失败!"); } } // 当实例化类拿到对象时,可以直接通过类名的()传入年龄,设置私有属性 if(age1 != undefined) this.setAge(age1); this.getAge = function(){ return age; } this.sayTime = function(){ alert("我说当前时间是"+getTime()); } this.writeTime = function (){ alert("我写了当前时间是"+getTime()); } /* * 私有化的方法,只能在类内部被其他方法调用,而不能对外提供功能。 这就是方法的封装! */ function getTime(){ return new Date(); } }
五、闭包
我们都知道在JS中函数存在作用域,函数外声明的变量为全局变量,而函数内声明的变量为全局变量。同时,在JS中没有块级作用域,也就是说,if/for等有{}的结构体,并不能具备自己的作用域;所以,函数外部不能访问函数内部的局部变量(私有属性)。因为函数内部的变量,在函数执行完成之后,就会被释放掉。
但是使用闭包,可以访问函数的私有变量! JS中提供了一种闭包的概念:在函数中,定义一个子函数,子函数可以访问父函数的私有变量, 可以在子函数中进行操作,最后将子函数通过return返回。
我们举个栗子来说明一下闭包的概念:
function func1 () { var num =1;//函数内部的局部变量 function func2 () { return num;//定义一个子函数,子函数可以访问父函数中声明的私有变量,并处理后返回。 } return func2();//返回子函数 } var num = func1()();
闭包的作用:
① 可以在函数外部访问函数的私有变量;
② 让函数内部的变量,可以始终存在于内存中,不会在函数调用完成之后立即释放!
接下来我们看一个典型的案例,来更好的理解闭包的作用:
我们想要实现点击一个li,都会弹出其对应的数值
<body> <ul> <li>11111</li> <li>22223</li> <li>33333</li> <li>44444</li> <li>55555</li> </ul> </body>
var lis = document.getElementsByClassName("li"); for(var i=0; i<lis.length; i++){ lis[i].onclick = function(){ alert(i); } }
在运行上面的一段代码时,发现无论点击任何一个li都会弹出5.
分析一下错误原因在于
代码从上自下,执行完毕后,li的onclick还没触发,for循环已经转完。 而for循环没有自己的作用域!所以循环5次,用的是同一个全局变量i。也就是说在for循环转完以后,这个全局变量i已经变成了5. 那么再点击li的时候,无论点击第几个,i都是5。
接下来我们提供了三种解决方法,具体如下:
1、【使用闭包解决上述问题】
解决原理:函数具有自己的作用域,在for循环转一次创建一个自执行函数,
在每个自执行函数中,都有自己独立的i,而不会被释放掉。
所以for循环转完以后,创建的5个自执行函数的作用域中,分别存储了5个
不同的i变量,也就解决了问题。
var lis = document.getElementsByClassName("li"); for(var i=0; i<lis.length; i++){ !function(i){ lis[i].onclick = function(){ alert(i); } }(i); }
2、【使用let解决】
解决原理:let具有自己的块级作用域,所以for循环转一次会创建一个块级作用域;
var lis = document.getElementsByClassName("li"); for(let i=0; i<lis.length; i++){ lis[i].onclick = function(){ alert(i); } }
3、【使用this解决原理】
出错的原因在于全局变量i在多次循环之后被污染。那么在点击事件中,就可以不使用i变量,而是使用this代替lis[i],这样不会出现错误
var lis = document.getElementsByClassName("li"); for(var i=0; i<lis.length; i++){ lis[i].onclick = function(){ alert(this。innerText); } }
使用修改后的方法,运行文件可以看到:
好了,有关JS的OOP相关知识就讲这么多吧,过几天还会为大家带来PHP语言的面向对象的介绍,在PHP中面向对象更正统也更加有趣,希望大家保持关注!
共勉,谢谢!
以上是关于面向对象详解之JavaScript篇的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript提高篇之面向对象之单利模式工厂模型构造函数原型链模式