第3章: 对象
3.1 语法
- 对象有两种方式定义:
- 字面量的定义方式
var obj = { }
- 构造函数的定义方式
var obj = new Object();
- 构造函数和字面量的定义所生成的对象是一样的,两者的区别是:字面量可以添加多个键/值对,但构造函数的定义如果想定义多个键/值对,则必须逐个添加属性。
3.2 类型
- JavaScript一共有6种主要类型:
string
、number
、boolean
、null
、underfined
、object
。但简单基本类型本身并不是对象。
- 实际上,JavaScript种有许多特殊的对象子类型,我们称之为复杂基本类型。比如函数,就是对象的一个子类型,它像普通对象一样但只可以调用,可以作为参数进行传递。除此之外,数组也是特殊的子对象类型。
内置对象
- JavaScript种还有一些对象子类型,被称为内置对象:
String
、Number
、Boolean
、Object
、Function
、Array
、Date
、RegExp
、Error
。
- 这些内置对象表面上很像传统语音中的类型(type)或者类(class),比如Java中的String类。但在JavaScript中,它们只是一些内置函数。可以通过这些内置函数通过构造函数的方式构造一个对应子类型的新对象。
var str = 'i am a string';
typeof str; //string
str instanceof String; // false
var strObj = new String('i am a string');
typeof strObj; // object
strObj instanceof String; // true
- 原始值'i am a string'并不是一个对象,只是一个字面量,且是不可改变的值。当对这个字面量上执行一些操作的时候,比如获取长度时,JavaScript会自动把字符串字面量转换成一个String对象,而不需要显式的创建一个对象。同样的,数值和布尔字面量也会自动转成对象。
- null和undefined只要字面量形式,而没有对应的构造形式。相反,Date只有构造形式,而没有字面量形式。
- 对于Object、Array、Function和RegExp来说,无论使用文字还是构造形式,都是对象不是字面量。因为相比起字面量创建对象,构造形式可以传入额外的参数。
- Error对象一般很少在代码中显式创建,只是在抛出异常时被自动创建。
3.3 内容
- 对象的内容是由任意类型的值组成的,我们称之为属性。
- 但这只是它的表现形式。在引擎内部,这些值的存储方式是多种多样的,一般不会直接存在对象容器内部。存的只是名称,就像指针一样,指向真正存储的位置。
var myObj = {
a : 2
}
myObj.a; // 2
myObj['a']; // 2
- 访问对象的属性值可以使用.操作符或者[]操作符。
- .语法通常被称为“属性访问”,['a']语法通常被称为“键访问”。这两种语法的主要区别在于.操作符要求属性名满足标识符的命名规则,而[]语法可以接受任意字符串作为属性名,也就是说后者可以传递变量名。
- 如果你使用sring字面量以外的其他值作为属性名,那它会转换成一个字符串。
var myObj = {};
myObj[true] = 'foo';
myObj[3] = 'bar';
myObj[myObj] = 'baz';
myObj['true']; // 'foo'
myObj['3']; // 'bar'
myObj['[object object]]']; // 'baz'
3.3.1 可计算属性名
- 上面提到,如果需要通过表达式来计算属性名,可以使用
myObj[prefix + name]
的方式。但使用字面量形式来声明时,这样做就不行的。
- 幸好ES6增加了可计算属性名,可以在字面量形式使用[]包裹一个表达式来当作属性名:
var prefix = 'foo';
var myObj = {
[prefix + 'bar'] : 'hello',
[prefix + 'baz'] : 'world'
}
myObj['foobar']; //hello
myObj['foobaz']; //world
- 可计算属性名通常用在ES6的符号(Symbol),它是一种新的基础数据类型。
var myObj = {
[Symbol.Something] : 'hello world'
}
//有点类似vuex中store定义函数的用法
3.3.2 属性与方法
- 在传统语言中,普通函数叫做函数,当函数属于某个对象时被称为“方法”。因此,也通常把“方法访问”说成是“方法访问”。
- 但在JavaScript中,函数永远不会真正“属于”一个对象,并没有把一个函数变成“方法”,函数和对象的关系,最多也只能是间接引用关系。
function foo(){
console..log('foo');
}
var someFoo = foo; //对foo函数的变量引用
var myObj = {
someFoo : someFoo
}
//输出是一样的
foo;
someFoo;
myObj.someFoo;
- 所以,“函数”和“方法”在JavaScript中是一个东西。
3.3.3 数组
- 数组支持以[]的访问形式,期望通过填入整数的数值下标(索引)来获取数组中的元素。
var arr = ['foo',42,'bar'];
arr.length; //3
arr[0]; //foo
arr[2]; //42
- 上文提到,数组是特殊的对象,所以也可以给数组添加属性:(但并不推荐这样做,还是应该用对象来存储键/值对,用数组来存储索引/值对)
var arr = ['foo',42,'bar'];
arr.baz = 'baz';
arr.length; // 3 看到虽然可以给数组添加属性,但length值未发生变化
arr.baz; //baz
- 当通过[]进行赋值时,如果属性名看起来像数字,就会被当做索引来使用,所以会直接改变到数组内的元素。
3.3.4 赋值对象
- JavaScript最常遇到的问题之一,就是如何复制一个对象。
function anotherFunction(){}
var anotherObj = {
active : true
}
var anotherArr = [];
var myObj = {
age : 2,
walk : anotherFunction,
alive : anotherObj,
scoreHistory : anotherArr
}
anotherArr.push(anotherObj,myObj);
- 我们首先要判断是浅复制还是深复制。如果是浅复制就比较简单了,比如要复制
myObj
这个对象,里面有值就复制值,有引用就复制引用。
- 比如ES6中的
Object.assign(targetObj,[sourceObj1,sourceObj2])
方法可以实现浅复制,参数一是目标对象,参数二可以是一个或多个的源对象。该方法会遍历一个或多个源对象中,所有可枚举的键,并把它们使用=操作符赋值到目标对象,最后返回目标对象。
- 但对于深复制就比较麻烦了,代码中
anotherArr
对象和anotherObj
互相引用,这时该怎么办呢?为了解决这个棘手的问题,许多javascript框架都提出了自己的解决方案,但具体该采用哪种方法作为标准,没有明确的答案。
3.3.5 属性描述符
- ES5之前,JavaScript语言没有提供针对属性特性的描述符,比如属性是否只读等。但从ES5开始,所有的属性都具备属性描述符(也称为数据描述符)。
var myObj = {
a : 2
};
- 比如
myObj.a
属性就包含四个描述符:value
(值)、writable
(可写)、enumerable
(可枚举)、configurable
(可配置)
- ES5提供
Object.defineProperty()
方法新增或者修改属性并对属性特性进行配置:
var myObj = {};
Object.defineProperty(myObj,'a',{
value : 2,
writable : true,
configurable : true,
enumerable : true
})
-
writable :决定是否可以修改属性值。当值为false时,非严格模式下默认不能修改值,而在严格模式下,会抛出TypeError错误。
-
configurable :决定属性是否可配置。当值未false时,该属性就不能再通过
defineProperty()
方法进行修改,同时也无法delete
该属性,否则会抛出一个TypeError错误。(注意:把configurable
修改为false是单向操作,无法撤销)
-
enumerable :决定属性是否会出现在对象的属性枚举中,比如说
for in
循环。如果把enumerable设置为false,这个属性就不会出现在枚举中,但仍可以正常访问它。
3.3.6 不变性
- ES5中有多种方法来让对象或者对象的属性不可改变,但这些方法设置的都是浅不变性,也就是说,只会影响目标对象和它的直接属性,对象引用的其他对象不受影响,仍然是可变的。
-
对象变量 :结合
writable:false
和configurable:false
可以创建一个不可修改、不可重新定义和删除的属性。
var myObj = {};
Object.defineProperty(myObj,'NUMBER',{
value : 42,
writable : false,
configurable : false
});
-
禁止扩展 :可以用
Object.preventExtensions()
方法一个对象只保留已有属性,而禁止添加新属性。如果修改,在非严格模式下默认失败,在严格模式下会报TypeError。
-
密封 :用
Object.seal()
方法可以创建一个“密封”对象,密封后的对象,不能新增或删除属性,也不能重新配置属性的特性,但可以修改属性的值。
-
冻结 : 用
Object.freeze()
方法可以对一个对象设置级别最高的不可变性,它将禁止对于对象本身及其任意直属属性的修改。
3.3.7 [[Get]]
var myObj = {
a : 2
};
myObj.a; //2
myObj.b; //undefined
-
myObj.a
获取属性实际上是进行了[[Get]]
操作(有点像函数调用:[[Get]]()
)
- 首先在对象中查找是否有名称相同的属性,如果找到就返回这个属性值;
- 如果没有找到名称相同的属性,则遍历可能存在的
[[Prototype]]
链(第五章会详细介绍);
- 如果无论如何都没有找到名称相同的属性,那
[[Get]]
操作会返回undefined
;
3.3.8 [[Put]]
- 有可以获取数据的
[[Put]]
操作,就一定有对应的[[Put]]
操作。
- 当
[[Put]]
被触发时,首先检查对象中是否已经存在这个属性,如果不存在,会执行更复杂的操作,会在第5章详细讨论。如果存在这个属性,会检查下面的这些内容:
- 属性是否是访问描述符(参见3.3.9小结),如果是描述符,并且存在setter
- 属性的数据描述符是否
writable
,如果是false,在非严格模式下静默失败,严格模式下抛出TypeError异常;
- 如果都不是,将该值设置为属性的值;
3.3.9 Getter和Setter
- ES5中可以用getter和setter改写默认操作,但只能应用在单个属性上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。
- 如果一个对象属性同时定义了getter和setter时,该属性会被定义为“访问描述符”。对于访问描述符来说,JavaScript会忽略属性的value和writable的属性,而只关心set和get特性。
var myObj = {
get a(){
return a;
}
};
Object.defineProperty(myObj,'b',{
get : function(){
return this.a * 2;
},
enumrable : true
});
myObj.a; // 2
myObj.b; // 4
- 通常getter和setter是成对出现的,如果只设置了对象属性的getter,对属性进行set操作会被忽略。
3.3.10 存在性
- 有个问题,我们进行属性访问时,返回值可能是undefined,而这个值有可能是属性存储中的undefined,也有可能是属性不存在而返回的undefined。那么应该如何区分呢?
var myObj = {
a : 2
};
("a" in myObj); //true
("b" in myObj); //false
myObj.hasOwnProperty("a"); // true
myObj.hasOwnProperty("b"); // false
-
in
操作符检查属性是否在对象以及其[[Prototype]]原型链中,而hasOwnProperty()
则只会检查属性是否在对象中,不会检查[[Prototype]]原型链。
- 除了通过for循环来确定属性是否可枚举之外,
propertyIsEnumerable()
方法也可以检查属性名是否在对象中,并符合enumerable:true
。
- 另外,
Object.keys()
和Object.getOwnPropertyNames()
方法会返回对象中可枚举的属性数组。
3.4 遍历
- 关于遍历属性值,除了可以用标准的for循环,ES5还增加了一些数组的辅助迭代器:
forEach()
、every()
、some()
-
forEach()
会遍历数组中的所有值,并忽略回调函数的返回值;
-
every()
会一直运行直到回调函数返回false;
-
some()
会一直运行直到回调函数返回true;
- ES6增加了一种用来遍历数组的
for of
循环语法:
var myArray = [1,2,3];
for(var item of myArray){
console.log(item);
}
// 1
// 2
// 3
-
for of
循环的工作原理是,会向被访问对象请求一个迭代器对象(iterator
),然后通过调用迭代器对象的next()
方法来遍历所有返回值。next()
方法会返回形式为{value:...,done:...}
的值,value是当前的遍历值,done是一个布尔值,表示十分还有可以遍历的值。
3.5 小结
- JavaScript中的声明对象有两种方式:字面量形式和构造形式。字面量形式更常用,但构造形式可以提供更多选项。
- 对象是6个基础类型之一,对象还包含具有不同行为的特殊子类型,比如function。
- 对象就是键值对的集合。可以通过
.propName
或者[propName]
语法来获取属性值。访问属性时,引擎实际上会调用内部默认的[[Get]]
操作,[[Get]]
操作会检查对象本身是否包含这个属性,如果没有找到还会查找[[Protype]]原型链。
- 属性的特性可以通过属性描述符来控制,比如
writable
和configurable
。此外,Object.preventExtensions()
、Object.seal()
和Obejct.freeze()
可以设置对象不可变性的级别。
- 属性不一定包含值,也有可能是具备了getter/setter的访问描述符。
- 可以使用ES6的
for...of
语法来遍历数组或对象中的值,for...of
循环会寻找内置或自定义的@@iterator
对象并调用它的next()
方法来遍历数据值。