JavaScript面向对象编程

来源:互联网 发布:张孝祥javascript视频 编辑:程序博客网 时间:2024/06/09 16:12
JavaScript面向对象编程(一)
1、引言
JavaScript 是一种解释性的, 基于对象的脚本语言( an
interpreted, object-based scripting language)。JavaScript
基于客户端运行,目前基本上所有的浏览器都支持JavaScript。1995年首次
出现时,JavaScript的主要目的还只是处理一些输入的有效性验证,随着互
联网的蓬勃发展,JavaScript的应用越来越广泛,特别是近几年AJAX技术
(Asynchronous JavaScript and XML)的发展,更使JavaScript的
应用达到了一个新的高度。在AJAX技术中,JavaScript是一项关键技术,请
求的发送、接收、接收数据后的界面处理都需要使用JavaScript技术,这对
JavaScript语言提出了新的需求,本文从JavaScript的基本特点出发,模
拟出了面向对象编程的大部分特点,使JavaScript摆脱了以往脚本语言杂乱
无章、难以阅读、难以维护的形象,而具有了面向对象特性,极大的方便了
JavaScript的开发、维护,提高了软件开发效率。
2、JavaScript的基本特点
JavaScript是解释性的,基于对象的脚本语言。它有下面几个显著特点,
这几个特点在后面的面向对象特性模拟中会反复用到,因此这里先详细说明这
几个特点。
 解释型语言:JavaScript是一种解释性语言,解释性语言相对于编
译型语言,编译型语言必须先通过编译才能执行,而解释性语言不需要
编译,直接从上到下解释执行,一边解释一边执行,这就决定了解释性
语言的代码是有先后顺序的,需要执行的代码必须已经解释过。因此
JavaScript需要注意代码的先后顺序。
 弱类型语言:JavaScript是一种弱类型语言,弱类型语言相对于强
类型语言,大部分面向对象语言都是强类型语言,强类型语言是一种需
要强制类型定义的语言,它要求每个变量都确定某一种类型,它和别的
类型转换必须显式转换。弱类型语言是一种类型可以被忽略的语言,它
在变量定义时不指定某一类型,在执行时通过执行结果才能确定类型,
不同类型之间不需要通过显式转换就可以转换。
 动态添加属性和方法:这个特点是指可以动态为某个对象添加以前没
有的属性和方法。这个特点使JavaScript非常灵活,正因为有了这个
特点,JavaScript的面向对象编程才有了可能。
 prototype(原型)属性:JavaScript 是一种基于对象的语言,
JavaScript中的所有对象,都具有prototype属性。prototype属
性返回对象的所有属性和方法,所有 JavaScript 内部对象都有只
读的 prototype 属性,可以向其原型中动态添加属性和方法,但该
对象不能被赋予不同的原型。但是自定义的对象可以被赋给新的原型。
3、面向对象的基本特点
面向对象有下列三个主要特点:封装、继承和多态。这里先详细说明这几个
特点,后面几个部分分别在JavaScript中实现这些特点,从而实现完整的面
向对象模拟。
 封装:封装就是把各种方法和变量合并到一个类,用这个类代表某个
对象为完成一定的任务所能保存的范围以及它能执行的操作。封装隐藏
了方法执行的细节。
 继承:继承就是根据现有类的方法和成员变量生成新的类的功能。
 多态:多态就是对象随着程序执行而使其形式发生改变的能力。
