声明:此文集下的文章都是看了Javascript
高级程序设计所做的笔记。此章对应书中的第六章。
一、理解对象
在JS
早期开发中经常使用以下两种方式创建对象:
var person = new Object();
person.name = "Tom";
person.age = 23;
person.job = "Software Engineer";
person.sayName = function(){
console.log(this.name);
}
var person = {
name : "Tom",
age : 29,
job : "Software Engineer",
sayName : function(){
console.log(this.name);
}
};
说明:这里创建了一个对象,此对象有三个属性和一个方法。在第二种方式中,属性名可以是name、'name'
或者"name"
,但是如果属性是保留字或者以数字开头则必须加引号。
1.1 属性类型
在ECMAScript
中有两种属性:数据属性和访问器属性。
1、数据属性
-
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,对于直接在对象上定义的属性(如上),默认为true
,表示可以。 -
[[Enumerable]]
:表示能否通过for-in
循环返回每个属性,对于直接在对象上定义的属性,默认为true
。 -
[[Writable]]
:表示能否修改属性的值,对于直接在对象上定义的属性,默认为true
。 -
[[Value]]
:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认为undefined
。
如果要修改属性默认的特性,必须使用ECMAScript5
中的Object.defineProperty()
方法。接收三个参数:属性所在对象、属性的名字和一个描述符对象,其中描述符对象必须是:configurable、enumberable、writable、value
,比如:
var person = {};
Object.defineProperty(person, 'name', {
writable : false,
value : "Tom"
});
console.log(person.name);
person.name = "Jerry";
console.log(person.name);
说明:这里我们将writable
属性设置为了false
,标明此属性的值不能被修改了,可以看到使用此方法就能精确控制每个属性了。这里没有设置的属性则取false
(这和之前定义时不同)。当然在后面的代码中我们还可以将此属性改为true
,但是如果属性configurable
被设置为了false
则表示不能使用delete
将某个属性删除掉了,同时也表示此属性变为不可配置了,即如果配置了writable
,则在后面的代码中不能改变其值了。同时configurable
本身也不能改变。如:
var person = {};
Object.defineProperty(person, 'name', {
configurable : false,
value : "Tom"
});
//抛出错误
Object.defineProperty(person, 'name', {
configurable : true,
value : "Tome"
});
2、访问器属性(存取器属性)
访问器属性不包括数据值,它们是一对儿getter
和setter
函数(不过,这两个函数都不是必须的),有以下四个属性:
-
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,对于直接在对象上定义的属性,默认为true
,表示可以。 -
[[Enumerable]]
:表示能否通过for-in
循环返回每个属性,对于直接在对象上定义的属性,默认为true
。 -
[[Get]]
:在读取属性时调用的函数。默认值为undefined
。 -
[[Set]]
:在写入属性时调用的函数。默认值为undefined
。
访问器属性不能直接定义,必须使用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 = 3000;
alert(book._year);
说明:以上代码中我们给对象book
定义了两个属性_year、edition
,其中_year
前面的下划线是一种标记,表示只能通过对象方法访问的属性,当然这里我们还是可以直接访问的。这里还定义了一个访问器属性year
,包含getter、setter
方法,修改year
的值会导致_year
值改变,而edition
变为2
,起始访问器属性就像一种标志,可以在方法中做一些操作来达到某个目的,比如这里在setter
方法中我们就让edition
的值随着year
的值改变而改变。
定义getter
与setter
方法还可以这样:
var person = {
name: "Tom",
get age() {
return 12;
}
}
console.log(person.age);
而此时age
也是对象的一个属性。
在这两个方法之前一般使用的是两个非标准的方法:__defineGetter__()、__defineSetter__()
,使用这两个方法实现之前的代码:
var book = {
_year: 2004,
edition: 1
};
//legacy accessor support
book.__defineGetter__("year", function(){
return this._year;
});
book.__defineSetter__("year", function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
});
book.year = 2005;
alert(book.edition); //2
1.2 定义多个属性
使用Object.defineProperty()
方法定义属性太麻烦,我们可以使用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;
book._year = 100;//无效,因为这样定义的属性是不能修改的
console.log(book._year);
console.log(book.edition);//2
1.3 读取属性的特性
使用ECMAScript 5
的Object.getOwnPropertyDescriptor()
方法,可以取得给定属性的描述符。接收两个参数:属性所在对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,则这个对象的属性有configurable、enumberable、get、set
;如果是数据属性,则这个对象的属性有configurable、enumberable、writable、value
。如:
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"
二、创建对象
虽然使用Object
构造函数或对象字面量都可以用来创建单个对象,但这些方式使用同一个接口创建很多对象,会产生大量的重复代码。下面看几种其他的方式。
2.1 工厂模式
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"
说明:这里使用函数来封装以特定接口创建对象的细节,这中方式虽然解决了相似对象的问题,但是却没有解决对象识别的问题(即怎样知道一个对象的类型)。
2.2 构造函数模式
可以使用构造函数将前面例子重写:
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");
说明:这里使用Person()
函数取代了createPerson()
函数,有几点不同的地方:
- 没有显式地创建对象
- 直接将属性和方法赋给
this
对象 - 没有
return
语句
这里要创建Person
新实例,必须使用new
操作符,上述代码创建了两个Person
实例,都有一个相同的construtor
(构造函数)属性:
console.log(person1.construtor == Person);//true
console.log(person2.construtor == Person);//true
对象的construtor
属性最初是来标识对象类型的,但是,在检测对象类型时,还是使用instanceof
操作符更可靠一些。
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true
这里所有对象均继承自Object
。
2.2.1 将构造函数当作函数
任何函数,只要通过new
操作符来调用,那它就可以作为构造函数,而任何函数,如果不通过new
操作符来调用,那它跟普通的函数也一样。如前面定义的Person
构造函数:
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"
说明:当在全局作用域中调用一个函数时,this对象总是指向Global
对象(在浏览器中就是window
对象)。也可以使用call()
或apply()
调用。
2.2.2 构造函数的问题
构造函数模式的主要问题就是每个方法都要在每个实例上重新创建一遍。因为函数就是对象,因此没定义一个函数,也就是实例化了一个对象,于是构造函数可以定义为这样:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)");
}
说明:不同实例上的同名函数sayName
不是相等的。这里我们可以将函数定义在构造函数之外:
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("Tom", 29, "Software Engineer");
var person2 = new Person("Jerry", 28, "Doctor");
说明:此时两个实例中的同名函数sayName
就是同一个了,但是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实,而且,如果要定义很多方法,那就得定义多个全局函数,那自定义的引用类型就没有封装性了。这需要使用原型模式解决。
2.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
说明:我们将sayName()
方法和所有属性都直接添加到了Person
的prototype
属性中,构造函数变成了空函数,当然还是可以通过构造函数来创建新对象,而且新对象还会具有相同的属性和方法。
1、理解原型对象
说明:无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个
prototype
属性,这个属性指向函数的原型对象,如图中的构造函数Person
、实例person1、person2
其中的prototype
属性都指向原型对象。创建了自定义的构造函数之后,其原型对象默认只会取得construtor
属性,至于其他方法,都是从Object
继承或我们自己添加的。一般使用属性__proto__
访问原型对象。虽然在所有实现中都无法访问到
[[Prototype]]
,但是可以通过isPrototypeOf()
方法来确定对象之间是否存在这种关系,本质上讲,如果[[Prototype]]
指向调用isPrototypeOf()
方法的对象(Person.prototype
),那么这个方法就返回true
:
alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true
在ECMAScript 5
中增加了一个新方法,叫Object.getPrototypeOf()
,在所有支持的实现中,这个方法返回[[Prototype]]
的值,如:
alert(Object.getPrototypeOf(person1) == Person.prototype);//true
alert(Object.getPrototypeOf(person1).name);//"Tom"
说明:每当代码读取某个对象的某个属性时,都会执行搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找了具有给定名字的属性,则返回该属性的值;如果没有,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果找到则返回相关的值。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。看如下例子:
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();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"来自实例
alert(person2.name); //"Nicholas"来自原型
说明:在实例中增加一个和原型对象中同名属性,只会屏蔽原型中的属性,并不会覆盖。当在搜素时,如果在实例中搜索到了相关属性,则不会继续向原型中搜索了。当然使用delete
方法可以完全删除实例中的某个属性,从而放我们能够重新访问原型中的属性。
delete person1.name;
alert(person1.name); //"Nicholas"来自原型
使用hasOwnProperty()
方法可以检测一个属性是存在于实例中,还是存在于原型中,这个方法是从Object
继承过来的。只在给定属性存在于对象实例中,才会返回true
。
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();
var person2 = new Person();
alert(person1.hasOwnProperty("name"));//false
person1.name = "Greg";
alert(person1.name); //"Greg"来自实例
alert(person1.hasOwnProperty("name"));//true
alert(person2.name); //"Nicholas"来自原型
alert(person2.hasOwnProperty("name"));//false
delete person1.name;
alert(person1.name); //"Nicholas"来自原型
alert(person1.hasOwnProperty("name"));//false