早期的博客3
创建对象的方法:
- object构造函数和对象字面量方法
- 工厂模式
- 自定义构造函数模式
- 原型模式
- 组合使用自定义构造函数和原型模式
- 动态原型模式、寄生构造模式、稳妥构造函数模式
一、Obejct构造函数和对象字面量方法
它们的优点是创建单个的对象非常方便。但是有一个明显的缺点:利用同一接口函数创建很多对象,会产生大量的重复代码。
//Object构造函数
var person = new Object();
person.name = "huazhen";
person.age = 22;
person.job = "yaoniguan";
person.sayName = function(){
alert(this.name);
};
//对象字面量方法
var person = {
name:"huazhen",
age:22,
job:"yaoniguan",
sayName:function(){
alert(this.name);
}
};
如何理解:利用同一接口创建很多对象,会产生大量的重复代码。示例如下:
//创建person1对象
var person1 = {
name:"huazhen",
age:22,
job:"yaoniguan",
sayName:function(){
alert(this.name);
}
};
//创建person2对象
var person2 = {
name:"heiheihei",
age:25,
job:"hehe",
sayName:function(){
alert(this.name);
}
};
可以看出,当我们创建两个类似的对象时,重复写了name,age,job以及对象的方法这些代码,随着类似对象的增多,显然,代码会凸显出复杂、重复的感觉。为解决这个问题,就要了解工厂模式。
二、工厂模式
为解决创建多个对象产生大量重复代码的问题,由此产生了工厂模式。那么,究竟什么是工厂模式?这种模式抽象了创建具体对象的过程。考虑到在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下面例子:
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("huazhen",22,"yaoniguan");
var person2 = createPerson("heiheihei",25,"hehe");
可以无数次的调用这个函数,每次它都会返回包含三个属性一个方法的对象。成功解决了Object构造函数或对象字面量创建单个对象而造成大量代码重复的问题。工厂模式有以下特点:
- 在函数内部显式的创建了对象。
- 在函数结尾处一定要返回这个新创建的对象。
但是可以发现,工厂模式创建的对象,例如这里的person1和person2,我们无法识别对象是什么类型(在示例中国得到的都是o对象,对象的类型都是Object),为了解决这个问题,自定义构造函数模式出现了。
三、构造函数模式
既然自定义构造函数模式是为了解决无法直接识别对象的类型才出现的,那么显然自定义构造函数需要解决两个问题。其一:直接识别创建的对象的类型。其二:解决工厂模式所解决的创建大量类似对象产生的代码重复问题。
在第一部分中,我们使用Object构造函数是原生构造函数,显然不能解决问题。我们可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。使用构造函数模式将前面的例子重写,代码如下:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("huazhen",22,"yaoniguan");
var person2 = new Person("heiheihei",25,"hehe");
我们验证第一个问题,是否可以识别创建的对象的类型:
//检测对象类型:
alert(person1 instanceof Object); //true
alert(person2 instanceof Person); //true
alert(person1 instanceof Object); //true
alert(person2 instanceof Person); //true
person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object。
对于第二个问题,显然创建大量的对象不会造成代码的重复。
1.对比自定义构造函数与工厂模式的不同之处:
自定义构造函数没有用var o = new Oeject() 那样显示的创建对象;
与o.name不同,它直接将属性和方法赋给this对象,this最终会指向新创建的对象
因为没有创建对象,所以最终没有return一个对象。
2.对于构造函数需要注意:
构造函数的函数名需要大写,用以区分普通函数。
构造函数也是函数,只是它的作用之一是创建对象。
构造函数在创建新对象时,需要使用new操作符。
创建的两个对象person1和person2的constructor(构造函数)属性都指向用于创建它们的Person构造函数。
3.理解构造函数也是函数:
构造函数与其他函数的唯一区别,就在于调用它们的方式不同,不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数。如下代码所示:
//当作构造函数使用
var person = new Person("huazhen",22,"yaoniguan");
person.sayName(); //"huazhen"
//当作普通函数调用
Person("heiheihei",25,"hehe"); //添加到window
window.sayName(); //"heiheihei"
当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中就是window对象)。
4.构造函数的问题:
使用构造函数的主要问题,就是每个方法都要在每个示例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但是两个方法不是同一个Function的实例。函数也是对象,因此每定义一个函数,也就是实例化了一个对象。从逻辑角度,构造函数也可以这样定义:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); //与声明函数在逻辑上是等价的
}
以下代码可以证明不同实例上的同名函数是不相等的:alert(person1.sayName == person2.sayName) //false
5.解决方法:
创建两个完成相同任务的Function实例的确没有必要,况且有this对象在,根本不用执行代码前就把函数绑定到特定对象上面。因此,通过把函数定义转移到构造函数外部来解决这个问题。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("huazhen",22,"yaoniguan");
var person2 = new Person("heiheihei",25,"hehe");
把sayName()函数的定义转移到构造函数外部,而在构造函数内部,我们将sayName属性设置成全局的sayName函数。由于sayName包含的是一个指向函数的指针,因此person1和person2对象就共享了全局作用域中定义的同一个sayName()函数,这样就解决了两个函数做同一件事情的问题。这样又有新的问题;
6.新的问题:
- 在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。
- 如果对象需要定义很多方法,那么就要定义很多个全局函数,那么就没有丝毫的封装性可言。
接下来,用原型模式来解决这个问题。
四、原型模式
为什么会出现原型模式呢?这个模式在上面讲了是为了解决自定义构造函数需要将方法放在构造函数之外造成封装性较差的问题。当然它又要解决构造函数能够解决的问题,所以,最终它需要解决以下几个问题。其一:可以直接识别创建的对象的类型。其二:解决工厂模式解决的创建大量相似对象时产生的代码重复的问题。其三:解决构造函数产生的封装性不好的问题。
1.理解原型对象
首先,我们应当知道:无论什么时候,只要创建了一个新函数(函数即对象),就会根据一组特定的规则创建一个函数(对象)的prototype属性(理解为指针),这个属性会指向函数的原型对象(原型对象也是一个对象),但是因为我们不能通过这个新函数访问prototype属性,所以写为[[prototype]]。同时,对于创建这个对象的构造函数也将获得一个prototype属性(理解为指针),同时指向它所创建的函数(对象)所指向的原型对象,这个构造函数是可以直接访问prototype属性的,所以我们可以通过访问它将定义对象实例的信息直接添加到原型对象中。这时原型对象拥有一个constructor属性(理解为指针)指向创建这个对象的构造函数(注意:这个constructor指针不会指向除了构造函数之外的函数)。
示例:
function Person(){}
Person.prototype.name = "huazhen";
Person.prototype.age = 22;
Person.prototype.job = "yaoniguan";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.sayName(); //huazhen
person2.sayName(); //huazhen
alert(person1.sayName == person2.sayName) //true
这个例子中,首先创建了一个内容为空的构造函数,因为可以通过访问构造函数的prototype属性来为原型对象中添加属性和方法。于是在下面几行代码中,我们便通过访问构造函数的prototype属性向原型对象中添加了属性和方法。接着,创建了两个对象实例person1和person2,并调用了原型对象中sayName()方法,得到了原型对象中的name值。这说明:构造函数创建的每一个对象和实例都拥有或者说是继承了原型对象的属性和方法。(因为无论是创建的对象实例还是创造函数的prototype属性都是指向原型对象的) 换句话说,原型对象中的属性和方法会被构造函数所创建的对象实例所共享,这也是原型对象的一个好处。
阐释这个问题:
从这个图中,我们可以看出:
- 构造函数和由构造函数创建的对象的prototype指针都指向原型对象。即原型对象即是构造函数的原型对象,又是构造函数创建的对象的原型对象。
- 原型对象有一个constructor指针指向构造函数,却不会指向构造函数创建的实例。
- 构造函数的实例[[prototype]]属性被实例访问来添加或修改(屏蔽)原型对象的属性和方法的,而构造函数的prototype属性可以被用来访问以修改原型对象的属性和方法。
- person1和person2与它们的构造函数之间没有直接的关系,只是它们的prototype属性同时指向了同一个原型对象而已。
- Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。
- 虽然这两个实例都不包含属性和方法,但却可以调用person1.name,这是通过查找对象属性的过程来实现的。
2.有关于原型对象中的方法以及实例中的属性和原型对象中的属性
两种方法来确定构造函数创建的实例对象与原型对象之间的关系:
第一种方法:isPortotypeOf()方法,通过原型对象调用,确定原型对象是否是某个实例的原型对象。比如
alert(Person.prototype.idPortotyprOf(person1)); //true
alert(Person.prototype.idPortotyprOf(person2)); //true
即说明person1实例和person2实例的原型对象都是Person.prototype
第二种方法:Object.getPrototypeOf()方法,通过此方法得到某个对象实例的原型,即[[Prototype]]。例如:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(Person1).name); //huazhen
问题:当实例本身自己有和原型中相同的属性名,而属性值不同,在代码获取某个对象的属性时,该从哪里获取呢?
规则:在代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。如果实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则会返回该属性的值。
例:
function Person(){}
Person.prototype.name = "huazhen";
Person.prototype.age = 22;
Person.prototype.job = "yaoniguan";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "heiheihei";
alert(person1.name); //heiheihei--来自实例
alert(person2.name); //huazhen--来自原型
delete person1.name;
alert(person1.name); //huazhen--来自原型
- 首先,把person1实例的name属性设置为"heiheihei",当我们直接获取person1的name属性时,会出现在person1本身找该属性,找不到则在原型对象中寻找。
- 当给person1对象添加了自身的属性name时,会得到person1自身的属性,也就输该属性屏蔽了原型中的同名属性
- 最后,可以通过delete删除实例中的属性,而原型中的属性不会被删除。
第三种方法:hasOwnProperty()方法,该方法可以检测一个属性是存在于实例还是存在于原型中。只有给定属性存在于对象实例中时,才会返回true,否则返回false。
例:
function Person(){}
Person.prototype.name = "huazhen";
Person.prototype.age = 22;
Person.prototype.job = "yaoniguan";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
//下面使用该方法:
alert(person1.hasOwnProperty("name")); //false
person1.name = "heiheihei";
alert(person1.name); //heiheihei--来自实例
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //huazhen--来自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //huazhen--来自原型
alert(person1.hasOwnProperty("name")); //false
3.in操作符的使用
in操作符会在通过对象能够访问给定属性时,返回true,无论该属性存在于事例还是原型中。
例:
function Person(){}
Person.prototype.name = "huazhen";
Person.prototype.age = 22;
Person.prototype.job = "yaoniguan";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
//下面使用方法
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "heiheihei";
alert(person1.name); //heiheihei--来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //huazhen--来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
alert(hasPrototypeProperty(person2,name)); //true
delete person1.name;
alert(person1.name); //huazhen--来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
可以看到,无论属性存在于实例对象本身还是在实例对象的原型对象都会返回true。用in操作符以及hasOwnProperty()方法就可以判断一个属性是否存在于原型对象(而不是存在于对象实例或者是不存在)。
hasPrototypeProperty()函数:
function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name) && (name in Object);
在函数中in操作符返回true而hasOwnProperty()方法返回false,那么如果得到true则说明对象一定存在于原型对象中。(注意!的优先级高于&&)
4.for-in循环和Object.keys()方法在原型中的使用
在通过for-in循环时,它返回的是所有能够通过对象访问的、可枚举的属性,其中既包含存在于实例中的属性,也包括存在于原型中的属性。且对于屏蔽了原型中不可枚举的属性(即将[[Enumberable]]标记为false的属性)也会在for-in中循环返回。
for(var prop in person1){
alert(prop); //name,age,job,sayName
}
通过for-in循环,可以枚举出name,age,job,sayName这几个属性。person1中的[[prototype]]属性不可被访问,因此不能通过for-in循环枚举出它。
object.keys()方法接收一个参数,这个参数可以是原型对象,也可以是由构造函数创建的实例对象,返回一个包含所有可枚举属性的字符串数组。
例:
function Person(){}
Person.prototype.name = "huazhen";
Person.prototype.age = 22;
Person.prototype.job = "yaoniguan";
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.keys(Person1.prototype);
alert(keys); //name,age,job,sayName
var p1 = new Person();
p1.name = "heiheihei"
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //heiheihei,age
可以看出Object.keys()方法只返回其自身的属性。如原型对象只返回原型对象中的属性,对象实例也只返回对象实例自己创建的属性,而不能返回继承自原型对象的属性。
5.更简单的原型语法
之前的例子,在构造函数的原型对象中添加属性和方法时,都要在前面桥Person.prototype,而原型对象说到底还是对象,只要是对象,那么就可以用对象字面量方法来创建
例:
function Person(){}
Person.prototype = {
name:"huazhen",
age:22,
job"yaoniguan",
sayName:function(){
alert(this.name);
}
}
这样就减少了不必要的输入,也可以更好的封装原型。但是,这时的原型对象的constructor就不会指向Person构造函数而是指向Object构造函数
为什么会这样???
当我们创建Person构造函数时,就会同时自动创建这个Person构造函数的原型(prototype)对象,这个原型对象也自动获取了一个constructor属性并指向Person构造函数,这个之前的图示可以看的很清楚。之前我们用Person.prototype.name="huazhen",这种方式向原型对象添加属性,并没有本质上的改变。然而,上述这种封装性较好的方式,即使用对象字面量的方法,实际上是使用Object构造函数的原型对象(对象字面量本质即使用Object构造函数创建新对象),注意,此时Person构造函数的原型对象不再是之前的原型对象(而之前的原型对象的constructor属性仍然指向Person构造函数),这个原型对象和创建Person构造函数时自动生成的原型对象根本不一样。即,对象字面量创建的原型对象的constructor属性此时指向Object构造函数。
例:
function Person(){}
Person.prototype = {
name:"huazhen",
age:22,
job"yaoniguan",
sayName:function(){
alert(this.name);
}
}
var person1 = new Person();
alert(Person.prototype.constructor == Person); //false
alert(Person.prototype.constructor == Object); //true
如果constructor的值真的很重要,可以将其设置回适当的值:
function Person(){}
Person.prototype = {
constructor:Person, //设置constructor
name:"huazhen",
age:22,
job"yaoniguan",
sayName:function(){
alert(this.name);
}
}
注意:这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true,而默认模式下,原生的constructor属性是不可枚举的。但是可以用Object.defineProperty()修改
6.原型的动态性
由于在原型中查找值的过程是一次搜索,因此对原型对象所做的任何修改都可以从实例上反应出来,即使先创建实例后修改原型也是这样:
例:
var person = new Person();
Person.prototype.sayHi = function(){
alert("hi");
}
friend.sayHi(); //hi
原因:可以归结为实例和原型之间松散的连接关系,当盗用friend.sayHi()时,首先首先会在实例中搜索名为sayHi的属性,没有找到,会继续搜索原型。可以在原型中找到并返回。
但是如果重写整个原型对象,情况就不同:调用构造函数时,回味实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数和最初原型之间的联系。实例中的指针仅指向原型,而不指向构造函数
例:
function Person(){}
//注意
var friend = new Person();
Person.prototype = {
constructor:Person, //设置constructor
name:"huazhen",
age:22,
job"yaoniguan",
sayName:function(){
alert(this.name);
}
};
friend.sayName(); //error
重写原型对象切断了现有原型与任何之前已存在的对象实例之间的联系;它们引用的仍然是最初的原型。
7.原生对象的原型
原型的重要性不仅体现在自定义类型方面,就连所有的原生的引用类型,都是使用这种模式创建的。所有原生引用类型(Object、Array、String等)都在其构造函数的原型上定义了方法。例如在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法。不推荐在产品化的程序中修改原生对象的原型,这样做可能导致命名冲突等问题。
8.原型模式存在的问题
回顾我们的问题:其一,可以直接识别创建对象的类型;其二,解决工厂模式解决的创建大量相似对象时产生的代码重复问题;其三,解决构造函数产生的封装性不好的问题。
其中第一个问题已解决,通过构造函数就可以看出函数类型。第二个问题解决的不太好,它省略了为构造函数传递初始化参数这一环节,结果所有的实例在默认情况下都将取得相同的默认值,我们只能通过在实例上添加同名属性来屏蔽原型中的属性,这无疑会造成代码重复的问题。第三个问题,封装性还好。算是勉强解决了问题。但是产生了额外的问题,如下:
例:
function Person(){}
Person.prototype = {
constructor:Person, //设置constructor
name:"huazhen",
age:22,
job"yaoniguan",
friends:["xiaohong","xiaozhang"],
sayName:function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("xiaoming");
alert(person1.friends); //["xiaohong","xiaozhang","xiaoming"]
alert(person2.friends); //["xiaohong","xiaozhang","xiaoming"]
产生的问题:由于friends数组存在于Person.prototype而非person1中,所以刚刚提到的修改也会通过person2.firends(与person1.friends指向同一个数组)反应出来。
五、组合使用自定义构造函数模式和原型模式
原型模式存在连个最大的问题:
- 问题1:由于没有为构造函数创建对象实例时传递初始化参数,所有的实例在默认情况下获取相同的默认值;
- 问题2:对于原型对象中包含引用类型的属性,在某一个实例中修改引用类型的值,会牵扯到其他的实例;
组合使用时:构造函数应用于定义实例属性,而原型模式用于定义方法和共享的属性。
例:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friend = ["xiaohong","xiaozhang"];
}
Person.prototype = {
constructor:Person;
sayName:function(){
alert(this.name);
}
}
var person1 = new Person("huazhen",22,"yaoniguan");
var person2 = new Person("heiheihei",25,"hehe");
person1.friends.push("xiaoming");
alert(person1.friends); //["xiaohong","xiaozhang","xiaoming"]
alert(person2.friends); //["xiaohong","xiaozhang"]
alert(person1.sayName == person2.sayName); //true
组合使用构造函数模式和原型模式解决的问题:
- 解决了Object构造函数和对象字面量方法在创建大量对象时造成的代码重复问题(因为只要在创建对象时向构造函数传递参数即可)。
- 解决了工厂模式产生的无法识别对象类型的问题(因为这里通过构造函数即可获知对象类型)。
- 解决了自定义构造函数模式封装性较差的问题(这里全部都被封装)。
- 解决了原型模式的两个问题:所有实例共享相同的属性以及包含引用类型的数组在实例中修改时会影响原型对象中的数组。
综上所述,组合使用构造函数模式和原型模式可以说是非常完美了。
六、动态原型模式、寄生构造模式、稳妥构造函数模式
实际上,组合使用构造函数模式和原型模式已经非常完美了,接下来的三种模式都是在特定情况下使用的,以便解决更多的问题。
1.动态原型模式
本质上通过检测某个应该存在的方法是否存在或有效,来决定是否初始化原型。如下所示:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var person = new Person("huazhen",22,"yaoniguan"); //使用new调用构造函数并创建一个实例对象
person.sayName(); //huazhen
alert(person.job); //yaoniguan
这里先声明了一个构造函数,然后当使用new操作符调用构造函数创建实例对象时进入了构造函数的函数执行环境,开始检测对象的sayName是否存在或是否是一个函数,如果不是,就使用原型修改的方式向原型中添加sayName函数。且由于原型的动态性,这里所做的修改可以在所有实例中立即得到反映。值得注意的是,在使用动态原型模式时,不能使用对象字面量重写原型,否则,在建立了实例的情况下重写原型会导致切断实例和新原型的联系。
2.寄生构造函数模式
寄生构造函数模式:
例:
function Person(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
return o;
}
var person = new Person("huazhen",22,"yaoniguan");
person.sayName(); //huazhen
寄生构造函数的特点如下:
- 声明一个构造函数,在构造函数内部创建对象,最后返回该对象,因此这个函数的作用仅仅是封装创建对象的代码。
- 可以看出,这种方式除了在创建对象的时候使用了构造函数的模式(函数名大写,用new关键字调用)以外与工厂模式一模一样。
- 构造函数在不返回值的情况下,默认会返回新对象实例,而通过构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
假设我们想要创建一个具有额外方法的特殊数组,通过改变Array构造函数的原型对象是可以实现的,但是前面提到过,这种方式可能会导致后续的命名冲突等一系列问题,是不推荐的。而寄生构造函数就能很好的解决这一问题。如下所示:
例:
function SpecialArray(){
var values = new Array();
values.push.apply(values,arguments);
values.toPipedString = function(){
return this.join("|");
};
return values;
}
var colors = new SpecialArray("red","blue","green");
alert(colors.toPipedString()); //red|blue|green
这里没有改变Array构造函数的原型对象,又完成了添加Array方法的目的。
3.稳妥构造函数模式
稳妥对象是指这没有公共属性,而且方法也不引用this的对象。稳妥对象适合在安全的环境中使用,或者在防止数据被其他应用程序改动时使用。
例:
function Person(name,age,job){
var o = new Object();
o.sayName = function(){
alert(name);
};
return o;
}
var person = Person("huazhen",22,"yaoniguan");
person.sayName(); //huazhen
可以看出来,这种模式和寄生构造函数模式非常相似,只是:
- 新创建对象的实例方法不用this。
- 不用new操作符调用构造函数(由函数名的首字母大写可以看出它的确是一个构造函数)。
注意:变量person中保存的是一个稳妥对象,除了调用sayName()方法外没有别的方式可以访问其数据成员
例:
alert(person.name); //undefined
alert(person.age); //undefined
【完结】