JavaScript面向对象程序设计基础
- 2019 年 12 月 25 日
- 筆記
一、对象
JavaScript的简单数据类型包括数字、字符串、布尔值、null值和undefined值。其他所有的值都是对象。数字、字符串和布尔值“貌似”对象,因为它们拥有方法,但它们是不可变的。JavaScript中的对象是可变的键控集合(Keyed collections)。在JavaScript中,数组时对象,函数是对象,正在表达式是对象,当然,对象自然也是对象。 对象是属性的集合,每一个属性具有一个名称和一个值。数学的名字可以是包括空字符串在内的任意字符串。属性值可以是除undefined值之外的任何值。
JavaScript里的对象是无类型的(class-free)。它对新属性的名字和值是没有限制的。对象适合用于汇集和管理数据。对象可以保护其他对象,所以它们可以很容易的表示成树状或图形结构。
1、创建(Create)
可以采用两种方法来实例化对象。
第一种方法是使用new关键字
1 |
var myObject = new Object(); |
---|
new 关键字调用构造函数,或者更通用的说法是构造器。构造器将初始化新创建的对象。下面的代码演示了一个名为Zombie(僵尸)对象的构造器,构造器初始化该对象的name属性,然后使用new关键字实例化一个Zombie对象。this关键字用于引用当前对象,不能对它进行赋值,但可以将this关键字的值赋给另外一个变量。
12345 |
//构造函数function Zombie( name ) { this.name = name;}var smallZombie = new Zombie( "Booger"); |
---|
使用对象字面量创建对象 另外一种实例化新对象的方法更加方便:使用对象字面量,这种方式创建的对象更像其他语言中的散列(hash)或关联数组。
12345 |
var myObject = { };var webSite= { "url": "www.mybry.com", "siteName": "读你"}; |
---|
在属性列表的最后一项值的末尾,请不要使用结尾逗号。不同的浏览器对此的解析并不一致。
2、检索(Retrieval)
既可以使用方括号来访问对象的属性,也可以使用点操作符来访问。下面的代码示例了者两种方式:
12345 |
webSite['url'];"www.mybry.com"webSite.siteName;"读你" |
---|
采用方括号方式访问属性时,可以使用JavaScript的关键字作为属性名,但不推荐这样做,使用点操作符方式访问属性时则不能使用。使用点操作符方式,代码更加简短。JSLint鼓励开发人员使用点操作符方式。因为属性也可以是对象,可以在任意层次上嵌套对象。
下面假设有一个父亲father对象,他有自己的名字name和年龄age,他有两个儿子childrenOne和childrenTwo,两个孩子当然也是对象,也有名字name个年龄age,然后我们访问孩子childrenOne的名字:
123456789101112131415 |
var father = { 'name': 'zhangsan', 'age': 50, 'childrenOne': { 'name': 'zhangsi', 'age': 22 }, 'childrenTwo': { 'name': 'zhangwu', 'age': 12 }};console.log(father.childrenOne.name);//输出:zhangsi |
---|
JavaScript是一种动态的语言,因此更新一个属性只需要重新对属性赋值即可。要从对象中移除一个属性,只需要使用delete操作符。删除一个不存在的属性并不会造成任何危险。要遍历对象的所有属性,可以使用for…in循环,比如下面的代码块:
123456789101112 |
var obj1 = { 'properties1': 1, 'properties2': 2};var i;for( i in obj1 ){ console.log( i );}properties1properties2 |
---|
3、原型(Prototype)
JavaScript使用原型继承(prototype inheritance),对象直接从其他对象继承,从而创建新的对象。简而言之,对象继承了另外一个对象的属性。JavaScript中没有类,这是与Java和C#等语言相比一个较大的差别。原型就是其他对象的模型(model)。
每个对象都连接到一个原型对象,并且它可以从中继承属性。所有通过对象字面量创建的对象都连接到Object.prototypr,它是JavaScript中的标配对象。
当你创建一个新对象时,你可以选择某个对象作为它的原型。JavaScript提供的实现机制杂乱而复杂,但其实可以被明显地简化。我们将给Object对象增加一个create方法。这个方法创建一个使用原型对象作为其原型的新对象。
123456789101112131415161718192021222324252627282930 |
if (typeof Object.beget !== 'function') { Object.create = function (o) { var F = function () { }; F.prototype = o; return new F(); };}var another = Object.create( webSite );console.log(another);“` 在firebug中输出如下结果,正是一个对象。原型连接在更新时是不起作用的。当我们对某个对象做出改变时,不会触及该对象的原型,原型连接只有在检索值的时候才会被用到。如果我们尝试去获取对象的某个属性值,但该对象没有此属性名,那么JavaScript会试着从原型对象中获取属性值。如果那个原型对象也没有该属性,那么再从他的原型中寻找,以此类推,直到该过程最后到达终点Object.prototype。如果想要的属性完全不存在与原型链中,那么结果就是undefined值。这个过程称为委托。原型关系是一种动态关系。如果我们添加一个新的属性到原型中,该属性会立即对所有基于该原型创建的对象可见。(更多关于原型链的内容后续文章将会介绍)### 4、引用(Reference)引用是一个指向对象实例位置的指针。Object是引用类型,由于所有对象都是通过引用传递的,,它们永远不会被复制:“`jsvar x = myObject;x.nickname = '这世间唯有梦想和好姑娘不可辜负~~~';var nick = myObject.nickname;//因为x和stooge是指向同一个对象的引用,所以nick为‘zhangsan’console.log(nick); //这世间唯有梦想和好姑娘不可辜负~~~//因为a、b和c每个都引用一个不同的空对象var a = { }, b = { }, c = { };//a、b和c都引用同一个空对象a = b = c = { }; |
---|
修改绑定于一个原型的属性,将修改基于该原型的所有其他对对象的原型。
对象是自知的,或者说对象知道自己的属性。要检查某个属性是否存在,可以使用hasOwnProperty()方法,该方法将返回一个布尔值。
5、减少全局变量污染
很多程序员认为,应该避免使用全局变量。有一些办法可以避免扰乱全局名称空间,一种办法是使用单个全局变量作为顶层对象,包含程序或框架中的所有方法和属性。按照惯例,名称空间的字母全部大写,但值得注意的是常量通常也大写格式。
1234567 |
ZOMBIE_GENERATOR = {};ZOMBIE_GENERATOR.Zombies = { smallZombie : 'Booger', largeZombie : 'Bruce'} |
---|
采用上面的代码定义名称空间之后,Zombies就不会与全局名称空间中的任何其他变量冲突。另外一种减少冲突的方式是使用闭包。
6、反射(Reflection)
检查对象并确定对象有什么属性时很容易的事情,只要试着去检索该属性并验证取得值。typeof操作符对确定属性的类型很有帮助:
1234 |
typeof father.name; // 'string'typeof father.age; // 'number'typeof father.childrenOne; // 'object'typeof father.xxx; // 'undefined' |
---|
请注意原型链中任何属性都会产生值:
12 |
typeof father.toString; // 'function'typeof father.constructor; // 'function' |
---|
有两种方法去处理掉这些不需要的属性。第一个是让你的程序做检查并丢弃掉值为函数的属性。一般来说,当你想让对象在运行时动态获取自身信息时,你关注更多的时数据,而你应该意识到一些值可能是函数。
另一个方法时使用hasOwnProperty方法,如果对象拥有独有的属性,它会返回true。hasOwnProperty方法不会检查原型链。
12 |
father.hasOwnProperty('name'); // truefather.hasOwnProperty('constructor'); // false |
---|
7、枚举(Enumeraton)
for...in
语句可以用来遍历一个对象中的所有属性名。该枚举过程将会列出所有的属性——包括函数和你可能不关心的原型中的属性——所以有必要过滤那些你不想要的值。最为常用的过滤时hasOwnPropery方法,一级使用typeof来排除函数。
属性名出现的顺序是不确定的,因此要对任何可能出现的顺序有所准备。如果你想要确保属性以特定的顺序出现,最好的办法就是完全避免使用for in语句,而是创建一个数组,在其中以正确的顺序包含属性名:
123456789 |
var properties = [ 'first-name', 'middle-name', 'last-name', 'profession'];for(var i = 0; i < properties.length; i++){ console.info(properties[i]);} |
---|
通过使用for而不是for in,可以得到我们想要的属性,而不必担心可能发掘出的原型链中的属性,并且我们按正确的顺序取得了它们的值。
二、函数
函数是一个代码块,它封装了一系列的JavaScript语句。函数可以接受参数,也可以返回值。如果一个函数没有返回一个特定的值,则它返回一个undefined值。下面的代码示例定义了一个没有返回值的函数。该函数依然执行了函数体中的操作,将变了x的值乘以2,但由于没有使用return语句返回值,因此在控制台中输出函数的返回值时,返回值为undefined。
12345 |
var x = 2;function calc( ){ x = x * 2;}console.log( calc() );// undefined |
---|
这正是函数有趣的地方,与Java不同,JavaScript中的函数时第一类对象。这意味着可以像处理其他JavaScript对象一样处理JavaScript函数。可以将函数赋予给一个变量,或者保存在另外一个数据结构中(比如一个数组或对象);可以将函数作为参数传递给其他函数;可以将函数作为另一个函数的返回值;函数还可以采用匿名函数的形式:即根本没有绑定于函数名标识符的函数。下面代码定义了一个函数表达式,并将其赋值给一个变量。
123456 |
var calc = function(x){ return x * 2;}calc(5);// 10 |
---|
给函数名使用圆括号,将执行该函数并返回函数的返回值,而不是返回对函数的引用。
1234567 |
var calc = function(x){ return x * 2;}var calcValue = calc( 5 );console.log( calcValue );// 10 |
---|
第一类函数的另外两个特性时非常重要的。第一个特性就是将函数作为参数传递给其他函数。第二个特性就是匿名函数。下面代码演示了JavaScript中函数的重要特性。reporter函数接收一个函数作为参数,并输出执行该参数函数的返回值。另外两个例子则演示了匿名函数。第一个函数是一个根本不包含任何语句的匿名函数,但根据前面的介绍,该函数将返回一个undefined。第二个函数时一个返回字符串的匿名函数。
123456789101112 |
function reporter( fn ) { console.log( "这个返回值是你传递过来的函数:" + fn() );}reporter( function(){}); //这个返回值是你传递过来的函数:undefinedreporter( function() { return "这是一个简单的字符串" });//这个返回值是你传递过来的函数:这是一个简单的字符串function calc(){ return 2 * 4;}reporter( calc );// 这个返回值是你传递过来的函数:8 |
---|
这是你会看到控制台还输出了一个undefined值,这是reporter函数本身的返回值,也就是说执行任何函数本身,它都会返回一个undefined值。
关于匿名函数,一个特别重要的变体就是立即调用的函数表达式,或称为自执行匿名函数。无论称为“立即调用的函数表达式”还是“自执行匿名函数”,这一模式的本质就是将一个函数表达式包装在一对圆括号中,然后立即调用该函数。这一技巧非常简单,将函数表达式包装在一对圆括号中,将迫使JavaScript引擎将function(){}块识别为一个函数表达式,而不是一个函数语句的开始。下面的代码示例描述了这一模式,在这个简单的例子中,函数表达式接受两个值并简单地将二者相加。
1234 |
(function( x,y ){ console.log( x+y );})( 5,6);// 11 |
---|
由于这样的函数表达式将被立即调用,因此该模式用于确保代码块的执行上下文按照预期的效果执行,这是这种模式最重要的用于之一。通过将参数传入函数,在执行时就可以捕获函数所创建闭包中的变量。闭包就是这样的一个函数:它处在一个函数体中,并引用了函数体当前执行上下文中的变量。闭包是一个极为强大的功能,下面的代码描述了闭包的基本应用。在下面的例子中还引入了JavaScript函数另外一个有趣的特性,即在函数中可以将另外一个函数作为返回值返回。
在下面的例子中,将自执行匿名函数赋给一个变量message。message返回另外一个函数,该函数只是简单的输出变量x的值。有趣的是,当我们把变量x的初始值作为参数传入函数时,可以在函数执行时所创建的闭包中捕获变量x的值。无论在外部作用域中的x的值发生了什么变化,闭包将记住函数执行时变量x的值。
1234567891011 |
var x = 42;console.log(x); // 42var message = (function ( x ){ return function(){ console.log( "x is " + x); }})( x );message(); //x is 42x = 12;console.log( x ); // 12message(); //x is 42 |
---|
即使只介绍了这个简单的例子,也应该看到JavaScript函数的强大功能。
三、作用域和闭包
当讨论作用域时,考虑定义变量的位置和变量的生存期时非常重要的。作用域指的是在什么地方可以访问该变量。在JavaScript中,作用域维持在函数级别,而并非块级别。因此,参数以及使用var关键字定义的变量,仅在当前函数中可见。
除了不能访问this关键字和参数之外,嵌套函数可以访问外部函数中定义的变量。这一机制时通过闭包来实现的,它是指:即使在外部函数结束执行之后,内部嵌套的函数继续保持它对外部函数的引用。闭包还有助于减少名称空间的冲突。
每次调用一个包裹的函数时,虽然函数的代码并没有改变,但是JavaScript将为每一次调用创建一个新的作用域。下面的代码说明了这一行为。
123456789101112131415 |
function getFunction(value){ return function(value){ return value; }}var a = getFunction(), b = getFunction(), c = getFunction();console.log(a(0)); // 0console.log(b(1)); // 1console.log(c(2)); // 2console.log(a === b); // false |
---|
当定义一个独立函数时(即不绑定于任何对象)时,this关键字绑定雨全局名称空间。作为一个最直接的结果,当在一个方法内创建一个内部函数时,内部函数的this关键字将绑定于全局名称空间,而不是绑定于该方法。为了解决这一问题,可以将包裹方法的this关键字简单的赋值给一个名为that的中间变量。
123456789101112131415161718 |
obj = {};obj.method = function(){ var that = this; this.counter = 0; var count = function(){ that.counter += 1; console.log(that.counter); } count(); count(); console.log(this.counter);}obj.method();// 输出:122 |
---|
四、访问级别
在JavaScript中并没有官方的访问级别语法,JavaScript没有类似于Java语言中的private或protected这样的访问级别关键字。默认情况下,对象中所有的成员都是公开和可访问的。但在JavaScript中可以实现与私有或专有属性类似的访问级别效果。要实现私有方法或属性,请使用闭包。
1234567891011121314 |
function TimeMachine(){ //私有成员 var destination = 'ShangHai CAOHEJING'; //公有成员 this.getDestination = function(){ return destination; };}var zhangsan = new TimeMachine();console.log(zhangsan.getDestination());// ShangHai CAOHEJINGconsole.log(zhangsan.destination);//undefined |
---|
getDestination方法是一个专有方法,它可以访问TimeMachine(时间机器)中的私有成员。另外,变量destination 是“私有”的,它只能通过专有方法getDestination进行访问。
五、模块
于私有和专有访问级别类似,JavaScript没有内置的包语法。模块模式是一种简单而流行的方式,用于创建自包含的、模块化的代码。要创建一个模块,只需要声明一个名称空间、将有关函数绑定在该名称空间,并定义私有成员和专有成员即可,下面将使用模块重写TimeMachine对象。
123456789101112131415161718192021222324252627282930313233343536373839404142434445 |
//创建名称空间对象TIMEMACCHINE = {};TIMEMACCHINE.createZhangsan = (function(){ //私有成员 var destination = ''; var model = ''; var fuel = ''; //公有访问方法 return { //设置器 setDestination: function(dest){ this.destination = dest; }, setModel: function(model){ this.model = model; }, setFuel: function(fuel){ this.fuel = fuel; }, //访问器 getDestination: function(){ return this.destination; }, getModel: function(){ return this.model; }, getFuel: function(){ return this.fuel; }, //其他成员 toString : function(){ console.log(this.getModel() + ' – Fuel Type:' + this.getFuel() + ' – Headed:' + this.getDestination()); } };})();var myTimeMachine = TIMEMACCHINE.createZhangsan;myTimeMachine.setModel('顺丰号');myTimeMachine.setFuel('钚');myTimeMachine.setDestination('漕河泾');myTimeMachine.toString();// 顺丰号 – Fuel Type:钚 – Headed:漕河泾 |
---|
该模块具有一个工厂函数,它返回一个带有public/privateAPI的对象。
六、数组
数组是一种特殊类型的对象,他作为一个有序的值的集合,这些值称为数组元素。在一个数组内,每一个元素都具有一个索引,或称为一个数字位置。在一个数组中可以存储相同类型或不同类型的元素。不要求数组元素全都具有相同的数据类型。于对象和函数类似,数组也可以任意嵌套。
与普通对象一样,可以采用两种方式来创建数组。第一种方法就是使用Array()构造函数:
123 |
var array1 = new Array(); //空数组array1[0] = 1; //在索引为0 的位置添加一个数组元素array1[1] = 'a string'; //在索引为1的位置添加一个字符串 |
---|
更常用的是第二种方式,即使用一个数组字面量来创建数组,它由一对方括号括起来,其中包含了0个或多个一逗号分隔的值。
1 |
var niceArray = [1, 2, 3,]; |
---|
在数组字面量中混合使用对象字面量,可以提供非常强大的结构,他是JavaScript对象表示法JSON的基础。JSON是一种流行的数据交换格式,它可以用于多种语言而不仅仅时JavaScript语言。
1234567 |
var myData = { 'root' : { 'numbers' : [1, 2, 3], 'letters' : ['a', 'b', 'c'], 'mirepoix' : ['tomatoes', 'carrots', 'potatoes'] }} |
---|
数组具有一个length属性,它的值总是等于数组中元素的个数减11。在数组中添加新元素将改变数组length属性。要从数组中移除元素,请使用delete操作符。
123456 |
var list = [1, '2', 'blah', {}];delete list[0];console.log(list);// [undefined, "2", "blah", Object {}]console.log(list.length);// 4 |
---|
删除一个元素并不会改变数组的长度,只是在数组中留下一个undefined值的元素(空元素)。
七、扩展类型
JavaScript支持将方法和其他属性绑定到内置类型。比如字符串、数值和数组类型。于其他任何对象类似,String对象也具有prototype属性。开放人员可以为String对象扩充一些便利的方法。例如,String没有将字符串false或true转换为布尔值的方法。开放人员可以使用下面的代码 为String对象添加该功能:
123456789 |
String.prototype.boolean = function(){ return "true" == this;}var t = 'true'.boolean();var f = 'false'.boolean();console.info(t); //trueconsole.info(f); //false |
---|
显然,每次想为制定类型添加方法时反复输入prototype显得有点累赘》以下提供一段简洁的代码,他使用一个名为method的方法扩充了Function.prototype。下面就是该方法的代码。
1234 |
Function.prototype.method = function(name,func){ this.prototype[name] = func; return this;} |
---|
接下来就可以重写String的boolean方法:
1234 |
String.method('boolean',function(){ return "true" == this;});"true".boolean(); |
---|
对于编写工具方法库并将其包含在项目中,这一技术非常有用。
八、JavaScript最佳实践
下面列出了在进行JavaScript开发时,一些应该注意或应该避免的事项:
- 使用parseInt()函数将字符串转换为整数,但请确保总是指定基数。更好的办法时使用一元操作符(+)将字符串转化为数值。 好的写法:parseInt(“020”,10); //转化为十进制而不是八进制 更好的写法:console.log(+ “010”); //简单而又高效
- 使用等同(===)操作符来比较两个值。避免意料之外的类型转换。
- 在在定义对象字面量时,在最后一个值的结尾不要使用逗号。例如: 12345var o = { "p1" : 1, "p2" : 2, //非常糟糕!}
- eval()函数可以接收一个字符串,并将其视为JavaScript代码执行。应该限制eval()函数的使用,它很容易在代码中引入各种各样严重的安全问题。
- 使用分号作为语句的结尾,当精简代码时这特别有用。
- 应该避免使用全局变量。应该总是使用var关键字来声明变量。将代码包裹在匿名函数中,以避免命名冲突,请使用模块来阻止代码。
- 对函数名使用首字母大写表示该函数将作为new操作符的构造函数,但请不要对其他任何函数的函数名使用首字母大写。
- 不要使用with语句。
- 在循环中创建函数应该谨慎小心。这种方式非常低效。
最佳摘自Cesar Otero,Rob Larsen《jQuery高级编程》