一、JS中对象的定义
无序属性的集合,其属性可以包含基本值、对象或者函数。换句话说,对象是一组没有特定顺序的值,对象的每个属性和方法都有一个名字,而每个名字都映射到一个值。此时,我们可以将对象想象成散列表,无非就是一组名值对,其中值可以是数据或函数。
js中的对象与C#或C++不同,后者是基于类和类的实例,而前者是基于原型继承的。所谓原型继承,即通过创建已有对象的新实例来进行。原型扩展可以通过新的属性和方法来扩展一个已有对象,而不是通过继承来扩展。
二、理解对象
1. 对象的创建,使用对象字面量
<script type="text/javascript">
var person = {
name: 'xushuai',
sex: '男',
age: 23,
job: '软件工程师',
sayName: function () {
alert(this.name);
}
};
</script>
2. 对象中的属性类型
2.1 数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值,且有4个描述其行为的特性:
[[Configurable]] | 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者把属性修改为访问器属性 该配置一旦修改后,就不能再修改回去 | 默认值true |
[[Enumerable]] | 表示能否通过for-in循环返回属性 | 默认值true |
[[Writable]] | 表示能否修改属性的值 | 默认值true |
[[Value]] | 包含这个属性的数据值,读取属性以及写入属性值都从这个位置 | 默认值undefined |
要修改属性默认的特性,必须使用Object.defineProperty()方法
属性所在的对象 | |
属性的名字 | |
描述符对象 | configuable、enumerable、writable、value |
【示例】
<script type="text/javascript">
var person = {
name: 'xushuai',
sex: '男',
age: 23,
job: '软件工程师',
sayName: function () {
alert(this.name);
}
};
alert(person.name);//xushuai
person.sayName();//xushuai
Object.defineProperty(person, 'name', {
writable: false//定义为name属性的值不可重写,因此,接下来无论怎么改都不会变
});
person.name = 'xushuaichanged';
person.sayName();//xushuai
Object.defineProperty(person, 'age', {
writable: false,
value: 22
});
alert(person.age);//22
</script>
2.2 访问器属性
访问器属性不包含数据值;它们包含一对getter和setter函数,只不过并不是必须的。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理函数。
[[Configurable]] | 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者把属性修改为访问器属性 该配置一旦修改后,就不能再修改回去 | 默认值true |
[[Enumerable]] | 表示能否通过for-in循环返回属性 | 默认值true |
[[Get]] | 在读取属性时调用的函数 | 默认值undefined |
[[Set]] | 在写入属性时调用的函数 | 默认值undefined |
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
读取属性的特性
方法:Object.getOwnPorpertyDescriptor()
参数:属性所在的对象,属性名称
<script type="text/javascript">
var person = {
name: 'xushuai',
sex: '男',
_age: 23,//下划线是一种标记,表示只能通过对象方法访问的属性
job: '软件工程师',
sayName: function () {
alert(this.name);
}
};
Object.defineProperty(person, 'age', {
get: function () {
return this._age;
},
set: function () {
if (arguments[0] > 23) {
this._age = arguments[0];//当所设置的值大于23时才设置有效
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(person, 'name');
alert(descriptor.configurable + " " + descriptor.value);//true xushuai
var descriptor2 = Object.getOwnPropertyDescriptor(person, '_age');
alert(descriptor2.configurable + " " + descriptor2.value);//true 23
</script>
三、创建对象
3.1 工厂模式
该模式抽象了创建具体对象的过程,用函数封装以特定接口创建的对象的细节。
<script type="text/javascript">
function createperson(name, sex, age, job) {
var o = new Object();
o.name = this.name;
o.sex = this.sex;
o.age = this.age;
o.job = this.job;
o.sayName = function () {
alert(this.name);
};
return o;
}
var person1 = createperson("xushaui", '男', 23, "软件工程师");
var person2 = createperson('hah', '女', 22, '个体');
alert(typeof person1);//object 工厂模式虽然解决了创建多个相似对象的问题,却没有解决对象识别的问题
</script>
3.2 构造函数模式
该模式解决了工厂模式的问题,可以识别对象类型。
<script type="text/javascript">
function Person(name, sex, age, job) {
this.name = name;
this.sex = sex;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
alert(person1 instanceof Person);//true
//构造函数也可以当作普通函数来使用
var o = new Object();
Person.call(o, 'xushaui', '男', 23, '软件工程师');
o.sayName();//xushuai
</script>
3.3 原型模式
我们创建的每一个函数都有prototype(原型)属性,该属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法,换言之,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
<script type="text/javascript">
function Person() {
}
Person.prototype = {
name: 'xushuai',
sex: '男',
age: 23,
sayName: function () {
alert(this.name);
}
};
</script>
无论何时,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,该属性包含一个指向prototype属性所在函数的指针。而我们创建的实例中都有一个prototype属性,指向原型。
虽然我们无法访问到[[prototype]],但是可以通过isPrototype()方法来确定对象之间是否存在这种关系。
alert(Person.prototype.isPrototypeOf(person1));//true
Object.getPrototypeOf()可以方便的取得一个对象的原型。
<script type="text/javascript">
function Person() {
}
Person.prototype = {
name: 'xushuai',
sex: '男',
age: 23,
sayName: function () {
alert(this.name);
}
};
var person1 = new Person();
alert(Object.getPrototypeOf(person1) == Person.prototype);//true
alert(Object.getPrototypeOf(person1).name);//xushuai
</script>
当为对象添加一个属性时,这个属性就会屏蔽原型中对象保存的同名属性,但不会修改那个属性。此时我们访问这个实例的属性时,就不会得到原型中对应属性的值。不过我们可以使用delete操作符删除实例属性,这样就能够重新访问原型中的属性。
<script type="text/javascript">
function Person() {
}
Person.prototype = {
name: 'xushuai',
sex: '男',
age: 23,
sayName: function () {
alert(this.name);
}
};
var person1 = new Person();
alert(person1.name);//xushuai
person1.name = 'exchanged';
alert(person1.name);//exchanged
var person2 = new Person();
alert(person2.name);//xushuai
delete person1.name;
alert(person1.name);//xushuai
</script>
使用hasOwnProperty()方法可以检测一个属性是存在于实例中还是原型中。in操作符无论属性存在于实例还是原型,只要存在即为真;而对于Object.keys()方法,这个方法取得所有可枚举的实例属性。
<script type="text/javascript">
function Person() {
}
Person.prototype = {
name: 'xushuai',
sex: '男',
age: 23,
sayName: function () {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.name = 'changed';
alert(person1.hasOwnProperty('name'));//true,来源于实例
alert(person1.hasOwnProperty('hah'));//false
alert(person2.hasOwnProperty('name'));//false,来源于原型
alert('name' in person1);//true,in无论属性来源于实例还是原型,只要存在就为真
alert('name' in person2);//true
for (var prop in Person.prototype) {
alert(prop);//name,sex,age,sayName
}
for (var prop in person1) {
alert(prop);//name,sex,age,sayName
}
var keys = Object.keys(person1);
alert(keys);//name
var keys1 = Object.keys(Person.prototype);
alert(keys1);//name,sex,age,sayName
var keys2 = Object.keys(person2);
alert(keys2);//空
</script>
原型模式的重要性不仅体现在创建自定义类型方面,就连多有原生的引用类型,都是采用这种模式创建的。所有原生类型都在其构造函数的原型上定义了方法。例如数组,我们可以通过Array.prototype找到sort()方法。通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。
<script type="text/javascript">
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = 'hello world';
alert(msg.startsWith('hello'));//true
String.prototype.test = function () {
return 'hahah';
};
alert(msg.test());//hahah
</script>
原生对象的问题由其共享的本质引起,尤其对于包含引用值类型的属性来说,将是很大的打击。
<script type="text/javascript">
function Person() {
}
Person.prototype={
name:'xushuai',
sex:'男',
age: 23,
friends:['xs','zdp'],
sayName:function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.add = 'add';
person2.friends.push('hehe');
alert(person1.add);//add
alert(person2.add);//undefined
alert(person1.friends);//xs,zdp,hehe
</script>
3.4 组合使用构造函数模式和原型模式
【描述】
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这样当使用new操作符时每个创建的对象实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。且构造函数模式还可以传递参数。
【示例】
<script type="text/javascript">
function person(name, sex, age, job) {
this.name = name;
this.sex = sex;
this.age = age;
this.job = job;
this.friends=['xushuai','huxiang']
}
person.prototype = {
constructor: person,
sayName: function () {
alert(this.name);
}
}
</script>
四、继承
4.1 继承方式
接口继承:由于JS不支持方法签名,因此无法进行接口继承
实现继承:在JS中,实现继承主要是依靠原型链来实现的
4.2 原型链
【原型链作为实现继承的主要方法,其基本思想为】
利用原型让一个引用类型继承另一个引用类型的属性和方法。
【描述】
假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型的指针也包含着指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,此时这样的关系依然成立,如此层层递进,就构成了实例与原型的链条。
【示例】
<script type="text/javascript">
function Person() {
}
Person.prototype = {
name: 'xushuai',
age: 23
};
function Student() {
}
Student.prototype = {
id:'123',
grade:1
}
function sun() {
}
sun.prototype = {
prop:'hahah'
}
//Student继承Person
Student.prototype = new Person();
var stu = new Student();
alert(stu.name);//xushuai
//sun继承Student
sun.prototype = new Student();
var su = new sun();
alert(su.age);//23
alert(su.id);//undefined
</script>
分析:以上主要定义了三个类型。它们的主要区别是Student类继承了Person,而sun继承了Student,也可以说直接继承了Person。值得注意的是,继承是通过创建实例达到的。实现的本质是重写原型对象,代之以一个新类型的实例。记住,我们不应该忽略了默认的原型Object。值得注意的是,上述代码有一个致命缺陷,就是我先定义了属性和方法,然后才实现继承,这就导致我并不能访问“子类”中的属性和方法了!!
【定义方法】
子类型有时需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法,无论如何,给原型添加方法的代码一定要放在替换原型的语句之后。
【错误示例---使用字面量代码无效】
<script type="text/javascript">
function Person() {
this.age = 23;
}
Person.prototype = {
name: 'xushuai',
sayAge: function () {
return this.age;
}
};
function Student() {
}
Student.prototype = {
id:'123',
grade: 1,
//添加新方法
sayGrade: function () {
alert(this.id);
},
//重写超类型中的方法
sayAge: function () {
return this.age + 1;
}
}
//Student继承Person
Student.prototype = new Person();
var stu = new Student();
alert(stu.sayAge());//23
</script>
【正确示范】
<script type="text/javascript">
function Person() {
this.age = 23;
}
Person.prototype.name = 'xushuai';
Person.prototype.sayAge = function () {
return this.age;
};
function Student() {
this.grade = '一年级';
}
//Student继承Person,值得注意的是,一定要先继承,否则,新添加的新方法以及重写的方法是访问不到的,包括属性
Student.prototype = new Person();
Student.prototype.id = '123';
//添加新方法
Student.prototype.sayGrade = function () {
return this.grade;
};
//重写超类型中的方法
Student.prototype.sayAge = function () {
return this.age+1;
};
var stu = new Student();
alert(stu.sayAge());//24
alert(stu.id);//123
</script>
4.3 继承一个对象的功能
<script>
//"父类"
function Person() {
this.prop = 'human';
this.level = 'hign';
}
Person.prototype.returnLevel = function () {
return this.level;
};
function Student() {
this.professior = 'student';
this.job = 'study';
//该方法将两个对象的构造函数连接起来,若需要在两个对象之间传递参数列表,则使用apply()
Person.call(this);//很重要,这涉及到是否能够访问到“父类”中的属性
}
//无"继承"关系
console.log(new Person().prop);//human
console.log(new Student().professior);//student
console.log(new Student().level);//undefined
//"继承"方式一
//Student.prototype = new Person();
//console.log(new Student().level);//hign
//"继承"方式二
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
var stu = new Student();
console.log(stu instanceof Student);//true
console.log(stu instanceof Person);//true
console.log(stu.prop);//human
console.log(stu.returnLevel());//hign
console.log(new Person().returnLevel());//hign
</script>
4.4 通过定义一个新的属性来扩展对象
4.4.1 Object.defineproperty()方法
该方法是给对象添加一个属性而不直接赋值的方法,能够对对象的行为和状态有某些控制,例如,是否可修改,是否可删除等等。该方法创建的属性可以通过两种方式来控制,一种是数据描述符;另一种就是访问方法描述符,在一个getter-setter函数对中定义。
<script>
var data = {};
//第一种变体
//给对象data添加一个属性type,并设置初始值,不能够修改或删除
Object.defineProperty(data, "type", {
value: 'student',
enumerable:true
});
console.log(data.type);//student
//测试:修改初始值
data.type = "changed";
console.log(data.type);//student
//测试:删除初始值
delete (data.type);
console.log(data.type);//student
//第二种变体
var test = {};
var num = 23;
Object.defineProperty(test, "age", {
get: function () { return num },
set: function (value) { num = value },
enumerable: true,
configurable:true
});
console.log(test.age);//23
num = 25;
console.log(test.age); 25
//注释
/*
configurable:默认为false,控制属性描述符是否可以修改
enumerable:默认为false,控制属性是否可以枚举
writable:默认为false,控制属性的值是否可以修改
value:该属性的初始值
get:默认为undefined,属性的getter
set:默认为undefined,属性的setter
*/
</script>
4.5 阻止对象可扩展性,即添加新的属性
<script>
"use strict"
var test = {
type: "01",
job: "02",
sayJob: function () {
return this.job
}
};
try{
Object.preventExtensions(test);
test.type2 = "44";
} catch (e) {
console.log(e);
}
</script>
方法 | 描述 |
Object.preventExtensions() | 不允许对象扩展新的属性 |
Object.seal() | 阻止扩展新的属性以及对属性描述符的修改,但可以修改已有的属性值 |
Object.freeze() | 阻止扩展新的属性,限制修改描述符,也不能修改已有的属性 |