学习JavaScript“对象”

263 阅读9分钟

在我们日常的开发中,对象无处不在,那么对象到底是什么,对象有哪些特性,本节就来学习一番。

对象基础

对象语法

对象可以通过两种形式定义:声明形式和构造形式。

  • 对象的声明形式
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'

我们访问objectname的值,一般通过.操作符或者[]操作符。.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
//}
  1. 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"
  1. Writable

表示是否能修改属性值。

var object = {};
Object.defineProperty(object, "name", {
    value: "tom",
    writable: false, // 不可写! 
    configurable: true, 
    enumerable: true
});
object.name = "cat";
object.name; //"tom";

严格模式下,会抛出异常。

  1. 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语句失败了,因为属性是不可配置的。
  1. 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就会让它出现在枚举中。

  1. 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属性添加getset方法,来控制属性的写入和读取。

属性访问在实现时有一个微妙却非常重要的细节,object.ageobject上实际上是实现了[[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]]也是同样如此。

属性描述符的一些特性

  1. 不变性

有时候你会希望属性或者对象是不可改变的,在ES5中可以通过很多种方法来实现。

  • 对象常量

使用writable:falseconfigurable: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"

这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改

  1. 存在性

前面我们介绍过,如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..inObject.keys(..)循环中(尽管可以通过in 操作符来判断是否存在)。原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。

总结一下API:

  1. inhasOwnProperty(..)的区别在于是否查找[[Prototype]]
  2. Object.keys(..)Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性。
  3. Object.keys(..)会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举。
  4. propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true

参考