JavaScript 的对象

123 阅读10分钟

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

        var myObj = {
            key: value
            // ...
        };
        var myObj = new Object();
        myObj.key = value;

一般来说我们都是使用声明形式来创建 Object,使用构造形式来给 Object 补充后续变量。

Object 是 JavaScript 的主要类型之一

在 JavaScript 中一共有六种主要类型:string、number、boolean、null、undefined 和 object

而在 Object 中,还有一些子类型。分别是:String、Number、Boolean、Object、Function、Array、Date、RegExp、Error

这些子类型其实是一些内置的构造函数,被广泛应用在创建变量时赋予类型的操作上。例如 let a = new String("hello world")

Object 的内容

Object 的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称这些值为属性。

虽然属性看起来是被存储在对象内部,但在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针一样,指向这些值真正的存储位置

访问 Object 属性值的方法

访问属性的值的方法有两种,一种是 . 操作符,一种是 [] 操作符。

        var myObject = {
            a: 2
        };

        myObject.a; // 2

        myObject["a"]; // 2

.a 语法通常被称为“属性访问”,["a"] 语法通常被称为“键访问”。实际上它们访问的是同一个位置,并且会返回相同的值2,所以这两种方法是可以互换的。

这两种语法的区别在于,键访问语法可以接受任意UTF-8/Unicode字符串作为属性名,而属性访问则需要满足标识符的命名规范。

在对象中,属性名永远是字符串,当你使用字符串以外的其他类型值作为属性名,那它首先会被转换为字符串。

        var myObject = { };

        myObject[true] = "foo";
        myObject[3] = "bar";
        myObject[myObject] = "baz";

        myObject["true"]; // "foo"
        myObject["3"]; // "bar"
        myObject["[object Object]"]; // "baz"

可计算属性名

ES6中增加了可计算属性名,这个特性与各个框架结合起来,可以诞生出许多高效编写动态属性以及访问动态属性的方法。

        var prefix = "foo";

        var myObject = {
            [prefix + "bar"]:"hello",
            [prefix + "baz"]: "world"
        };

        myObject["foobar"]; // hello
        myObject["foobaz"]; // world

可计算属性名最常用的场景可能是 ES6 的 Symbol,每个 Symbol 的实际值基本都是无意义的,所以通常你接触到的是 Symbol 的名称。

        var myObject = {
            [Symbol.Something]: "hello world"
        }

浅拷贝与深拷贝

当我们简单的将A对象赋值给B对象,那么B对象将只拥有A的表层属性,而不包含A对象中的各种地址值。只有通过深拷贝,才能将A拥有的所有值与引用都传递给B对象。

        function anotherFunction() { /*..*/ }

        var anotherObject = {
            c: true
        };

        var anotherArray = [];

        var myObject = {
            a: 2,
            b: anotherObject, // 引用,不是复本!
            c: anotherArray, // 另一个引用!
            d: anotherFunction
        };
        
        anotherArray.push(anotherObject, myObject);

当我们可以保证对象是 JSON 安全的时候,可以使用 var newObj = JSON.parse(JSON.stringify(someObj));方法来实现深拷贝。

ES6 提供了 Object.assign(..)方法来实现浅复制。Object.assign()方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举的自有键,并把它们复制到目标对象,最后返回目标对象。

        var newObj = Object.assign({}, myObject);

        newObj.a; // 2
        newObj.b === anotherObject; // true
        newObj.c === anotherArray; // true
        newObj.d === anotherFunction; // true

Object 的属性描述符

        var myObject = {
            a:2
        };

        Object.getOwnPropertyDescriptor(myObject, "a");
        // {
        //    value: 2,
        //    writable: true,
        //    enumerable: true,
        //    configurable: true
        // }

如你所见,这个普通的对象属性对应的属性描述符包含有三个特性:writable(可写)、enumerable(可枚举)和 configurable(可配置)

