前言
JavaScript作为一种脚本语言,语法简单(求其),易上手,适合开发;同时,作为当今前端编程方面占据垄断地位,甚至逐步向后端发展的强势语言,它的前景十分美好,功能足够强大。既然是脚本语言,自然没有c,c++,Java等传统语言的严谨,但是利用它仍然可以基本覆盖其他语言能做到的高级功能。
下面我就从面向对象的角度,整合JavaScript里函数,对象,引用类型,原型,闭包,作用域链等知识点,去探讨JavaScript是如何定义对象,构造类,设置属性和函数的私有公有权限,实现继承,利用作用域以及管理内存的。下面的代码例子基本都出自《JavaScript高级程序设计》。
创建单个对象
与Java类似,JavaScript的所有对象都继承自Object类,我们可以在Object类的基础上扩展对象的属性与函数,从而创建出一个新的对象:
|
|
对象包含属性与函数,可以直接在Object类对象上扩展。
用对象字面量的方式,可以更简单地定义对象:
|
|
对象属性
对象包含属性与函数,其中函数在JavaScript里也可以视为一种特殊的属性,会在下一节重点讲述。这里先总结一下属性,属性可以分为数据属性与构造器属性。
数据属性
JavaScript里的每一个属性都有一些特征,相当于是属性的属性,用于JavaScript底层维护属性。数据属性有4个特性,分别是Configurable,Enumerable,Writable与Value,规范里记为[[Configurable],[[Enumerable]],[[Writable]],[[Value]]。这些特性是不能直接访问的。
- [[Configurable]]:能否通过delete删除属性,能否修改属性特性,能否把属性修改为访问器属性。如果像上面直接在对象上创建属性,则默认为true。
- [[Enumerable]]:能否通过for-in语法循环返回属性,默认为true。
- [[Writable]]:能否修改属性的值,默认为true。
- [[Value]]:属性的数据值,默认为undefined,像上面创建属性后就保存属性的值。
除了直接在对象上创建属性以外,还可以通过Object.defineProperty()方法创建属性。这个方法接收3个参数,分别是属性所在对象,属性的名字,以及一个描述符对象。描述符对象的属性必须是configurable,enmuerable,writable和value。通过这个描述符对象,则可以定义属性的一或多个特性。在这种情况下,configurable,enumerable和writable默认为false。
这里要注意一点是,当把configurable设置为false,则不能再设置属性的特性,包括不能将configuralble重新设为true。
访问器属性
访问器属性也具有[[Configurable]]和[[Enumerable]]特性,但没有[[Writable]]和[[Value]],而替换为[[Get]],[[Set]]。[[Get]]和[[Set]]保存相应的函数,默认为undefined。当用一般的JavaSript读写该属性时,则会分别调用[[Get]]和[[Set]]里保存的函数,从而返回或写入相应的值。这就相当于Java里的getter和setter方法。只定义getter则不能写,只定义setter则不能读。访问器属性只能通过Object.defineProperty方法定义。
|
|
第一次访问book.year是写数据,调用setter方法,第二次访问book.year是读数据,调用getter方法。在Java里,getter和setter往往是针对私有属性的,用于向外界提供访问私有属性的一个公共接口,但访问器属性我觉得并不是为了这个目的而产生的。访问器属性,往往与一个数据属性相关联,例如这里的_year。这里_year前面的下划线是一种规范,用于表示与某个访问器属性相关联的数据属性。访问器属性是用于在设置一个属性的同时,导致其他属性的变化,这是它的最主要作用。至于私有公有属性的权限设定,则是由另外的技术实现的,这我也将会在后面几讲阐述。
同时定义多个属性
可以用Object.defineProperties()方法,这个方法接收两个对象参数,一个是要添加和修改其属性的对象,另一个对象包含多个对象属性,每个属性与要添加或修改的属性一一对应,这些对象属性里的属性则是要定义的属性特性。
读取属性的特性
用Object.getOwnPropertyDescriptor()方法可以获取给定属性的描述符,这个描述符对象的属性则是相应属性的特性。这个方法接收两个参数,分别是属性所在的对象和其描述符的属性名称。
这个描述符对象的属性便是原对象对应属性的特性。
构造类
简易生产对象——工厂模式
如果按照上面创建对象的方法,那么每次创建具有相同属性和方法的对象,都需要手动写一遍重复的代码。我们希望像其他面向对象语言一样,有“类”的概念,可以通过一个封装好的方法,批量地生成同一类型的对象。例如:
|
|
creatPerson就是一个工厂方法,封装了创建对象的代码,使得批量生产对象变得十分简单。
但是,工厂模式存在两个主要的问题。第一个是虽然能够封装创建对象的代码,但是仍然不能将这个对象称为“类”,无法识别这些具有相同属性类型和方法的对象是属于同一个类的;第二个是,一般的面向对象语言,属于同一类的对象共享同一套方法,但属性则各有不同。具体到内存里,以java为例,它的类里的实例方法都是存储在代码区的,同一类的不同对象是共享代码区的相同方法的,而属性则存储在堆栈或堆里,各个对象不同。
JavaScript作为成熟的面向对象语言,这两个问题当然是可以解决的。下面我们分别来分析一下。
构造函数模式
|
|
构造函数本身与普通函数是没有区别的。按照惯例,构造函数一般以大写字母开头,普通函数则以小写字母开头。如果不用new操作符,则调用构造函数与调用普通函数没有任何区别。如果调用了new操作符,且构造函数内部没有return,则会经历以下四个步骤:
- 创建一个新对象。
- 将构造函数的作用域赋给新对象。(this指向新对象)
- 执行构造函数中的代码。
- 返回新对象
利用构造函数生成的对象,都有一个constructor属性,指向Person函数。我们可以用这个属性来作为类判别的依据。但更可靠的做法是用instanceof操作符,如:
|
|
由于Person类继承自Object类,因此person1既可以说是Object类的对象,也可以说是Person类的对象。
动态原型模式
每当创建一个新函数,该函数都自动获得一个prototype属性,该属性指向函数的原型对象。这个原型对象本身与普通的实例对象没有任何区别。通过构造函数创建的对象,都会含有一个内部属性[[prototype]],这个内部属性不能直接访问,其指向构造函数的原型对象。显然,所有通过该构造函数创建的对象,都有这么一个指针指向同一个原型对象。我们访问实例对象的属性和方法,会先看看实例对象有没有相应的属性或方法,没有的话再从原型对象上找。因此,我们可以把类里的实例方法以及共有的属性都定义在原型对象上,而把每个实例对象都不同的属性定义在实例上。
|
|
sayName方法定义在原型对象上,其他属性则直接定义在实例对象上。为了更好地封装代码,动态原型模式把原型的属性和方法定义都包含在构造函数里,而没有放在构造函数外面进行。但这样存在一个问题,就是每创建一个实例,都会定义一个新的函数,因此我们需要通过一个判断语句,来判断sayName函数是否已被初始化。如果有多个函数定义在原型对象上,我们也只需要判断一个函数有没有被初始化就可以了,因为初始化都是一起进行的,一个函数初始化了说明其他函数也被初始化。
原型对象,实例对象,构造函数的关系:
由于访问对象的方法和属性时,是按照先实例对象再原型对象进行的,因此在实例对象上创建的同名属性或方法可以覆盖原型对象上的属性或方法。
总结
面向对象语言的最基本特点就是有对象,有类,类里面定义了属性和方法。这一讲简单地介绍了怎么创建对象,怎么构造类,以及引入了原型的概念,总结了怎么利用原型与构造函数来定义实例属性,实例方法和共享属性。关于JavaScript的面向对象设计部分,还有很多内容,欲知后事如何,请看下回分解。