在我们日常的开发中,对象无处不在,那么对象到底是什么,对象有哪些特性,本节就来学习一番。
对象基础
对象语法
对象可以通过两种形式定义:声明形式和构造形式。
- 对象的声明形式
var object = {
key:value
}
- 构造形式
var object = new Object();
object.key = value;
这两种方法生成的对象是一样的。区别在于声明的形式可以添加多个键/值对,但是在构造形式中你必须逐个添加属性。
对象类型
对象是JavaScript的基础,在JavaScript中还有一些对象子类型,通常被成为内置对象。
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
这些内置对象很像java中的类,但其实在JavaScript中,他们实际上只是一些内置函数。举例:
var str = "this is a string";
typeof str; // "string"
str instanceof String; //false
var strObj = new String("this is a string");
typeof strObj; //"object"
strObj instanceof String; //true
this is a string字符串并不是一个对象,它只是一个字面量,并且是一个不可变的值。如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为String对象。
在必要时JavaScript会自动把字符串字面量转换成一个String对象。
var str = "this is a string";
console.log( str.length ); // 16
console.log(str.charAt(2)); //"i"
使用以上两种方法,我们都可以直接在字符串字面量上访问属性或者方法,之所以可以这样做,是因为引擎自动把字面量转换成String对象,所以可以访问属性和方法。(这里先简单介绍到这里后续再展开说明其他内置对象)
对象属性
对象的属性是由一些存储在特定命名位置的值组成的。
var object = {
name:'tom'
};
object.name; //'tom'
object["name"]; //'tom'
我们访问object中name的值,一般通过.操作符或者[]操作符。.name语法通常被称为“属性访问”,["name"]语法通常被称为“键访问”。这两个术语是可以互换的。
这两种语法的主要区别在于.操作符要求属性名满足标识符的命名规范,而[".."]语法可以接受任意 UTF-8/Unicode字符串作为属性名。
在对象中,属性名永远都是字符串。如果你使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法。
var object = {};
object[true] = "cat";
object[7] = "dog";
object[object] = "bird";
object["true"]; //"cat"
object["7"]; //"dog"
object["[object Object]"]; //"bird"
对象属性描述
ES5开始,所有的属性都具备了属性描述符。一个数据属性和访问器属性。
var object = {
name:"tom"
};
Object.getOwnPropertyDescriptor(object, "name");
//{
// configurable: true,
// enumerable: true,
// value: "tom",
// writable: true
//}
- Value
包含这个属性的数据值也就是value:"tom",读取属性值的时候,从value中获取,写入属性值的时候,也是通过value来赋值,这是属性值默认值为undefined。
var object = {
name:"tom"
};
//读取
object.name; // "tom";
//写入2种方法都可以,一般开发我们都选择第一种。
object.name = "cat";
Object.defineProperty( object, "name", {
value: "cat"
} );
object.name; //"cat"
- Writable
表示是否能修改属性值。
var object = {};
Object.defineProperty(object, "name", {
value: "tom",
writable: false, // 不可写!
configurable: true,
enumerable: true
});
object.name = "cat";
object.name; //"tom";
严格模式下,会抛出异常。
- Configurable
表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。使用defineProperty(..)方法来修改属性描述符。
var object = {
name: "tom"
}
object.name = "cat";
console.log(object.name); //"cat"
Object.defineProperty(object, "name", {
value:"dog",
writable:true,
configurable:false,
enumerable:true
});
console.log(object.name);
object.name = "bird";
console.log(object.name);
Object.defineProperty(object, "name", {
value:"monkey",
writable:true,
configurable:true,
enumerable:true
});
delete myObject.name;
最后一次修改Object.defineProperty(..)会产生一个TypeError错误,试图修改一个不可配置的属性描述符都会出错。把configurable修改成false是单向操作,无法撤销!
注意:
- 即便属性是
configurable:false,我们还是可以把writable的状态由true改为false,但是无法由false改为true。 - 最后一个
delete语句失败了,因为属性是不可配置的。
- Enumerable
属性是否会出现在对象的属性枚举中。也就是能否通过for..in循环返回属性。
var object = {
name:"tom",
age:12,
year:2019,
job:"code"
};
Object.defineProperty(object, "year", {
writable:true,
configurable:true,
enumerable:false
});
for(var i in object){
console.log(i); //name,age,job
}
如果把enumerable设置成false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成true就会让它出现在枚举中。
- Getter和Setter
Getter在读取属性时调用的函数。默认值为undefined。Setter在写入属性时调用的函数。默认值为undefined。
var object = {};
Object.defineProperty(object,"age",{
get: function(){
return this._age;
},
set: function(value){
this._age = value * 2;
}
});
object.age = 12; //调用set方法
console.log(object.age); //调用get方法返回24
console.log(object.name); //调用get方法返回undefined
创建一个object对象,通过Object.defineProperty(..)给age属性添加get和set方法,来控制属性的写入和读取。
属性访问在实现时有一个微妙却非常重要的细节,object.age在object上实际上是实现了[[Get]] 操作(有点像函数调)。对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。然而,如果没有找到名称相同的属性,按照[[Get]]算法的定义会执行另外一种非常重要 的行为。(其实就是遍历可能存在的[[Prototype]]链,也就是原型链)。
如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回值undefined。
var object = {
foo: undefined
};
console.log(object.foo); // undefined
console.log(object.bar);// undefined
从返回值的角度来说,这两个引用没有区别——它们都返回了undefined。然而,尽管乍看之下没什么区别,实际上底层的[[Get]]操作对object.bar进行了更复杂的处理。
由于仅根据返回值无法判断出到底变量的值为undefined还是变量不存在,所以[[Get]]操作返回了 undefined。稍后会介绍如何区分这两种情况。
var object = {
name:"tom"
};
object.name = "cat";
[[Set]]被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性。(如果对象中不存在这个属性,[[Set]]操作会更加复杂。之后的[[Prototype]]会详细进行介绍。)
- 属性是否在属性数据描述符中,如果是并且存在
Setter就调用Setter。 - 属性的数据描述符中
writable是否是false。 - 如果都不是,将该值设置为属性的值。
var object = {
get a(){
return 1;
}
}
Object.defineProperty(object, "b" ,{
get: function() {
return this.a * 2
},
});
console.log(object.a); // 1
console.log(object.b); // 2
不管是用对象字面量语法中的get a(){..}还是使用Object.defineProperty(..)中的显示定义。二者 都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。[[Set]]也是同样如此。
属性描述符的一些特性
- 不变性
有时候你会希望属性或者对象是不可改变的,在ES5中可以通过很多种方法来实现。
- 对象常量
使用writable:false和configurable:false就可以创建一个真正的常量属性(不可修改、 重定义或者删除)
var object = {};
Object.defineProperty( object, "name", {
value: "tom",
writable: false,
configurable: false
});
- 禁止扩展
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..)
var object = {
name:"tom"
}
Object.preventExtensions(object);
object.age = 12;
console.log(object.age); //undefined
- 密封
Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)并把所有现有属性标记为configurable:false。
var object = {
name:"tom"
}
Object.seal(object);
delete object.name; // 无法被delete删除
object.age = 12; //无法添加新的属性
object.name = "cat";
console.log(object.name); //"cat"
- 冻结
Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..) 并把所有“数据访问”属性标记为writable:false,这样就无法修改它们的值。
var object = {
name:"tom"
}
Object.freeze(object);
object.name = "cat"; // 无法被修改
console.log(object.name); //"tom"
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改
- 存在性
前面我们介绍过,如object.foo的属性访问返回值可能是undefined,但是这个值有可能是属性中存储的undefined,也可能是因为属性不存在所以返回undefined。
我们可以在不访问属性值的情况下判断对象中是否存在这个属性:
var object = {
foo:1
};
("foo" in object); // true
("bar" in object); // false
object.hasOwnProperty("foo"); // true
object.hasOwnProperty("bar"); // false
in操作符会检查属性是否在对象及其[[Prototype]]原型链中。相比之下,hasOwnProperty(..)只会检查属性是否在object对象中,不会检查[[Prototype]]链。
- 枚举
之前介绍 enumerable 属性描述符特性时我们简单解释过什么是“可枚举性”,现在详细介绍一下。
var object = {};
Object.defineProperty(object,"name",{
enumerable: true,
value:"tom"
});
Object.defineProperty(object,"age",{
enumerable: false,
value:12
});
console.log(object.age); // 12
("age" in myObject); // true
object.hasOwnProperty("age"); //true
for (var i in object) {
console.log( i, myObject[i] );
}// "name" "tom"
Object.keys(obejct); // ["name"]
Object.getOwnPropertyNames(object); // ["name", "age"]
object.propertyIsEnumerable("name"); // true
object.propertyIsEnumerable("age"); // false
可以看到,object.age确实存在并且有访问值,但是却不会出现在for..in和Object.keys(..)循环中(尽管可以通过in 操作符来判断是否存在)。原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。
总结一下API:
in和hasOwnProperty(..)的区别在于是否查找[[Prototype]]链Object.keys(..)和Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性。Object.keys(..)会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举。propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true。