理解js的对象,下面定义一个对象:
var Person = { name: "gao", age: 12, say: function () { document.write(this.name); } }
以上定义了一个Person对象,有 name 和age属性,和 say()方法.那么这些属性在创建时 都带有一些特征值, Javascript通过这些特征值来定义它们的行为.
属性类型:
ECMAScript-262 第五版 在定义只有内部采用的特性时,描述了 属性的各种特征. ECMAScript-262定义这些特性是为了 实现
Javascript引擎用的. 因为在 javascript中不能直接访问它们, 为了表示特性时 内部值, 用如下表示法: [[Enumerable]].
ECMAScript中有两种属性:
数据属性:
var Person={ name:"gao" }
定义如上 的Person对象, 那么 定义一个name属性, 也就说 [[Value]]特性,将被设置为 gao,而对这个值的任何修改,都将反映在这个位置.
要修改属性的默认特性,必须使用 ECMAScript 5 的 Object.defineProperty() 方法,看下面实例:
var Person = {}; Object.defineProperty(Person, "name", {
writable: false, value:"gao" }); document.write(Person.name); //gao Person.name = "new name"; document.write(Person.name); //gao
访问器属性:
访问器属性不包含 数据值,它们包含 一对儿 Getter 和Setter函数, 如下四个特性:
访问器属性不能直接定义,而需要 object.defineProperty()来定义,看下面例子:
var book = { _year: 2010, edition: 1 }; Object.defineProperty(book, "year", { get: function () { return this._year; } , set: function (newValue) { if (newValue > 2010) { this._year = newValue; this.edition += newValue - 2010; } } }); book.year = 2012; document.write(book.edition); //3
一次定义多个属性:
var book = {}; Object.defineProperties(book, { _year: { value: 2010 }, edition: { value: 1 }, year: { get: function () { return this._year; } , set: function (newValue) { if (newValue > 2010) { this._year = newValue; this.edition += newValue - 2010; } } } }); book.year = 2012; document.write(book.edition); //1
这样定义之后,发现book.editon的值变成1了,原因就在于,使用Object.defineProperties()定义属性时,会自动设置 属性的特性值 为false,
即 _year和edition的特性值 writable 都为false , 即 _year属性和edition属性的的Value将不会被改变. 所以即使在访问器属性中改变了edition的值,却是不生效的.
如下定义,将会解决这个问题:
var book = {}; Object.defineProperties(book, { _year: { writable: true, value: 2010, }, edition: { writable:true, value: 1 }, year: { get: function () { return this._year; } , set: function (newValue) { if (newValue > 2010) { this._year = newValue; this.edition += newValue - 2010; } } } }); book.year = 2012; document.write(book.edition); //3
发现又输出3了.
读取属性的特性
使用 ECMAScript 5的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符.
var book = {}; Object.defineProperties(book, { _year: { writable: true, value: 2010, }, edition: { writable: true, value: 1 }, year: { get: function () { return this._year; } , set: function (newValue) { if (newValue > 2010) { this._year = newValue; this.edition += newValue - 2010; } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); document.write(descriptor.value); //2010 document.write(descriptor.writable); //true document.write(typeof descriptor.get); //undefined descriptor = Object.getOwnPropertyDescriptor(book, "year"); document.write(descriptor.value); // undefined document.write(descriptor.enumerable); //false document.write(typeof descriptor.get); //function
工厂模式:
工厂模式 是软件工程领域的一种广为人知的设计模式.这种模式 抽象了创建具体对象的过程.下面看一个例子:
function createPerson(name, age) { var o = new Object(); o.name = name; o.age = age; o.say = function () { document.write(this.name); } return o; } var p1 = createPerson("小明", 12); var p2 = createPerson("小刚", 14);
函数 createPerson()可以根据不同的参数,创建不同的person对象,可以无数次的调用这个函数.
每次调用该函数,都换返回一个 包含两个属性和一个方法的对象.工厂模式虽然解决了创建多个相似对象的问题.
但是却没有解决 对象识别的问题.(即怎样知道一个对象的类型.),那就看 下面的新模式.
构造函数模式:
ECMAScript中的构造函数 可用来创建特定类型的对象. 像 Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中,
此外,我们可以自己定义构造函数,从而定义自定义对象类型的属性和方法,看下面例子:
function Person() { this.name = arguments[0]; this.age = arguments[1]; this.say = function () { document.write(this.name); } } var p3 = new Person("小丽", 18); p3.say();
页面会输出 小丽.
创建Person实例,必须使用new操作符.以这种方式调用构造函数实际会经历一下4步:
在这个例子中.p3保存着Person的一个实例.这个对象都有 一个 constructor(构造函数)属性,该属性指向Person,如下所示:
document.write(p3.constructor==Person); //true
对象的 constructor属性 最初是用来标示 对象类型的.但是检测对象类型,还是用 instanceof来比较可靠.如下:
document.write(p3 instanceof Object); //true document.write(p3 instanceof Person); //true
创建自定义的构造函数 意味着将来可以将它的实例标示为一种特定的类型.而这正是 工厂模式的不足之处. p3是 Object的实例,是因为
所有的对象均继承自 Object.
将构造函数当做函数使用.
构造函数与其他函数的唯一区别 ,就在于调用它们的方式不同.下面看 如下 调用 Person构造函数:
//当做构造函数使用 var p3 = new Person("小丽", 18); p3.say(); //当做普通函数使用 Person("小李", 15); //添加到 window window.say(); //小李 //在另一个对象的作用于中调用 var o = new Object(); Person.call(o, "小寒", 20); o.say(); //小寒
构造函数的问题
构造函数的问题在于每个方法都要在 每个实例上重复创建一遍. 下面实例:
var per1 = new Person("小", 1); var per2 = new Person("大", 2); document.write(per1.say == per2.say); //false
说明,per1 和per2的 say()方法 是不同的,说明是 不同的Function实例(以显示不同的name属性)的本质.
然而创建完全相同任务的 Function实例的确没有必要,况且this对象在,根本不用来 执行代码前 就 把函数绑定到特定的对象上.
因此,可以如下定义构造函数.来解决这个问题:
function Person() { this.name = arguments[0]; this.age = arguments[1]; this.say = say; } function say() { document.write(this.name); }
下载把 say方法的函数定义在构造函数的外面.在 构造函数内部,把say属性设置成一个全局的 say函数.
这样一来,由于 say包含了一个指向函数的指针,因此per1和per2对象就共享了 全局作用域中定义的同一个say函数,
但是这样 , 在全局作用域中定义的函数实际上只能被某个对象调用,有点奢侈. 下面 就用 原型模式 解决这个问题.
原型模式
每一个创建的函数都有一个 prototype(原型)属性,这个属性是个指针,指向一个对象.而这个对象的用途是 包含可以由特定类型的所有实例
共享的属性和方法. 字面的意思就是, prototype就是通过调用构造函数而创建的那个对象实例的原型对象.
使用原型对象的好处是 可以让所有对象共享它的所包含的属性和方法. 通俗的说就是 不必再构造函数中定义对象实例的信息,
而是可以将这些信息 直接添加到 原型对象中,下面的例子所示:
function Person() { } Person.prototype.name = "gao"; Person.prototype.age = 12; Person.prototype.say = function () { document.write(this.name); } var p = new Person(); p.say(); //gao var p2 = new Person(); p2.say(); //gao
document.write(p.say == p2.say); //true
虽然构造函数是空的,但是也可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法. 与构造函数模式不同的是
新对象的这些属性和方法是由 所有实例 共享的. 通俗的讲就似乎 p 和p2访问的都是 同一组属性 和同一个 say()函数.
下面看一下 原型对象的性质
只要创建一个对象,就会根据一组特定的规则为 该函数创建一个 prototype属性,这个属性 指向函数的原型对象.
默认情况下,所有 原型对象 都会自动获得 一个 constructor(构造函数)属性,这个属性 包含一个指向 prototype属性所在的函数指针.
比如前面的例子: Person.prototype.constructor指向 Person,而通过这个构造函数,还可以继续为 原型对象添加其他属性和方法.
创建了自定义函数后,其原型对象默认只会取得 constructor属性,至于其他的方法,则都是从Object继承来的. 当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型对象.ECMAScript-261第五版 管这个指针叫 [[Prototype]].
下面看一个图例:
此外 ECMAScript提供 getPrototypeOf(), 可以 找到实例的原型对象.
document.write(Object.getPrototypeOf(p).name); //gao
虽然我们可以通过对象实例 访问 保存在原型中的值,但是却不能通过实例重写原型中的值.如果 实例的属性和原型的属性同名,那么
将会屏蔽原型的那个属性.
function Person() { } Person.prototype.name = "gao"; Person.prototype.age = 12; Person.prototype.say = function () { document.write(this.name); } var p = new Person(); p.name = "new name"; p.say(); //new name 来自实例
var p2 = new Person(); p2.say(); //gao
delete p.name; //删除 实例的name属性 p.say(); //gao 来自原型
可以使用hasOwnProperty()方法 检测一个属性 是 来自原型还是来自实例.
function Person() { } Person.prototype.name = "gao"; Person.prototype.age = 12; Person.prototype.say = function () { document.write(this.name); } var p = new Person(); document.write( p.hasOwnProperty("name")); //false p.name = "new name"; document.write(p.hasOwnProperty("name"));//true p.say(); var p2 = new Person(); p2.say(); delete p.name; document.write(p.hasOwnProperty("name"));//fasle p.say();
ECMAScript 还提供一个函数,用于返回 对象上 所有可枚举 的实例属性.
function Person() { } Person.prototype.name = "gao"; Person.prototype.age = 12; Person.prototype.say = function () { document.write(this.name); } document.write(Object.keys(Person.prototype)); //name,age,say
Object.keys() 接受一个 对象参数, 返回的是一个 数组.
还有一种方法: 可以列出所有属性不管是否可枚举:
document.write(Object.getOwnPropertyNames(Person.prototype)); //age,name,say,constructor
更简单的原型语法:
function Person() { } Person.prototype = { name: "gao", age: 12, say: function () { alert(this.name); } } var p = new Person(); document.write(p instanceof Person); //true document.write(p.constructor); //function Object() { [native code] }
可以看到 这样的写法,会使 实例的 构造函数 变成 Object ,而不再是 Person.如想想强制改变,那么看如下代码:
function Person() { } Person.prototype = { name: "gao", age: 12, say: function () { alert(this.name); } } Object.defineProperty(Person.prototype, "constructor", { // 重设 Person 原型对象的 constructor属性 enumerable: false, // 由于这种重设的方式,会导致 constructor属性的 [[Enumerable]]特性变为true,即可以枚举,顾显示设置成 false,即屏蔽枚举 此属性 value: Person //设置 构造函数的名字 }); var p = new Person(); document.write(p instanceof Person); //true document.write(p.constructor); //Person
原型模式的问题:
由于原型模式的原因, 原型对象的属性和方法是 所有实例共享的.那么问题就来了,看下面的实例:
function Person() { }; Person.prototype = { constructor: Person, name: "gao", age: 14, friends: ["lin", "feng"], say: function () { alert(this.friends); } } var p1 = new Person(); var p2 = new Person(); p1.friends.push("newFriend"); p1.say(); //lin feng newFriend 3为朋友 p2.say(); //由于p1多了一位朋友,所以 在Person的原型对象中也相应加了一位朋友,而p2是Person的实例,所以也变了
下面介绍一中方法解决这种 连带的问题, 混合模式(构造函数模式+原型模式);
混合模式
构造函数用于定义实例属性, 而原型模式用于定义方法和 共享的属性. 结果每个实例都有自己的一份实例属性的副本,但同时
又共享着对方法的引用. 从而最大限度的节省了 内存. 另外,混合模式还支持 传递参数.看下例:
function Person() { this.name = arguments[0]; this.age = arguments[1]; this.friends = arguments[2]; } Person.prototype.say = { constructor: Person, say: function () { alert(this.name); } } var p1 = new Person("p1", 12, ["p11", "p12"]); var p2 = new Person("p2", 18, ["p21"]); p1.friends.push("p13"); alert(p1.friends); //p11 p12 p13 alert(p2.friends); //p21
动态原型模式
function Person() { this.name = arguments[0]; this.age = arguments[1]; if (typeof this.say != "function") { Person.prototype.say = function () { alert(this.name); } } } var p1 = new Person("p1", 14); var p2 = new Person("p2", 16);
创建p1时 this.say()由于不是函数,所以会 实例化一个say()函数,当实例化p2时,person的原型对象已经存在函数say()了,那么将共享say(),而无需再创建一个 say()函数的实例了. 这样就节省了内存.
寄生构造函数模式
基本思想就是 : 创建一个函数,该函数的作用仅仅是 封装创建对象的代码,然后在返回对象的实例.看下面例子:
function Person(name) { var o = new Object(); o.name = name; o.say = function () { alert(this.name); }; return o; } var p = new Person("gao"); p.say(); //gao
这个例子中,Person函数创建了一个对象o,给o附加属性和方法,在返回该对象. 除了使用new操作符并把使用的包装函数叫做构造函数之外,和工厂模式没有什么区别.
构造函数在不返回值的情况下,默认返回一个新对象实例. 而通过在 构造函数 末尾添加一个return 语句,可以重写 调用构造函数时返回的值.
这个模式可以再 特殊的情况下 用来为对象创建构造函数.看下面例子:
function CustomArray() { var value = new Array(); value.push.apply(value, arguments); value.toCutomString = function () { return this.join('|'); } return value; } var array = new CustomArray("1", "2", "3"); alert(array.toCutomString()); // 1|2|3
alert( array instanceof CustomArray); //false
使用这个模式需要注意一下几点:
返回的对象与构造函数或者与构造函数原型之间没有关系. 为此,不能依赖instanceof操作符查看对象类型.