在创建普通属性时描述符会使用默认值,我们也可以使用 Object.defineProperty(..)来添加一个新属性或者修改一个已有属性并对特性进行设置

        var myObject = {};

        Object.defineProperty(myObject, "a", {
            value: 2,
            writable: true,
            configurable: true,
            enumerable: true
        } );

        myObject.a; // 2

当我们将 writable 设置为 false 后,我们尝试修改 a 的值,将不会生效。

        var myObject = {};

        Object.defineProperty(myObject, "a", {
            value: 2,
            writable: false, // 不可写!
            configurable: true,
            enumerable: true
        } );

        myObject.a = 3;

        myObject.a; // 2

当我们将 configurable 设置为 false 后,这个值将禁止被删除,我们只允许将 writable 属性从 true 改成 false,无法修改 configurable 与 writable 的值为 true。

        var myObject = {
            a:2
        };

        myObject.a; // 2

        delete myObject.a;
        myObject.a; // undefined

        Object.defineProperty(myObject, "a", {
            value: 2,
            writable: true,
            configurable: false,
            enumerable: true
        } );

        myObject.a; // 2
        delete myObject.a;
        myObject.a; // 2

当我们将 enumerable 设置为 false 后,这个值将不会出现在枚举中,比如说 for..in 循环。

如何让 Object 的变量不可变

我们通过 const a = new Object(),只是能确保 a 变量的引用地址不会被修改,是一种浅不变性。

结合 writable: false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除)。

        var myObject = {};

        Object.defineProperty(myObject, "FAVORITE NUMBER", {
            value: 42,
            writable: false,
            configurable: false
        } );

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions(..)

        var myObject = {
            a:2
        };

        Object.preventExtensions(myObject);

        myObject.b = 3;
        myObject.b; // undefined

我们可以更进一步,让对象不仅不能添加新属性,也不能重新配置或者删除任何现有属性(但是可以修改属性的值)。那就是通过 Object.seal(..)方法来创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..)并把所有现有属性标记为 configurable:false。

JavaScript 还提供了一个赋予对象最高级别的不可变性的方法,它会禁止对于对象本身及其任意直接属性的修改。Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..)并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值

Object 的 [[Get]] 和 [[Put]] 操作

单单说到 get 和 put 操作,似乎挺陌生的,其实这些是很基础很常见的操作。

        var myObject = {
            a: 2
        };

        myObject.a; // 2
        myObject.b; // undefined

通过 Get 操作,不仅顺利将 a 属性的值返回给我们,同时把 b 属性返回值设置为 undefined,而不是直接抛出错误。 Put 操作就是设置值的操作, Put 的步骤大致如下:

  1. 属性是否是访问描述符?如果是并且存在 setter 就调用 setter。
  2. 属性的数据描述符中 writable 是否是 false?如果是,赋值失败。
  3. 如果都不是,将该值设置为属性的值。
        var myObject = {
            // 给a定义一个getter
            get a() {
              return 2;
            }
        };

        Object.defineProperty(
            myObject,  // 目标对象
            "b",         // 属性名
            {           // 描述符
                // 给b设置一个getter
                get: function(){ return this.a * 2 },
                // 确保b会出现在对象的属性列表中
                enumerable: true
            }
        );

        myObject.a; // 2

        myObject.b; // 4

在ES5中可以使用getter和setter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。

在ES5中可以使用getter和setter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。

        var myObject = {
            // 给a定义一个getter
            get a() {
              return this. a ;
            },

            // 给a定义一个setter
            set a(val) {
              this. a  = val * 2;
            }
        };

        myObject.a = 2;

        myObject.a; // 4

检查 Object 里属性的存在性

当我们访问一个 Object 中不存在的属性,或者访问一个 Object 中设置值为 undefined 的属性时,它们的返回值都是 undefined。那么如何区分这两种情况呢?

我们可以在不访问属性值的情况下判断对象中是否存在这个属性:

        var myObject = {
            a:2
        };

        ("a" in myObject); // true
        ("b" in myObject); // false

        myObject.hasOwnProperty("a"); // true
        myObject.hasOwnProperty("b"); // false

