前言
面对对象的语言有一个标志,那就是他们都有类的概念,而通过类可以创建任意多具有相同属性和方法的对象。js语言没有类的概念,因此他的对象也与基于类的语言中对象有所不同,esma-262中定义js对象为:
无序属性的集合,其属性可以包含基本值、对象和函数
属性类型
ECMAScript中有两种属性:数据属性和访问器属性
数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据值有四个描述其行为的特性:
- configuration,表示能否通过delete删除属性从而重新定义属性。
- enumerable,表示能否通过for-in循环返回属性
- writable,能否修改属性的值
- value,包含这个属性的数据值
修改属性值必须通过Object.defineProperty()方法,该方法接受三个参数,属性所在的对象,属性的名称,和一个描述符对象描述符对象的属性必须是上面四个特性。
访问器属性
访问器属性不包含数据值,它包含一对setter和getter函数。读取调用getter,写入调用setter,访问器属性包含四个特性:
- configuration,表示能否通过delete删除属性从而重新定义属性。
- enumerable,表示能否通过for-in循环返回属性
- get,在读取属性值是调用的函数
- set,在写入属性时调用的函数
访问器属性不能直接定义,必须通过Object.defineProperty()方法。
由于定义多个属性的概率很大,es5又定义了一个Object.defineProperties()方法
该方法接受两个参数,第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应,eg:
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;
}
}
}
})
对象创建
工厂模式
function createPerson(){
var obj = new Object();
obj.name = 'abc';
obj.age = 30;
obj.sex = 'M';
return obj;
}
工厂模式虽然解决了创建多个像是对象的问题,但是没有解决对象识别的问题,即怎么知道一个对象的类型。
构造函数模式
function Person(){
this.name = 'abc';
this.age = 30;
this.sex = 'M'
}
构造函数解决了对象识别的问题,也很好用,但是也有缺点,这种方式的缺点会在每一个实例中保存一份属性的副本,会对性能造成影响。
原型对象模式
function Person(){
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
alert(this.name)
};
var p1 = new Person();
p1.sayName(); // Nicholas
var p2 = new Person();
p2.sayName();// Nicholas
alert(p1.sayName == p2.sayName) //true
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途包含可以由特定类型的所有实例共享的属性和方法。用这个对象的好处是可以让所有对象实例共享它所包含的属性和方法。
上图描述了Person构造函数、Person的原型对象以及Person现有的两个实例之间的关系。此外,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。找到则返回,没找到则继续搜索指针指向的原型对象,也就是说如果原型对象和实例的属性名相同的话,则优先执行实例的属性。hasOwnProperty()可以判断属性值是存在于实例还是原型。p1.hasOwnProperty("name") 返回true。
原型与in操作符
单独使用in还是在for-in循环中使用,in操作符都会访问到实例属性和原型属性。如果判断属性是否存在于原型中,可以通过下面这种方式实现:
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
当然也可以通过 Object.keys(Person.prototype) 来获取,方法返回一个可枚举属性的字符串数组。或者Object.getOwnPropertyNames(Person.prototype)返回一个可枚举属性的字符串数组。但是上面两种方法对不可枚举的属性失效,只能用for-in更简单的原型语法:
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
但是问题是,原型的constructor指针不再只想Person了。本质上这种写法是重写了prototype对象可以增加一个从constructor并只想Person来解决:
function Person(){
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
但是也有问题,这样做会把constructor的enumerable特性被改成true,最好的解决方案是如下:
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
原型对象的缺点和解决办法
原型对象的缺点是初始化参数共享,对函数共享是非常合适,但是基本值的属性却会引发安全性问题。综合构造函数模式的缺点和原型对象的缺点,通过组合使用构造函数模式与原型对象模式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性副本,但同时又共享着对方法的引用,最大限度地节省了内存。
组合模式如下:
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,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
上文为《JavaScript高级编程》的读书笔记,关于JavaScript面对对象编程的章节。