在javascript中对象是一种基本的数据类型,在数据结构上是一种散列表,可以看作是属性的无序集合,除了原始值其他一切都是对象。它可以用来表示现实世界中或者我们大脑中抽象出来的客体,这和其他面向对象的编程语言有些类似,但js并不是面向对象的而是基于对象的,因为典型的面向对象要求封装、继承和多态,而javascript中的对象只是做到了封装,而继承可以通过原型或者借用别人家的构造函数来模拟实现,至于多态,这是我最喜欢的地方,js由于是弱类型语言的,所以它是生来就是多态,特别方便。又由于函数也是对象,也就是说它也是一种值,既然是值它就既可以作为参数传入也可以作为返回值返回并以此来产生高阶函数,所以js也可以进行函数式编程。
javascript中关于对象的操作共有七种:创建对象、设置属性、查找属性、删除属性、检测属性、枚举属性、检测实例,这七种操作涵盖了关于对象的绝大部分知识点。
- 创建对象
js对象的创建一共有三种方法:对象直接量法、构造函数法、Object.create法。
对象直接量是最简单的创建方法,一个大括号里面加上用逗号分隔的名值对就搞定了,只是有些细节要注意,因为属性名可以是包括空字符在内的任何字符,所以一个属性名称如果不是合法的标识符,则它必须要用引号括起来,如果是一个合法标识符的话,则属性名可以不用引号括起来。假如所有的属性名都用引号括起来的话,那么这个对象也就是一个标准的JSON对象。JSON格式的要求就是属性名要用引号括起来,JSON字符串也是一样的,所以用ajax从后台获取的JSON字符串时它的属性名一定要用引号括起来,否则用JSON.parse方法转换为对象时会出错。
构造函数法就是用new关键字加上一个函数调用来返回一个对象。这个函数可以是内置函数也可以是自定义函数,返回对象的属性初始化工作是在这个函数体内进行的,当执行到new的时候就已经创建了一个新对象了,函数体内的this被初始化为这个对象。这个函数体内不必有return语句,它是自动返回对象的,如果强制加入return语句,那么除非return返回的是一个对象,否则返回其他任何值都是无效的,只是如果没有初始化完就return会阻止后续属性的初始化。那么构造函数创建出来的对象和直接量返回的对象有什么不同呢?首先构造函数创建的对象的属性初始化实在函数体内进行的,有函数的地方就有闭包,有了闭包就可以让对象拥有隐藏的私有变量,这在直接量中是做不到的。其次就是原型对象prototype(在javascript中每个对象都和另一个对象相关联,这个相关联的对象就叫做原型对象,在查找属性的时候如果本对象没有那么就会查找它的原型对象,原型对象没有再查找原型对象的原型对象,一直找到没有原型对象为止,这就相当于该对象继承了它的原型对象的属性,而且这种继承还是一种动态继承),每个函数都有一个prototype属性,用构造函数创造出来的对象的原型就是这个构造函数的prototype属性指向的对象,而通过直接量返回的对象它的原型对象是Object.prototype对象。如果默认情况下没有设置过构造函数prototype属性,则该构造函数的prototype对象只有一个用for/in不可枚举出来的constructor属性,该constructor属性的值是该构造函数本身,这也是一种通过constructor来判断实例对象类型的方法原理。如果是用prototype={巴拉巴拉巴拉}来重写原型对象,那最好在巴拉巴拉巴拉中加上constructor:createfun把这个constructor补上,这也是为什么并不是所有对象都有constructor的原因。
Object.create方法也可以创建对象,它创建出来的对象的原型就是传给Object.create方法的对象参数。如果想创建一个没有任何原型的对象,只需把null传进去:Object.create(null)。这种创建对象的方法是在ECMAScript5中才加入的,原来的版中只能构造函数模拟出来。
- 设置属性
- 查找属性
设置属性和查找属性之所以放到一起是因为它俩的写法是一样子的。语法都是.操作符和[]操作符,在查询时如果存在属性就返回相应的值,如果不存在就返回undefined,同样在设置属性的时候,如果存在属性就重新覆盖,如果不存在属性就会在对象上创建新属性并赋初始值。而.操作符和[]操作符是有些区别的,[]操作符里面是一个字符串或者可以转化为字符串的值,.操作符后面跟的是一个标识符,如果要动态的指定属性名只能用[]操作符,因为标识符不可能是动态的。其中赋值的话如果原型中也存在相同的属性名,那么是不会对原型有影响的。
- 删除属性
删除属性用delete操作符,后面跟要删除的对象和属性。删除非原始类型的时候有一个特点,例如 a={y:{x:1}} b=a.y delete a.y 因为b还是引用着{x:1}的,所以这个delete只是断开了{x:1}和对象啊的关系,打印b.x还是等于1,这个y对应的对象并没有删除掉,因为y还被另一变量给引用了,js垃圾回收机制是不会把这个y从内存中删掉的,这种垃圾回收机制也是产生闭包的原因之一。
- 检测属性
检测属性就是判断某个属性是不是对象的存在属性。首先是in操作符,in操作符左边是属性名称字符串,右边是对象,如果左边的属性存在在右边的对象中则返回ture;其实直接通过查询可以检测属性,例如 如果a.x!==undefined,那么x就在对象a中,否则不在,这种方法有一个小缺陷,就是当x属性的值本身就是undefined的时候就不灵验了。对于以上的检测方法,其实都会包含对象的原型属性,所以如果对象本身没有而原型上有那么返回的也是true。所以要想区分属性是否是对象本身的就要用到另外的方法:hasOwnProperty和propertyIsEnumerable。如果O.hasOwnProperty("x")为true,那么就是说x是对象o的自有属性,propertyIsEnumerable是hasOwnProperty的加强版,不但要求是自有属性还要求属性是可枚举的才返回true。
- 枚举属性
枚举属性就是把对象中的可枚举的属性名字一一返回,一般对象的属性都是可枚举的,但内置的继承方法一般都是不可枚举的,例如toString。一般枚举属性用的是for/in语句,同样该语句返回的属性也是包括继承的属性的。如果想直接获取到自有的可枚举的属性集合,可以使用Object.keys(),这个方法返回一个*属性的数组。同样Object.keys还有一个加强版Object.getOwnPropertyNames,这个方法返回的不单单是可枚举的自有属性,还包括不可枚举的自有属性。
- 检测实例
检测实例就是判断实例对象是否属于某个类或者实例对象的类名是什么。而什么是类呢?在面向对象中类是一个产生对象的模板,在javascript中起到这种作用的是原型对象,所以可以说一个原型对象prototype属性就是一个类。判断实例对象是否是某个类,就是判断实例对象是否继承自某个原型对象,这样就会出现不同的构造函数产生的对象属于同一个类,因为这不同的构造函数的prototype属性可能是同一个对象。判断实例对象是否继承某个原型对象的方法有两个:isPrototypeOf、instanceof,例如Class.prototype.isPrototypeOf(o)、o instanceof Class,这两种方法是有区别的,首先调用isPrototypeOf方法的要是一个原型对象而不能是一个构造函数对象,参数是实例对象;instanceof 的左操作数要是实例对象,右操作数必须是一个构造函数而不能是一个原型对象,如果两个构造函数a、b的原型对象都是p,那么用a构造出来的实例对象o用instanceof判断 o instanceof b 其结果也是true,所以用instanceof判断为true时,实例对象不一定是由该构造函数构造出来的。其实这两种判断方法都是判断的原型链,不管是直接继承还是在更远的间接继承,只要原型链上存在该对象就会返回true。
以上两种是能判断实例对象是否属于某个类,并不能直接获取类名,要获取类名只能用并不是总是有效的两种方法:toString和利用constructor属性。关于toString方法,首先要清楚一点的就是每个对象都有一个类属性,这个属性的值是一个字符串来表明该对象是什么类型,很遗憾这种类属性是不能认为设定的,js没有提供相应的接口,只能查询到,而查询的方法就是最原始的toString方法。最原始的toString方法指的是Object.prototype中的toString方法,因为很多其他对象都是继承了该原型并且是重写toString方法了的,所以直接调用对象的toString方法一般是查不到类属性值的,所以只能借用Object.prototype中的toString方法。写法如下Object.prototype.toString.call(o)。这样返回的是一个类属性字符串,该字符串有其特有的固定格式,例如"[object Array]",所以要只要截取第8个到倒数第二个字符即可,slice(8,-1)。通过toString方法获得的类型名称只是适用于内置对象的,像Date、Array等,而对于自定义对象,由于没有提供设置类属性的接口,不能重写,自定义对象的类属性都是一个值"[object Object]",看不出其所属的类别。所以要获取自定义属性的类名就要用到constructor属性。用constructor属性判断最好是那种构造函数和原型对象是一一对应的那种情况,如果不是一一对应的会让本来相同类的不同对象看起来是不同类的,因为constructor获取的不是原型对象而是构造函数对象的名字。这里又会产生一个问题,就是不一定所有对象有constructor,就像前面说过的,同时也并不是所有的构造函数都有名字,像不带名字的函数表达式就没有名字,这就是这种判断方法的一个不总是有效的原因。
除了以上关于对象的操作还有对象的序列化,对象的序列化和反序列化就是JSON对象和JSON字符串之间的相互转换,用到的方法有JSON.parse和JSON.stringify这两种。其他的像getter/setter属性的设置、对象的可扩展性、属性的可枚举性等可查阅相关资料。
如果想要自己的程序更具有模块化,对象还可以当作命名空间来使用,把自己定义的变量、函数都当作一个对象的属性,使用的时候通过.操作符来调用,这样可以有效的避免命名的冲突。