1.1 语法
对象有两种形式定义:
1. 声明(文字)形式
2. 构造形式
1.声明形式
var myObj={
key:value,
//....
}
2.构造形式
var myObj=new Object();
myObj.key=value;
构造形式和文字形式生成的对象是一样的。唯一的区别是:在文字声明种你可以添加多个键/值对,但在构造形式中,你必须逐个添加属性。
注意: 通常使用内置对象的构造函数声明对象往往影响性能。
1.2 类型
在javascript中,一共有六种主要类型:
- string
- number
- boolean
- null
- undefined
- object
注意,简单的基本类型(string,number,boolean,null,undefined)本身并不是对象。null有时会被当做对象类型,但实际上这是语言本身的一个bug。在javascript中,不同对象在底层都表示为二进制,二进制前三位都为0的话会被判定为object类型,null的二进制表示全为0,所以执行typeof时会返回object类型。实际上,null只是一个基本类型。
注意: 函数与数组是对象的一个子类型,因此用typeof判定类型时都返回object。
1.3 内置对象
javaScript中的对象子类型,被称为“内置对象”。
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
这些内置对象可以运用new操作符来创建一个对象。
举例:
var str="I'm a string";
typeof str;//"string"
str instanceof String;//false
var strObject=new String("I'm a string");
typeof strObject;//"object"
strObject instanceof String;//true
str=“I’m a string”;并不是一个对象,它只是一个字面量。如果在字面量上执行一些操作,比如获取高度、访问其中某个字符等,那需要转化为String对象。不过,幸运的是:在必要时,语言会自动把字符串字面量转换成一个String对象,也就是说你并不需要显示创建一个对象。应记住少用内置对象函数来创造对象。
看下面代码:
var str="I am a string";
console.log(str.length);//13
console.log(str.charAt(3);//"m"
我们可以直接在字符串字面量上访问属性或方法。之所以能这么做,是因为引擎自动把字面量转换成String对象。
注意: null和undefined没有相应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式。
注意: 对于Object、Array、Function和RegExp来说,无论使用文字形式还是构造函数形式,它们都是对象,不是字面量。这在使用typeof时都是object。
1.4对象属性的value
我们需要知道的是,属性的value并不是存储在对象的内部。在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器的内部。存储在对象容器内部的是这些属性的名称,它们就像指针一样,指向这些值真正的存储位置。
看下面代码:
声明:
var myObject={};
myObject[true]="foo";
myObject[3]="bar";
调用:
myObject["true"];//"foo"
myObject["3"];//"bar"
从上面可以看出,在对象中,属性名永远是字符串。如果你是用string意外的其它值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串。
数组也是对象,所以即使每个下标都是整数,你仍然可以给数组添加属性:
var myArray=["foo","bar",4];
myArray.baz="baz";
myArray.length;//3
myArray.baz;//"baz"
可以看到虽然添加了命名属性(无论是通过.语法还是[]语法),数组的length值并未发生变化。在这里不建议大家在数组中使用属性。最好是对象使用来存储键/值对,只用数组来存储数值下标/值对。
注意: 如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值的下标(因此会修改数组的内容,而不是添加一个属性)。
看下面代码:
var myArray=["foo",42,"baz"];
myArray["2"]="bar";
myArray[2];//"bar",第二行代码修改了数组的第三个元素值
1.5 复制对象
复制对象即是拷贝对象,拷贝有深拷贝、浅拷贝之分。并不是使用简单的“=”操作符即可。由于这个相对复杂,我已在另一篇博客详细解释,有兴趣可以查看。复制对象是对象的重点,务必掌握。
1.6 属性描述符
var obj={
a:"foo"
}
我们学习不能只看表面,上面就是一个简单的带有属性a的对象,这看上去谁都懂,但你真的了解每个属性吗?
带你了解属性内在东西。
语法:Object.getOwnPropertyDescriptor(obj,"某个属性");
查看对象的a属性:
var obj={
a:"foo"
}
console.log(Object.getOwnPropertyDescriptor(obj,"a"));
打印结果:
如图所见,这个普通的对象属性对应的属性描述符可不仅仅只有value:“foo”,它还有另外三个特征:
- writable:可写
- enumerable:可枚举
- configurable:可配置
在创建普通属性时,属性描述符会使用默认值。我们也可以使用下面语法来添加一个新属性或修改一个已有属性(修改的前提是该属性configurable:true)。
语法:Object.defineProperty()
举例:添加一个属性b,使之不能在被修改值,总是55
Object.defineProperty(obj,"b",{
value:55,
writable:false,
configurable:true,
enumerable:true
})
一般我们都不会使用这总方式,除非我们想修改属性描述符。
1.6.1 writable
决定是否可以修改属性的值
看下面代码:
var obj={
a:"foo"
}
Object.defineProperty(obj,"a",{
value:"foo",
writable:false,
configurable:true,
enumerable:true
})
obj.a="FOO";//尝试修改属性a
看运行结果:
TypeError: 表示我们不可以修改一个不可写的属性。
1.6.2 configurable
只要属性是可配置的,就可以使用Object.defineProperty()来修改属性描述符。一旦configurable:false,则不再能使用它修改属性描述符的任意一个值。
var obj={
a:"foo"
}
obj.a="FOO";
console.log(obj.a);
Object.defineProperty(obj,"a",{
value:"FOO",
writable:true,
configurable:false,
enumerable:true
})
obj.a="AAA";
console.log(obj.a);
Object.defineProperty(obj,"a",{
value:"FOO",
writable:true,
configurable:true,
enumerable:true
})
看运行结果:
看到了吗?在将configurable修改为false前后都可以修改属性a的值,但是你想再用Object.defineProperty()修改属性描述符就会报错TypeError。所以将configurable修改为false是单向操作,不可再更改。
注意: 当configurable:false时,我们还是可以将writable的状态由true修改为false,但无法由false改为true。
1.6.3 enumerable
该描述符控制着属性是否会出现在对象的属性枚举中。比如说,在for…in循环,当enumerable:false时,这个属性就不会出现在枚举中,即循环遍历不到它,但还是可以正常用.操作符访问它。
1.7 不变性
有时候我们希望属性或者对象是不可改变的。在ES5是提供了很多方法来实现,但所有方法都是浅不变性。即它们只会影响目标对象的和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数等) ,其它对象的内容不受影响,仍然是可变的。
举例使用代码:
myObj={
a:[1,2,3]
}
myObj.a.push(4);
myObj.a;//[1,2,3,4]
假设上面myObj已经是被设置为不可变的,但由于属性a是数组,所以它仍然可变。
想要实现深不变性还需添加下面方法使myObj.a也不变。
1.7.1 常量对象
结合writable:false和configurable:false就可以创建一个真正的常量属性(不可修改、重定义、删除)
var myObj={};
Object.defineProperty(obj,"属性子对象",{
writable:false,
configurable:false
})
1.7.2 禁止扩展
如果你想禁止一个对象添加新属性并保留自己的属性,可以使用下面方法。
Object.preventExtensions(obj)
看下面代码:
var myObj={
a:2
};
Object.preventExtensions(obj);
Object.defineProperty(obj,"b",{});//尝试再添加属性b
结果是报错,不可再扩展。
1.7.3 密封
Object.seal()会创建一个“密封”对象,这个方法实际会在一个现有对象上调用Object.preventExtensions()并把所有现有属性设置为configurable:false;密封之后不能再添加新对象,也不能再重新配置或者删除任何现有属性(还是可以改变属性值)
1.7.4 冻结(最高级别)
Object.freeze()会创建一个冻结对象,这个方法实际上是在一个现有对象上调用Object.seal()并把所有属性标记为writable:false。这样就无法修改属性值。(不过就像之前所说的,这个对象引用的其他对象是不受影响的)
1.8 存在性
看下面代码:
var myObj={
a:undefined
}
console.log(myObj.a,myObj.b);//undefined undefined
两者都返回undefined,但第一个是属性a的真实值,第二个属性是未定义的属性。
由于仅根据返回值无法判断出是变量的值为undefined还是变量不存在,那我们怎么区分的?
看下面代码:
var obj={
a:"aaa"
}
function check(obj){
console.log("a" in obj);
console.log("b" in obj);
console.log(obj.hasOwnProperty("a"));
console.log(obj.hasOwnProperty("b"));
}
in操作符会检查属性是否在对象及其[prototype]原型链中。而hasOwnProperty()只会检查obj对象中的属性,不会检查[prototype]链
检查是否存在某属性也可以使用“枚举”:
var obj={
a:"aaa"
}
function check(obj){
Object.defineProperty(obj,"b",{
value:5,
enumerable:true//可枚举
})
Object.defineProperty(obj,"c",{
value:6,
enumerable:false//不可枚举
})
for(var key in obj){
console.log(key);
}
}
看到了吧?属性c不可被枚举,for…in遍历不到它,但在所有属性都是可枚举时,我们可以使用for…in来检查某个属性是否存在,从而判断出undefined是变量值还是为定义的变量。
1.9 遍历
由上面代码只是遍历出属性名,那怎么遍历属性值呢?
1.9.1 尝试使用.操作符:
var obj={
a:"aaa"
}
function check(obj){
Object.defineProperty(obj,"b",{
value:5,
enumerable:true
})
Object.defineProperty(obj,"c",{
value:6,
enumerable:false//不可枚举
})
for(var key in obj){
console.log(obj.key);
}
}
看运行结果可知道用.操作符是无法读取到属性值的。
1.9.2 [ ]代替.操作符
var obj={
a:"aaa"
}
function check(obj){
Object.defineProperty(obj,"b",{
value:5,
enumerable:true
})
Object.defineProperty(obj,"c",{
value:6,
enumerable:false//不可枚举
})
for(var key in obj){
console.log(obj[key]);
}
}
这就遍历出属性值了。
1.9.3 ES6提供了for…of循环语法
该语法用在数组上:
var array=[1,2,3,5,4]
function check(array){
for(var key of array){
console.log(key);
}
}
那这么好的方法能用在对象上吗?
var obj={
a:"aaa",
b:"bbb",
c:"ccc"
}
function check(obj){
for(var key of obj){
console.log(key);
}
}
直接用在对象身上是会报错的,对象不是一个迭代器。
看看for…of原理:for…of循环首先会向被访问对象请求一个迭代对象,然后通过迭代对象的next()方法来遍历所有返回的值。数组之所以能直接使用,是因为数组由内置的@@iterator。ES6使用Symbol.iterator来获取对象的@@iterator内部属性。@@iterator本身不是一个迭代器对象,而是一个返回迭代器对象的函数。
普通对象没有内置的@@iterator,所以无法自动完成遍历。
当然,我们可以给任何想遍历的对象定义@@iterator,看代码:
var obj={
a:"aaa",
b:"bbb",
c:"ccc"
}
function check(obj){
Object.defineProperty(obj,Symbol.iterator,{//添加迭代对象
enumerable:false,//在遍历时该迭代属性不可读取
writable:false,
configurable:true,
value:function(){
var that=this;//指向obj
var index=0;
var k=Object.keys(that);//返回一个数组,记录着对象的属性名
return{
next:function(){
return {
value:that[k[index++]],
done:(index>k.length)//不可忽视
}
}
}
}
})
for(var key of obj){
console.log(key);
}
}
你看,给对象添加一个迭代对象就可以使用for…of循环来遍历对象的属性值了。
分析:
1.enumerable:false,//在遍历时该迭代属性不可读取
2.Object.keys(obj) 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该
对象时返回的顺序一致 。
参数必须是一个对象,否则报错typeError。
3.next()方法返回一个对象,value是当前遍历的属性值,done是一个布尔类型的值,表示是否继续遍历,控制着循环的
结束。