目录
面向对象的程序设计
6.1 理解对象
-
创建对象
var person = new Object person.name = "Tom" person.age = 29 person.job = "SOftware Engineer" person.sayName = function(){ alert(this.name) }
6.1.1 属性类型
-
数据属性
-
Configurable
-
Enumerate
-
Writeable
-
Value
var person = { } Object.defineProperty(person, "name", { writeable: false value: "Nicholas" }) alert(person.name) // "Nicholas" person.name = "Greg" alert(person.name) // "Nicholas"
-
-
访问器属性
-
Configurable
-
Enumerable
-
Get
-
Set
var book = { // _year 前面的下划线是一种常用的记号,用来表示只能通过对象方法访问的属性 _year = 2004, edition: 1 } Object.defineProperty(book, "year", { get: function(){ return thie._year }, set: function(){ if (newValue > 2004) { this._year = newValue this.edition += newValue - 2004 } } }) book.year = 2005 alert(book.edition); // 2
getter函数返回_year的值, setter函数通过计算确定正确的版本.因此,把year属性修改为2005会导致 _year变成2005, 而edition变成2。
这是使用访问器属性的常见方式,既设置一个属性的值会导致其他属性发生变化。
不一定非要同时指定getter和setter,只指定getter意味着属性是不能写的,尝试写入属性会被忽略。
6.1.2 定义多个属性
由于为对象定义多个属性的可能性很大,可以使用
Object.defineProperties( )
var book = { }; Object.defineProperties(book, { _year: { value: 2004 }, edition: { value: 1 }, year: { get: function(){ return this._year; }, set: function(newValue){ if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } }); book.year = 2005; alert(book.edition); //2
6.1.3 读取属性的特性
使用
Object.getOwnPropertyDescriptor( )
方法,可以取得给定属性的描述符。接受两个参数:
- 属性所在的对象
- 读取其描述符的属性名称
返回值:一个对象
var book = { }; Object.defineProperties(book, { _year: { value: 2004 }, edition: { value: 1 }, year: { get: function(){ return this._year; }, set: function(newValue){ if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); alert(descriptor.value); //2004 alert(descriptor.configurable); //false alert(typeof descriptor.get); //"undefined" var descriptor = Object.getOwnPropertyDescriptor(book, "year"); alert(descriptor.value); //undefined alert(descriptor.enumerable); //false alert(typeof descriptor.get); //"function"
对于数据属性
_year
,value
等于最初的值,configurable
是false, 而get
等于undefined
。对于访问其属性
year
,value
等于undefined
,enumerateble
是false
,而get
是一个指向getter
函数的指针。
-
6.2 创建对象
虽然object构造函数和对象字面量均可以用来创建对象,但这些方式有个很明显的确定:
使用同一个接口创建很多对象,会产生大量的重复代码。
为了解决这个问题,热门尝试使用工厂模式这一种变体。
6.2.1 工厂模式
-
套路:通过工厂函数(返回一个对象的函数)动态创建对象并返回
-
使用场景:需要创建多个对象
-
缺点:对象没有一个具体的类型,都是Object类型
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("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
6.2.2 构造函数模式
构造函数可以用来创建特定类型的对象
像 Object
和 Array
这样的原生构造函数,爱运行是会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
alert(person1.sayName == person2.sayName); //false
- 没有显示的创建对象
- 直接将属性和方法赋值给了
this
对象 - 没有
rerturn
语句
要创建Person
的新实例,必须使用 new
操作符。以这种方式调用操作符实际上会经历一下四个步骤:
- 创建一个新对象
- 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象
在前面的例子中,person1
和 person2
分别保存着Person
的一个不同的实例。这两个对象都有一个constructor
(构造函数)属性,该属性指向Person
,如下所示:
alert(person1.constructor == Person) // true
alert(person2.constructor == Person) // true
我们在这个例子中创建的所有对象既是Object
的实例,也是Person的实例,这一点可以通过instanceof
操作符来验证:
alert(person1 instanceof Object) // true
alert(person1 instanceof Person) // true
alert(person2 instanceof Object) // true
alert(person2 instanceof Person) // true
创建自定义构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数胜过工厂模式的地方。
- 将构造函数当做函数
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
// 当做构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作为普通函数调用
Person("Greg", 27, "Doctor"); //adds to window
window.sayName(); //"Greg"
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
可以看到,不使用new
操作符来调用函数Person()
会出现什么结果呢:
属性和方法都被添加给了window
对象了。
当在全局作用域中调用一个函数时,this
对象总是指向Global
对象(在浏览器中就是Window
对象)
也可以使用call()
( 或者apply()
)在某个特殊对象的作用域中来调用Person()
函数。这里是在对象o
的作用域中调用大,因此调用后o
就拥有了所有的属性和sayName()
方法。
- 构造函数的问题
-
每个方法都要在每个实例中重新创建一遍
在前面的例子中,
person1
和person2
都有一个名为sayName(
)的方法,但两个方法不是同一个Function
的实例。(函数就是对象,因此没定义一个函数,也就是实例化了一个对象。alert(person1.sayName === person2.sayName) // false
然而,创建两个完成同样任务的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("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.sayName(); //"Nicholas" person2.sayName(); //"Greg" alert(person1 instanceof Object); //true alert(person1 instanceof Person); //true alert(person2 instanceof Object); //true alert(person2 instanceof Person); //true alert(person1.constructor == Person); //true alert(person2.constructor == Person); //true alert(person1.sayName == person2.sayName); //true
6.2.3 原型模式
我们创建的每一个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
原型对象的好处:可以让所有对象实例共享它所包含的属性和方法。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
//only works if Object.getPrototypeOf() is available
if (Object.getPrototypeOf){
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
}
-
使用
hasOwnProperty()
方法可以检测一个属性是否存在于实例中,还是存在于原型中。只有在给定属性存在于实例对象本身,才会返回true。 -
原型与 in 操作符
- 单独使用(in 操作符会在通过对象能够访问给定属性时返回true, 无论该属性存在于实例中还是原型中)
- for-in 循环中使用
-
Object.keys( )
: 取得对象上所有可枚举
的实例属性-
接受一个对象作为参数
-
返回一个包含所有可枚举属性的字符串数组
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var keys = Object.keys(Person.prototype); alert(keys); // "name,age,job,sayName" var p1 = new Person() p1.name = "rob" p1.age = 31 var p1keys = Object.keys(p1) alert(p1keys) // "name, age"
这里, 变量keys将保存一个数组,数组中的是字符串
"name"
、"age"
、"job"
和"sayName"
如果
想要得到所有的实例属性,无论它是否可枚举
,都可以使用Object.getOwnPropertyNames( )
方法。var keys = Object.getOwnPropertyNames(Person.prototype) alert(keys) // "constructor, name, age, job, sayName"
注意结果包含了不可枚举的constructor属性
-
更简单的原型语法
我们大概注意到了,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。
Person.prototype () { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { alert(this.name) } }
在上面的代码中,我们将
Person.prototype
设置为等于一个以对象字面量形式创建的新对象,最终结果相同,但有一个例外:constructor
属性不再指向Person
了。前面介绍过,每创建一个函数,就会同时创建它的prototype
对象,这个对象就会自动获得constructor
属性。而我们在这里的方法,本质上完全重写了默认的
prototype
对象,因此constructor
属性也就变成了新对象的constructor
属性(指向Object
构造函数),不再指向Person
函数。var friend = new Person() alert(friend instanceof Object) // true alert(friend instanceof Person) // true alert(friend.constructor === Person) // false alert(friend.constructor === Objectt) // true
如果constructor的值真的很重要,可以特意将它设置会合适的值。
function Person () { } Person.prototype = { constructor: Person, name:"Nicholas", age: 29, job: "Software Engineer", sayName: function () { alert(this.name) } }
以上代码特意包含了一个
constructor
属性,并将它设置为Person
,从而确保了通过该属性能够访问到适当的值。注意这种方式会使
constructor
属性的[[Enumberable
]]特性被设置为true。默认情况下,原生的constructor
属性是不可枚举的。因此,可以试一试Object.defineProperty( )
function Person () { } Person.prototype = { constructor: Person, name:"Nicholas", age: 29, job: "Software Engineer", sayName: function () { alert(this.name) } } // 重设构造函数 Object.defineProperty(Person.prototype, "constructor", { enumberable: false, value: Person })
-
-
原型模式的缺点
-
省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
-
原型中的所有属性被很多实例共享,这种共享对于函数非常合适,对于基本值的属性也说得过去。然而对于包含引用类型值得属性来说,问题就比较突出了。
function Person(){ } Person.prototype = { constructor: Person, name : "Nicholas", age : 29, job : "Software Engineer", friends : ["Shelby", "Court"], sayName : function () { alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(person1.friends === person2.friends); //true
在此,
Person.prototype
对象有一个名为friends
的属性, 该属性包含一个字符串数组。然后创建了Person
的两个实例。接着,又修改了person1.friends
引用的数组,向数组中添加了一个字符串。由于friends
存在于Person.prototype
而非person1
中,所以刚刚提到的修改也会通过person2.friends
(与person1.friends
指向同一个数组)反映出来。
-
6.2.4 组合使用构造函数模式和原型模式
创建自定义类型的最常见模式就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节约了内存。
这种混合模式还支持向构造函数传递参数。
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ["Shelby", "Court"]
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name)
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer")
var person2 = new Person("Greg", 27, "Doctor")
person1.friends.push("Van")
alert(person1.friends) // "Shelby, Court, Van"
alert(person2.friends) // "Shelby, Court"
alert(person1.friends === person2.friends) // false
alert(person1.sayName === person2.sayName) // true
在这个例子中,实例属性都是在不同的构造函数中定义的,而由所有实例共享的属性constructor
和方法 sayName()
则是在原型中定义的。而修改了person1.friends
(向其中添加了一个新字符串),并不会影响person2.friends
,因为它们分别引用了不同的数组。