JavaScript4.1

第 6 章“面向对象的程序设计”
ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。

6.1.1ECMAScript 中有两种属性:数据属性和访问器属性。
1.数据属性
 [[Configurable]]:表示能否通过delete 删除属性从而重新定义属性,能否修改属性的特
性,或者能否把属性修改为访问器属性
 [[Enumerable]]:表示能否通过for-in 循环返回属性
 [[Writable]]:表示能否修改属性的值。
 [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,
把新值保存在这个位置。这个特性的默认值为undefined。

直接在对象上定义的属性,它们的[[Configurable]]、[[Enumerable]]和[[Writable]]特性都被设置为true,而[[Value]]特性被设置为指定的值。例如:

var person = {
name: "Nicholas"
};
var person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Nicholas"
});
alert(person.name); //"Nicholas"
delete person.name;
alert(person.name); //"Nicholas"

要修改属性默认的特性,必须使用ECMAScript 5 的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable 和value。设置其中的一或多个值,可以修改对应的特性值。例如:

var person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"

2.访问器属性
使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。
访问器属性有如下4 个特性。
 [[Configurable]]:表示能否通过delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true。
 [[Enumerable]]:表示能否通过for-in 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true。
 [[Get]]:在读取属性时调用的函数。默认值为undefined。
 [[Set]]:在写入属性时调用的函数。默认值为undefined。

//创建了一个book对象,对象有两个默认的属性。
var book = {
	_year: 2004,
	edition: 1
};
//访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
//定义访问器属性year
Object.defineProperty(book, "year", {
	//访问器属性year包含一个getter函数和一个setter函数
	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.2 定义多个属性
ECMAScript 5 又定义了一个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 和edition)一个访问器属性(year)
最终的对象与上一节中定义的对象相同。唯一的区别是这里的属性都是在同一时间创建的。

6.1.3 读取属性的特性
ECMAScript 5 的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get 和set;如果是数据属性,这个对象的属性有configurable、enumerable、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"

6.2.1 JavaScript的工厂模式
考虑到在ECMAScript 中无法创建类,就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下面的例子所示。

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");

6.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");

//instanceod 判断类型
alert(person1, instanceof Object);
alert(pewrson1, instanceof Person);

1.构造函数不存在定义构造函数的特殊语法。
2.任何函数只要通过new操作符来调用,那它就可以作为构造函数

//当做构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();//"Nicholas"

//当做普通函数调用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); // "Greg"

//在另一个的对象o的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"

//案例1:

//sayName放在构造函数中的话,每创建一个实例就会产生一个sayName的Function
//而放在构造函数外面的话,无论创建多少个实例只会产生一个sayName的Function
//但是这样做会破坏Person的封装性

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");

实例属性和原型属性
6.2.3 原型模式
每个函数都有一个prototype(原型)属性,这个属性是一个指针。
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则返回的是false

在此,我们将sayName()方法和所有属性直接添加到了Person 的prototype 属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。

在我们调用person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例person1 有sayName 属性吗?”答:“没有。”然后,它继续搜索,再问:“person1 的原型有sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

前面提到过,原型最初只包含constructor 属性,而该属性也是共享的,因此可以通过对象实例访问。

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"——来自原型

在这个例子中,person1 的name 被一个新值给屏蔽了。但无论访问person1.name 还是访问
person2.name 都能够正常地返回值,即分别是"Greg"(来自对象实例)和"Nicholas"(来自原型)。
当在alert()中访问person1.name 时,需要读取它的值,因此就会在这个实例上搜索一个名为name
的属性。这个属性确实存在,于是就返回它的值而不必再搜索原型了。当以同样的方式访问person2.
name 时,并没有在实例上发现该属性,因此就会继续搜索原型,结果在那里找到了name 属性。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这
个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,如下所示。

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 person1.name;
alert(person1.name); //"Nicholas"——来自原型

在这个修改后的例子中,我们使用delete 操作符删除了person1.name,之前它保存的"Greg"
值屏蔽了同名的原型属性。把它删除以后,就恢复了对原型中name 属性的连接。因此,接下来再调用person1.name 时,返回的就是原型中name 属性的值了。
使用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

通过使用hasOwnProperty()方法,什么时候访问的是实例属性,什么时候访问的是原型属性就
一清二楚了。调用person1.hasOwnProperty( “name”)时,只有当person1 重写name 属性后才会
返回true,因为只有这时候name 才是一个实例属性,而非原型属性。

in 操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。

//in的用法之一:
alert("name" in person1); //true

同时使用hasOwnProperty()方法和in 操作符,就可以确定该属性到底是存在于对象中,还是存在于
原型中,如下所示。例子:

function hasPrototypeProperty(object, name){
	return !object.hasOwnProperty(name) && (name in object);
}
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false

在这里,name 属性先是存在于原型中,因此hasPrototypeProperty()返回true。当在实例中
重写name 属性后,该属性就存在于实例中了,因此hasPrototypeProperty()返回false。即使原
型中仍然有name 属性,但由于现在实例中也有了这个属性,因此原型中的name 属性就用不到了。

//in的用法之二:
在使用for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false 的属性)的实例属性也会在for-in 循环中返回,因为根据规定,所
有开发人员定义的属性都是可枚举的——只有在IE8 及更早版本中例外。

