本文系统梳理JavaScript面向对象编程核心概念,详解Object构造函数、字面量及五种对象创建模式(工厂、构造函数、原型、组合、动态原型)。深入解析ECMAScript数据属性与访问器属性的四大特征,涵盖Object.defineProperty等API的实际应用,帮助开发者掌握高效、规范的JS对象设计与内存优化技巧。
JavaScript对象基础概念
面向对象语言的核心标志是具备类的概念,通过抽象实例对象的公共属性与方法,基于类可创建任意多个实例,通常具备封装、继承和多态特性。然而,JavaScript中的对象与纯面向对象语言存在差异。ECMA标准将JS对象定义为无序属性的集合,属性值可以是基本类型、对象或函数。简言之,JS对象是一组无序值的集合,每个属性或方法都有对应名称,通过该名称可访问映射的值(基本值、对象或方法)。
一、理解对象:
第一种方式:基于Object对象
var person = new Object();
person.name = 'My Name';
person.age = 18;
person.getName = function(){
return this.name;
}
第二种方式:对象字面量(能更清晰地展示对象包含的属性及方法)
var person = {
name : 'My name',
age : 18,
getName : function(){
return this.name;
}
}
JS对象支持使用‘.'操作符动态扩展属性,也可通过'delete'操作符或将属性值设为'undefined'来删除属性。示例如下:
person.newAtt='new Attr';//添加属性
alert(person.newAtt);//new Attr
delete person.age;
alert(person.age);//undefined(删除属性后值为undefined);
对象属性类型与特征
二、对象属性类型
ECMA-262第5版定义了JS对象属性的特征(供JS引擎使用,外部无法直接访问)。ECMAScript包含两种属性:数据属性和访问器属性。
1、数据属性:
数据属性指包含数据值的位置,可在此读取或写入值。该属性具有四个描述其行为特性的特征:
[[configurable]]:表示能否使用delete操作符删除从而重新定义,或能否修改为访问器属性。默认为true;
[[Enumberable]]:表示是否可通过for-in循环返回属性。默认true;
[[Writable]]:表示是否可修改属性的值。默认true;
[[Value]]:包含该属性的数据值。读取或写入均针对此值。默认为undefined;如前述实例对象person中定义了name属性,其值为'My name',对该值的修改均反映在此位置。
若要修改对象属性的默认特征(默认均为true),可调用Object.defineProperty()方法。它接收三个参数:属性所在对象、属性名以及一个描述符对象(必须包含configurable、enumberable、writable和value,可设置一个或多个值)。
示例如下:(浏览器支持情况:IE9+、Firefox 4+、Chrome、Safari5+)
var person = {};
Object.defineProperty(person, 'name', {
configurable: false,
writable: false,
value: 'Jack'
});
alert(person.name);//Jack
delete person.name;
person.name = 'lily';
alert(person.name);//Jack
可以看出,delete及重置person.name的值均未生效,这是因为调用defineProperty函数修改了对象属性的特征。值得注意的是,一旦将configurable设置为false,则无法再使用defineProperty将其修改为true(执行会报错:can't redefine non-configurable property);
2、访问器属性:
它主要包含一对getter和setter函数。读取访问器属性时,会调用getter返回有效值;写入访问器属性时,调用setter写入新值。该属性具有以下四个特征:
[[Configurable]]:是否可通过delete操作符删除重新定义属性;
[[Numberable]]:是否可通过for-in循环查找该属性;
[[Get]]:读取属性时调用,默认:undefined;
[[Set]]:写入属性时调用,默认:undefined;
访问器属性不能直接定义,必须使用defineProperty()来定义,示例如下:
var person = {
_age: 18
};
Object.defineProperty(person, 'isAdult', {
get: function () {
if (this._age >= 18) {
return true;
} else {
return false;
}
}
});
alert(person.isAdult?'成年':'未成年');//成年
从上述代码可知,定义访问器属性时getter与setter函数并非必须,并且在定义getter与setter时不能指定属性的configurable及writable特性;
此外,ECMA-262(5)还提供了一个Object.defineProperties()方法,可用于一次性定义多个属性的特性:
var person = {};
Object.defineProperties(person,{
_age:{
value:19
},
isAdult:{
get: function () {
if (this._age >= 18) {
return true;
} else {
return false;
}
}
}
});
alert(person.isAdult?'成年':'未成年');//成年
上述代码使用Object.defineProperties()方法同时定义了_age及isAudlt两个属性的特性
此外,使用Object.getOwnPropertyDescriptor()方法可以取得给定属性的特性:
var descriptor = Object.getOwnPropertyDescriptor(person,'_age');
alert(descriptor.value);//19
对于数据属性,可以取得:configurable, enumberable, writable和value;
对于访问器属性,可以取得:configurable, enumberable, get和set
JavaScript对象创建模式
三、创建对象
使用Object构造函数或对象字面量均可创建对象,但缺点是创建多个对象时会产生大量重复代码。因此,下文介绍可解决此问题的对象创建方法。
1、工厂模式
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.getName = function () {
return this.name;
}
return o;//使用return返回生成的对象实例
}
var person = createPerson('Jack', 19, 'SoftWare Engineer');
将创建对象的任务交给一个工厂方法来实现,可传递参数。主要缺点是无法识别对象类型,因为创建对象均使用Object的原生构造函数完成。
2、构造函数模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.getName = function () {
return this.name;
}
}
var person1 = new Person('Jack', 19, 'SoftWare Engineer');
var person2 = new Person('Liye', 23, 'Mechanical Engineer');
使用自定义的构造函数(与普通函数类似,仅用于创建对象),定义对象类型(如:Person)的属性和方法。它与工厂方法的区别在于:
- 没有显式地创建对象
- 直接将属性和方法赋值给this对象;
- 没有return语句;
此外,要创建Person的实例,必须使用new关键字,以Person函数为构造函数并传递参数完成对象创建。实际创建过程经过以下4个步骤:
- 创建一个对象
- 将函数的作用域赋给新对象(因此this指向这个新对象,如:person1)
- 执行构造函数的代码
- 返回该对象
上述由Person构造函数生成的两个对象person1与person2均为Person的实例,因此可使用instanceof判断,并且因为所有对象都继承Object,所以person1 instanceof Object也返回真:
alert(person1 instanceof Person);//true;
alert(person2 instanceof Person);//true;
alert(person1 instanceof Object);//true;
alert(person1.constructor === person2.constructor);//ture;
虽然构造函数方式较为理想,但也存在缺点。特别是在对象属性指向函数时,会重复创建函数实例。基于上述代码,可改写为:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.getName = new Function () {//改写后效果与原代码相同,不过是为了方便理解
return this.name;
}
}
上述代码在创建多个实例时,会重复调用new Function()以创建多个函数实例。这些函数实例并非处于同一作用域中,虽然这通常不会导致错误,但会造成内存浪费。当然,可以在函数中定义一个getName = getName的引用,而getName函数在Person外定义,以此解决重复创建函数实例的问题,但在效果上并未起到封装的作用,示例如下:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.getName = getName;
}
function getName() {//到处是代码,看着乱!!
return this.name;
}
3、原型模式
JS每个函数都有一个prototype(原型)属性,该属性是一个指针,指向一个对象,它是所有通过new操作符使用函数创建的实例的原型对象。原型对象的最大特点是,所有对象实例共享它所包含的属性和方法,即在原型对象中创建的属性或方法直接被所有对象实例共享。
function Person(){
}
Person.prototype.name = 'Jack';//使用原型来添加属性
Person.prototype.age = 29;
Person.prototype.getName = function(){
return this.name;
}
var person1 = new Person();
alert(person1.getName());//Jack
var person2 = new Person();
alert(person1.getName === person2.getName);//true;共享一个原型对象的方法
原型是指向原型对象的,这个原型对象与构造函数关系不大,唯一的关系是函数的prototype指向这个原型对象!而基于构造函数创建的对象实例也包含一个内部指针[[prototype]]指向原型对象。
实例属性或方法的访问过程是一次搜索过程:
- 首先从对象实例本身开始,如果找到属性就直接返回该属性值;
- 如果实例本身不存在要查找的属性,就继续搜索指针指向的原型对象,在其中查找给定名字的属性,如果有就返回;
基于以上分析,原型模式创建的对象实例,其属性是共享原型对象的;但也可以在实例中自行定义,在查找时,就不从原型对象获取,而是根据搜索原则,得到本实例的返回;简单来说,就是实例中属性会屏蔽原型对象中的属性;
原型与in操作符
一句话总结:无论原型中属性,还是对象实例的属性,都可以使用in操作符访问到;要想判断是否是实例本身的属性可以使用object.hasOwnProperty(‘attr')来判断;
原生对象中原型 原生对象中原型与普通对象的原型一样,可以添加/修改属性或方法,如以下代码为所有字符串对象添加去左右空白原型方法:
String.prototype.trim = function(){
return this.replace(/^\s+/,'').replace(/\s+$/,'');
}
var str = ' word space ';
alert('!'+str.trim()+'!');//!word space!
原型模式的缺点在于,它省略了为构造函数传递初始化参数,这在一定程度上带来不便;另外,最主要的问题是当对象的属性是引用类型时,它的值是不变的,总是引用同一个外部对象,所有实例对该对象的操作都会影响其他实例:
function Person() {
}
Person.prototype.name = 'Jack';
Person.prototype.lessons = ['Math','Physics'];
var person1 = new Person();
person1.lessons.push('Biology');
var person2 = new Person();
alert(person2.lessons);//Math,Physics,Biology,person1修改影响了person2
4、组合构造函数及原型模式 目前最为常用的定义类型方式,是组合构造函数模式与原型模式。构造函数模式用于定义实例的属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方方法的引用,最大限度地节约内存。此外,组合模式还支持向构造函数传递参数,可谓集两家之长。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.lessons = ['Math', 'Physics'];
}
Person.prototype = {
constructor: Person,//原型字面量方式会将对象的constructor变为Object,此外强制指回Person
getName: function () {
return this.name;
}
}
var person1 = new Person('Jack', 19, 'SoftWare Engneer');
person1.lessons.push('Biology');
var person2 = new Person('Lily', 39, 'Mechanical Engneer');
alert(person1.lessons);//Math,Physics,Biology
alert(person2.lessons);//Math,Physics
alert(person1.getName === person2.getName);//true,//共享原型中定义方法
在所接触的JS库中,jQuery类型的封装就是使用组合模式来实例的!!!
5、动态原型模式 组合模式中实例属性与共享方法(由原型定义)是分离的,这与纯面向对象语言不太一致;动态原型模式将所有构造信息都封装在构造函数中,又保持了组合的优点。其原理就是通过判断构造函数的原型中是否已经定义了共享的方法或属性,如果没有则定义,否则不再执行定义过程。该方式只原型上方法或属性只定义一次,且将所有构造过程都封装在构造函数中,对原型所做的修改能立即体现所有实例中:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.lessons = ['Math', 'Physics'];
}
if (typeof this.getName != 'function') {//通过判断实例封装
Person.prototype = {
constructor: Person,//原型字面量方式会将对象的constructor变为Object,此外强制指回Person
getName: function () {
return this.name;
}
}
}
var person1 = new Person('Jack', 19, 'SoftWare Engneer');
person1.lessons.push('Biology');
var person2 = new Person('Lily', 39, 'Mechanical Engneer');
alert(person1.lessons);//Math,Physics,Biology
alert(person2.lessons);//Math,Physics
alert(person1.getName === person2.getName);//true,//共享原型中定义方法
注:以上内容参考《JavaScript 高级程序设计》第3版