in 操作符会检查属性是否在对象及其[[Prototype]]原型链中。相比之下,hasOwnProperty(..)只会检查属性是否在对象中,不会检查[[Prototype]]链

所有的普通对象都可以通过对于Object.prototype的委托)来访问hasOwnProperty(..),但是有的对象可能没有连接到Object.prototype(通过Object. create(null)来创建)​。

在这种情况下,形如myObejct.hasOwnProperty(..)就会失败。这时可以使用一种更加强硬的方法来进行判断:Object.prototype.hasOwnProperty. call(myObject, "a"),它借用基础的hasOwnProperty(..)方法并把它显式绑定到myObject上

检测 Object 里属性的可枚举性

        var myObject = { };

        Object.defineProperty(
            myObject,
            "a",
            // 让a像普通属性一样可以枚举
            { enumerable: true, value: 2 }
        );

        Object.defineProperty(
            myObject,
            "b",
            // 让b不可枚举
            { enumerable: false, value: 3 }
        );

        myObject.b; // 3
        ("b" in myObject); // true
        myObject.hasOwnProperty("b"); // true

        // .......

        for (var k in myObject) {
            console.log(k, myObject[k]);
        }
        // "a" 2

通过 for..in 可以判断对象中哪些属性是具有枚举值的,只有拥有枚举值的属性才能被 in 操作符遍历出来。

也可以通过另一种方式来区分属性是否可枚举:

        var myObject = { };

        Object.defineProperty(
            myObject,
            "a",
            // 让a像普通属性一样可以枚举
            { enumerable: true, value: 2 }
        );

        Object.defineProperty(
              myObject,
              "b",
              // 让b不可枚举
              { enumerable: false, value: 3 }
          );

          myObject.propertyIsEnumerable("a"); // true
          myObject.propertyIsEnumerable("b"); // false

          Object.keys(myObject); // ["a"]
          Object.getOwnPropertyNames(myObject); // ["a", "b"]

propertyIsEnumerable(..))会检查给定的属性名是否直接存在于对象中(而不是原型链上)并且满足 enumerable:true。

Object.keys(..)会返回一个数组,包含所有可枚举属性, Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举。

in 和 hasOwnProperty(..)的区别在于是否查找[[Prototype]]链,然而,Object.keys(..)和 Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性

如何遍历属性的值?

for..in循环可以用来遍历对象的可枚举属性列表(包括[​[Prototype]​]链)​。

但是如何遍历属性的值呢?对于数值索引的数组来说,可以使用标准的for循环来遍历值:

        var myArray = [1, 2, 3];

        for (var i = 0; i < myArray.length; i++) {
            console.log(myArray[i]);
        }
        // 1 2 3

这实际上并不是在遍历值,而是遍历下标来指向值,如myArray[i]​。

ES5中增加了一些数组的辅助迭代器,包括forEach(..)、every(..)和some(..)。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。

forEach(..)会遍历数组中的所有值并忽略回调函数的返回值every(..)会一直运行直到回调函数返回false(或者“假”值),**some(..)会一直运行直到回调函数返回true(或者“真”值)**​。

every(..)和some(..)中特殊的返回值和普通for循环中的break语句类似,它们会提前终止遍历。使用for..in遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。

for..of循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。

        var myArray = [ 1, 2, 3 ];

        for (var v of myArray) {
            console.log(v);
        }
        // 1
        // 2
        // 3

数组有内置的@@iterator,因此for..of可以直接应用在数组上。我们使用内置的@@iterator来手动遍历数组,看看它是怎么工作的:

        var myArray = [ 1, 2, 3 ];
        var it = myArray[Symbol.iterator]();

        it.next(); // { value:1, done:false }
        it.next(); // { value:2, done:false }
        it.next(); // { value:3, done:false }
        it.next(); // { done:true }