要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5 的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"

//如果想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"
//结果中包含了不可枚举的constructor 属性。Object.keys()和Object.getOwnPropertyNames()方法都可以用来替代for-in 循环。

//在JavaScript中,对象的属性分为可枚举和不可枚举之分,它们是由属性的enumerable值决定的。可枚举性决定了这个属性能否被for…in查找遍历到。

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,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor 和方法sayName()则是在原型中定义的。而修改了person1.friends(向其中添加一个新字符串),并不
会影响到person2.friends,因为它们分别引用了不同的数组。

6.2.5 动态原型模式
错误用法案例:

function Person(){
}
var friend = new Person();
Person.prototype = {
	constructor: Person,
	name : "Nicholas",
	age : 29,
	job : "Software Engineer",
	sayName : function () {
		alert(this.name);
	}
};
friend.sayName(); //error
//重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
function Person(name, age, job){
	//属性
	this.name = name;
	this.age = age;
	this.job = job;
	//方法
	if (typeof this.sayName != "function"){
		Person.prototype.sayName = function(){
			alert(this.name);
		};
	}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

//这里只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。

使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果
在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

重要:
//回顾ECMAScript规范所有函数都包含两个方法(这两个方法非继承而来),call、apply。这两个函数都是在特定的作用域中调用函数,能改变函数的作用域,实际上是改变函数体内的this值。

//父类 Person
function Person(){   
	this.sayName = function(){
		return this.name;
	}
}

//子类Chinese
function Chinese(name){
	//借助call实现继承
	
	Person.call(this);//可以使用Person是因为在定义Chinese函数之前已经定义过函数Person
				  //Person可以看做 是一个函数指针对象。这个函数指针对象Person都包含两个方法call/apply
				  //调用Person对象的一个方法,以另一个对象this替换当前Person对象。
	 //在特定的作用域中调用函数,tihs是谁,作用域就是哪,现在的this指的不是Person,是Chinese的调用对象
	 //作用域就是Chinese的调用对象的作用域
	 
	this.name = name;//初始化Chinese的成员数据name

	this.ch = function(){
		alert("我是中国人");
	};
}

//子类America
function America(){
	Person.call(this);
	this.name = name;
	
	this.ch = function(){
		alert("I am America");
	};
}


//测试代码 
var chinese = new Chinese("张三");//创建函数对象chinese;调用构造Chinese
							    //而在构造中 借助了Person.call实现了继承
console.log(chinese.sayName());  //因为实现了继承,所以chinese可以调用父类Person的sayName
							   //然而传入的参数是this,现在的this指的不是Person,chinese

var america = new America('America');
 //调用 父类方法
  console.log(america.sayName());   //输出 America


猜你喜欢

转载自blog.csdn.net/baidu_19552787/article/details/84649078