javascript的数据有两种,简单数据和复杂数据。简单数据有undefined,null,boolean,number和string这五种。复杂数据只有一种,即对象(object)。
那么,在javascript中,对象有哪些呢?数组是对象,函数也是对象。
在说这个话题之前,我想先说几句题外话:最近偶然碰到有朋友问我“hoisting”的问题。即在js里所有变量的声明都是置顶的,而赋值则是在之后发生的。可以看看这个例子:var a = 'global'
(function () {
alert(a)
var a = 'local'
})()
大家第一眼看到这个例子觉得输出结果是什么?‘global’?还是‘local’?其实都不是,输出的是undefined,不用迷惑,我的题外话就是为了讲这个东西的。
其实很简单,看一看JavaScript运行机制就会明白。我们可以把这种现象看做“预声明”。但是如果稍微深究一下,会明白得更透彻。
这里其实涉及到对象属性绑定机制。因为所有JavaScript函数都是一个对象。在函数里声明的变量可以看做这个对象的“类似属性”。对象属性的绑定在语言里是有分“早绑定”和“晚绑定”之分的。
【早绑定】
是指在实例化对象之前定义其属性和方法。解析程序时可以提前转换为机器代码。通常的强类型语言如C++,java等,都是早绑定机制的。而JavaScript不是强类型语言。它使用的是“晚绑定”机制。
【晚绑定】
是指在程序运行前,无需检查对象类型,只要检查对象是否支持特性和方法即可。可以在绑定前对对象执行大量操作而不受任何惩罚。
上面代码出现的“预声明”现象,我们大可用“晚绑定”机制来解释。在函数的作用域中,所有变量都是“晚绑定”的。 即声明是顶级的。所以上面的代码和下面的一致:
var a = 'global'
(function () {
var a
alert(a)
a = 'local'
})()
在alert(a)之前只对a作了声明而没有赋值。所以结果可想而知。
<!-- 题外话到此结束 -->
RT:本文要说的是,在JavaScript里,我所知道的几种定义类和对象的方式:<! -- 声明:以下内容大部分来自《JavaScript高级程序设计》,只是个人叙述方式不同而已 -- >
【直接量方式】
使用直接量构建对象是最基础的方式,但也有很多弊端。
var Obj = new Object
Obj.name = 'sun'
Obj.showName = function() {
alert('this.name')
}
我们构建了一个对象Obj,它有一个属性name,一个方法showName。但是如果我们要再构建一个类似的对象呢?难道还要再重复一遍?
NO!,我们可以用一个返回特定类型对象的工厂函数来实现。就像工厂一样,流水线的输出我们要的特定类型结果。
【工厂方式】
function createObj(name) {
var tempObj = new Object
tempObj.name = name
tempObj.showName = function () {
alert(this.name)
}
return tempObj
}
var obj1 = createObj('obj_one')
var obj2 = createObj('obj_two')
这种工厂函数很多人是不把他当做构建对象的一种形式的。一部分原因是语义:即它并不像使用了运算符new来构建的那么正规。还有一个更大的原因,是因为这个工厂每次产出一个对象都会创建一个新函数showName(),即每个对象拥有不同的版本,但实际上他们共享的是同一个函数。
有些人把showName在工厂函数外定义,然后通过属性指向该方法,可以避开这个问题:
代码
可惜的是,这种方式让showName()这个函数看起来不像对象的一个方法。
【构造函数方式】
这种方式是为了解决上面工厂函数的第一个问题,即没有new运算符的问题。可是第二个问题它依然不能解决。我们来看看。
function Obj(name) {
this.name = name
this.showName = function () {
alert(this.name)
}
}
var obj1 = new Obj('obj_one')
var obj2 = new Obj('obj_two')
它的好处是不用在构造函数内新建一个对象了,因为new运算符执行的时候会自动创建一个对象,并且只有通过this才能访问这个对象。所以我们可以直接通过this来对这个对象进行赋值。而且不用再return,因为this指向默认为构造函数的返回值。
同时,用了new关键字来创建我们想要的对象是不是感觉更“正式”了。
可惜,它仍然不能解决会重复生成方法函数的问题,这个情况和工厂函数一样。
【原型方式】
这种方式对比以上方式,有个很大的优势,就是它解决了方法函数会被生成多次的问题。它利用了对象的prototype属性。我们依赖原型可以重写对象实例。
var Obj = function () {}
Obj.prototype.name = 'me'
Obj.prototype.showName = function () {
alert(this.name)
}
var obj1 = new Obj()
var obj2 = new Obj()
我们依赖原型对构造函数进行重写,无论是属性还是方法都是通过原型引用的方式给新建的对象,因此都只会被创建一次。可惜的是,这种方式存在两个致命的问题:
1。没办法在构建对象的时候就写入想要的属性,因为原型在构造函数作用域外边,没办法通过传递参数的方式在对象创建的时候就写入属性值。只能在对象创建完毕后对值进行重写。
2。致命问题在于当属性指向对象时,这个对象会被多个实例所共享。考虑下面的代码:
var Obj = function () {}
Obj.prototype.name = 'me'
Obj.prototype.flag = new Array('A', 'B')
Obj.prototype.showName = function () {
alert(this.name)
}
var obj1 = new Obj()
var obj2 = new Obj()
obj1.flag.push('C')
alert(obj1.flag)// A,B,C
alert(obj2.flag)//A,B,C
是的,当flag属性指向对象时,那么实例obj1和obj2都共享它,哪怕我们仅仅改变了obj1的flag属性,但是它的改变在实例obj2中任然可见。
面对这个问题,让我们不得不想是否应该把【构造函数方式】和【原型方式】结合起来,让他们互补。。。
【构造函数和原型混合方式】
我们让属性用构造函数方式创建,方法用原型方式创建即可:
var Obj = function (name) {
this.name = name
this.flag = new Array('A', 'B')
}
Obj.prototype = {
showName : function () {
alert(this.name)
}
}
var obj1 = new Obj()
var obj2 = new Obj()
obj1.flag.push('C')
alert(obj1.flag)// A,B,C
alert(obj2.flag)//A,B
这种方式有效地结合了原型和构造函数的优势,是目前用的最多,也是副作用最少的方式。
不过,有些追求完美的家伙还不满足,因为在视觉上还没达到他们的要求,因为通过原型来创建方法的过程在视觉上还是会让人觉得它不太像实例的方法(尤其对于传统OOP语言的开发者来说。)
所以,我们可以让原型活动起来,让他也加入到构造函数里面去,好让这个构造函数在视觉上更为统一。而这一系列的过程只需用一个判断即可完成。
var Obj = function (name) {
this.name = name
this.flag = new Array('A', 'B')
if (typeof Obj._init == 'undefined') {
Obj.prototype = {
showName : function () {
alert(this.name)
}
}
Obj._init = true
}
}
如上,用_init作为一个标志来判断是否已经给原型创建了方法。如果是那么就不再执行。这样其实在本质上是没有任何变化的,方法仍是通过原型创建,唯一的区别在于这个构造函数看起来“江山统一”了。
但是这种动态原型的方式是有问题的,《JavaScript高级程序设计》里并没有深究。创建第一个对象的时候会因为prototype在对象实例化之前没来的及建起来,是根本无法访问的。所以第一个对象是无法访问原型方法的。同时这种方式在子类继承中也会有问题。
关于解决方案,我会在下一文中说明。
其实就使用方便来说的话,个人觉得是没必要做这个判断的。。。呵呵 ^_^
在javascript中,对象被定义为一组属性的无序集合。即对象是一组没有特定顺序的值。
对象的构成为:属性和方法。一个对象可以拥有多个属性和多个方法。
一个JavaScript对象中有很多属性。一个对象的属性可以被解释为一个附加到对象上的变量。
属性由一个字符串类型的名字(name)和一个属性描述符(property descriptor)对象构成。
JavaScript中使用一些内部特性来描述属性的特征。开发者不能在JavaScript中直接访问这些特性。规范中用两个中括号把特性的名称括起来,如 [[Enumerable]]
属性分两种:数据属性和访问器属性
数据属性包含一个保存数据值的位置。数据属性有四个特性描述它们的行为。
数据属性定义
属性描述符是必须是一个对象,对象中最多有四个属性: configurable 、 enumerable 、 writable 和 value 。如果 configurable 、 enumerable 、 writable 没有指定,将被指定为 false ; value 指定为 undefined
访问器属性不包含数据值。相反,它们包含一个获取(get)函数和一个设置(set)函数,不过这两个函数不是必须的。
访问器属性常常用来设置一些 私有 属性,从而达到属性的保护。
访问器属性定义
属性描述符中的获取和设置函数不一定都要定义。只定义设置函数,表示不能读取;只定义获取函数,表示只能读取。
通过 Object.defineProperties() 方法定义多个属性及其属性描述符。
注意事项
【1】数据属性的 configurable 、 writable 为false,可以通过访问器属性的 set() 方法进行修改。
【2】如果数据属性 configurable 、 enumerable 、 writable 没有指定,将被指定为 false ; value 指定为 undefined
给对象添加变量时,属性名和变量名一致,我们可以使用属性简写的进行添加变量。
计算属性:在 [ ] 中放入表达式,计算结果可以当做属性名。
这种用法和用方括号访问属性非常类似。
注意事项
对象解构就是使用与对象匹配的结构来实现对象属性的赋值。
展开语法 (...):三个点,将对象中的属性全部提取出来。【与数组中的展开语法类似,但有区别】
解构语法 (...):将对象中的某些属性从中拿出来。【注意:左边的源数据,右边是声明的变量】解构语法可以解构内置对象中的某些属性。
合并的方法:
一个方法是关联到某个对象的函数,或者简单说,一个方法是一个值为某个函数的对象属性。定义方法就像定义普通的函数,然后赋值给对象的某个属性。
方法的定义有两种方式:
对象和函数、数组一样是引用类型,即复制只会复制引用地址。
对象作为函数的形参,实际上传递的是对象的地址
this 关键字表示当前对象
以下方法受到 enumerable 和 原型链 的影响。
作用:以任意顺序遍历一个对象的除 Symbol 以外的 可枚举 属性【包含原型链上可枚举的属性】
作用:返回一个由一个给定对象的 自身可枚举属性 组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
作用:返回一个给定对象 自身的所有可枚举属性值 的数组。
作用:返回一个给定对象 自身可枚举属性 的键值对数组
作用:返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
作用:返回一个给定对象自身的所有 Symbol 属性的数组。
所有的JavScript对象至少继承于一个对象,被继承的对象被称为原型。
每个对象可以通过构造函数的 prototype 属性找到原型 或每个实例对象有一个私有属性 __proto__ 指向原型
方法一: Object.getPrototypeOf() 方法返回指定对象的原型。
方法二: 实例化对象.__proto__
Object.getPrototypeOf(object)
方法一: Object.create() 创建对象的时候指定原型.
方法二: Object.prototype.__proto__
方法三: Objcet.setPrototypeOf()
方法四: Reflect.setPrototypeOf()
Object.create(proto, propertiesObject) :
Objcet.setPrototypeOf(obj, prototype)
Reflect.setPrototypeOf(target, prototype)