JavaScript 高级程序设计-第六章-面向对象的程序设计-理解对象
理解对象
下面这种创建新对象的模式(Object 的构造函数)是最早期的:
var person = new Object()
person.name = "test"
person.age = 29
person.sayName = function () {
alert(this.name)
}
可以用对象字面量来替代创建:
var person = {
name: "test",
age: 29,
sayName: function () {
alert(this.name)
}
}
这两个例子是一样的。
属性类型
ES 中有两种属性:数据属性、访问器属性。
数据属性
数据属性有四个描述其行为的特性:
- [[Configurable]](可配置的):是否能通过 delete 来删除属性从而重新定义,能否把属性类型修改为访问器属性。上面例子中直接定义的属性的这个特性默认是 true。
- [[Enumerable]](可枚举的):是否能用 for in 遍历出来,上面的例子中的属性这个特性默认是 true。
- [[Writable]](可写的):是否可以修改属性的值,上面的例子中的属性这个特性默认是 true。
- [[Value]](属性的数据值):上面的例子中属性 name 的 Value 特性就是 test。
可以使用 ES5 的 Object.defineProperty() 方法来设定属性。可以多次调用这个方法来修改同一个属性的特性,但一旦把 Configurable 修改为 false,之后再改就会有限制。使用这个方法如果不指定属性的特性是什么,默认都是 false。
访问器属性
访问器属性相比数据属性没有 [[Writable]] ,[[Value]] 这两个特性,多了 [[Get]] ,[[Get]] 两个特性。
访问器属性不能直接定义,必须通过 Object.defineProperty() 来设定。
var book = {
_year: 2004,
edition: 1
}
Object.defineProperty(book, "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
以上的例子 book 对象有三个属性,_year 和 edition 是数据属性,year 是访问器属性。通过 year 属性的 setter 函数可以修改 _year 值的同时修改 edition 的值,这种设置一个属性的值会导致其他属性发生变化是访问器属性的常见使用方式。
定义多个属性
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
与上面的例子是一样,不过这些属性都是同时定义的。
读取属性的特性
那么如何知道一个对象的属性是何特性呢?可以使用 Object.getOwnPropertyDescriptor() 方法。这个方法接收两个参数,第一个是目标对象,第二个是要读取特性的属性名称,返回一个对象。
以上面例子的 book 对象为例:
var descriptor=Object.getOwnPropertyDescriptor(book,"_year")
alert(descriptor.value) // 2004
alert(descriptor.configurable) // false
创建对象
使用 Object 构造器或对象字面量来创建很多对象时会产生大量的重复代码,可以使用工厂模式的一种变体。
工厂模式
该模式抽象了创建具体对象的过程,因为 ES 没有类,因此使用一种函数来封装以特定的接口来创建对象。
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("test", 29, "Software Engineer")
函数根据接收的参数来构建包含相应信息的对象,多次调用这个函数,每次返回的对象都包含这三个属性和一个方法。工厂模式解决了多次创建对象的问题,但没有解决对象识别的问题(即怎么知道一个对象的类型)。随着 js 的发展,又一个模式出现了。
构造函数模式
Object 和 Array 都是 js 的原生构造函数,可以创造自定义的构造函数,从而定义自定义对象类型的属性和方法。用构造函数模式将上面的工厂模式例子重写:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
alert(this.name)
}
}
var person1 = new Person("test1", 26, "Software Engineer")
var person2 = new Person("test2", 35, "Doctor")
与工厂模式相比自定义的构造函数:
- 没有显示的创建对象;
- 直接将属性和方法赋给了 this 对象;
- 没有 return;
- 按照惯例构造函数名开头字母是大写。
通过 new 操作符创建 Person 的实例有以下几个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this就指向了这个新对象);
- 执行构造函数中的代码(为新对象添加属性);
- 新对象复制给一个变量。
上面的例子中变量 person1,person2 分别保存着构造函数的两个不同的实例。两个实例(对象)各有一个 constructor(构造函数)属性,指向 Person。这就是构造函数胜过工厂模式的地方,两个实例的类型都可以被检测为 Person(对象的 constructor 属性最初就是用来检测类型的,当然,检测对象类型还是 instanceof 更可靠一些)。
将构造函数当做函数
构造函数也是函数,不是什么特殊的语法。任何用 new 操作符来调用的函数就可看做是构造函数。
如果不用 new 操作符调用 Person(),属性和方法会被添加给 Global 对象(在浏览器中就是 window 对象)。
构造函数模式的问题
构造函数中的方法在每个实例上都要重新创建一遍,上面例子中的 person1 和 person2 都有一个名为 sayName() 的方法,都不是同一个 Funtion 的实例。会导致不同的作用域和标识符解析。不同实例上的同名函数不相等:
alert(person1.sayName == person2.sayName); // false
可以有以下改进:
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("test1", 26, "Software Engineer")
var person2 = new Person("test2", 35, "Doctor")
在改进中把 sayName 函数的定义放在构造函数外,在构造函数内部把外部全局的 sayName 函数设置为 sayName 属性,构造函数里面的 sayName 属性是一个指向全局 sayName 函数的指针。
但这样还是有问题,如果对象需要很多方法呢,那就要定义很多的全局函数,那这个自定义的引用类型就没有封装性可言了,这些问题可以由原型模式来解决。
原型模式
我们创建的每一个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象包含可以由特定类型的所有实例共享的属性和方法。使用原型对象可以让所有的对象实例共享原型对象所包含的属性和方法。就是说创建实例的时候构造函数不用传入参数:
function Person() {}
Person.prototype.name = "test"
Person.prototype.age = 32
Person.prototype.job = "Doctor"
Person.prototype.sayName = function () {
alert(this.name)
}
var person1 = new Person()
person1.sayName() // "test"
var person2 = new Person()
person2.sayName() // "test"
alert(person1.sayName == person2.sayName) // true
与构造函数模式不同的是,这些实例里面的属性和方法都是共享的。
理解原型对象
几条规则:
- 创建一个新函数,会为该函数创建一个 prototype 属性,该属性指向函数的原型对象。
- 默认条件下,函数的原型对象会自动获得一个 constructor 属性,该属性指向 prototype 属性所在的函数。
- 构造函数的原型对象默认只有一个 constructor 属性,其他的属性或方法都是从 Object 继承来的。
- 调用构造函数创建的实例,内部包含一个指针指向这个实例的构造函数的原型对象,ES5 中管这个指针叫[[Prototype]],在各个浏览器中该指针实现为
__proto__
属性。
因此在上面的例子中:
person1.__proto__ == person2.__proto__ // true
ES5 中有一个 Object.getPrototypeOf() 方法,可以返回参数的构造函数的原型对象:
Object.getPrototypeOf(person1) === person1.__proto__ // true
Object.getPrototypeOf(person1) === Person.prototype // true
Object.getPrototypeOf(person1).name // "test"
如果创建一个属性与实例原型对象的一个属性同名,那访问这个对象的属性会把原型对象上的属性屏蔽(并不是替代删除,原型对象上的那个属性仍然存在)。
要像重新访问到原型对象上的这个属性可以用 delete 把对象上的同名属性删除,这样对原型对象上属性的屏蔽就消失了。
原型与 in 操作符
in 的用法:1.在for in 中遍历。2. in 操作符可以判断对象能不能访问到某个属性(不管是在实例还是原型中),例子:
function Person() {}
Person.prototype.name = "test"
Person.prototype.age = 32
Person.prototype.job = "Doctor"
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 = "test2"
alert(person1.hasOwnProperty("name")) // true
alert("name" in person1) // true
hasOwnProperty() 方法只有属性在实例上才返回 true,因此可以通过 hasOwnProperty 返回 false,in 返回 true 确定属性在原型上。
更简单的原型语法
用包含属性和方法的对象字面量来重写原型对象,例子:
function Person() {}
Person.prototype = {
name : "test",
age : 32,
job : "Doctor",
sayName : function () {
alert(this.name)
}
}
之前的方法时创建 Person 这个函数后,会自动创建他的 prototype 对象,同时这个原型对象有一个 constructor 属性指回 Person 函数,然后我们在这个原型对象里面添加属性。但是这个新的方法是用一个新对象完全重写了创建函数时自动生成的原型对象,因此 constructor 属性指向了 Object 构造函数,不再指回 Person 函数。因此 constructor 属性不能再用来确定类型了。如果要避免这种情况, constructor 赋为 Person。也可以使用 Object.defineProperty() 方法来重设 constructor 属性。
原型的动态性
由于访问一个对象的属性本质是原型链上的搜索,因此只要在访问实例属性之前修改原型链,都可以在访问实例属性的时候体现出来。
尽管像上面说的可以随时修改原型对象的属性和方法,但是如果是重写整个原型对象,那情况就不一样了。由于原型对象被重写,实例上的[[Prototype]](也就是__proto__
属性,它是指向原型对象的)指向的原型对象还是老的那个,访问人为重写的原型对象的属性和方法会报错,例子:
function Person() {}
var friend = new Person()
Person.prototype = {
name : "test",
age : 32,
job : "Doctor",
sayName : function () {
alert(this.name)
}
}
friend.sayName() // error 报错
重写前:
原生对象的原型
所有的原生引用类型,都是采用原型模式创建的。比如 Array 的构造函数的原型上就有 sort() 方法,而 String.prototype 中有 substring() 方法。
因此可以在引用类型的原型对象上添加新的方法。但是不推荐这样做,可能会导致命名冲突,也可能会意外的屏蔽对原生方法的指向。
原型对象的问题
- 原型模式省略了构造函数初始化的环节,所有的实例默认的属性值都是相同的。
- 最大的问题如果原型对象中有引用类型的属性,例子:
function Person() {}
Person.prototype = {
name : "test",
age : 32,
job : "Doctor",
aa : ['1','2'],
sayName : function () {
alert(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
person1.aa.push('3')
console.log(person1.aa) // ['1','2','3']
console.log(person2.aa) // ['1','2','3']
person1.aa == ['aa','bb']
console.log(person1.aa) // ['aa','bb']
console.log(person2.aa) // ['1','2','3']
例子中在数组中添加了一个字符串,实际上是给在原型对象中的数组添加了一个字符串,所以修改会被其他的实例反映出来。当然了,如果第一个实例是重写一个数组,那会屏蔽原来的数组,所以另一个实例不会反应出来。
所以很少有人单独使用原型模式。
*组合使用构造函数模式和原型模式
是创建自定义类型最常见的方式。构造函数模式用来定义实例属性和传递参数初始化,原型模式用来定义方法和共享的属性,重写之前代码的例子:
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)
}
}
这是目前使用最广泛,认同度最高的一种创建自定义类型的模式。
。。。还有几种模式,不想看了