JavaScript中什么是构造函数:http://www.qdfuns.com/notes/23720/d3990bb38177fd0c14789a7c54e614d9.html
深入理解js构造函数:http://www.cnblogs.com/wangyingblog/p/5583825.html
现在来创建一个构造函数用做房子和公寓对象的模板。
function Accommodation(){};
要想把这个函数用作模板来创建对象,需要用到new这个关键字。
var house= new Accommodation();
var apartment = new Accommodation();
用关键字new创建的所有对象都被称为这个函数所示结构的对象实例,创建对象的过程就是这个模板实例化的过程。同一个模板的对象实例之间互无关联,这些对象实例是完全独立的变量,只不过共享同一个模板结构而已。
1.找出函数的构造器
通过上面的方法用模板创建的对象还有一个额外的属性,叫做constructor,这个属性指向创建该对象时使用的JavaScript构造函数。我们可以直接将对象的constructor属性和某个构造函数进行比较。
house.constructor === Accommodation;//true
apartment.constructor === Accommodation;//true
这种比较也可以使用关键字 instanceof 来完成,该关键字的作用就是检查对象是否是某个构造函数的实例。
house instanceof Accommodation;//true
apartment instanceof Accommodation;//true
2.通过原型添加属性和方法
JavaScript中的每个函数,即每个构造器,都有一个叫prototype的属性。
(1)使用prototype关键字和点标记法为构造器添加属性和方法
//定义一个名为Accommodation的构造函数
function Accommodation(){}
//为这个“类”添加属性
Accommodation.prototype.floors=0;
Accommodation.prototype.rooms=0;
//为这个“类”添加方法
Accommodation.prototype.unlock=function(){};
创建“类”的对象实例
var house= new Accommodation();
var apartment = new Accommodation();
(2)通过对象直接量为构造函数添加属性和方法
1 //定义一个名为Accommodation的构造函数 2 3 function Accommodation(){} 4 5 Accommodation.prototype={ 6 7 floors:0, 8 9 lock:function(){} 10 11 };
prototype这个关键字有一个强大的特性是允许在对象实例已经创建之后继续添加属性和方法,而这些新添属性和方法会自动添加到所有对象实例中,不管是已经创建的还是将要创建的————
1 function Accommodation(){} 2 3 Accommodation.prototype={ 4 5 floors:0, 6 7 lock:function(){} 8 9 }; 10 11 var house = new Accommodation(); 12 13 house.prototype.unlock=function(){}; 14 15 house.unlock();
3.通过作用域添加属性和方法
函数体内定义的任何变量或函数,其作用域都限于函数体内,就是说在该函数体以外无法访问这些变量或函数 -- 对这些变量和函数来说,包裹它们的外层函数提供了一个沙箱般的编程环境,或者说一个闭包。
(1)变量作用域
//定义在任何函数之外的变量在全局作用域内,可以再任何位置访问
1 //定义在任何函数之外的变量在全局作用域内,可以再任何位置访问 2 var myLibrary={ 3 myName:"Dennis" 4 }; 5 function doSomething(){ 6 //函数体内定义的变量无法在函数体外访问到 7 var innerVariable=123; 8 myLibrary.myName="Hello"; 9 function doSomethingElse(){ 10 innerVariable=1234; 11 } 12 doSomethingElse(); 13 alert(innerVariable); 14 } 15 doSomething(); 16 //该属性在doSomething函数中已被覆盖 17 alert(myLibrary.myName); 18 19 //在一个函数之外访问其内部定义的变量将导致错误 20 alert(innerVariable);//Error
4.上下文和this关键字
JavaScript中this关键字代表的是一个函数的上下文环境,这个上下文环境在大多数情况下指的是函数运行时封装这个函数的那个对象。当不通过任何对象单独调用一个函数时,上下文环境指的就是全局的window对象。
在一个对象的方法中使用this指向的是这个对象本身,比如下面例子中的house对象。使用this关键字而不是对象的变量名,这么做的好处在于你可以随意改变对象的变量名而不用担心对象中方法的行为受到影响。
this关键字成为了其指向的对象的代名词,所以和对象一样,你可以对这个关键字本身使用点标记法。
(1)使用this关键字和点标记法
1 //在所有函数之外,this表示的是全局的window对象 2 alert(this===window);//true 3 //因为doSomething函数在对象外部被调用,this指向的是浏览器的window对象 4 function doSomething(){ 5 alert(this===window);//true 6 } 7 doSomething(); 8 var house={ 9 floors:2, 10 isLocked:false, 11 lock:function(){ 12 alert(this===house);//true,因为this关键字表示的是包含这个方法的那个对象 13 14 //我们可以把this看作house对象的替身,可以使用点标记法 15 this.isLocked=true; 16 } 17 }; 18 house.lock(); 19 alert(house.isLocked);//true
对象中的嵌套函数其上下文环境是全局的window对象,而非包含它的那个对象,这一点可能出乎你的预料,很多人都会在这里出错。要想绕过这个陷阱,我们可以再this指向包含这个函数的对象时,将this的值保存在一个变量中,然后在用到该对象时,用这个变量来代替。很多开发者都用一个名为that的变量来保存这个对象引用-----
(2)将this关键字的值保存在变量中
1 var apartment={ 2 isLocked:false, 3 lock:function(){ 4 var that=this; 5 //设置isLocked属性 6 this.isLocked=true; 7 function doSomething(){ 8 alert(this===apartment);//false 9 alert(this===window);//true 10 alert(that===apartment);//true 11 //通过that变量来修改apartment对象的isLocked属性 12 that.isLocked=false; 13 } 14 doSomething(); 15 } 16 }; 17 apartment.lock(); 18 alert(apartment.isLocked);//false
在使用new关键字创建对象时,this指向的值和一般情况下又有区别。在这种情况下,this指向的是通过构造函数所创建的那个对象实例。正是因为这个特性,我们得以构造函数中通过this来设置所有对象实例的属性和方法,而非像之前那样使用prototype关键字--
(3)在构造函数中使用this关键字
1 //定义一个新的构造函数来表示一种豪宅 2 function Accommodation(){ 3 //this关键字指向的是通过这个“类”创建的对象实例 4 this.floors=0; 5 this.rooms=0; 6 this.sharedEntrance=false; 7 this.isLocked=false; 8 this.lock=function(){ 9 //函数中的this一般指向包含函数的那个对象,本例中this指向的是创建对象实例, 10 //因为这个函数时通过这个被创建的对象实例来调用的 11 this.isLocked=true; 12 } 13 this.unlock=function(){ 14 this.isLocked=false 15 }; 16 } 17 //通过构造函数来创建对象实例 18 var house =new Accommodation(); 19 var apartment =new Accommodation(); 20 21 //读取和修改属性值,调用方法等操作都和普通对象一样 22 alert(house.floors);//0 23 house.floors=2; 24 apartment.lock();
JavaScript开发者一般会结合使用prototype和this关键字来定义对象实例的属性和方法,其中前者用来定义方法,后者用来定义属性。每次通过构造器创建一个新的对象实例,构造函数都会被执行一次。
之所以组合使用这两个关键字,是为了避免每次初始化一个对象实例时都要执行那些对方法进行初始化的代码。通过prototype关键字来定义的方法只需定义一次,然后就可以为所有通过这个构造函数创建的对象所用,这个使得对象的创建变得更加高效。在原型上定义的方法可以通过this来得到对象实例的引用---
(4)组合使用this和prototype关键字编写高效的构造函数
1 //通过一个构造函数来表示各种类型的住宅 2 function Accommodation(){ 3 //利用this关键字来设置实例对象的属性 4 this.floors=0; 5 this.isLocked=false; 6 } 7 //利用prototype关键字来定义实例对象的方法 8 Accommodation.prototype.lock=function(){ 9 //通过原型定义的方法可以通过this关键字访问在构造函数中定义的属性 10 this.isLocked=true; 11 } 12 Accommodation.prototype.unlock=function(){ 13 //通过原型定义的方法可以通过this关键字访问在构造函数中定义的属性 14 this.isLocked=false; 15 } 16 //实例化一个Accommodation类型的对象 17 var house =new Accommodation(); 18 //执行“lock”方法 19 house.lock(); 20 //检查“isLocked”属性是否正确设置 21 alert(house.isLocked);//true 22
开发者们喜欢在构造函数中使用this关键字来设置属性的另一个原因是可以给构造函数传递参数,这样我们就能再构造函数时通过传递参数来对某些属性进行初始化了。我个人喜欢用this关键字来初始化那些我希望在对象创建时进行初始化的属性,
而用prototype设置其他属性以及对象的方法。这样一来,构造函数就不需要出现任何在对象初始化时不需要被执行的代码了,代码变得更高效。
(5)在构造函数中通过this关键字初始化属性
1 //定义一个带有三个参数的构造函数,这些参数的值用于初始化实例对象的属性 2 function Accommodation(floors,rooms,sharedEntrance){ 3 //当该“类”的一个对象被实例化时,用传进来的参数值初始化该对象的三个属性 4 //逻辑或操作的作用是在传入值为空时指定一个默认值 5 this.floors=floors||0; 6 this.rooms=rooms||0; 7 this.sharedEntrance=sharedEntrance||false; 8 } 9 //不需要在实例化时赋值的属性应该通过prototype来进行设置,因为这样就只需定义和执行一次 10 Accommodation.prototype.isLocked=false; 11 12 Accommodation.prototype.lock=function(){ 13 this.isLocked=true; 14 } 15 16 Accommodation.prototype.unlock =function(){ 17 this.isLocked=false; 18 } 19 20 //实例化“类” 的一个对象,传递三个参数中的两个值用于初始化 21 //参数值是按照构造函数的参数定义顺序进行传递的 22 var house =new Accommodation(2,7); 23 alert(house.floors); 24 alert(house.rooms); 25 26 //参数sharedEntrance的值没有被传入构造函数,所以它的值通过逻辑或操作被设置为默认值false 27 alert(house.sharedEntrance);
当你的“类”规模越来越大时,你会发现为了给对象实例的属性设置初始值,需要给构造函数传递一系列参数值。当参数个数不多时,依次列出每个参数的值是可行的,但是当参数个数超过三个或四个的时候,这么做就变得既困难又容易出错。幸运的是,对象直接量为我们提供了一种解决方案。我们可以向构造函数传递一个对象直接量作为唯一参数,这个对象直接量包含了进行属性设置所需的所有初始值。
这样一来我们不但消除了多个函数参数带来的不便,同时也使代码变得更加清晰易懂,因为对象直接量是以名对值对的形式出现的,这比没有名字的函数参数值更直观。当需要给一个函数传递两个或者三个以上参数值时,我会选择这种方式
(6)用对象直接量作为构造函数的参数
1 function Accommodation(defaults) { 2 //如果没有传入值,默认为空的对象直接量 3 defaults = defaults || {}; 4 //如果default对象含有某个属性,就将实例对象中同名属性的值设为default提供的值,否则设为默认值 5 this.floors = defaults.floors || 0; 6 this.rooms = defaults.rooms || 0; 7 this.sharedEntrance = defaults.sharedEntrance || false; 8 } 9 10 Accommodation.prototype.isLocked = false; 11 12 Accommodation.prototype.lock = function() { 13 this.isLocked = true; 14 } 15 Accommodation.prototype.unlock = function() { 16 this.isLocked = false; 17 } 18 //实例化两个Accommodtion“类”的对象,通过对象直接量传递命名的参数 19 var house =new Accommodation({ 20 floors:2, 21 rooms:7 22 }); 23 var apartment=new Accommodation({ 24 floors:1, 25 rooms:4, 26 sharedEntrance:true 27 });
5.方法的链式调用
我们已经在对象实例上定义过方法了,这些方法的调用方式和普通函数并无不同,只需在方法名后面加上一对括号即可。要想连续调用对象实例的多个方法,我们现在必须依次逐个调用,每个调用独占一行,而且每次调用都必须写出对象名。
house.lock();
house.alarm();
house.unlock();
只要对每个方法做一个小改变,我们就能对其进行链式调用了,就是说一个方法调用可以紧跟在另外一个后面。如果你用过jquery,你或许见过类似的语法。
house.lock().alarm().unlock();
要实现链式调用,只需在“类”中的每个方法最后通过this关键字返回对象实例的引用即可。
(1)通过this关键字来实现方法的链式调用
1 function Accommodation() {} 2 3 Accommodation.prototype.isLocked = false; 4 Accommodation.prototype.lock = function() { 5 this.isLocked = true; 6 7 //通过返回上下文,我们实际上返回了调用这个函数的那个对象实例。因为这个对象包含了所有的方法, 8 //所以我们可以在本方法调用结束后马上调用其他方法 9 return this; 10 } 11 12 Accommodation.prototype.unlock=function(){ 13 this.isLocked=false; 14 return this; 15 } 16 17 Accommodation.prototype.alarm = function(){ 18 alert("Sounding alarm"); 19 return this; 20 } 21 //创建一个新实例 22 var house =new Accommodation(); 23 24 //因为每个方法都返回其执行上下文,(在本例中即包含这些方法的对象实例) 25 //我们得以将这些方法调用一个接一个地链接在一起 26 house.lock().alarm().unlock();
6.继承
传统编程语言的一项关键功能就是可以创建一些新的类,来继承或者扩展某个父类的属性和方法,这些新的类和该父类都有某种类似的逻辑关联。这些新的类被称为子类。JavaScript中也可以实现这种继承,不过实现方式和传统语言不尽相同。在JavaScript中被称为原型继承,通过JavaScript对象的原型链来实现。
(1)通过原型继承创建一个子类
1 //定义一个有两个方法的“类” 2 function Accommodation() {} 3 Accommodation.prototype.lock = function() {}; 4 Accommodation.prototype.unlock = function() {}; 5 6 //定义一个构造函数,它将成为我们的子类 7 function House(defaults) { 8 defaults = defaults || {}; 9 //将本“类”所有实例的floors属性初始化为“2” 10 this.floors = 2; 11 //如果构造函数的对象直接量参数包含“rooms”属性,则使用传进来的值,否则默认设为7个房间 12 this.rooms = defaults.rooms || 7; 13 } 14 15 //将House“类”的原型设为Accommodation“类”的一个实例 16 //使用关键字new来调用Accommodation的构造函数,这样就能创建并返回一个包含其所有属性和方法的对象 17 //这个对象被传递给House“类”的原型,这样House“类”就得以继承Accommodation的所有内容 18 House.prototype = new Accommodation(); 19 20 //对象实例的constructor属性指向创建该对象的那个构造函数。然而,由于House继承了Accommodation的所有内容, 21 //constructor的值也被复制了,所以我们现在需要重设constructor的值,使其指向新的子类,如果没有这一步, 22 //通过House 类 创建的对象就会报告说它们是通过 Accommodation 类 创建的 23 24 House.prototype.constructor = House; 25 //创建House的一个实例,继承Accommodation的属性和方法 26 var myHouse = new House(); 27 28 //传入rooms的值从而在对象实例化时对rooms进行赋值 29 var myNeighborsHouse = new House({ 30 rooms: 8 31 }); 32 alert(myHouse.rooms); //7(House构造函数中的默认值) 33 alert(myNeighborsHouse.rooms); //8 34 35 //Accommodation的方法对House的对象也可用 36 myHouse.lock(); 37 myNeighborsHouse.unlock(); 38 39 //由于之前我们修改了constructor的值,所以由House创建的对象能如实报告这一点 40 alert(myHouse.constructor === House); //true 41 alert(myHouse.constructor === Accommodation); //true 42 43 44 //instanceof关键字会沿原型链进行查询,所以也可用于检查一个对象实例是不是在某个父类 45 46 // alert(myNeighborsHouse instances House); 47 // alert(myNeighborsHouse instances Accommodation);
我们之前曾使用prototype关键字来传给构造函数添加过方法和属性,所有通过这个构造函数创建的对象都能访问到这些方法和属性。
如果我们试图访问对象的某个方法或属性,但构造函数的原型并没有这个方法或属性,此时JavaScript并不会马上抛出异常,而是会首先检查当前构造函数的所有父构造函数,
看这些父构造函数是否包含该方法或属性。
*注意:当创建一个子类时,要确保将子类的constructor属性指向子类本身的构造函数,因为从原型中直接复制过来的constructor值默认指向的是父类的构造函数。
我们看到instanceof关键字会沿原型链进行查询,这意味着通过instanceof关键字可以判断一个对象实例是不是由某个构造器,或者该构造器的某一个父构造器创建的。
JavaScript中的原型链可以一直向上追溯到内建的Object的类型,因为JavaScript的所有变量最终都是继承自该类型。
alert(myHouse instanceof House);//true
alert(myHouse instanceof Accommodation);//true 因为House继承自Accommodation
alert(myHouse instanceof Object);//true,因为所有对象都继承自JavaScript的内置对象Object
●封装
当通过继承对已有的类进行改变或特殊化时,父“类”的所有属性和方法对子类都是可用的。在子类中不需要额外生命或者定义任何东西就能够使用父类的属性和方法。这种特性被称为封装;子类只需要定义那些在父类基础上新增的属性和方法即可。
●多态
在构造一个新的子类来继承并扩展一个“类”的时候,你可能需要将某个方法替换为一个同名的新方法,新方法和原方法功能类似,但对子类做了针对性的改变。这就是多态,在JavaScript中实现多态很简单,只需重写一个函数并给它一个和原方法相同的方法名即可。
(2)多态
1 // 定义父"类" Accommodation 2 function Accommodation(){ 3 this.isLocked=false; 4 this.isAlarmed=false; 5 } 6 // 为所有的Accommodation添加方法,执行一些常见动作 7 Accommodation.prototype.lock=function(){ 8 this.isLocked=true; 9 } 10 Accommodation.prototype.unlock=function(){ 11 this.isLocked=false; 12 } 13 Accommodation.prototype.alarm=function(){ 14 this.isAlarmed=true; 15 alert("Alarm activated"); 16 } 17 Accommodation.prototype.deactivateAlarm=function(){ 18 this.isAlarmed=false; 19 alert("Alarm deactivated"); 20 } 21 //为House定义一个子类 22 function House(){} 23 24 //继承自Accommodation 25 House.prototype=new Accommodation(); 26 //针对House"类"重定义lock方法,即多态 27 28 House.prototype.lock=function(){ 29 //执行父类Accommodation的lock方法。可以通过“类”的原型直接访问这个方法。我们通过函数的call 30 //方法对上下文传递给该方法,从而确保在lock方法中任何对this的引用都指向当前这个House的对象实例 31 Accommodation.prototype.lock.call(this); 32 alert(this.isLocked);//true 说明上面对lock方法的调用是正确的 33 //调用继承自Accommodation的alarm方法 34 // this.alarm(); 35 } 36 //以同样的方式重定义unlock方法 37 House.prototype.unlock=function(){ 38 Accommodation.prototype.unlock.call(this); 39 this.deactivateAlarm(); 40 }
注意观察我们是如何在重写的新方法中访问正在进行多态化的原方法的,我们只需要通过父“类”定义中的prototype属性直接访问这个方法就可以了。因为该方法中包含对其上下文的引用,即this,我们需要保证this指向的是通过子类创建的对象实例所代表的上下文。
我们通过调用call方法来实现这一点,该方法对JavaScript中所有函数都可用,其作用就是将一个函数的上下文应用到另外一个函数身上。
●JavaScript函数的apply和call方法
我们之前讨论上下文的问题;JavaScript中的this关键字指向的是包含当前方法的那个对象,而在面向对象的JavaScript编程中,this指向的就是由某个“类”创建的一个对象实例。
当调用其他对象而非代表当前上下文的对象的方法时,该方法中所有对this的引用都指向此方法所在的对象,而非当前代码的执行上下文——就是说在调用这个方法时,你切换到了另外一个上下文中。在调用其他对象的方法时,我们需要一种机制来保持this原来的值。
为实现这一点,JavaScript提供了相似的两个方法——apply和call,这两个方法可以用于所有函数。
前面讨论多态时,我们看到call这个方法可以用于在子类中调用父类的方法。在那个例子中,我们将指向子类对象实例的上下文直接传给了一个通过父类原型来调用的方法。这个一来该方法中所有的this就会指向那个子类的对象实例,通过这种方式,我们将一个位置的上下文应用到了另一个位置上。如果需要向函数传递函数,可以将这些函数在上下文后面列出。call和apply的区别在于,使用apply时,所有的参数都应放在一个单独数组参数中,而在使用call的时候,参数应该一次列出把并用逗号隔开。
(3)在一个函数上使用apply和call方法
1 // 定义一个简单的"类" 2 function Accommodation(){ 3 this.isAlarmed=false; 4 } 5 //创建一个对象,其方法可以被代码中的其他对象所使用一该对象也被称为一个“mixin”(混入) 6 var AlarmSystem={ 7 arm:function(message){ 8 this.isAlarmed=true; 9 alert(message); 10 }, 11 disarm:function(message){ 12 this.isAlarmed=false; 13 alert(message); 14 } 15 }; 16 var myHouse =new Accommodation(); 17 //通过call,将对象实例上下文传入arm函数 18 AlarmSystem.arm.call(myHouse,"Alarm activated"); 19 //arm函数中this的值指向通过call传入的对象实例,所有myHouse对象的isAlarmed属性被改变了 20 alert(myHouse.isAlarmed);//true 21 //通过apply也能达到同样的效果,只不过参数是通过数组来进行传递的 22 AlarmSystem.disarm.apply(myHouse,["Alarm activated"]); 23 alert(myHouse.isAlarmed);//false
◆arguments
执行一个函数时,我们传入函数的所有参数都成为可以在该函数中使用的变量。除此之外,JavaScript还有一个可以在函数中使用的保留关键字arguments,我们可以把arguments看作一个数组arguments,其中依次包含了传入该函数的各个参数。
设想你有一个函数,你想用这个函数来对所有作为参数传入的数字进行加和。如果你不想指定具体有几个参数,可以将参数列表清空,转而使用arguments这个伪数组来获取参数,如代码(4)所示。之所以称arguments为伪数组时因为虽然可以通过for循环对其进行遍历,但它不具备标准数组的方法,例如排序方法,所有在使用arguments的时候不应该用到这些数组方法。
(4)arguments对象
1 // 创建一个函数,对传入函数的所有参数进行加和 2 var add=function(){ 3 //创建一个变量用来保存总和 4 var total=0; 5 //arguments这个伪数组包含所有传入该函数的参数,遍历每个参数并将加入总和中 6 7 for(var index=0,length=arguments.length;index<length;index++){ 8 total=total+ arguments[index]; 9 } 10 return total; 11 } 12 //用不同数量的参数对函数进行测试 13 alert(add(1,1));//2 14 alert(add(1,2,3));//6 15 alert(add(12,123,3,1234));//1372
当使用函数的apply方法时,arguments伪数组就体现其价值了。因为apply方法将所有参数放入了一个数组内进行传递,当被调用的函数与当前函数拥有相同参数时,我们只需要传入arguments,就把当前函数的所有参数传递给了另一个函数。这对对象的继承和多态很有用,因为我们可以通过arguments,将子类方法的所有参数直接传递给父类中的相似方法。
(5)在子类中使用arguments伪数组
1 // 定义父类Accommodation 2 function Accommodation(){ 3 this.isAlarmed=false; 4 } 5 Accommodation.prototype.alarm=function(note,time){ 6 var message="Alarm activted"+time+"with the note:"+note; 7 this.isAlarmed=true; 8 alert(message); 9 } 10 //定义子类House 11 function House(){ 12 this.isLocked=false; 13 } 14 //继承Accommodation 15 House.prototype=new Accommodation(); 16 //为House子类重定义alarm方法,方法定义中没有列出参数,因为我们会把所有参数直接传递给父类中的同名方法 17 House.prototype.alarm=function(){ 18 this.isLocked=true; 19 //调用父类Accommodation的alarm方法,将当前函数的所有参数直接传递给父类方法以无需将这些参数一一列出 20 Accommodation.prototype.alarm.apply(this,arguments); 21 } 22 //创建子类的一个对象实例并进行测试 23 var myHouse=new House(); 24 myHouse.alarm("Activating",new Date());//弹出警告消息 25 26 alert(myHouse.isLocked);//true
●公有,私有,以及受保护的属性和方法
在之前的例子中,我们创建了很多“类”模板,这些类模板将属性和方法绑定到构造函数的prototype属性上,或者通过this关键字,将其绑定到这些“类”创建的对象实例所代表的上下文中。通过这种方法创建的属性和方法都是公有的,也就是说这些属性和方法对一个“类”的所有对象实例都可用,从而代码中所有能够访问这些对象实例的地方都能使用这些属性和方法。
然而在某些情况下,你可能希望能够限制某些属性和方法的暴露程度,使它们不能直接通过对象实例本身被随意访问,修改或调用。许多传统的编程语言可以将属性和方法定义为公有,私有,或者受保护的,以此来限制对这些属性和方法的访问。私有变量或方法在类定义之外不能进行读写;受保护的变量不能被直接访问,但可以通过一个包装方法对其读写。这些包装方法通常被称为getter和setter,你可以通过这些方法读取或者设置对象实例的属性值。如果你只定义了一个getter函数,那么变量在类定义之外就变为了只读的了。在JavaScript中并没有具体的语法来定义私有或者受保护的变量或方法,不过我们可以对声明“类”的方法做一些改变,从而限制对属性和变量的访问。
在构造函数中通过var定义的变量其作用域局限于该构造函数内——在prototype上定义的方法无法访问这个变量,因为这些方法有其自己的作用域。要想通过公有的方法来访问私有的变量,需要创建一个同时包含两个作用域的心作用域。为此,我们可以创建一个自我执行的函数,被称为闭包。该函数完全包含了“类”的定义时,包括所有私有变量以及原型方法,如代码(6)
JavaScript有一个非强制性的但很有用的编程惯例,就是对所有私有变量或函数名加一个下划线(_)作为前缀,以标识它们是私有的。这有助于你以及项目组的其他开发人员更好地理解每个“类”的作者的意图。
(6)公有、私有以及受保护的属性和方法
1 //我们将“类”的定义包在一个自我执行的函数里,这个函数返回我们所创建的“类”并将其保存在一个变量中以便在后面的代码中使用 2 3 var Accommodation =(function(){ 4 //定义"类"的构造函数,因为处在一个新的函数内,我们也切换到了一个新的作用域中,所以可以使用与保存函数返回值得那个变量相同的名字 5 function Accommodation(){} 6 //此处定义所有变量都是“私有的”,这些变量在当前作用域之外不可用,可以通过给变量名添加下划线前缀来标识这一点 7 var _isLocked=false, 8 _isAlarmed=false, 9 _alarmMessage="Alarm activated"; 10 11 //仅在当前作用域中定义的函数(而未在构造函数的原型上定义)也都是“私有的” 12 function _alarm(){ 13 _isAlarmed=true; 14 alert(_alarmMessage); 15 } 16 function _disableAlarm(){ 17 _isAlarmed=false; 18 } 19 //所有定义在原型上的方法都是“公有的”,当我们在此处创建的“类”在闭包结束处被返回后,就可以再当前作用域之外访问这些方法了 20 Accommodation.prototype.lock=function(){ 21 _isLocked=true; 22 _alarm(); 23 24 } 25 Accommodation.prototype.unlock=function(){ 26 _isLocked=false; 27 _disableAlarm(); 28 } 29 //定义一个getter函数来对私有变量_isLocked的值进行只读访问————相当于把变量定义为了“受保护的” 30 Accommodation.prototype.getIsLocked=function(){ 31 return _isLocked; 32 } 33 //定义一个setter函数来对私有变量_alarmMessage进行只写访问——相当于定义为了“受保护的” 34 Accommodation.prototype.setAlarmMessage=function(message){ 35 _alarmMessage=message; 36 } 37 //返回在这个作用域中创建的“类”,使之在外层作用域中即后面代码的所有位置都可用。只有公有的属性和方法是可用的 38 39 return Accommodation; 40 }()); 41 42 43 //创建一个对象实例 44 var house =new Accommodation(); 45 house.lock();//弹出警告消息 “Alarm activated” 46 // house._alarm();//错误! _alarm函数从未被公开暴露,所以无法直接通过“类”的对象实例进行访问 47 48 alert(house._isLocked);//undefined (_isLocked是私有的,在闭包外都访问不到) 49 50 house.getIsLocked();//true (返回__isLocked的值,但是不允许对其进行直接访问,所以该变量是只读的) 51 52 house.setAlarmMessage("Hello world"); 53 house.lock();//弹出警告消息 “Hello world”
一般情况下,我们应该将所有变量和函数都定义为私有的,除非明确需要将某些变量或方法公开暴露给外部。即使需要公开暴露,也应先考虑使用getter以及setter方法来访问变量,
这么做的好处是可以显示他人对称的“类”所能实施的操作,使其只能通过“类”提供的功能完成其需求,这样有助于减少“类”的使用者代码出错的机会。
●简化继承
我们可以通过定义一个基类来简化对象的创建和继承,其他所有类的创建都可以通过这个基类来完成。我们在这个基类上定义一个方法使之可以通过该方法来继承其自身,并允许子类通过一个属性来访问父类,这么做可以让子类的创建和使用变得简单很多。我们还可以将那些用来在原型上设置方法的代码包装在一个单独的对象直接量里,甚至将构造函数也包含在该直接量中,这样一来“类”的创建就变得轻而易举了。如代码1-19
代码清单1-19 一个用于简化其他“类”创建的基“类”
代码清单1-20 “类”创建器的实际使用
1.2 代码规范和命名
前面详细介绍了如何在JavaScript中进行面向对象编程,现在来看一下我们遵循什么样的代码规范,以及如何对变量和函数的命名,以便让名字本身传达含义,并确保大型团队里的所有成员都能保持相似的编程风格。
在JavaScript中,我们通过关键字var加变量名以及一个可选的变量初值来定义变量,这样就可以将变量保存在内存中以便在代码中对其复用。同样的函数也可以被保存在内存中并重复执行,我们通过关键字function加函数名来定义函数。
你可以随便对变量和函数进行命名,前提是这些名字遵守下面规则.
名字的开头必须时下面这些字符之一
□ a-z、A-Z中的一个字母
□ 下划线_
□ $符号
第一个字符之后,除了以上字符之外还可以使用0-9的数字
以下例子都是合法的JavaScript变量名和函数名
1 var a; 2 3 function A() {}; 4 var a1; 5 6 function _() {}; 7 var _a; 8 9 function $() {}; 10 var $_$;
这些是都是语言本身的固定规则,但是作为开发人员为了便于开发和维护,我们希望代码是易读易懂的,所以除了这些固定的规则之外,我们会规定一些额外的命名规范,这些规范被很多开发人员和编程语言所采纳。要切实遵循这些命名规范,你就能更容易清楚自己的代码中每个变量时干什么的,当然也更好的理解别人的代码。
1.2.1 规则1:使用描述性的名字
这条规则是最为重要的,所以我把它放在了第一位。变量名代表了变量中所保存的数据,所以我们需要给变量起一个最能确切描述其用途的名字,使代码更易读易懂。如下所示:
var greeting = "Hello world";
1.2.2 规则2:以小写字母开头
变量名开头使用小写字母,然后在后面的部分也尽可能多地使用小写字母。这么做时为了避免与JavaScript的内建类型和对象发生混淆,这些内建类型和对象都以大写字母开头,例如String,Object,Math.
var age = 35;
不过在我的代码中这条规则有几个体例。首先是使用jQuery的时候,我会将查找到的DOM元素保存在变量中以免对其进行重复加载。在这种情况下,我会在这些变量名前面加一个$符号作为前缀,以便将这些表示DOM结点的变量和代码中的其他变量加以区别。$前缀后面的部分则遵循和其他变量一样的规则。例如:
var $body = $(document.body);
第二个特征时如何对那些作为构造器来使用的函数进行命名。稍后会对构造器进行更详细的考察,简单说JavaScript中的内建类型都是构造器,例如String,Number,Boolean等。所有用new关键字来调用函数都是构造器。这些构造器名字的首字母都应该是大写,例如:
function MyTime() {};
var myTime = new MyTime();
第三个特例,我们在之前章节也提到了,就是构造函数内的私有变量和函数应该在名字前加一个下划线(_)作为前缀,以区别于那些公有的变量和方法
1.2.3 规则3:使用骆驼命名(又称驼峰命名法)来分割单词
规则1要求我们使用描述性名字,但如果我们只使用小写字母的话,当名字包含多个单词时就会难以阅读,例如:
var myemailaddress = "123@qq.com";
驼峰命名:
var myEmailAddress = "123@qq.com";
1.2.4 规则4:全局常量使用全大写的名字
这条规则关心的是那些经常在计算中用到的所谓的幻数(magic number),例如在计算日期和时间时经常用到的一些零散数组,或者在一些计算中用到的真是世界中的常数值例如Pi,不少开发人员习惯在用到这些数字的时候直接使用它们,这虽然可行但是会导致混乱。下面例子演示了这一点。
var today = new Date();
todayInDays = today * 1000 * 60 * 60 * 24;
乍看上去,这段代码只是一系列数字而已,这些数字时干什么的并不清楚。通过定义变量来对这些数字进行命名之后,读懂这段代码就容易多了。我们用全大写的字符来标明这些变量的类型是固定数值,也就是常量————虽然常量在其他很多编程语言中是一个特性,但JavaScript并不具备这一点。常量名字中的单词通过下划线(_)来进行分割。如代码所示:
1 var today = new Date(), 2 MILLISECS_IN_1_SEC = 1000, 3 SECS_IN_1_MIN = 60, 4 MINS_IN_1HOUR = 60; 5 todayInDays = today * MILLISECS_IN_1_SEC * SECS_IN_1_MIN * MINS_IN_1HOUR;
这样的确增加了代码量,但我认为为了让代码具备更好的可读性这是值得的。这些常量可以在代码中反复重用,代码中的各种计算也因此变得更容易理解。
1.2.5 规则5:集中在一个语句中声明函数体的所有变量并将其置于函数体顶部
JavaScript中可以使用关键字var用简写的方式在一个语句中同时定义多个变量,具体方式是用逗号隔开每个变量声明。确保在使用变量之前对其进行声明时明智的,这样做可以避免执行代码时出错。因此我建议你将用到的所有变量在函数体顶部和JavaScript文件顶部进行声明,并将这些生命合并到一个语句中。注意你不需要立即对这些变量进行初始化,初始化可以稍后进行,这里需要做的是一次性地提前所有变量进行声明。用逗号和换行符分割各个变量,然后为了保证可读性,我们将变量名首字母对其,如下所示:
1 var myString = "hi", 2 allStrongTags = "/<strong>(.*?)</strong>/g", 3 tagContents = "&1", 4 outputString; 5 outputString = myString.replace(allStrongTags, tagContents);
变量和函数名提升(Hoisting)
在其他很多编程语言中,变量可以在任意一个代码块中进行定义,例如一个for循环或者任何一个通常用一对花括号来标识的代码块,然后该变量的作用域就限定在那个代码块中了。而在
JavaScript中,我们知道作用域被限定为只在函数级起作用,这就使一些习惯于用其他语言进行开发的开发人员可能在这个问题上出错,如代码所示:
代码清单1-21 代码块和作用域
1 function myFunction() { 2 var myArray = ["January", "February", "Maich", "April", "May"]; 3 myArrayLength = myArray.length; 4 counter = 0; 5 for(var index = 0; index < myArrayLength; index++) { 6 //每循环一次counter的值加1 7 counter = index + 1; 8 } 9 //这些变量的值应该是符合期望的 10 alert(counter); // 5 11 alert(index); //5(因为在判定循环条件之前循环递进了一步) 12 alert(myArrayLength); //5 13 if(myArrayLength > 0) { 14 //在很多语言中,在这样一个代码块中定义的变量其作用域也局限于该代码中 15 //但JavaScript不是这样的,所以在代码块中定义变量时要注意这一点 16 var counter, 17 index = 0, 18 myArrayLength, 19 counter = 0; 20 } 21 //即使代码块中使用了var来进行定义,counter和index的值还是在if语句中被改变了 22 alert(counter); //0 23 alert(index); //0 24 //注意虽然在代码块中用var对myArrayLength进行了重定义,但其值并未发生改变 25 //这是因为JavaScript在函数执行之前就将变量名“提升”到了函数顶部 26 alert(myArrayLength); //5 27 28 } 29 myFunction();
JavaScript有一种特性叫作"提升",就是说变量和函数声明会在内部被提升到其定义所在函数体的顶部。这意味着任何一个变量名的定义都在其所在的作用域(通常是一个函数)的顶部就可以了,
虽然并不一定是初始值。为了减少麻烦,最好在函数开头就列出函数中所要用到的所有变量,不管是否进行初始化,因为这么做是对JavaScript内部进行的变量提升行为是最好的模仿,能够减少由于使用未知变量定义和变量值所引起的困惑,如代码所示:
在函数开头处对函数中用到的所有变量进行定义
代码清单1-22 在函数开头处对函数中用到的所有变量进行定义
2 function myFunction(){ 3 //为了防止变量提升引起的错误,我们在函数顶部对其所有变量进行定义 4 5 var myArray=['January','February','March','April','May']; 6 myArrayLength=myArray.length, 7 counter=0, 8 index=0; 9 //for循环的第一部分通常是用来定义循环变量的,现在由于我们将定义都放在函数体顶部了,所以可以省略这一部分 10 11 for(;index<myArrayLength;index++){ 12 counter=index+1; 13 } 14 //变量的值应该不出所料 15 alert(counter);//5 16 alert(index);//5 17 alert(myArrayLength);//5 18 19 } 20 myFunction();
这一点对函数也同样适用,函数名也会被提升,所以一个函数名在其当前作用域的任何位置都可用,即使在定义函数之前,如代码所示:
代码清单1-23 函数的提升
function myFunction(){ //因为JavaScript的“提升”,在函数定义之前执行一个函数是可行的 doSomething(); function doSomething(){ alert("Doing something"); } } myFunction();
1.3 ECMAScript 5
1.3.1 JSON数据格式解析
1 var obj = { 2 "success": false, 3 error_message: "error" 4 }; 5 JSON.parse(obj); //返回对象直接量 6 JSON.stringify(obj); //返回包含JSON格式的数据的字符串
1.3.2 严格模式
在包含该字符串的文件或函数中,所有代码都必须符合更严格的语言规则,这些规则有助于避免潜在的错误和陷阱。在严格模式下,如果你使用了未定义的变量,JavaScript会报错。
同样,如果你使用了包含两个同名属性和对象的直接量(我自己也曾因这一点而吃亏),JavaScript也会发出警告;又比如detele关键字本应该用在对象的属性上,如果你将其用在变量或函数身上,
JavaScript也会提示。严格模式还禁止使用eval来执行包含JavaScript代码的字符串,因为这么做会引起安全问题,其他代码可能会将控制权从你所写的代码中夺走。
因为执行严格模式只需要添加一个简单的字符串,所以老的浏览器不会因此而出错,当老浏览器执行你的代码时遇到这个语句,它们会将其作为一个字符串来执行,因为这个字符串没有被赋给任何变量,
所以实际上就被忽略了。比如代码1-24的两个函数,其中一个应用了普通模式,而另一个应用了严格模式。我已经开始在我所有的代码中使用这个新的严格模式了,这么做是为了确保代码质量足够高,我也建议你来使用。
代码清单1-24 演示ECMAScript5的严格模式
1 //定义一个函数 2 function myFunction(){ 3 //使用一个之前未定义的变量将隐式地将其创建为全局变量 4 counter=1; 5 //用eval()来执行包含JavaScript代码的字符串不会报错 6 eval("alert(counter)");// 7 //delete关键字的作用是移除对象的属性和方法,但是将其用在变量身上不会报错 8 delete counter; 9 } 10 myFunction();
1 function myFunction(){ 2 "use strict" 3 //执行这一条语句时会报错,因为counter变量未定义 4 counter=1; 5 //eval因为安全问题应该避免使用,所以这里会报错 6 eval("alert(counter)");// 7 //delete关键字只应该被用于移除对象直接量的属性和方法,所以这里会报错 8 delete counter; 9 } 10 myFunction();
1.3.3 函数绑定
我们之前介绍过可以用于JavaScript中所有函数的apply和call方法。ECMAScript5中增加了一个新的方法bind,这个方法不会直接执行函数,而是会返回一个新的函数。这个新函数的上下文设定为调用bind方法时,作为第一个参数传入的任意对象.
你在自己的代码中可能已经遇到
代码清单1-15 函数的bind方法
1 var header=document.getElementById("header"); 2 eventHandlers={ 3 //定义一个包含三个方法的对象 4 onClick:function(){ 5 //如果onClick函数被调用时的执行上下文是错误的,以下两个调用将失败 6 this.onMouseDown(); 7 this.onMouseUp(); 8 }, 9 onMouseDown:function(){ 10 mouseState="down"; 11 }, 12 onMouseUp:function(){ 13 mouseState="up"; 14 } 15 }; 16 //强制eventHandlers.onClick使用正确的上下文,为此我们通过bind方法返回一个新的函数 17 //该函数就根据我们的要求绑定了相应的上下文 18 header.addEventListener("click",eventHandlers.onClick.bind(eventHandlers),false); 19 //将<header>元素添加到页面 20 document.body.appendChild(header);
1.3.4 数组方法
大多数专业的javascript开发者每天都会用到数组,不管是用来进行循环,排序还是组织数据。ECMAScript5为javascript开发者的工具箱添加了一些大家期待已久的新方法,我么可以利用这写新方法来处理这些数据结构。
首先,也许也是最重要的,是我们需要一种方法,以便更容易地判断一个变量是否包含数组数据。这也许听起来有点奇怪,但别忘了要判断一个变量是否包含数组数据,我们需要首先将其转成对象,然后再把它的值读取到一个字符串中————简直是疯 了!而在ECMAScript中,要检查变量是否包含数组数据,我们只需要调用Array.isArray方法即可,如代码1-26所示
代码清单1-26 ECMAScript5中的isArray方法
1 var months=["January","February","March","April","May"], 2 items={ 3 "0":"January" 4 }; 5 alert(Array.isArray(months));//true 6 alert(Array.isArray(items));//false
在之前版本中,要遍历一个数组,我们需要创建一个for循环,然后对某种类型的索引计数器进行迭代。EMAScript5引入了一个新的forEach方法,通过这个方法可以让遍历简单很多;只需要给方法传递一个函数,它就会对数组中的每个元素调用一次该函数,同时会将当前遍历的值、数组索引以及整个数组的引用传递给这个函数,如代码清单1-27所示
代码清单1-27 ECMAScript5的forEach方法
1 var months=["January","February","March","April","May"]; 2 //通过forEach方法我们可以遍历数组中的每一个元素,同时每次执行一个函数 3 months.forEach(function(value,index,fullArray){ 4 alert(value+"is months number"+(index+1)+"of"+fullArray.length); 5 });
如果你曾有过这样的需求,需要判断数组中的每个元素是否满足一个由某函数所定义的特定条件,那你可苦苦等待ECMAScript5中的新every方法太久了。与之类似的还有一个some方法,该方法会在数组中至少有一个元素满足给定条件时返回true。every方法和some方法的参数都和forEach方法相同,如代码清单1-28所示。
代码清单1-28 ECMAScript5中的every和some方法
1 var months=["January","February","March","April","May"], 2 //every方法遍历数组中的每个元素,将每个元素和一个条件进行比较 3 //如果数组中的每个元素都满足这个条件,则every方法返回true,否则返回false 4 everyItemContaiinsR=months.every(function(value,index,fullArray){ 5 //根据当前遍历到的元素是否满足你指定的条件来返回true或者false,这里的条件就是看value是否包含字母r 6 return value.indexOf("r")>=0; 7 }), 8 //some 方法便利数组中的每个元素并将其和某个元素对比 9 //如果数组中的任意一个元素满足该条件,则some方法返回true,否则返回false 10 someItemContainsR=months.some(function(value,index,fullArrary){ 11 return value.indexOf("r")>=0; 12 }); 13 14 //不是所有元素都包含字母r 15 alert(everyItemContaiinsR);//false 16 //但有些元素包含 17 alert(someItemContainsR);//true
新的map方法可以让你根据一个已有的数组创建一个新的数组,它在创建新数组的过程中,每生成一个元素时都执行一个函数,如代码清单1-29所示
代码清单1-29 ECMAScript5中的map方法
1 var daysOfTheWeek=["Mondy","Tuesday","Wednesday"]; 2 //map方法通过遍历一个已有的数组来生成一个全新的数组,它会在遍历每个元素时执行一个函数, 3 //并通过该函数来生成新数组中的对应元素 4 daysFirstLetters=daysOfTheWeek.map(function(value,index,fullArray){ 5 return value="starts with"+value.charAt(0); 6 }); 7 alert(daysFirstLetters.join(","));//starts withM,starts withT,starts withW
ECMAScript5中新的filter数组方法和map一样也会创建一个新的数组,不过新数组只包含满足某个特定条件的那些元素,如代码清单1-30所示
代码清单1-30 ECMAScript5中的filter方法
1 var months=["January","February","March","April","May"], 2 //filer方法根据原有的数组创建一个削减版的数组,该数组只包含那些满足某个特定条件的元素 3 monthsContainingR=months.filter(function(value,index,fullArray){ 4 //返回true或false来指示当前数组元素是否应该包含在过滤后的数组中,这里的判断条件是看元素值是否包含字幕r 5 return value.indexOf("r")>=0; 6 }); 7 //唯一不包含字母r的月份是五月(May) 8 alert(monthsContainingR.join(","));//January,February,March,April
1.3.5 对象方法
ECMAScript5中对Object类型进行了很多扩展,这个javascript带来了很多其他编程语言中的功能。首先,如果使用严格模式,ECMAScript5引入了一个新功能,就是可以将一个对象进行锁定,这样在你代码中的某个点之后,就不能向该对象添加新的属性或者方法了。实现这一功能的新方法是object.preventExtensions,以及一个与之相关的Object.isExtensible方法,通过该方法你可以判断是否可以对一个对象进行扩展,如代码清单1-31所示
代码清单1-31 ECMAScript5中的对象方法
1 //定义一个包含两个属性的简单对象 2 var personalDetails={ 3 name:"Den Odell", 4 email:"den.odell@me.com" 5 }; 6 alert(Object.isExtensible(personalDetails));//true ,因为默认对象都是可以扩展的 7 //阻止对personalDetails对象进行扩展 8 Object.preventExtensions(personalDetails); 9 10 alert(Object.isExtensible(personalDetails));//false ,因为该对象被锁定了 11 12 //尝试为personalDetails对象添加一个新的属性 13 personalDetails.age=35;//如果使用严格模式的话会抛出错误,因为对象是锁定的
如果你想进一步锁定一个对象,使其已有的属性值也无法改变,可以使用ECMAScript5中新的Object.freeze方法,如代码清单1-32所示。
代码清单1-32 ECMAScript5中对象的freeze方法
1 //定义一个有两个属性的简单对象 2 var personalDetails={ 3 name:"Den Odell", 4 email:"den.odell@me.com" 5 }; 6 //锁定该对象,使其已有的属性也无法改变 7 Object.freeze(personalDetails); 8 alert(Object.isFrozen(personalDetails));//true 9 personalDetails.name="wing";//如果在严格模式下会报错,因为对象一旦被"冻住"就无法再改变其属性值
对象中的每个属性现在都有一系列的选项值,它们决定这个属性将如何在之后的代码中被使用。这些选项值包含在一个属性描述符中,该属性描述符是一个有四个属性的对象直接量。要想读取某个属性的属性描述符,可以使用新的Object.getOwnPropertyDescriptor方法,如代码清单1-33所示。描述符中的所有属性,除了value属性之外,默认值都是true
代码清单1-33 ECMAScript5中对象的getOwnPropertyDescriptor方法
1 //定义包含两个属性的简单对象 2 3 var personalDetails={ 4 name:"wing", 5 email:"den.odell@me.com" 6 }; 7 Object.getOwnPropertyDescriptor(personalDetails,"name"); 8 //返回代表name属性的如下对象直接量 9 //{ 10 // configurable:true, 11 // enumerable:true, 12 // value:"wing", 13 // writable:true 14 //}
在ECMAScript5中你可以在创建属性的同时定义其属性描述值,如代码清单1-34所示
代码清单1-34 ECMAScript5中的属性定义
1 var personalDetails={ 2 name:"wing", 3 email:"den.odell@me.com" 4 }; 5 //为该对象单独定义一个新的属性 6 Object.defineProperty(personalDetails,"age",{ 7 value:35, 8 writable:false, 9 enumerable:true, 10 configurable:true 11 }); 12 //同时定义多个属性 13 Object.defineProperty(personalDetails,{ 14 age:{ 15 value:35, 16 writable:false, 17 enumerable:true, 18 configurable:true 19 }, 20 town:{ 21 value:"wing", 22 writable:true 23 } 24 })
如果你需要得到一個包含某个对象所有属性名的数组,那么可以用Object.keys方法来实现,如代码清单1-35
代码清单1-35 ECMAScript5中对象的keys方法
1 var personalDetails={ 2 name:"wing", 3 email:"den.odell@me.com" 4 }, 5 keys=Object.keys(personalDetails); 6 alert(keys.join(","));//"name,email"
Object.create方法是一个功能强大的新方法,我们可以用该方法根据某个已有对象的属性来创建一个新的对象。该方法一个可能的用处是创建某个已有对象的副本,如代码清单1-36所示。
代码清单1-36 ECMAScript5中对象的create方法
1 var personalDetails={ 2 firstName:"zhang", 3 lastName:"wei" 4 }, 5 //创建该对象的一个副本 6 fatherDetails=Object.create(personalDetails); 7 8 //定制这个副本对象 9 fatherDetails.firstName="chen"; 10 //通过原有对象所设置的属性值未被更改 11 alert(fatherDetails.lastName);//"wei"
如果ECMAScript5中有一个值得你继续深入研究的方法,那就是Object.create方法.