1、什么是面向对象编程
要理解面向对象,得先搞清楚什么是对象,首先需要明确一点这里所说的对象,不是生活中的搞男女朋友对象,面向对象就是面向着对象,换在代码中,就是一段代码相中了另一段代码,自此夜以继日的含情脉脉的面向着这一段代码,这就叫做面向对象,谁要这么给人解释,那笑话可就闹大了,但是可以把男朋友或者女朋友视为一个对象,之前我们也简单的介绍过对象,即可以把一个人视为一个对象,对象有他的属性和方法,属性如:性别、身高、体重、籍贯等,方法有走、跑、跳等。那么我们就可以从两方面理解对象:
(1)、从对象本身理解,对象就是单个实物的抽象。
一本书、一辆车、一台电视可以被视为对象,一张网页、一个数据库、一个服务器请求也可以被视为一个对象,当实物被抽象成对象,那么实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对"对象"进行编程。
(2)、从对象的性质理解,对象是一个容器,包含属性和方法。
所谓属性,就是对象的状态,所谓方法,就是对象的行为(完成某种任务),比如,我们可以把动物抽象为对象,属性记录具体是哪一种动物,方法表示动物的行为,比如:捕猎、奔跑、攻击、飞、爬、休息等。
总体来讲,对象是一个整体,对外提供一些操作,比如电视,我们并不了解其内部构成以及工作原理,但是我们都会使用,对于电视来说,只要用好按钮,会操作,这个电路那个元件怎么工作,跟我们没什么关系,只要电视能正常运行就好了,我们只要知道每个按钮是干嘛的,就可以使用这些功能,这就是面向对象。再比如获取时间 Date,通过不同的属性我们可以获取到不同的时间,比如年份月份星期,我们并不知道他具体是怎么实现的,但是都知道使用哪个属性可以获取到所需要的,这就是面向对象。
那到底什么是面向对象?简单说就是在不了解内部原理的情况下,会使用其功能。就是使用对象时,只关注对象提供的功能,不关注其内部细节。典型的应用实例就是 jQuery。
面向对象是一种通用的思想,并非只有编程中能用,任何事情都可以使用,生活中充满了面向对象的思想,只是我们不直接叫面向对象,而是叫一些别的什么。比如你去吃饭,你就告诉厨师来一份红烧肉,然后就可以坐下来等着吃了,你不可能给厨师说要把肉切成方的或者圆的,要先放盐,再放酱油,还要加红糖,加冰糖也可以,谁真要这样,厨师非得跟你急,他是厨师还是你是厨师,你只要把想吃的告诉他,你不用去管他是怎么做的,他自然会做好给你端上来,这就是生活中典型的面向对象的思想。
虽然不同于传统的面向对象编程语言,但是 JS 也有很强的面向对象编程能力,接下来就具体分析以下什么是 JS 面向对象编程。
面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式,所谓范式,就是符合某一种级别的关系模式的集合。他的核心思想是将真实世界中各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。面向对象编程的程序就是符合某一种级别的关系模式的集合,是一系列对象的组合,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。因此,面向对象编程具有灵活性、代码的可重用性、模块性等特点,并且容易维护和开发,非常适合多人合作的大型项目,而在平时项目中一般不常使用。
面向对象编程(OOP)的特点:
(1)、抽象:抓住核心问题
所谓抽象,先来看看百度对于抽象的解释:抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征。例如苹果、香蕉、鸭梨、葡萄、桃子等,它们共同的特性就是水果。得出水果概念的过程,就是一个抽象的过程。要抽象,就必须进行比较,没有比较就无法找到在本质上共同的部分。共同特征是指那些能把一类事物与其他类事物区分开来的特征,这些具有区分作用的特征又称本质特征。因此抽取事物的共同特征就是抽取事物的本质特征,舍弃非本质的特征。所以抽象的过程也是一个裁剪的过程。在抽象时,同与不同,决定于从什么角度上来抽象。抽象的角度取决于分析问题的目的。
在 JS 中,抽象的核心就是抽,就是抓住共同特征,抓住核心的问题。比如说人,有很多特征,比如姓名、性别、籍贯、出生日期、身高、体重、血型、家庭住址、父母是谁、孩子叫啥等,如果一个公司要建立员工档案,不可能将每个特征都注明,需要抓住一些主要的特征,比如:姓名、性别、部门、职位,或者再加上入职日期就完了。如果需要注册一个婚恋网站,那这时候就不是需要员工档案中注明的那些特点了,要一些比如:性别、年龄、身高、体形、星座、有车否、有房否、工作、收入、家庭状况等。就是把一类事物主要的特征、跟问题相关的特征抽取出来。这就是面向对象编程的抽象。
(2)、封装:不考虑内部实现,只考虑功能使用
何为封装,就好比一台电视机,我们能看到电视机,比如外观、颜色等,但是看不到内部的构成,我们也不用知道内部是什么鬼,依然可以正常使用,除非这货坏了,这内部的东西就是封装。JS 就是不考虑内部的实现,只考虑功能的使用,就像使用 jQuery 一样,jQuery 就是对 JS 的封装,我们使用 jQuery 的功能,能完成与 JS 相同的效果,并且还比使用 JS 更方便。
(3)、继承:从已有对象上,继承出新的对象
所谓继承,也可以叫做遗传,通俗理解就是父母能干的事孩子也能干,比如吃饭,睡觉。在 JS 中,比如有一个对象 A,A 中有一些功能,现在从 A 中继承出一个对象 B,这个对象 B 就具有对象 A 的所有功能。
还有一种情况是多重继承,好比一个孩子可以有好多个爹,显然这是不可能的事,但是在程序中这是可行的,比如有一类盒子,盒子有一个特征可以用来装东西,还有一类汽车,汽车的特征就是会跑,有轱辘,这时候就可以多重继承,继承出另一类集装箱货车,他特征既可以装东西又会跑,有轱辘。
(4)、多态
多态,顾名思义就是多种状态,在面向对象语言中,接口的多种不同的实现方式即为多态。多态在 JS 中不是那么明显,但是对于强语言比较有用,比如 Java,C++。对于 JS 这种弱语言,意义并不大。
2、对象的组成
对象可分为宿主对象,本地对象和内置对象。
宿主对象就是 DOM 和 BOM,即由浏览器提供的对象。
本地对象为非静态对象,所谓本地对象,就是需要先 new,再使用。常用的对象如:Object、Function、Array、String、Boolean、Number、Date、RegExp、Error。
内置对象为静态对象,就是不需要 new,直接可以使用的类。Math 是最常见,也是可以直接使用的仅有的内置对象。
面向对象的第一步,就是要创建对象。典型的面向对象编程的语言都存在 "类"(class) 这样一个概念,所谓类,就是对象的抽象,表示某一类事物的共同特征,比如水果,而对象就是类的具体实例,比如苹果就是水果的一种,类是抽象的,不占用内存,而对象是具体的,占用存储空间。但是在 JS 中没有 "类" 这个概念,不过可以使用构造函数实现。
之前我们说过,所谓"构造函数",就是用来创建新对象的函数,作为对象的基本结构,一个构造函数,可以创建多个对象,这些对象都有相同的结构。构造函数就是一个普通的函数,但是他的特征与用法和普通函数不一样。构造函数的最大特点就是,在创建对象时必须使用 new 关键字,并且函数体内部可以使用 this 关键字,代表了所要创建的对象实例,this 就用于指向函数执行时的当前对象。具体情况下面我们再做分析,现在先来研究下对象的组成。
其实我们已经理解了对象的概念,也就不难看出他是由什么构成的,对象就是由属性和方法组成的,JS 中一切皆对象,那在 JS 中,属性和方法到底该怎么理解呢?属性就是变量,方法就是函数。属性代表状态,就像动物的属性记录他具体是哪一种动物一样,他是静态的,变量名称也可以说是方法名称,是对方法的描述。而方法也就是行为,是完成某种任务的过程,他是动态的。
3、面向对象编程
我们通过实例的方式,为对象添加属性和方法,来理解对象的组成和面向对象。
(1)、实例:给对象添加属性
1 <script> 2 var a = 2; 3 alert(a); //返回:2 4 5 var arr = [5,6,7,8,9]; 6 //给数组定义一个属性a,等于2。 7 arr.a = 2; 8 alert(arr.a); //返回:2 9 arr.a++; 10 alert(arr.a); //返回:3 11 </script>
通过上面的实例,我们可以看到,变量和属性就是一样的,变量可以做的事,属性也可以做,属性可以做的事,变量也可以做。他们的区别就在于,变量是自由的,不属于任何对象,而属性不是自由的,他是属于一个对象的,就像例子中的对象 a,他是属于数组 arr 的,在使用的时候就写为 arr.a。我们可以给任何对象定义属性,比如给 DIV 定义一个属性用于索引:oDiv[i].index = i。
(2)、实例:给对象添加方法
1 <script> 2 function a(){ 3 alert('abc'); //返回:abc 4 } 5 6 var arr = [5,6,7,8,9]; 7 //给函数添加一个a函数的方法 8 arr.a = function (){ 9 alert('abc'); //返回:abc 10 }; 11 a(); 12 arr.a(); 13 </script>
通过上面的实例,可以看到,a 函数也是自由的,而当这个 a 函数属于一个对象的时候,这就是方法,是数组 arr 的 a 方法,也就是这个对象的方法。所以函数和方法也是等同的,函数可以做的事,方法就可以做,他们的不同,也是在于函数是自由的,而方法是属于一个对象的。
我们不能在系统对象中随意附加属性和方法,否则会覆盖已有的属性和方法。比如实例中我们是在数组对象上附加属性和方法的,数组有他自己的属性和方法,我们再给其附加属性和方法,就会覆盖掉数组本身的属性和方法,这一点需要注意。
(3)、实例:创建对象
1 <script> 2 var obj = new Object(); 3 var d = new Date(); 4 var arr = new Array(); 5 alert(obj); //返回:[object Object] 6 alert(d); //返回当前时间 7 alert(arr); //返回为空,空数组 8 </script>
创建一个新对象,可以 new 一个 Object。object 是一个空白对象,只有系统自带的一些很少量的东西,所以在实现面向对象的时候,就可以给 object 上加方法,加属性。这样可以最大限度的避免跟其他起冲突。
(4)、实例:面向对象程序
1 <script> 2 //创建一个对象 3 var obj = new Object(); 4 //可写为:var obj={}; 5 6 //给对象添加属性 7 obj.name = '小白'; 8 obj.qq = '89898989'; 9 10 //给对象添加方法 11 obj.showName = function (){ 12 alert('我的名字叫:'+this.name); 13 }; 14 obj.showQQ = function (){ 15 alert('我的QQ是:'+this.qq); 16 }; 17 obj.showName(); //返回:我的名字叫:小白 18 obj.showQQ(); //返回:我的QQ是:89898989 19 20 //再创建一个对象 21 var obj2 = new Object(); 22 23 obj2.name = '小明'; 24 obj2.qq = '12345678'; 25 26 obj2.showName = function (){ 27 alert('我的名字叫:'+this.name); 28 }; 29 obj2.showQQ = function (){ 30 alert('我的QQ是:'+this.qq); 31 }; 32 obj2.showName(); //返回:我的名字叫:小白 33 obj2.showQQ(); //返回:我的QQ是:12345678 34 </script>
这就是一个最简单的面向对象编程,创建一个对象,给对象添加属性和方法,模拟现实情况,针对对象进行编程。这个小程序运行是没有什么问题,但是存在很严重的缺陷,一个网站中不可能只有一个用户对象,可能有成千上万个,不可能给每个用户都 new 一个 object。其实可以将其封装为一个函数,然后再调用,有多少个用户,调用多少次,这样的函数就被称为构造函数。
4、构造函数
构造函数(英文:constructor)就是一个普通的函数,没什么区别,但是为什么要叫"构造"函数呢?并不是这个函数有什么特别,而是这个函数的功能有一些特别,跟别的函数就不一样,那就是构造函数可以构建一个类。构造函数的方式也可以叫做工厂模式,因为构造函数的工作方式和工厂的工作方式是一样的。工厂模式又是怎样的呢?这个也不难理解,首先需要原料,然后就是对原料进行加工,最后出厂,这就完事了。构造函数也是同样的方式,先创建一个对象,再添加属性和方法,最后返回。既然说构造函数可以构建一个类出来,这个该怎么理解呢?很 easy,可以用工厂方式理解,类就相当于工厂中的模具,也可以叫模板,而对象就是零件、产品或者叫成品,类本身不具备实际的功能,仅仅只是用来生产产品的,而对象才具备实际的功能。比如:var arr = new Array(1,2,3,4,5); Array 就是类,arr 就是对象, 类 Array 没有实际的功能,就是用来存放数据的,而对象 arr 具有实际功能,比如:排序sort()、删除shift()、添加push()等。我们不可能这么写:new arr(); 或 Array.push();,正确的写法:arr.push();。
1 <script> 2 function userInfo(name, qq){ 3 4 //1.原料 - 创建对象 5 var obj = new Object(); 6 7 //2.加工 - 添加属性和方法 8 obj.name = name; 9 obj.qq = qq; 10 obj.showName = function (){ 11 alert('我的名字叫:' + this.name); 12 }; 13 obj.showQQ = function (){ 14 alert('我的QQ是:' + this.qq); 15 }; 16 //3.出厂 - 返回 17 return obj; 18 } 19 20 var obj1 = userInfo('小白', '89898989'); 21 obj1.showName(); 22 obj1.showQQ(); 23 24 var obj2 = userInfo('小明', '12345678'); 25 obj2.showName(); 26 obj2.showQQ(); 27 </script>
这个函数的功能就是构建一个对象,userInfo() 就是构造函数,构造函数作为对象的类,提供一个模具,用来生产用户对象,我们以后在使用时,只调用这个模板,就可以无限创建用户对象。我们都知道,函数如果用于创建新的对象,就称之为对象的构造函数,我们还知道,在创建新对象时必须使用 new 关键字,但是上面的代码,userInfo() 构造函数在使用时并没有使用 new关 键字,这是为什么呢?且看下文分解。
5、new 和 this
(1)new
new 关键字的作用,就是执行构造函数,返回一个实例对象。看下面例子:
<script> var user = function (){ this.name = '小明'; }; var info = new user(); alert(info.name); //返回:小明 </script>
上面实例通过 new 关键字,让构造函数 user 生产一个实例对象,保存在变量 info 中,这个新创建的实例对象,从构造函数 user 继承了 name 属性。在 new 命令执行时,构造函数内部的 this,就代表了新生产的实例对象,this.name 表示实例有一个 name 属性,他的值是小明。
使用 new 命令时,根据需要,构造函数也可以接受参数。
1 <script> 2 var user = function (n){ 3 this.name = n; 4 }; 5 6 var info = new user('小明'); 7 alert(info.name); //返回:小明 8 </script>
new 命令本身就可以执行执行构造函数,所以后面的构造函数可以带括号,也可以不带括号,下面两行代码是等价的。
var info = new user; var info = new user();
那如果没有使用 new 命令,直接调用构造函数会怎样呢?这种情况下,构造函数就变成了普通函数,并不会生产实例对象,this 这时候就代表全局对象。
1 <script> 2 var user = function (n){ 3 this.name = n; 4 }; 5 alert(this.name); //返回:小明 6 7 var info = user('小明'); 8 alert(info.name); //报错 9 </script>
上面实例中,调用 user 构造函数时,没有使用 new 命令,结果 name 变成了全局变量,而变量 info 就变成了 undefined,报错:无法读取未定义的属性 'name'。使用 new 命令时,他后边的函数调用就不是正常的调用,而是被 new 命令控制了,内部的流程是,先创建一个空对象,赋值给函数内部的 this 关键字,this 就指向一个新创建的空对象,所有针对 this 的操作,都会发生在这个空对象上,构造函数之所以叫"构造函数",就是说这个函数的目的,可以操作 this 对象,将其构造为需要的样子。下面我们看一下 new 和函数。
1 <script> 2 var user = function (){ 3 //function = user(){ 4 alert(this); 5 } 6 user(); //返回:Window 7 new user();//返回:Object 8 </script>
通过上面实例,可以看到,在调用函数时,前边加个 new,构造函数内部的 this 就不是指向 window 了,而是指向一个新创建出来的空白对象。
说了这么多,那为什么我们第四章的构造函数,在使用的时候没有加 new 关键字呢,因为我们完全是按照工厂模式,也就是构造函数的结构直接编写的,我们的步骤已经完成了 new 关键字的使命,也就是把本来 new 需要做的事,我们已经做了,所以就用不着 new 了。那这样岂不是做了很多无用功,写了不必要的代码,浪费资源,那肯定是了,这也是构造函数的一个小问题,我们在下一章再做具体分析。
(2)、this
this 翻译为中文就是这,这个,表示指向。之前我们提到过,this 指向函数执行时的当前对象。那么我们先来看看函数调用,函数有四种调用方式,每种方式的不同方式,就在于 this 的初始化。
①、作为一个函数调用
1 <script> 2 function show(a, b) { 3 return a * b; 4 } 5 alert(show(2, 3)); //返回:6 6 </script>
实例中的函数不属于任何对象,但是在 JS 中他始终是默认的全局对象,在 HTML 中默认的全局对象是 HTML 页面本身,所以函数是属于 HTML 页面,在浏览器中的页面对象是浏览器窗口(window 对象),所以该函数会自动变为 window 对象的函数。
1 <script> 2 function show(a, b) { 3 return a * b; 4 } 5 alert(show(2, 3)); //返回:6 6 alert(window.show(2, 3));//返回:6 7 </script>
上面代码中,可以看到,show() 和 window.show() 是等价的。这是调用 JS 函数最常用的方法,但不是良好的编程习惯,因为全局变量,方法或函数容易造成命名冲突的 Bug。
当函数没有被自身的对象调用时,this 的值就会变成全局对象。
1 <script> 2 function show() { 3 return this; 4 } 5 alert(show()); //返回:[object Window] 6 </script>
全局对象就是 window 对象,函数作为全局对象对象调用,this 的值也会成为全局对象,这里需要注意,使用 window 对象作为一个变量容易造成程序崩溃。
②、函数作为方法调用
1 <script> 2 var user = { 3 name : '小明', 4 qq : 12345678, 5 info : function (){ 6 return this.name + 'QQ是:' + this.qq; 7 } 8 } 9 alert(user.info()); 10 </script>
在 JS 中可以将函数定义为对象的方法,上面实例创建了一个对象 user,对象拥有两个属性(name和qq),及一个方法 info,该方法是一个函数,函数属于对象,user 是函数的所有者,this 对象拥有 JS 代码,实例中 this 的值为 user 对象,看下面示例:
1 <script> 2 var user = { 3 name : '小明', 4 qq : 12345678, 5 info : function (){ 6 return this; 7 } 8 } 9 alert(user.info()); //返回:[object Object] 10 </script>
函数作为对象方法调用,this 就指向对象本身。
③、使用构造函数调用函数
如果函数调用前使用了 new关键字,就是调用了构造函数。
1 <script> 2 function user(n, q){ 3 this.name = n; 4 this.qq = q; 5 } 6 7 var info = new user('小明', 12345678); 8 alert(info.name); //返回:小明 9 alert(info.qq); //返回:12345678 10 </script>
这看起来就像创建了新的函数,但实际上 JS 函数是新创建的对象,构造函数的调用就会创建一个新的对象,新对象会继承构造函数的属性和方法。构造函数中的 this 并没有任何的值,this 的值在函数调用时实例化对象(new object)时创建,也就是指向一个新创建的空白对象。
④、作为方法函数调用函数
在 JS 中,函数是对象,对象有他的属性和方法。call() 和 apply() 是预定义的函数方法,这两个方法可用于调用函数,而且这两个方法的第一个参数都必须为对象本身。
1 <script> 2 function show(a, b) { 3 return a * b; 4 } 5 var x = show.call(show, 2, 3); 6 alert(x); //返回:6 7 8 function shows(a, b) { 9 return a * b; 10 } 11 var arr = [2,3]; 12 var y = shows.apply(shows, arr); 13 var y1 = shows.call(shows, arr); 14 alert(y); //返回:6 15 alert(y1); //返回:NaN 16 </script>
上面代码中的两个方法都使用了对象本身作为作为第一个参数,两者的区别在于:apply()方法传入的是一个参数数组,也就是将多个参数组合称为一个数组传入,而call()方法则作为call的参数传入(从第二个参数开始),不能传入一个参数数组。
通过 call() 或 apply() 方法可以设置 this 的值, 且作为已存在对象的新方法调用。在下面用到的时候,我们再具体分析。
this 就是用于指向函数执行时的当前对象,下面再看一个实例:
1 <body> 2 <div id="div1"></div> 3 <script> 4 var oDiv = document.getElementById('div1'); 5 //给一个对象添加事件,本质上是给这个对象添加方法。 6 oDiv.onclick = function (){ 7 alert(this); //this就是oDiv 8 }; 9 10 var arr = [1,2,3,4,5]; 11 //给数组添加属性 12 arr.a = 12; 13 //给数组添加方法 14 arr.show = function (){ 15 alert(this.a); //this就是arr 16 }; 17 arr.show(); //返回:12 18 19 20 function shows(){ 21 alert(this); //this就是window 22 } 23 24 //全局函数是属于window的。 25 //所以写一个全局函数shows和给window加一个shows方法是一样的。 26 window.shows = function (){ 27 alert(this); 28 }; 29 shows(); //返回:[object Window] 30 </script> 31 </body>
上面的代码,this 就代表着当前的函数(方法)属于谁,如果是一个事件方法,this 就是当前发生事件的对象,如果是一个数组方法,this 就是数组对象,全局的方法是属于 window 的,所以 this 指向 window。
6、原型
前面我们说过构造函数在使用时没有加 new,这只能算是一个小问题,没有加我们可以给加上,无伤大雅,但其实他还存在着一个更严重的问题,那就是函数重复定义。
1 <script> 2 function userInfo(name, qq){ 3 4 //1.原料 - 创建对象 5 var obj = new Object(); 6 7 //2.加工 - 添加属性和方法 8 obj.name = name; 9 obj.qq = qq; 10 obj.showName = function (){ 11 alert('我的名字叫:'+this.name); 12 }; 13 obj.showQQ = function (){ 14 alert('我的QQ是:'+this.qq); 15 }; 16 //3.出厂 - 返回 17 return obj; 18 } 19 20 //1.没有new。 21 var obj1 = userInfo('小白', '89898989'); 22 var obj2 = userInfo('小明', '1234567'); 23 24 //调用的showName返回的函数都是相同的。 25 alert(obj1.showName); 26 alert(obj2.showName); 27 28 //2.函数重复。 29 alert(obj1.showName == obj2.showName); //返回:false 30 </script>
通过上面的代码,我们可以看到,弹出这两个对象的 showName,调用的 showName 返回的函数是相同的,他们新创建对象所使用的方法都是一样的,尽管这两个函数长的是一样的,但其实他们并不是一个东西,我们将 对象1 和 对象2 做相等比较,结果返回 false。这时候就带来了一个相对严重的问题,一个网站中也不可能只有 2 个用户,比如有 1 万个用户对象,那么就会有 1 万 showName 和 showQQ 方法,每一个对象都有自己的函数,但明明这两个函数都是一样的,结果却并非如此。这样就很浪费系统资源,而且性能低,可能还会出现一些意想不到的问题。该怎么解决这个问题呢?方法也很简单,就是使用原型。
(1)、什么是原型
JS 对象都有一个之前我们没有讲过的属性,即 prototype 属性,该属性让我们有能力向对象添加属性和方法,包括 String对象、Array对象、Number对象、Date对象、Boolean对象,Math对象 并不像 String 和 Date 那样是对象的类,因此没有构造函数 Math(),该对象只用于执行数学任务。
所有 JS 的函数都有一个prototype属性,这个属性引用了一个对象,即原型对象,也简称原型。这个函数包括构造函数和普通函数,我们讲的更多是构造函数的原型,但是也不能否定普通函数也是有原型的。
在看实例之前,我们先来看几个小东西:typeof运算符、constructor属性、instanceof运算符。
typeof 大家都熟悉,JS 中判断一个变量的数据类型就会用到 typeof 运算符,返回结果为 JS 基本的数据类型,包括 number、string、boolean、object、function、undefined,语法:typeof obj。
constructor 属性返回所有 JS 变量的构造函数,typeof 无法判断 Array对象 和 Date对象 的类型,因为都返回 object,所以我们可以利用 constructor 属性来查看对象是否为数组或者日期,语法:obj.constructor。
1 <script> 2 var arr = [1,2,3,4,5]; 3 function isArray(obj) { 4 return arr.constructor.toString().indexOf("Array") > -1; 5 } 6 alert(isArray(arr)); //返回:ture 7 8 var d = new Date(); 9 function isDate(obj) { 10 return d.constructor.toString().indexOf("Date") > -1; 11 } 12 alert(isDate(d)); //返回:ture 13 </script>
这里需要注意,constructor 只能对已有变量进行判断,对于未声明的变量进行判断会报错,而 typeof 则可对未声明变量进行判断(返回 undefined)。
instanceof 这东西比较高级,可用于判断一个对象是否是某一种数据类型,查看对象是否是某个类的实例,返回值为 boolean 类型。另外,更重要的一点是 instanceof 还可以在继承关系中用来判断一个实例是否属于他的父类型,语法:a instanceof b。
1 <script> 2 // 判断 a 是否是 A 类的实例 , 并且是否是其父类型的实例 3 function A(){} 4 function B(){} 5 B.prototype = new A(); //JS原型继承 6 7 var a = new B(); 8 alert(a instanceof A); //返回:true 9 alert(a instanceof B); //返回:true 10 </script>
上面的实例中判断了一层继承关系中的父类,在多层继承关系中,instanceof 运算符同样适用。
下面我们就来看看普通函数的原型:
1 <script> 2 function A(){} 3 alert(A.prototype instanceof Object); //返回:true 4 </script>
上面代码中 A 是一个普通的函数,我们判断函数 A 的原型是否是对象,结果返回 true。
说了这么多,原型到底是个什么东西,说简单点原型就是往类的上面添加方法,类似于class,修改他可以影响一类元素。原型就是在已有对象中加入自己的属性和方法,原型修改已有对象的影响,prototype 属性可返回对象类型原型的引用,如果对象创建在修改原型之前,那么该对象不会拥有修改后的原型方法,就是说原型链的改变,不会影响之前产生的对象。有关原型链的知识,下面我们在讲继承时,再做分析。
下面我们通过实例的方式,进一步的理解原型。
实例:给数组添加方法
1 <script> 2 var arr1 = new Array(2,8,8); 3 var arr2 = new Array(5,5,10); 4 5 arr1.sum = function (){ 6 var result = 0; 7 for(var i=0; i<this.length; i++){ 8 result += this[i]; 9 } 10 return result; 11 }; 12 13 alert(arr1.sum()); //返回:18 14 alert(arr2.sum()); //报错:arr2没有sum方法 15 </script>
上面的实例只给 数组1 添加了 sum 方法,这就类似于行间样式,只给 arr1 设置了,所以 arr2 肯定会报错,这个并不难理解。
实例:给原型添加方法
1 <script> 2 var arr1 = new Array(2,8,8); 3 var arr2 = new Array(5,5,10); 4 5 Array.prototype.sum = function (){ 6 var result = 0; 7 for(var i=0; i<this.length; i++){ 8 result += this[i]; 9 } 10 return result; 11 }; 12 13 alert(arr1.sum()); //返回:18 14 alert(arr2.sum()); //返回:20 15 </script>
通过上面的实例,我们可以看到,通过原型 prototype 给 Array 这个类添加一个 sum 方法,就类似于 class,一次可以设置一组元素,那么所有的 Array 类都具有这个方法,arr1 返回结果为 18,而 arr2 在加了原型之后,也返回了正确的计算结果 20。
(2)、解决历史遗留问题
现在我们就可以使用原型,来解决没有 new 和函数重复定义的问题了。
1 <script> 2 function UserInfo(name, qq){ 3 4 //1.原料 - 创建对象 5 //var obj = new Object(); 6 7 //加了new之后,系统(浏览器)会自动替你声明一个变量: 8 //var this = new Object(); 9 10 //2.加工 - 添加属性和方法 11 /* 12 obj.name = name; 13 obj.qq = qq; 14 obj.showName = function (){ 15 alert('我的名字叫:'+this.name); 16 }; 17 obj.showQQ = function (){ 18 alert('我的QQ是:'+this.qq); 19 }; 20 */ 21 this.name = name; 22 this.qq = qq; 23 24 //3.出厂 - 返回 25 //return obj; 26 27 //系统也会自动替你返回: 28 //return this; 29 } 30 31 //2.函数重复的解决:userInfo给类加原型。 32 UserInfo.prototype.showName = function (){ 33 alert('我的名字叫:' + this.name); 34 }; 35 36 UserInfo.prototype.showQQ = function (){ 37 alert('我的QQ是:' + this.qq); 38 }; 39 40 41 //1.加上没有new。 42 var obj1 = new UserInfo('小白', '89898989'); 43 var obj2 = new UserInfo('小明', '1234567'); 44 45 obj1.showName(); 46 obj1.showQQ(); 47 obj2.showName(); 48 obj2.showQQ(); 49 50 //加了原型之后 51 alert(obj1.showName == obj2.showName); //返回:true 52 </script>
上面的代码看着有点复杂,我们把不必要的省略,如下:
1 <script> 2 function UserInfo(name, qq){ 3 this.name = name; 4 this.qq = qq; 5 } 6 7 UserInfo.prototype.showName = function (){ 8 alert('我的名字叫:' + this.name); 9 }; 10 UserInfo.prototype.showQQ = function (){ 11 alert('我的QQ是:' + this.qq); 12 }; 13 14 var obj1 = new UserInfo('小白', '89898989'); 15 var obj2 = new UserInfo('小明', '1234567'); 16 17 obj1.showName(); 18 obj1.showQQ(); 19 obj2.showName(); 20 obj2.showQQ(); 21 22 alert(obj1.showName == obj2.showName); //返回:true 23 </script>
现在代码是不是比最初的样子,简洁了很多,new 关键字也使用了,而且每个对象都是相等的。通过上面的实例,我们可以看到,再加上 new 之后,使用就方便了很多,代码明显减少了,因为在加了 new 之后,系统也就是浏览器自动为你做两件事,这就是 new 的使命,第一件事是替你创建了一个空白对象,也就是替你声明了一个变量:var this = new Object();,第二件事就是再提你返回这个对象:return this;,这里需要注意,在之前我们也讲过,在调用函数的时候,前边加个 new,构造函数内部的 this 就不是指向 window 了,而是指向一个新创建出来的空白对象。
这种方式就是流行的面向对象编写方式,即混合方式构造函数,混合的构造函数/原型方式(Mixed Constructor Function/Prototype Method),他的原则是:用构造函数加属性,用原型加方法,也就是用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数方法。用原型的作用,就是此对象的所有实例共享原型定义的数据和(对象)引用,防止重复创建函数,浪费内存。原型中定义的所有函数和引用的对象都只创建一次,构造函数中的方法则会随着实例的创建重复创建(如果有对象或方法的话)。这里需要注意,不管在原型中还是构造函数中,属性(值)都不共享,构造函数中的属性和方法都不共享,原型中属性不共享,但是对象和方法共享。所以创建类的最好方式就是用构造函数定义属性,用原型定义方法。使用该方式,类名的首字母要大写,这也是一种对象命名的规范。
7、面向对象实例
通常我们在写程序时,都使用的是面向过程,即要呈现出什么效果,基于这样的效果,一步步编写实现效果的代码,接下来我们就把面向过程的程序,改写成面向对象的形式。面向过程的程序写起来相对容易些,代码也比较直观,易读性强,我们先看一个面向过程的实例。
实例:面向过程的选项卡
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>JavaScript实例</title> 6 <style> 7 #div1 input{background:white;} 8 #div1 input.active{background:green;color:white;} 9 #div1 div{ 10 width:200px; 11 height:200px; 12 background:#ccc; 13 display:none; 14 } 15 </style> 16 <script> 17 window.onload = function (){ 18 //1、获取所需元素。 19 var oDiv = document.getElementById('div1'); 20 var oBtn = oDiv.getElementsByTagName('input'); 21 var aDiv = oDiv.getElementsByTagName('div'); 22 23 //2、循环遍历所有按钮。 24 for(var i=0; i<oBtn.length; i++){ 25 //5、给按钮定义index属性,当前按钮的索引号为按钮的索引号i 26 oBtn[i].index = i; 27 //3、给当前按钮添加点击事件。 28 oBtn[i].onclick = function (){ 29 //4、再循环所有按钮,清空当前按钮的class属性,并将当前内容的样式设置为隐藏 30 //在执行清空和设置之前,需要给当前按钮定义一个索引 31 //这一步的目的:主要就是实现切换效果,点击下一个按钮时,当前按钮失去焦点,内容失去焦点 32 for(var i=0; i<oBtn.length; i++){ 33 oBtn[i].className = ''; 34 aDiv[i].style.display = 'none'; 35 } 36 //6、最后给当前按钮class属性,再设置当前展示内容的样式为显示 37 this.className = 'active'; 38 aDiv[this.index].style.display = 'block'; 39 }; 40 } 41 }; 42 </script> 43 </head> 44 <body> 45 <div id="div1"> 46 <input class="active" type="button" value="新闻"> 47 <input type="button" value="热点"> 48 <input type="button" value="推荐"> 49 <div style="display:block;">天气预报</div> 50 <div>历史实事</div> 51 <div>人文地理</div> 52 </div> 53 </body> 54 </html>
这样一个简单的效果,谁都可以做的出来,那要怎么写成面向对象的形式呢,我们先来看代码,再做分析。
实例:面向对象的选项卡
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>JavaScript实例</title> 6 <style> 7 #div1 input{background:white;} 8 #div1 input.active{background:green;color:white;} 9 #div1 div{ 10 width:200px; 11 height:200px; 12 background:#ccc; 13 display:none; 14 } 15 </style> 16 <script> 17 window.onload = function(){ 18 new TabShow('div1'); 19 }; 20 21 function TabShow(id){ 22 var _this = this; 23 var oDiv = document.getElementById(id); 24 this.oBtn = oDiv.getElementsByTagName('input'); 25 this.aDiv = oDiv.getElementsByTagName('div'); 26 for(var i=0; i<this.oBtn.length; i++){ 27 this.oBtn[i].index = i; 28 this.oBtn[i].onclick = function (){ 29 _this.fnClick(this); 30 }; 31 } 32 } 33 34 TabShow.prototype.fnClick = function (oBtn){ 35 for(var i=0; i<this.oBtn.length; i++){ 36 this.oBtn[i].className = ''; 37 this.aDiv[i].style.display = 'none'; 38 } 39 oBtn.className = 'active'; 40 this.aDiv[oBtn.index].style.display = 'block'; 41 }; 42 </script> 43 </head> 44 <body> 45 <div id="div1"> 46 <input class="active" type="button" value="新闻"> 47 <input type="button" value="热点"> 48 <input type="button" value="推荐"> 49 <div style="display:block;">天气预报</div> 50 <div>历史实事</div> 51 <div>人文地理</div> 52 </div> 53 </body> 54 </html>
将面向过程的程序,改写成面向对象的形式,原则就是不能有函数套函数,但可以有全局变量,其过程是先将 onload 改为构造函数,再将全局变量改为属性,函数改为方法,这就是面向对象的思维,所以第一步就是把嵌套函数单独出来,当函数单独出去之后,onload 中定义的变量在点击函数中就会报错,onload 也相当于一个构造函数,初始化整个程序,所以再对 onload 函数作出一些修改,让他初始化这个对象,然后就是添加属性和方法,我们说变量就是属性,函数就是方法,所以这里也只是改变所属关系。这个过程中最需要注意的是 this 的指向问题,通过闭包传递 this,以及函数传参,把对象作为参数传递。之前的 this 都是指向当前发生事件的对象,将函数改为方法后,我们给这个方法添加的是按钮点击事件,所以这时候 this 就指向这个按钮,本应该这个 this 是指向新创建的对象,这就需要转换 this 的指向 var _this = this;。TabShow 函数就是 onload 函数的改造,fnClick 方法是第一步单独出去的函数,最后被改为了选项卡函数 (TabShow函数) 的方法。
8、继承和原型链
(1)、继承
前边我们简单的说过继承是从已有对象上,再继承出一个新对象,继承就是在原有类的基础上,略作修改,得到一个新的类,不影响原有类的功能。继承的实现有好几种方法,最常用的就是 call() 方法和原型实现继承。下面看一个继承的实例:
1 <script> 2 function A(){ 3 this.abc = 12; 4 } 5 A.prototype.show = function (){ 6 alert(this.abc); 7 }; 8 9 function B(){ 10 A.call(this); 11 } 12 13 for(var i in A.prototype){ 14 B.prototype[i]=A.prototype[i]; 15 } 16 17 B.prototype.fn=function (){ 18 alert('abc'); 19 }; 20 21 var objB = new B(); 22 alert(objB.abc); //返回:12 23 objB.show(); //返回:12 24 objB.fn(); //返回:abc 25 26 var objA = new A(); 27 objA.fn(); //报错:A没有该方法 28 </script>
上面的代码,B函数 继承了 A函数 的属性,通过 call 方法,该方法有一个功能,可以改变这个函数在执行时里边的 this 的指向,如果 B函数 中不使用 call,this 则指向 new B(),使用 call 后,this 则指向 A。方法继承 B.prototype = A.prototype;,A 的方法写在原型里,赋给 原型B,原型也是引用,将 A的原型 引用给 B的原型,就相当于 原型A 和 原型B 公用引用一个空间,所以 原型B 自己的方法,原型A 也可以用,给 原型B 添加一个方法,也就是给 原型A 添加一个方法。所以可以使用循环遍历 原型A 中的内容,再将这些内容赋给 原型B,这样 原型A 就没有 原型B 的方法了,也就是给 B 再添加方法,A 将不会受到影响(objA.fn() 报错),B 不仅有从父级继承来的方法(objB.show()),还有自己的方法(obj.fn())。
(2)、原型链
在 JS 中,每当定义一个对象(函数)时,对象中都会包含一些预定义的属性。其中函数对象的一个属性就是原型对象 prototype。这里需要注意:普通对象没有 prototype,但有__proto__ 属性。原型对象的主要对象就是用于继承。
1 <script> 2 var A = function(name){ 3 this.name = name; 4 }; 5 A.prototype.getName = function(){ 6 alert(this.name); 7 } 8 var obj = new A('小明'); 9 obj.getName(); //返回:小明 10 11 </script>
上面的代码,通过给 A.prototype 定义了一个函数对象的属性,再 new 出来的对象就继承了这个属性。
JS 在创建对象(不论是普通对象还是函数对象)时,都有一个叫做 __proto__ 的内置属性,用于指向创建它的函数对象的原型对象 prototype。
1 <script> 2 var A = function(name){ 3 this.name = name; 4 } 5 A.prototype.getName = function(){ 6 alert(this.name); 7 } 8 var obj = new A('小明'); 9 obj.getName(); //返回:小明 10 11 alert(obj.__proto__ === A.prototype); //返回:true 12 </script>
同样,A.prototype 对象也有 __proto__ 属性,它指向创建它的函数对象(Object)的 prototype。
1 <script> 2 var A = function(name){ 3 this.name = name; 4 } 5 A.prototype.getName = function(){ 6 alert(this.name); 7 } 8 var obj = new A('小明'); 9 obj.getName(); //返回:小明 10 11 alert(A.prototype.__proto__ === Object.prototype); //返回:true 12 </script>
Object.prototype 对象也有 __proto__ 属性,但它比较特殊,为 null。
1 <script> 2 var A = function(name){ 3 this.name = name; 4 } 5 A.prototype.getName = function(){ 6 alert(this.name); 7 } 8 var obj = new A('小明'); 9 obj.getName(); //返回:小明 10 11 alert(Object.prototype.__proto__); //返回:null 12 </script>
综上,我们把这个由 __proto__ 串起来的直到 Object.prototype.__proto__ 为 null 的链就叫做原型链。
在 JS 中,可以简单的将值分为两种类型,即原始值和对象值。每个对象都有一个内部属性 (prototype),通常称之为原型。原型的值可以是一个对象,也可以是 null。如果他的值是一个对象,则这个对象也一定有自己的原型,由于原型对象本身也是对象,而他自己的原型对象又可以有自己的原型,这样就组成了一条链,我们就称之为原型链。JS 引擎在访问对象的属性时,如果在对象本身中没有找到,则会去原型链中查找,如果找到,直接返回值,如果整个链都遍历且没有找到属性,则返回 undefined。原型链一般实现为一个链表,这样就可以按照一定的顺序来查找,如果对象没有显式的声明自己的 ”__proto__”属性,那么这个值默认的设置为 Object.prototype,而当 Object.prototype 的 ”__proto__”属性值为 ”null”时,则标志着原型链的终结。
9、JSON 的面向对象
JSON 的面向对象,就是把方法包含在一个 JSON 中,在仅仅只有一个对象时使用,整个程序只有一个对象,写起来比较简单,但是不适合多个对象。这种方式也被称为命名空间,所谓命名空间,就是把很多 JSON 用附加属性的方式创建,然后每个里边都有自己的方法,这种方法主要用来分类,使用方便,避免冲突。就相当于把同一类方法归纳在一起,既可以不冲突,而且找起来方便。
1 <script> 2 //创建一个空的json 3 var json = {}; 4 5 //现在就有了3个空的json 6 json.a = {}; 7 json.b = {}; 8 json.c = {}; 9 10 //现在3个json里边各有一个getUser函数,而且各不相同。 11 //在JS中,如果是相同命名的函数就会产生冲突,相互覆盖。 12 //但是这3个json不会相互冲突,相互覆盖。 13 json.a.getUser = function (){ 14 alert('a'); 15 }; 16 json.b.getUser = function (){ 17 alert('b'); 18 }; 19 json.c.getUser = function (){ 20 alert('c'); 21 }; 22 json.a.getUser(); //返回:a 23 json.b.getUser(); //返回:b 24 json.c.getUser(); //返回:c 25 </script>