1 常用的创建对象的方式
var person = { name: "larry", age: 29, sayName: function(){ alert(this.name); } };
2 Object.defineProperty() 方法
这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象(a descriptor object)。
描述符对象中的属性必须是configurable、 enumerable、 writable 和 value中的一个或多个。
var person = {}; Object.defineProperty(person, "name", {
//定义数据属性 writable: true, value: "Nicholas" }); alert(person.name); //"Nicholas" person.name = "Greg"; alert(person.name); //"Greg"
3 访问器属性
访问器属性不包含数据值;它们包含一对儿 get 和 set 函数(不过,这两个函数都不是必需的)。
在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function(){ return this._year; }, set: function(newValue){ this._year = newValue + 1; } }); book.year = 2005; alert(book.year); alert(book.year === book._year);
以上代码创建了一个 book 对象,并给它定义两个默认的属性: _year 和 edition。
访问器属性 year 则包含一个get 函数和一个 set 函数。
4 定义多个属性
由于为对象定义多个属性的可能性很大, ECMAScript 5 又定义了一个 Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。
var book = { //默认属性 _year: 2004 }; Object.defineProperties(book, { //数据属性 edition: { value: 1 }, //访问器属性 year: { get: function(){ return this._year + 2; }, set: function(newValue){ this._year = newValue + 1; } } });
注意: 一定要分清,默认属性,数据属性,访问器属性的区别!
5 读取属性的特性
使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。
var book = { //默认属性 _year: 2004 }; Object.defineProperties(book, { //数据属性 edition: { value: 1 }, //访问器属性 year: { get: function(){ return this._year + 2; }, set: function(newValue){ this._year = newValue + 1; } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "edition"); alert(descriptor.value);
alert(descriptor.configurable); // false,当直接在对象上定义的属性,它们的这个特性默认值才为 true。
6 创建对象 - 工厂模式
function createPerson(name) {
//先创建一个对象 var o = new Object();
//初始化该对象 o.name = name; o.sayName = function () { alert(this.name); }
//再返回该对象 return o; } var person1 = createPerson("larry"); person1.sayName();
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
6 创建对象 - 构造函数模式
这种方式与工厂模式相比,存在以下不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了 this 对象;
- 没有 return 语句。
function Person(name) { this.name = name; this.sayName = function () { alert(this.name); } } var person1 = new Person("larry"); var person2 = new Person("Jaye"); person1.sayName();
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。
上面代码中, person1 和 person2 分别保存着 Person 的一个不同的实例。
这两个对象都有一个 constructor(构造函数)属性,该属性指向 Person,如下所示。
alert(person1.constructor == Person); //true alert(person2.constructor == Person); //true
们在这个例子中创建的所有对象既是 Object 的实例,同时也是 Person的实例,这一点通过 instanceof 操作符可以得到验证。
在这个例子中, person1 和 person2 之所以同时是 Object 的实例,是因为所有对象均继承自 Object。
alert(person1 instanceof Object); //true alert(person1 instanceof Person); //true
任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;
而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。
Person("Greg"); // 添加到window window.sayName(); // "Greg"
不使用 new 操作符调用 Person():属性和方法都被添加给 window对象。
在全局作用域中调用一个函数时, this 对象总是指向 Global 对象(在浏览器中就是 window 对象)。
因此,在调用完函数之后,可以通过 window 对象来调用 sayName()方法,并且还返回了"Greg"。
构造函数方式的缺点:在前面的例子中, person1 和 person2 都有一个名为 sayName()的方法,但那两个方法不是同一个 Function 的实例。
alert(person1.sayName == person2.sayName); //false
不要忘了——ECMAScript 中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。(性能不好,资源浪费)。
因为创建两个完成同样任务的 Function 实例的确没有必要;
因此,可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
function Person(name) { this.name = name; this.sayName = sayName; } function sayName(){ alert(this.name); } var person1 = new Person("larry");
这样一来,由于 sayName 包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的同一个 sayName()函数。
缺陷:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。
好在,这些问题可以通过使用原型模式来解决。
7 创建对象 - 原型模式
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象包含所有实例共享的属性和方法。
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
如下代码中,person1 和 person2 访问的都是同一组属性和同一个 sayName()函数。
function Person(){ } Person.prototype.name = "larry"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //"larry" var person2 = new Person(); person2.sayName(); //"larry"
缺点:所有实例在默认情况下都将取得相同的属性值。
8 理解原型
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向该函数的原型对象。
在默认情况下,所有原型对象都会自动获得一个 constructor属性,这个属性包含一个指向 prototype属性所在函数的指针。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。 ECMA-262 第 5 版中把这个指针叫[[Prototype]]。
虽然在脚本中没有标准的方式访问[[Prototype]],但 Firefox、 Safari 和 Chrome 在每个对象上都支持一个属性__proto__;
有一个问题:如何判断person1的原型对象是不是Person.prototype呢?
可以通过isPrototypeOf方法,如下:
有一个问题,如何获得person1对象的原型对象呢?
可以通过ECMAScript 5 的一个方法,叫 Object.getPrototypeOf(),如下:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
使用 Object.getPrototypeOf() 可以方便地取得一个对象的原型,而这在利用原型实现继承(本章稍后会讨论)的情况下是非常重要的。
如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那该属性将会屏蔽原型中的那个属性。
那有一个问题,如果person1又需要访问原型中的name,怎么办?
使用 delete 操作符可以删除实例的属性,从而让我们能够重新访问原型中的属性。
判断一个属性是不是存在于原型对象中,怎么做?
hasOwnProperty() 方法 (是从 Object 继承来的),returns true only if a property of the given name exists on the object instance.
只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确定属性是原型中的属性。
9 in 操作符
有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。
单独使用时,
the in operator returns true when a property of the given name is accessible by the object,
which is to say that the property may exist on the instance or on the prototype.
也就是说,能不能搜索到name属性,不管name属性是在实例中,还是在原型中。
在使用 for-in 循环时,
所有能被对象访问到的enumerated为true的属性将被返回
all properties that are accessible by the object and can be enumerated will be returned.
根据规定,所有开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外。
var o = { name: "larry", sayName : function(){ alert(this.name); } }; for (var prop in o){ alert(prop + ": " + o[prop]); }
要取得对象上所有可枚举的实例属性,怎么做?
可以使用 ECMAScript 5 的 Object.keys()方法。该方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
function Person(){ } Person.prototype.name = "larry"; Person.prototype.age = 29; Person.prototype.sayName = function(){ alert(this.name); }; var keys = Object.keys(Person.prototype); alert(keys); //"name,age,sayName" var p1 = new Person(); p1.name = "Rob"; var p1keys = Object.keys(p1); alert(p1keys); //"name"
说说如下两种写法方式有什么不同?
function Person(){ } Person.prototype.name = "larry"; Person.prototype.sayName = function(){ alert(this.name); };
function Person(){ } Person.prototype = { name : "larry", sayName : function () { alert(this.name); } };
第二种方式最终结果与第一种方式的创建结果相同。
但有一个例外: constructor 属性不再指向 Person 了。
第二种方式的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性 (指向 Object 构造函数),不再指向 Person 函数。
原来是Person函数产生了Person.prototype(原型对象),重写后,prototype已经变成了一个由Object()产生的实例对象。
如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。
注意,以这种方式重设 constructor 属性会导致它的[[Enumerable]]特性被设置为 true。
默认情况下,原生的 constructor 属性是不可枚举的。
10 创建对象:组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
function Person(name){ this.name = name; this.friends = ["A", "B"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person("larry"); var person2 = new Person("Greg"); person1.friends.push("C"); alert(person1.friends); //"A,B,C" alert(person2.friends); //"A,B" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
11 创建对象: 动态原型
function Person(name){ //属性 this.name = name; //方法 if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); }; } } var friend = new Person("larry"); friend.sayName();
这里只在 sayName()方法不存在的情况下,才会将它添加到原型中。
这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。
要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。