本系列的第三篇文章跳过了《JavaScript权威指南》的第四章和第五章的内容(个人觉得这两章内容偏介绍和字典向),直接来聊一聊JavaScript中最最复杂,也是最重要的基本数据类型——对象。
对象:特指恋爱的对方,指男、女朋友关系,恋爱双方的行为的称呼,在想屁吃?
JavaScript的对象指的是除字符串,数字,布尔值,null和undefined之外所有的值。
在判断数据类型的时候,我们往往会借助关键字typeof,如下:
typeof(1) //number
typeof(undefined) //undefined
typeof('undefined') //string
typeof(true) //boolean
然而我们不能仅用typeof就判断一个值是否为对象,如下:
typeof({}) //object
typeof(null) //object
typeof(function a(){}) //function
因此有时候我们需要用到一个“传统对象”作为参数时,需要过滤掉null,同时,我们不需要担心用户会传入一个函数对象,因为函数对象会被typeof直接过滤成function。
//判断一个值是否为对象
function isObject(o){
return typeof(o)==='object'&&o!==null
}
isObject({}) //true
isObject(null) //false
isObject(1) //false
从狭义的角度讲,对象是JavaScript六大数据类型之一(ES6新增了symbol类型,本文不讨论),然而在实际开发场景中,对象却拥有更广阔的应用。
在本系列第二章中,我介绍了“包装对象”的概念,即字符串,数字,布尔类型在实际使用场景中,也会呈现出对象的特性,他们的表现类似一种特殊的“不可变对象”,基于这种特性,我们可以得出以下结论:
除了null和undefined外,所有的数据类型都拥有对象的特性。
创建对象最简单的方式就是在JavaScript代码中使用代码直接量,当然你也可以通过关键字new来创建一个构造函数的实例,除此之外,还有一种比较冷门的方法,你可以调用内置的Object.create()函数来创建对象。三种创建对象的方式如下所示:
// 代码直接量
var point = {x:1}
// new关键字
var empty= new Object() // {}
// Object.create()
var rect= Object.create({width:3,height:4}) // rect继承了width和height
需要注意的是,通过new运算符创建并初始化一个新对象,需要对应的构造函数,像Date,Array,Object这些都是系统内置的构造函数,因此可以直接调用。关于构造函数的细节,本文不过多讨论(因为我也还没看到,所以等看完了再说)。
关于Object.create(),我特别标注了注释。ES5定义的Object.create()方法,可以创建一个新对象,其中第一个参数设置的是这个对象的原型,我们需要将其打印出来,才能看到他的特殊性。
如上图所示,Object.create()创建的新对象(假设变量名为字母o)本身并没有“x”属性,但我们仍然可以访问到o.x = 1,这是因为o虽然本身没有x属性,但他的原型链上有x属性,因此当我们方问o.x的时候,返回的结果是o的原型链上最先找到的x属性(如果x属性可读的话)。
说了半天,云里雾里不知道在说啥,我们重新理一遍逻辑,通过Object.create(),来理一理原型链的概念。
关于原型和原型链的概念网上有一大堆,这里我们通过Object.create()来打个比喻:
Object.create()是一个律师事务所的律师,他是专门管理财产继承的
有一天,有一位父亲(father),他名下有{ 车子: 10辆 ,房产': 5套,公司 : 2家} ,来到了律师事务所Object,找到了律师create,父亲(father),想把自己的财产过继给自己的儿子(son)。
于是我们得到以下公式:son = Object.create(father)。
问:现在儿子有哪些财产,父亲又有哪些财产?
答:son.车子 = 10辆 ,son.房产 = 5套 ,son.公司 = 2家 ,father.车子 = 10辆 ,father.房产 = 5套 , father.公司 = 2家
有人说他卜信,我们眼见为实:
有人觉得这样就完事了?不!真相远没有这么简单。
惊!杭州某小伙继承了父亲千万财产,却买得起上亿的钻石,答案竟然是他爷爷是马云!
其实上面的例子就是对象原型链的概念,虽然儿子本身是一个穷光蛋,但若要问起他的房产,他会循着族谱往上找,直到找到“最近”的那个属性,他会告诉你他有5套(父亲的)房产,我们假设父亲还继承了他爷爷100套房产,那么儿子会怎么回答呢?答案还是:5套!因为一旦找到对应的属性,对象的查询便不会继续深入了。
说完了故事,我们回归原型和原型链的概念,所有JavaScript对象(null除外)都和另外一个对象“关联”,另外一个对象就是我们熟知的原型,每一个对象都从原型继承属性,而原型,也会从原型的原型继承属性,这样就形成了一条“原型链”,那么这条原型链什么时候终止呢?那就不得不提Object.prototype了。
Object.prototype是为数不多的没有原型的对象,他不继承任何属性,所有的内置构造函数都具有一个继承自Object.prototype的原型,所有通过对象直接量创建的也都具有同一个原型对象Object.prototype。因此我们可以通过Object.create(Object.prototype)来创建一个普通的空对象。
通过原型继承方法,我们也可以获得一个继承自原型的新对象,这个方法在操作对象的时候十分有用,代码如下:
// 对象继承
function inherit(p){
if(p===null){throw TypeError()}
if(Object.create){
return Object.create(p)
}
// 如果浏览器不支持Object.create
if(typeof(p)!=="object"&&typeof(p)!=="function"){throw TypeError()}
function fn(){} //创建构造函数
fn.prototype = p //构造函数原型继承p
return new fn() //返回一个继承了父对象的子实例
}
实际操作中可以不用这么麻烦使用继承函数,你可以通过 o = Object.create(p) 来实现原型的继承。
我们可以通过点(.)或方括号([])运算符来获取属性的值,对于点(.)来说,右侧必须是一个以属性名称命名的简单标识符,而方括号内既可以是一个简单标识符,也可以是一个计算值。在设置属性时,尽量不要使用保留字(ES3不支持)。
我们往往需要使用方括号([])运算符来帮我们获取动态属性的查询和设置,如下
let json = {}
for(let i=0;i<10;i++){
json[i] = '属性'+ i
}
console.log(json) //{0:'属性0','1':'属性1' ....}
属性访问并不总是返回活设置一个值,如果查询一个不存在的属性,JavaScript并不会报错,而是返回undefined,但如果要查询一个不存在的对象,JavaScript就要报错了。
如o对象里有一个属性{x:1},此时你想查询,o.y,由于x的自有属性和原型链上都没有y,因此这个表达式会返回一个undefined,但如果你想查询o.y.z,那JavaScript就忍不了了,因为你想查询的是一个x.undefined.z,这种情况下,JavaScript就会抛出一个类型异常的错误。
在实际项目中,我们经常会遇到这种情况,如 o.arr 在一些情况下成立,再某些情况下o.arr = undefined,此时我们访问o.arr.length系统就会抛出一个类型错误异常,解决这种不确定性的最优方案就是利用&&运算符的短路行为。
我们可以访问o.arr&&o.arr.length,由于“与”运算符只有在所有表达式均为true的情况下才会返回true,因此一旦检查到其中某个表达式的值为false时,他就会直接返回false,不会继续执行后面的语句了,这就避免了访问undefined的属性。
JavaScript对象具有自有属性,例如 o = {x:1} , 你访问o.x,就是访问o的自有属性x,同时你可以使用o.toString()方法,toString就是从Object.prototype里继承来的属性。因此当你查询一个对象的属性,他会优先查询该对象的自有属性,然后是该对象的原型,然后是原型的原型,该查询会在遇到一个原型是null的对象终止,也就意味着,该对象上没有你想要查找的属性,系统会返回一个undefined。可以看到,对象的原型属性构成了一个“链式查询”,通过这个链就实现了属性的继承。
上面的理论转换成代码,如下所示:
let o = {} // 对象直接量创建的时候会继承Object.prototype
o.x = 1 // o拥有一个自有属性x
let p = inherit(o) // p继承了o,也继承了o的原型Object.prototype
p.y = 2 // p拥有一个自有属性y
var q = inherit(p) // q继承了p,也继承了o,也继承了Object.prototype
q.x = 3 // q拥有一个自有属性x,跟o的自有属性同名
q.x + q.y// 5 y继承自p,x是自有属性x
上面的代码中,由于p中的自有属性x和继承的x属性同名,因此在查询改属性的时候,JavaScript会优先选取“先查到”的属性。当然也不总是如此,在这里有一个比较细节的情况
JavaScript中对象属性赋值操作会先检查原型链,以此判定是否允许赋值操作。
有些情况下,你可能无法去设置一个自有属性,这跟你要设置的属性继承自一个“只读”属性有关,关于如何设置属性的特性,我们会在下一章节详细讨论,我们修改一下上面的例子,来看一下这种情况下会发生什么
let o = {} // 对象直接量创建的时候会继承Object.prototype
// o.x = 1 // o拥有一个自有属性x
Object.defineProperty(o,'x',{value:1,writable:false}) //设置 x属性只读
let p = inherit(o) // p继承了o,也继承了o的原型Object.prototype
p.y = 2 // p拥有一个自有属性y
var q = inherit(p) // q继承了p,也继承了o,也继承了Object.prototype
q.x = 3 // 不能设置该属性
console.log(q.x + q.y)// 3 y继承自p,x继承自o
console.log(q) //{} 自有属性是空
当我们把o.x设为只读之后,设置q.x就失效了,我们来看下浏览器打印的q长什么样子。
对象的继承可以访问原型链上的属性同时又不破坏原型链上的对象,这个特性可以让程序员有选择性的覆盖继承的属性。
其实很多人都不知道对象的属性可以删除,可能大家觉得无用的属性放着不管就好了。
delete运算符可以删除对象的属性,他的操作数应当是一个属性表达式。需要注意的时,delete只会断开属性和宿主对象的凉席,而不会去操作属性中的属性。这话听起来十分拗口,我们写个例子看一下就知道了
var a = {p:{x:1}}
var b = a.p
delele a.p
b.x //1
由于delete只能删除自有属性,不能删除继承属性,因此我们在销毁对象属性的时候需要遍历属性中的属性,依次删除,以防止内存泄漏。delete不能删除哪些不可配置的属性,如Object.prototype,就已经被系统锁死了,如果你想delete Object.prototype,delete操作会返回false。