4、JavaScript语言基础
4.1数据类型
基本数据类型: Number, String, Boolean, Function, Object,
Array, null, undefined,注意null和undefined的区别。
日期和时间: 日期类型并不是JavaScript 的基本数据类型, 但
JavaScript提供了一个处理日期的类:Date,用法如下:
var now = new Date();
var year = now.getYear(); // 年
var month = now.getMonth() + 1; // 月
var day = now.getDate(); // 日
var hour = now.getHours(); // 时
var minute = now.getMinutes(); // 分
var second = now.getSeconds(); // 秒
alert(" 现在时间是: "+year+"-"+month+"-"+day+"
"+hour+":"+minute+":"+second);
正则表达式:主要用于对文本进行模式匹配,实现对文本的查找和替换操作。
在JavaScript中,提供了一个RegExp类来处理正则表达式,创建方式和
Date一样,用关键字new就可以创建,如var re = new RegExp();
和Date不一样的地方在于,虽然RegExp类也不是JavaScript的基本数
据类型,但我们在创建正则表达式对象时,可以不需要用new关键字就可以创
建,如 var re = /[1-9][0-9]*/; 这样就直接定义了正则表达式对象,因为在
JavaScript中,一对斜线中包括一个文本就认为构成了一个正则表达式对象。
下面就示例用正则表达式判断输入的Email和手机号是否合法:
// 判断Email是否合法
function isEmail(p_addr)
{
var reEmail = /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/;
return reEmail.test(p_addr);
}
// 判断手机号是否合法
function isPhoneNumber(p_num)
{
var rePhone = /^1\d{10}$/gi;
return rePhone.test(p_num);
}
// 测试
var email = "tenderghost@163.com";
alert(isEmail(email));
var phone = 137;
alert(isPhoneNumber(phone));
错误对象:JavaScript中定义了几个用于处理错误类型的类,有:Error,
EvalError, RangeError, ReferenceError, SyntaxError,
TypeError, URIError。和Java中的异常处理方式类似,JavaScript中的
错误对象可以用try...catch...finally语句来处理,示例如下:
try{
throw new Error("自定义错误示例");
}
catch (ex) {
alert("Error对象被捕捉,消息为: " + ex.message);
}
finally {
alert("无论如何都会被执行!");
}
4.2 变量
JavaScript是一种弱类型的语言,这就意味着一个JavaScript变量可以指
向任何数据类型,例如:
var i = 10;
i = “ten”;
变量的作用域
var scope = “global scope”; // 全局变量
function checkscope() {
var local = “local scope”; // 局部变量
}
注意:除函数中的变量为局部变量外,其他的全部为全局变量。
4.3 函数
JavaScript中:
function add(a, b) {
return a + b;
}
Java中:
public int add(int a, int b) {
return a + b;
}
函数的参数: arguments对象
在一个函数中,会有一个隐含的arguments对象来保存函数的参数,这样
在有些时候,我们在定义函数时,可以不明确指定函数所需要的参数,如下:
// 求最大值
function max() {
var m = Number.NEGATIVE_INFINITY; // 无穷小
for (var i = 0; i < arguments.length; i++) {
if (arguments[i] > m)
m = arguments[i];
}
return m;
}
// 测试
var largest = max(1, 7, 9, 23, 88, 2, 5);
alert(largest);
5、封装的实现
下面以一个详细的示例来说明常用的私有实例成员、公有实例成员和公有静
态成员的封装。
Human = function(name) // 等于function Human(name)
{
var me = this;
// 私有属性
var _name = null;
// 公有属性
me.name = null;
// 私有方法
function setName()
{
_name = name;
me.name = _name;
}
// 公有方法
me.sayHello = function()
{
alert("Hello, my name is " + me.name);
}
// 模拟构造函数
function constructor()
{
setName();
}
constructor();
return me;
}
// 增加类的静态方法
Human.classMethod = function()
{
alert("Human's classMethod");
}
// 通过原型(prototype)增加公有方法
Human.prototype.sayGoodbye = function()
{
alert("Goodbye, " + this.name);
}
// 当成类来使用
var m_human = new Human("pengfei");
m_human.sayHello();
// 调用类的静态方法
Human.classMethod();
// 直接当成函数使用
Human("huang");
JavaScript语言中类的定义和函数的定义都是使用function关键字,
使用function定义的过程,即可以看成是类的定义,也可以看成是函数的定
义。从示例代码中可以得出:
· 私有属性和方法的定义,直接在类内部定义一个变量,因为这个变量的
作用域只限定在类内部,外部不能使用,因此这样定义的属性是私有属性,私
有方法的定义也类似。
· 公有属性和方法的定义,通过定义一个私有变量me等于this,然后动
态添加me变量的属性和方法,最后把me变量作为创建的实例对象返回。这样
给me变量添加的属性和方法在类外部可以使用,也就是共有属性和方法。
· 构造函数的定义,构造函数是在创建一个对象时,自动执行的一个函数。
在Java,C#等面向对象的语言中,只要定义一个函数和类名相同即可。在
JavaScript中,可以随便定义一个私有函数,这个函数需要在类定义体中执
行,这样的函数即成为构造函数,需要注意的是,为了确保构造函数中的代码
都已经被解释过,构造函数最好放在类定义的最后。
· 类静态方法的定义,类静态方法是指不需要通过类实例来调用,而是可
以直接通过类名来调用的方法。在Java,C#等面向对象语言中,一般是通过
关键字static 来指明一个方法是静态方法。在JavaScript 中,没有
static关键字,不能在类的定义体中实现静态方法,必须在类的定义体外,
通过直接在类上动态添加方法来定义静态方法。需要注意,JavaScript静态
方法中不能访问类的公有属性和公有方法,这和Java,C#等语言是一致的。
· 类的公有属性和公有方法也可以使用prototype来实现, 但是使用
prototype有以下几个注意点:需要定义在类定义体外,和Java等语言的封
装习惯不一致;prototype方式不能访问类的私有属性。
· JavaScript不能实现只读属性、只写属性的定义,所有的公有属性都
是可读可写。
6、继承的实现
JavaScript中没有Java,.Net中实现继承的关键字,JavaScript中
的继承都是通过JavaScript语言本身的特性模拟出来的,可以通过两种方式
实现继承:
 创建对象方式
// 定义父类
Human = function()
{
var me = this;
me.name = "";
me.age = 0;
me.setName = function(name)
{
me.name = name;
}
me.setAge = function(age)
{
me.age = age;
}
me.sayHello = function()
{
alert("Human sayHello, name:"+ me.name +", age:"+ me.age);
}
return me;
}
// 定义子类
Chinese = function(name, age)
{
// 继承
var me = new Human();
// 覆盖父类的sayHello方法
me.sayHello = function()
{
alert("中国人问好,名字:"+ me.name +",年龄:"+ me.age);
}
// 设置name和age
me.setName(name);
me.setAge(age);
return me;
}
// 测试
var c = new Chinese("李四", 21);
c.sayHello();
定义一个变量me,赋予父类实例,这样me就有了父类的属性和方法,然
后给me增加子类的属性和方法,最后把me变量作为创建的实例对象返回。这
样定义的类就有了父类的属性和方法,即实现了继承。
 原型(prototype)方式
// 定义父类
function Human()
{
this.name = "";
this.age = 0;
}
Human.prototype =
{
setName : function(name)
{
this.name = name;
},
setAge : function(age)
{
this.age = age;
},
sayHello : function()
{
alert("Human sayHello, name:"+ this.name +", age:"+ this.age);
}
}
// 定义子类
function Chinese(name, age)
{
this.setName(name);
this.setAge(age);
}
// 继承
Chinese.prototype = new Human();
// 覆盖父类的sayHello方法
Chinese.prototype.sayHello = function()
{
alert("中国人问好,名字:"+ this.name +",年龄:"+ this.age);
}
// 测试
var c = new Chinese("张三", 20);
c.sayHello();
首先封装好子类的属性和方法,然后创建一个父类实例附给子类的
prototype属性,这样子类就有了父类的属性和方法,即实现了继承。
这两种方式都实现了继承,但是和Java,.Net 等面向对象语言相关,
JavaScript中模拟的继承还要以下问题:
 不能定义保护的方法和属性。
 不能继承父类的静态方法。
7、多态的实现
多态主要包括重载(overload)和覆盖(override),重载是指同一个
名字的函数或方法可以有多个实现,他们依靠参数的类型或参数的个数来区分
识别。而覆盖是指子类中可以定义与父类中同名的方法,这些方法定义后,在子
类的实例化对象中,父类中继承的这些同名方法将被隐藏。
由于JavaScript的弱类型性,JavaScript在定义函数时,不需要指定
函数参数的类型和个数,这种特性为重载的实现提供了便利。如下函数:
function say(param)
{
// 通过typeof函数,判定不同类型的参数。
if (typeof(param)=="string")
alert("string");
else if (typeof(param)=="number")
alert("number");
else
alert("others");
}
函数调用时可以传入string,也可以传入number,或别的类型,在函数
实现体中可以使用typeof函数得到传入参数的类型,从而实现不同的功能。这
样虽然是一个函数实现体,但是也有了多个实现,即重载。JavaScript中另
外一种比较常见的重载方式是通过函数的arguments对象来实现,这种方式
已在前面讲过,不再赘述。
JavaScript中覆盖的实现,只需在子类中定义一个与父类同名的方法即
可。
8、静态类的实现
静态类,是一种不能被实例化,并且只包含有静态成员的类。在Java,C
#等面向对象语言中,一般使用static 关键字来指明类是静态类,
JavaScript中没有static关键字,但是可以通过实例化匿名函数来实现静
态类。如下:
UtilTool = new function() // 关键在于这个new
{
var me = this;
// 增加一个静态方法
me.add = function(a, b)
{
return a+b;
}
return me;
}
// 测试
var sum = UtilTool.add(10, 100);
alert(sum);
UtilTool 其实是一个对象,只是这个对象属于匿名类,该类在创建完
UtilTool这个对象后,就不能再被使用了。而UtilTool是一个对象,不是
一个 function,所以不能作为一个类被实例化,因此,它就相当于一个静态
类。
JavaScript面向对象编程(二)
对象和数组(Objects and Arrays)
什么是对象?把一些"名字-属性"的组合放在一个单元里面,就组成了一个对象.我们可以理解
为javascript中的对象就是一些"键-值"对的集合
"名字"只能是string类型,不能是其他类型,而属性的类型则是
任意的(数字/字符串/其他对象..).可以用new Object()来创建一个空对象,也可以简单的用"{}"来
创建一个空对象,这两者的作用是等同的.
Js代码
1. var emptyObject1 = {}; //创建空对象
2. var emptyObject2 = new Object(); //创建空对象
3. var person = {"name":"sdcyst",
4. "age":18,
5. "sex":"male"}; //创建一个包含初始值的对象person
6. alert(person.name); //sdcyst
7. alert(person["age"]); //18
从上面的例子我们也可以看到,访问一个对象的属性,可以简单的用对象名加"."后加属性
的名字,也可以用"[]"操作符来获取,此时在[]里面的属性名字要加引号,这是因为对象中的索
引都是字符串类型的.
javasript对象中属性的个数是可变的,在创建了一个对象之后可以随时对它赋予任何的属性.
Js代码
1. var person = {};
2. person.name = "sdcyst";
3. person["age"] = 18;
4. alert(person.name + "__" + person.age); //sdcyst__18
5.
6. var _person = {name:"balala","age":23}; //在构建一个对象时,属性的名字可以不用引号
来标注(name),
7. //但是仍旧是一个字符串类型.在访问的时候[]内仍旧需要引号
8. alert(_person["name"] + "__" + person.age); //balala__23
9. alert(_person[name]); //undefinied
通过"."操作符获取对象的属性,必须得知道属性的名字.一般来说"[]"操作符获取对象属性的功
能更强大一些,
可以在[]中放入一些表达式来取属性的值,
比如可以用在循环控制语句中,而"."操作符则没有这种灵活性。
Js代码
1. var name = {"name1":"NAME1","name2":"NAME2","name3":"NAME3","name4":"N
AME4"};
2. var namestring = "";
3. for(var props in name) { //循环name对象中的属性名字
4. namestring += name[props];
5. }
6. alert(namestring); //NAME1NAME2NAME3NAME4
7.
8. namestring = "";
9. for(var i=0; i<4; i++) {
10. namestring += name["name"+(i+1)];
11. }
12. alert(namestring); //NAME1NAME2NAME3NAME4
delete操作符可以删除对象中的某个属性,判断某个属性是否存在可以使用"in"操作符.
Js代码
1. var name = {"name1":"NAME1","name2":"NAME2","name3":"NAME3","name4":"N
AME4"};
2. var namestring = "";
3. for(var props in name) { //循环name对象中的属性名字
4. namestring += name[props];
5. }
6. alert(namestring); //NAME1NAME2NAME3NAME4
7.
8. delete name.name1; //删除name1属性
9. delete name["name3"]; //删除name3属性
10. namestring = "";
11. for(var props in name) { //循环name对象中的属性名字
12. namestring += name[props];
13. }
14. alert(namestring); //NAME2NAME4
15.
16. alert("name1" in name); //false
17. alert("name4" in name); //true
需要注意,对象中的属性是没有顺序的.
对象的constructor属性
每一个javascript对象都有一个constructor属性.这个属性对应了对象初始化时的构造函数(函
数也是对象).
Js代码
1. var date = new Date();
2. alert(date.constructor); //Date
3. alert(date.constructor == "Date"); //false
4. alert(date.constructor == Date); //true
“===”和”==”
严格相等: === 严格不等:!==
js是弱类型,动态脚本语言。
变量类型的自动转化是它的主要特性之一。
在使用 == 对变量进行比较时,如果两表达式的类型不同,则试图将它们转换为字符串、数字或 Boolean 值。
在使用 === 对变量进行比较时则不做类型的自动转化。
===两端的数据类型不相同时,等式绝对不成立。
1==“1” // 成立 1===“1” //不成立
同一类的不同实例不严格相等,也不相等。
var a=new Array(), b=new Array() ; // a 不等于 b
数组
我们已经提到过,对象是无序数据的集合,而数组则是有序数据的集合,数组中的数据(元素)通
过索引(从0开始)来访问,
数组中的数据可以是任何的数据类型.数组本身仍旧是对象,但是由于数组的很多特性,通常
情况下把数组和对象区别开来分别对待.
创建数组可以用"[]"操作符,或者是用Array()构造函数来new一个.
Js代码
1. var array1 = []; //创建空数组
2. var array2 = new Array(); //创建空数组
3. array1 = [1,"s",[3,4],{"name1":"NAME1"}]; //
4. alert(array1[2][1]); //4 访问数组中的数组元素
5. alert(array1[3].name1); //NAME1 访问数组中的对象
6. alert(array1[8]); //undefined
7. array2 = [,,]; //没有数值填入只有逗号,则对应索引处的元素为undefined
8. alert(array2.length); //3
9. alert(array2[1]); //undefined
用new Array()来创建数组时,可以指定一个默认的大小,其中的值此时为undefined,以后可以再
给他们赋值.但是由于
javascript中的数组的长度是可以任意改变的,同时数组中的内容也是可以任意改变的,因此这
个初始化的长度实际上
对数组没有任何的约束力.对于一个数组,如果对超过它最大长度的索引赋值,则会改变数组
的长度,同时会对没有赋值
的索引处赋值undefined,看下面的例子.
Js代码
1. var array = new Array(10);
2. alert(array.length); //10
3. alert(array[4]); //undefined
4. array[100] = "100th"; //这个操作会改变数组的长度,同时将10-99索引对应的值设为
undefined
5. alert(array.length); //101
6. alert(array[87]); //undefined
可以用delete操作符删除数组的元素,注意这个删除仅仅是将数组在该位置的元素设为
undefined,数组的长度并没有改变.
我们已经使用过了数组的length属性,length属性是一个可以读/写的属性,也就是说我们可以
通过改变数组的length属性来
任意的改变数组的长度.如果将length设为小于数组长度的值,则原数组中索引大于length-1的
值都会被删除.如果length
的值大于原始数组的长度,则在它们之间的值设为undefined.
Js代码
1. var array = new Array("n1","n2","n3","n4","n5"); //五个元素的数组
2. var astring = "";
3. for(var i=0; i<array.length; i++) { //循环数组元素
4. astring += array[i];
5. }
6. alert(astring); //n1n2n3n4n5
7. delete array[3]; //删除数组元素的值
8. alert(array.length + "_" + array[3]) //5_undefined
9.
10. array.length = 3; //缩减数组的长度
11. alert(array[3]); //undefined
12. array.length = 8; //扩充数组的长度
13. alert(array[4]); //undefined
对于数组的其他方法诸如join/reverse等等,在这就不再一一举例.
通过上面的解释,我们已经知道,对象的属性值是通过属性的名字(字符串类型)来获取,而数组
的元素是通过索
引(整数型 0~~2**32-1)来得到值.数组本身也是一个对象,所以对象属性的操作也完全适合于数
组.
Js代码
1. var array = new Array("no1","no2");
2. array["po"] = "props1";
3. alert(array.length); //2
4. //对于数组来说,array[0]同array["0"]效果是一样的(?不确定,测试时如此)
5. alert(array[0] + "_" + array["1"] + "_" + array.po);//no1_no2_props1
函数
javascript函数相信大家都写过不少了,所以我们这里只是简单介绍一下.
创建函数:
function f(x) {........}
var f = function(x) {......}
上面这两种形式都可以创建名为f()的函数,不过后一种形式可以创建匿名函数
函数定义时可以设置参数,如果传给函数的参数个数不够,则从最左边起依次对应,其余的用
undefined赋值,如果传给函数的参数多于函数定义参数的个数,则多出的参数被忽略.
Js代码
1. function myprint(s1,s2,s3) {
2. alert(s1+"_"+s2+"_"+s3);
3. }
4. myprint(); //undefined_undefined_undefined
5. myprint("string1","string2"); //string1_string2_undefined
6. myprint("string1","string2","string3","string4"); //string1_string2_string3
因此,对于定义好的函数,我们不能指望调用者将所有的参数全部传进来.对于那些必须用到
的参数应该在函数体中
加以检测(用!操作符),或者设置默认值然后同参数进行或(||)操作来取得参数.
Js代码
1. function myprint(s1,person) {
2. var defaultperson = { //默认person对象
3. "name":"name1",
4. "age":18,
5. "sex":"female"
6. };
7. if(!s1) { //s1不允许为空
8. alert("s1 must be input!");
9. return false;
10. }
11. person = person || defaultperson; //接受person对象参数
12. alert(s1+"_"+person.name+":"+person.age+":"+person.sex);
13. };
14.
15. myprint(); //s1 must be input!
16. myprint("s1"); //s1_name1:18:female
17. myprint("s1",{"name":"sdcyst","age":23,"sex":"male"}); //s1_sdcyst:23:male
函数的arguments属性
在每一个函数体的内部,都有一个arguments标识符,这个标识符代表了一个Arguments对
象.Arguments对象非常类似
于Array(数组)对象,比如都有length属性,访问它的值用"[]"操作符利用索引来访问参数值,但是,
二者是完全不同的
东西,仅仅是表面上有共同点而已(比如说修改Arguments对象的length属性并不会改变它的长
度).
Js代码
1. function myargs() {
2. alert(arguments.length);
3. alert(arguments[0]);
4. }
5. myargs(); //0 --- undefined
6. myargs("1",[1,2]); //2 --- 1
Arguments对象有一个callee属性,标示了当前Arguments对象所在的方法.可以使用它来实现
匿名函数的内部递归调用.
Js代码
1. function(x) {
2. if (x <= 1) return 1;
3. return x * arguments.callee(x-1);
4. } (section8.2)
------------------------------------------------------------------
Method--方法
方法就是函数.我们知道,每一个对象都包含0个或多个属性,属性可以是任意类型,当然也包
括对象.函数本身就是一种
对象,因此我们完全可以把一个函数放到一个对象里面,此时,这个函数就成了对象的一个方
法.此后如果要使用该方法,
则可以通过对象名利用"."操作符来实现.
Js代码
1. var obj = {f0:function(){alert("f0");}}; //对象包含一个方法
2. function f1() {alert("f1");}
3. obj.f1 = f1; //为对象添加方法
4.
5. obj.f0(); //f0 f0是obj的方法
6. obj.f1(); //f1 f1是obj的方法
7. f1(); //f1 f1同时又是一个函数,可以直接调用
8. f0(); //f0仅仅是obj的方法,只能通过对象来调用
方法的调用需要对象的支持,那么在方法中如何获取对象的属性呢?this!this关键字我们已经
很熟悉了,在javascript的方
法中,我们可以用this来取得对方法调用者(对象)的引用,从而获取方法调用者的各种属性.
Js代码
1. var obj = {"name":"NAME","sex":"female"};
2. obj.print = function() { //为对象添加方法
3. alert(this.name + "_" + this["sex"]);
4. };
5. obj.print(); //NAME_female
6. obj.sex = "male";
7. obj.print(); //NAME_male
下面我们来一个更加面向对象的例子.
Js代码
1. var person = {name:"defaultname",
2. setName:function(s){
3. this.name = s;
4. },
5. "printName":function(){
6. alert(this.name);
7. }}
8. person.printName(); //defaultname
9. person.setName("newName");
10. person.printName(); //newName
在上面的例子中,完全可以用person.name=..来直接改变person的name属性,在此我们只是为
了展示一下刚才提到的内容.
另一种改变person属性的方法就是:定义一个function,接收两个参数,一个是person,一个是
name的值,看起来像是这样:
changeName(person,"newName").哪种方法好呢?很明显,例子中的方法更形象,更直观一些,而且
好像有了那么一点面向对象的影子.
再次强调一下,方法(Method)本身就是是函数(function),只不过方法的使用更受限制.在后面的篇
幅中,如果提到函数,那么提到的内容同样适用于方法,反之则不尽然.
函数的prototype属性
每一个函数都包含了一个prototype(原型)属性,这个属性构成了javascript面向对象的核心基础.
在后面我们会详细讨论.
类、构造函数、原型
先来说明一点:在上面的内容中提到,每一个函数都包含了一个prototype属性,这个属性指向了
一个prototype对象.
构造函数:
new操作符用来生成一个新的对象.new后面必须要跟上一个函数,也就是我们常说的构造函
数.构造函数的工作原理又是怎样的呢?
先看一个例子:
Js代码
1. function Person(name,sex) {
2. this.name = name;
3. this.sex = sex;
4. }
5. var per = new Person("sdcyst","male");
6. alert("name:"+per.name+"_sex:"+per.sex); //name:sdcyst_sex:male
下面说明一下这个工作的步骤:
开始创建了一个函数(不是方法,只是一个普通的函数),注意用到了this关键字.以前我们提到
过this关键字表示调用该方法的对象,也就是说通过对象调用"方法"的时候,this关键字会指向
该对象(不使用对象直接调用该函数则this指向整个的script域,或者函数所在的域,在此我们
不做详细的讨论).当我们使用new操作符时,javascript会先创建一个空的对象,然后这个对象被
new后面的方法(函数)的this关键字引用!然后在方法中通过操作this,就给这个新创建的对象
相应的赋予了属性.最后返回这个经过处理的对象.这样上面的例子就很清楚:先创建一个空
对象,然后调用Person方法对其进行赋值,最后返回该对象,我们就得到了一个per对象.
prototype(原型)--在这里会反复提到"原型对象"和"原型属性",注意区分这两个概念.
在javascript中,每个对象都有一个prototype属性,这个属性指向了一个prototype对象.
上面我们提到了用new来创建一个对象的过程,事实上在这个过程中,当创建了空对象后,new
会接着操作刚生成的这个对象的prototype属性.
每个方法都有一个prototype属性(因为方法本身也是对象),new操作符生成的新对象的
prototype属性值和构造方法的prototype属性值是一致的.构造方法的prototype属性指向了一个
prototype对象,这个prototype对象初始只有一个属性constructor,而这个constructor属性又指向了
prototype属性所在的方法
有点晕,看下面的图:
这样,当用构造函数创建一个新的对象时,它会获取构造函数的prototype属性所指向的
prototype对象的所有属性.对构造函数对应的prototype对象
所做的任何操作都会反应到它所生成的对象身上,所有的这些对象共享构造函数对应的
prototype对象的属性(包括方法).
看个具体的例子吧:
Js代码
1. function Person(name,sex) { //构造函数
2. this.name = name;
3. this.sex = sex;
4. }
5. Person.prototype.age = 12; //为prototype属性对应的prototype对象的属性赋值
6. Person.prototype.print = function() { //添加方法
7. alert(this.name+"_"+this.sex+"_"+this.age);
8. };
9.
10. var p1 = new Person("name1","male");
11. var p2 = new Person("name2","male");
12. p1.print(); //name1_male_12
13. p2.print(); //name2_male_12
14.
15. Person.prototype.age = 18; //改变prototype对象的属性值,注意是操作构造函数的
prototype属性
16. p1.print(); //name1_male_18
17. p2.print(); //name2_male_18
到目前为止,我们已经模拟出了简单的类的实现,我们有了构造函数,有了类属性,有了类方法,
可以创建"实例".
在下面的文章中,我们就用"类"这个名字来代替构造方法,但是,这仅仅是模拟,并不是真正的面
向对象的"类".
在下一步的介绍之前,我们先来看看改变对象的prototype属性和设置prototype属性的注意事
项:给出一种不是很恰当的解释,或许有助于我们理解:当我们new了一个对象之后,这个对象
就会获得构造函数的prototype属性(包括函数和变量),可以认为是构造函数(类)继承了它的
prototype属性对应的prototype对象的函数和变量,也就是说,
prototype对象模拟了一个超类的效果.听着比较拗口,我们直接看个实例吧:
Js代码
1. function Person(name,sex) { //Person类的构造函数
2. this.name = name;
3. this.sex = sex;
4. }
5. Person.prototype.age = 12; //为Person类的prototype属性对应的prototype对象的属
性赋值,
6. //相当于为Person类的父类添加属性
7. Person.prototype.print = function() { //为Person类的父类添加方法
8. alert(this.name+"_"+this.sex+"_"+this.age);
9. };
10.
11. var p1 = new Person("name1","male"); //p1的age属性继承子Person类的父类(即
prototype对象)
12. var p2 = new Person("name2","male");
13.
14. p1.print(); //name1_male_12
15. p2.print(); //name2_male_12
16.
17. p1.age = 34; //改变p1实例的age属性
18. p1.print(); //name1_male_34
19. p2.print(); //name2_male_12
20.
21. Person.prototype.age = 22; //改变Person类的超类的age属性
22. p1.print(); //name1_male_34(p1的age属性并没有随着prototype属性的改变而改变)
23. p2.print(); //name2_male_22(p2的age属性发生了改变)
24.
25. p1.print = function() { //改变p1对象的print方法
26. alert("i am p1");
27. }
28.
29. p1.print(); //i am p1(p1的方法发生了改变)
30. p2.print(); //name2_male_22(p2的方法并没有改变)
31.
32. Person.prototype.print = function() { //改变Person超类的print方法
33. alert("new print method!");
34. }
35.
36. p1.print(); //i am p1(p1的print方法仍旧是自己的方法)
37. p2.print(); //new print method!(p2的print方法随着超类方法的改变而改变)
javascript中对象的prototype属性相当于java中的static变量,可以被这个类下的所有对象共用.
而上面的例子似乎表明实际情况并不是这样:
在JS中,当我们用new操作符创建了一个类的实例对象后,它的方法和属性确实继承了类的
prototype属性,类的prototype属性中定义的方法和属性,确实可以被这些实例对象直接引用.但
是,当我们对这些实例对象的属性和方法重新赋值或定义后,那么实例对象的属性或方法就不
再指向类的prototype属性中定义的属性和方法,此时,即使再对类的prototype属性中相应的方
法或属性做修改,也不会反应在实例对象身上.这就解释了上面的例子:
一开始,用new操作符生成了两个对象p1,p2,他们的age属性和print方法都来自(继承于)Person
类的prototype属性.然后,我们修改了p1的age属性,后面对Person类的prototype属性中的age
重新赋值(Person.prototype.age = 22),p1的age属性并不会随之改变,但是p2的age属性却随之
发生了变化,因为p2的age属性还是引自Person类的prototype属性.同样的情况在后面的print
方法中也体现了出来.
通过上面的介绍,我们知道prototype属性在javascript中模拟了父类(超类)的角色,在js中体现
面向对象的思想,prototype属性是非常关键的.
类变量/类方法/实例变量/实例方法
在javascript中,所有的方法都有一个call方法和apply方法.这两个方法可以模拟对象调用方法.
它的第一个参数是对象,后面的
参数表示对象调用这个方法时的参数.比如我们定义了一个方法f(),然后调用下面的语句:
f.call(o, 1, 2);
作用就相当于
o.m = f;
o.m(1,2);
delete o.m;
举个例子:
Js代码
1. function Person(name,age) { //定义方法
2. this.name = name;
3. this.age = age;
4. }
5. var o = new Object(); //空对象
6. alert(o.name + "_" + o.age); //undefined_undefined
7.
8. Person.call(o,"sdcyst",18); //相当于调用:o.Person("sdcyst",18)
9. alert(o.name + "_" + o.age); //sdcyst_18
10.
11. Person.apply(o,["name",89]);//apply方法作用同call,不同之处在于传递参数的形式是用数
组来传递
12. alert(o.name + "_" + o.age); //name_89
---------------------------------
实例变量和实例方法都是通过实例对象加"."操作符然后跟上属性名或方法名来访问的,但是
我们也可以为类来设置方法或变量,
这样就可以直接用类名加"."操作符然后跟上属性名或方法名来访问.定义类属性和类方法很
简单:
Js代码
1. Person.counter = 0; //定义类变量,创建的Person实例的个数
2. function Person(name,age) {
3. this.name = name;
4. this.age = age;
5. Person.counter++; //没创建一个实例,类变量counter加1
6. };
7.
8. Person.whoIsOlder = function(p1,p2) { //类方法,判断谁的年龄较大
9. if(p1.age > p2.age) {
10. return p1;
11. } else {
12. return p2;
13. }
14. }
15.
16. var p1 = new Person("p1",18);
17. var p2 = new Person("p2",22);
18.
19. alert("现在有 " + Person.counter + "个人"); //现在有2个人
20. var p = Person.whoIsOlder(p1,p2);
21. alert(p.name + "的年龄较大"); //p2的年龄较大
prototype属性的应用:
假设我们定义了一个Circle类,有一个radius属性和area方法,实现如下:
Js代码
1. function Circle(radius) {
2. this.radius = radius;
3. this.area = function() {
4. return 3.14 * this.radius * this.radius;
5. }
6. }
7. var c = new Circle(1);
8. alert(c.area()); //3.14
假设我们定义了100个Circle类的实例对象,那么每个实例对象都有一个radius属性和area方
法,实际上,除了radius属性,每个Circle类的实例对象的area方法都是一样,这样的话,我们就可
以把area方法抽出来定义在Circle类的prototype属性中,这样所有的实例对象就可以调用这个
方法,
从而节省空间.
Js代码
1. function Circle(radius) {
2. this.radius = radius;
3. }
4. Circle.prototype.area = function() {
5. return 3.14 * this.radius * this.radius;
6. }
7. var c = new Circle(1);
8. alert(c.area()); //3.14
现在,让我们用prototype属性来模拟一下类的继承:首先定义一个Circle类作为父类,然后定义
子类
PositionCircle.
Js代码
1. function Circle(radius) { //定义父类Circle
2. this.radius = radius;
3. }
4. Circle.prototype.area = function() { //定义父类的方法area计算面积
5. return this.radius * this.radius * 3.14;
6. }
7.
8. function PositionCircle(x,y,radius) { //定义类PositionCircle
9. this.x = x; //属性横坐标
10. this.y = y; //属性纵坐标
11. Circle.call(this,radius); //调用父类的方法,相当于调用this.Circle(radius),设置
PositionCircle类的
12. //radius属性
13. }
14. PositionCircle.prototype = new Circle(); //设置PositionCircle的父类为Circle类
15.
16. var pc = new PositionCircle(1,2,1);
17. alert(pc.area()); //3.14
18. //PositionCircle类的area方法继承自Circle类,而Circle类的
19. //area方法又继承自它的prototype属性对应的prototype对象
20. alert(pc.radius); //1 PositionCircle类的radius属性继承自Circle类
21.
22. /*
23. 注意:在前面我们设置PositionCircle类的prototype属性指向了一个Circle对象,
24. 因此pc的prototype属性继承了Circle对象的prototype属性,而Circle对象的constructor

25. 性(即Circle对象对应的prototype对象的constructor属性)是指向Circle的,所以此处弹出
26. 的是Circ.
27. */
28. alert(pc.constructor); //Circle
29.
30. /*为此,我们在设计好了类的继承关系后,还要设置子类的constructor属性,否则它会指向父类
31. 的constructor属性
32. */
33. PositionCircle.prototype.constructor = PositionCircle
34. alert(pc.constructor); //PositionCircle
作用域、闭包、模拟私有属性
先来简单说一下变量作用域,这些东西我们都很熟悉了,所以也不详细介绍。
Js代码
1. var sco = "global"; //全局变量
2. function t() {
3. var sco = "local"; //函数内部的局部变量
4. alert(sco); //local 优先调用局部变量
5. }
6. t(); //local
7. alert(sco); //global 不能使用函数内的局部变量
注意一点,在javascript中没有块级别的作用域,也就是说在java或c/c++中我们可以
用"{}"来包围一个块,从而在其中定义块内的局部变量,在"{}"块外部,这些变量不再起作用,
同时,也可以在for循环等控制语句中定义局部的变量,但在javascript中没有此项特性:
Js代码
1. function f(props) {
2. for(var i=0; i<10; i++) {}
3. alert(i); //10 虽然i定义在for循环的控制语句中,但在函数
4. //的其他位置仍旧可以访问该变量.
5. if(props == "local") {
6. var sco = "local";
7. alert(sco);
8. }
9. alert(sco); //同样,函数仍可引用if语句内定义的变量
10. }
11. f("local"); //10 local local
在函数内部定义局部变量时要格外小心:
Js代码
1. var sco = "global";
2. function print1() {
3. alert(sco); //global
4. }
5. function print2() {
6. var sco = "local";
7. alert(sco); //local
8. }
9. function print3() {
10. alert(sco); //undefined
11. var sco = "local";
12. alert(sco); local
13. }
14.
15. print1(); //global
16. print2(); //local
17. print3(); //undefined local
前面两个函数都很容易理解,关键是第三个:第一个alert语句并没有把全局变量"global"显示
出来,
而是undefined,这是因为在print3函数中,我们定义了sco局部变量(不管位置在何处),那么
全局的
sco属性在函数内部将不起作用,所以第一个alert中sco其实是局部sco变量,相当于:
Js代码
1. function print3() {
2. var sco;
3. alert(sco);
4. sco = "local";
5. alert(sco);
6. }
从这个例子我们得出,在函数内部定义局部变量时,最好是在开头就把所需的变量定义好,
以免出错。
函数的作用域在定义函数的时候已经确定了,例如:
Js代码
1. var scope = "global" //定义全局变量
2. function print() {
3. alert(scope);
4. }
5. function change() {
6. var scope = "local"; //定义局部变量
7. print(); //虽然是在change函数的作用域内调用print函数,
8. //但是print函数执行时仍旧按照它定义时的作用域起作用
9. }
10. change(); //golbal
闭包
闭包是拥有变量、代码和作用域的表达式.在javascript中,函数就是变量、代码和函数的作用域
的组合体,因此所有的函数都是闭包.好像挺简单.但是闭包到底有什么作用呢?看一个例子。
我们想写一个方法,每次都得到一个整数,这个整数是每次加1的,没有思索,马上下笔:
Js代码
1. var i = 0;
2. function getNext() {
3. i++;
4. return i;
5. }
6. alert(getNext()); //1
7. alert(getNext()); //2
8. alert(getNext()); //3
一直用getNext函数得到下一个整数,而后不小心或者故意的将全局变量i的值设为0,然后再
次调用getNext,你会发现又从1开始了........这时你会想到,要是把i设置成一个私有变量该多
好,这样只有在方法内部才可能改变它,在函数之外就没有办法修改了.下面的代码就是按照
这个要求来做得,后面我们详细讨论。
为了解释方便,我们就把下面的代码称为demo1.
Js代码
1. function temp() {
2. var i = 0;
3. function b() {
4. return ++i;
5. }
6. return b;
7. }
8. var getNext = temp();
9. alert(getNext()); //1
10. alert(getNext()); //2
11. alert(getNext()); //3
12. alert(getNext()); //4
因为我们平时所说的javascript绝大多数都是指的在客户端(浏览器)下,所以这里也不例外。
在javascript解释器启动时,会首先创建一个全局的对象(global object),也就是"window"所引用的
对象.然后我们定义的所有全局属性和方法等都会成为这个对象的属性.
不同的函数和变量的作用域是不同的,因而构成了一个作用域链(scope chain).很显然,在
javascript解释器启动时,这个作用域链只有一个对象:window(Window Object,即global object).
在demo1中,temp函数是一个全局函数,因此temp()函数的作用域(scopr)对应的作用域链就是
js解释器启动时的作用域链,只有一个window对象。
当temp执行时,首先创建一个call对象(活动对象),然后把这个call对象添加到temp函数对
应的作用域链的最前头,这是,temp()函数对应的作用域链就包含了两个对象:window对象和
temp函数对应的call object(活动对象).然后呢,因为我们在temp函数里定义了变量i,
定义了函数b(),这些都会成为call object的属性。当然,在这之前会首先给call object对象添加
arguments属性,保存了temp()函数执行时传递过来的参数。此时,整个的作用域链如下图所
示:
同理可以得出函数b()执行时的整个作用域链:
注意在b()的作用域链中,b()函数对应的call object只有一个arguemnts属性,并没有i属性,
这是因为在b()的定义中,并没有用var关键字来
声明i属性,只有用var 关键字声明的属性才会添加到对应的call object上.
在函数执行时,首先查找对应的call object有没有需要的属性,如果没有,再往上一级查找,
直到找到为止,如果找不到,那就是undefined了.
这样我们再来看demo1的执行情况。我们用getNext引用了temp函数,而temp函数返回了函
数b,这样getNext函数其实就是b函数的引用。
执行一次getNext,就执行一次b()函数。因为函数b()的作用域依赖于函数temp,因此temp函
数在内存中会一直存在。函数b执行时,首先查找
i,在b对应的call object中没有,于是往上一级找,在temp函数对应的call object中找到了,
于是将其值加1,然后返回这个值。
这样,只要getNext函数有效,那么b()函数就一直有效,同时,b()函数依赖的temp函数也
不会消失,变量i也不会消失,而且这个变量在temp函数
外部根本就访问不到,只能在temp()函数内部访问(b当然可以了).
来看一个利用闭包来模拟私有属性的例子:
Js代码
1. function Person(name, age) {
2. this.getName = function() { return name; };
3. this.setName = function(newName) { name = newName; };
4. this.getAge = function() { return age; };
5. this.setAge = function(newAge) { age = newAge; };
6. }
7.
8. var p1 = new Person("sdcyst",3);
9. alert(p1.getName()); //sdcyst
10. alert(p1.name); //undefined 因为Person('类')没有name属性
11. p1.name = "mypara" //显示的给p1添加name属性
12. alert(p1.getName()); //sdcyst 但是并不会改变getName方法的返回值
13. alert(p1.name); //mypara 显示出p1对象的name属性
14. p1.setName("sss"); //改变私有的"name"属性
15. alert(p1.getName()); //sss
16. alert(p1.name); //仍旧为mypara
定义了一个Person类,有两个私有属性name,age,分别定义对应的get/set方法。
虽然可以显示的设置p1的name、age属性,但是这种显示的设置,并不会改变我们
最初设计时模拟出来的"name/age"私有属性。
下面是另一篇解释javascript闭包的文章:
http://softbbs.pconline.com.cn/9497825.html
0 0