Javascript 复杂数据类型 —— Object(对象)

1,705 阅读10分钟

简单理解

ECMA-262将对象定义为一组属性的无序集合。简单来说就是由多个键值对构成的数据。

其中“键”可以是字符串,数字或者Symbol类型数据

而“值”可以是基础类型的数据、array、map、set、{}、function() 等数据

在JS中存在一种“一切皆对象”的说法,原因是绝大多数值都可以使用对象表示,例如函数、数组、对象字面量等本质上都是对象,包括一些原始数据类型(比如String类型),JS也提供了相应的构造函数。

区分概念:对象(引用值),类

对象是某个特定 引用类型 的实例。在ECMAScript中,引用类型是把数据和功能组织到一起的结构。

虽然从技术上将Javascript是一门面向对象语言,但ECMAScript缺少传统的面向对象编程语言所具备的某些基本结构,包括类和接口。

引用类型有时候也被称为对象定义,因为他们描述了自己的对象应有的属性和方法

引用类型虽然有点像类,但跟类并不是一个概念。ES6新引入的class关键字具有正式定义类的能力,但也只是封装了 构造函数 + 原型继承 的语法糖而已

简单理解对象

  • 创建Object的一个新实例

    let obj = new Object();
    obj.name = 'echo';
    
  • 使用对象字面量

    let obj = {
        name: 'echo'
    };
    

以上两种创建对象的行为是等价的

在使用对象字面量表示法定义对象时,并不会实际调用Object构造函数

  • 访问对象中属性的两种方式

    • obj.name 常用方式

    • obj['name'] 可以用来读取动态属性的值

复杂一点的对象

let obj = {
    name: 'echo',
    friends: [
        {
            name: 'Jon',
            age: 12
        }
    ],
    sayName() {
        console.log(this.name);
    }
}
obj.foo = Symbol('foo');
console.log(obj);

截图.PNG

遍历对象

  • for-in 不常用

    返回实例和原型所有(包括可枚举与不可枚举)属性。

    注意,原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会被返回,因为默认情况下开发者定义的属性都是可枚举的

  • Object.getOwnPropertyNames(obj)

    列出所有实例属性,无论是否可枚举。

    console.log(Object.getOwnPropertyNames(Person.prototype)); // ['constructor', 'name', 'age']
    
  • Object.getOwnPropertySymbols(obj)

    ES6新增,与Object.getOwnPropertyNames(obj)相似,只是针对Symbol类型而已。

  • Object.keys(obj)

    返回对象(obj)上所有可枚举实例属性,返回值是 可枚举属性名称的字符串 数组

    function Person() {}
    Person.prototype.name = 'echo';
    Person.prototype.age = 12;
    let person1 = new Person();
    person1.name = 'Jon';
    console.log(Object.keys(person1)); // ['name']
    

以下是ES2017新增,用于将对象内容转换为序列化的——更重要的是可迭代的——格式 的两个方法

注意:

非字符串属性会被转换为字符串输出

另外,这两个方法执行对象的浅复制

Symbol类型的属性会被忽略

  • Object.values(obj)

    ES2017新增,返回 属性值 的数组

  • Object.entrues(obj)

    ES2017新增,返回 键值对 的数组

属性的内部特性

ECMA-262使用一些内部特性来描述属性的特征。这些特征是由为Javascript实现引擎的规范定义的。

因此,开发者不能在JS中直接访问这些特性。

规范用[[]]将某个特性标识为内部特性,例如[[Enumerable]]

  • 数据属性

    数据属性有4个特性描述他们的行为:

    • [[Configurable]] —— 是否可配置,默认值为true

      表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。

    • [[Enumerable]] —— 是否可遍历,默认值为true

      表示属性是否可以通过for-in循环返回

    • [[Writable]] —— 是否可修改,默认值为true

    • [Value]] —— 属性实际的值,默认值为undefined

    要修改属性的默认特性,就必须使用Object.defineProperty()方法。

    Vue中的 双向数据绑定 就是使用了Object.definePropertygettersetter + 观察者模式 来实现的

    let person = {};
    Object.defineProperty(person, 'name', {
        writable: false,
        value: 'echo'
    });
    console.log(person.name); // 'echo'
    person.name = 'Jon'; 
    console.log(person.name); // 'echo'
    

    上例中,当writable被设置为false时,name这个属性的值就不能再修改了。

    在非严格模式下尝试修改这个属性会被忽略,严格模式下则会抛出错误

    一个属性被定义为不可配置之后configurable: false,就不能再变回可配置的了。再次调用Object.defineProperty()并修改任何非writable属性会导致错误

    在调用Object.defineProperty()时,configurableenumerablewritable的值如果不指定,则都默认false

  • 访问器属性

    访问器属性不包含数据值。相反,它们包含两个非必需函数 —— getter函数和setter函数,

    在读取访问器属性时,调用getter函数,返回属性对应的值

    在写入访问器属性时,调用setter函数,设置属性的最新值

    访问器属性有4个特性描述他们的行为:

    • [[Configurable]] —— 是否可配置,默认true

    • [[Enumerable]] —— 是否可遍历,默认true

    • [[Get]] —— 获取函数,在读取属性时调用。默认值为undefined

    • [[Set]] —— 设置函数,在写入属性时调用。默认值为undefined

    let person = {
        name_: 'echo',
        age: 1
    }
    Object.defineProperty(person, 'name', {
        get() {
            return this.name_;
        },
        set(newVal) {
            this.name_ = newVal;
            this.age ++; // 访问器属性的典型使用场景即设置一个属性值会导致一些其他变化发生
        }
    });
    person.name = 'Jon';
    

    带下划线的属性常用来表示该属性并不希望在对象方法的外部被访问

    获取函数和设置函数不一定都要定义。

    只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。严格模式下会抛出错误。

    类似的,只有一个设置函数的属性时不能读取的,非严格模式下读取返回undefined,严格模式下则会抛出错误

  • 定义多个属性

    let person = {};
    Object.defineProperties(person, {
        name_: {
            value: 'echo'
        },
        age: {
            value: 1
        },
        name: {
            get() {
                return this.name_;
            },
            set(newVal) {
                this.name_ = newVal;
                this.age++;
            }
        }
    })
    

读取属性的特性

  • Object.getOwnPropertyDescriptor(obj, propertyName)

    取得指定属性的属性描述符

    // 这里引用上文中的person
    let descriptor = Object.getOwnPropertyDescriptor(person, "name_");
    console.log(descriptor.value); // 'echo'
    console.log(descriptor.configurable); // false
    console.log(typeof descriptor.get); // 'undefined', 如果是获取name属性的get,结果为 'function'
    console.log(descriptor.enumerable); // false
    
  • Object.getOwnPropertyDescriptors() ES2017新增静态方法

    该方法实际上会在每个自有属性上调用Object.getOwnPropertyDescriptor()并在一个新对象中返回它们

    console.log(Object.getOwnPropertyDescriptors(person));
    // {
    //    age: {
    //        configurable: false,
    //        enumerable: false,
    //        value: 1,
    //        writable: false
    //    },
    //    name: {
    //        configurable: false,
    //        enumerable: false,
    //        get: f(),
    //        set: f(newVal),
    //    },
    //    name_: {
    //        configurable: false,
    //        enumerable: false,
    //        value: 'echo',
    //        writable: false
    //    }
    // }
    

Object实例都有以下属性和方法

  • obj.constructor

    用于创建当前对象的函数

  • obj.hasOwnPropety(name)

    用于判断当前对象实例上是否存在给定的属性,不检查其原型

  • obj.isPrototypeOf(obj)

    用于判断当前对象是否为另一个对象的原型

  • obj.propertyIsEnumerable(name)

    用于判断给定的属性是否是可枚举的

  • obj.toLocaleString()

    返回对象的字符串表示

  • obj.toString()

    返回对象的字符串表示

  • obj.valueOf()

    返回对象对应的字符串、数值或布尔值表示。

    通常与toString()的返回值相同

对象解构 (ES6新增)

对象解构就是使用与对象匹配的结构来实现对象属性赋值。

const person = {
    name: 'echo',
    age: 12
}
let {name, age} = person;
console.log(name); // 'echo'
console.log(age); // 12
let {name: pName, age: pAge} = person;
console.log(pName); // 'echo'
console.log(pAge); // 12

解构赋值不一定与对象的属性匹配,赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量值为undefined

let {name, like} = person;
console.log(name); // 'echo'
console.log(like); // undefined

在解构赋值的同时定义默认值

let {name, job = 'engineer'} = person;

如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中

let pName, pAge;
let person = {
    name: 'echo',
    age: 12
};
({name: pName, age: pAge} = person);
console.log(pName, pAge); // echo 12

如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分

在函数参数列表中也可以进行解构赋值

let person = {
    name: 'echo',
    age: 12
};
function printPerson(foo, {name, age}, bar) {
    console.log(arguments);
    console.log(name, age);
}

printPerson('a', person, 2);
// ['a', {name: 'echo', age: 12}, 2]
// 'echo', 12

可选链运算符

obj?.name 可选链运算符是2019年12月纳入ECMAScript标准中的新特性。

当尝试访问对象属性时,如果对象的值为undefined或null,那么属性访问将产生错误。为了提高程序的健壮性,在访问对象属性时通常需要检查对象是否已经初始化,只有当对象不为undefined和null时才去访问对象的属性。

可选链运算符旨在帮助开发者省去冗长的undefined值和null值检查代码,增强了代码的表达能力。

  • 可选地静态属性访问 obj?.name

  • 可选地计算属性访问 obj?[expr]

  • 可选地函数调用或方法调用 fn?.()

  • 短路求值

    &&||也具有短路求值的特性

    let x = 0;
    let a = undefined;
    a?.[++num];
    
    console.log(x); // 0
    

对象的相等判断

在ES6之前,有一些特殊情况使用 === 也无法区别,

例如 console.log(+0 === -0); // true

为改善这类情况,ES6新增了 Object.is()

console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is('2', 2)); // false
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(NaN, NaN)); // true

要检查超过两个值,递归地利用相等性传递即可:

function checkEqual(x, ...rest) {
    return Object,is(x, rest[0]) && (rest.length < 2 || checkEqual(...rest));
}

合并对象 —— Object.assign(target, origin [, origin1, origin2 ...])

延伸:浅拷贝&深拷贝

就是把源对象所有的本地属性一起复制到目标对象上。有时候这种操作也被称为“混入”(mixin),因为目标对象通过混入源对象的属性得到了增强

let obj = Object.assign({ name: 'echo' }, { age: 12 })

  • Object.assign修改目标对象,也会返回修改后的目标对象
const a = { name: 'echo' };
const c = Object.assign(a, {age: 12});

console.log(a); // {name: 'echo', age: 12}
console.log(c); // {name: 'echo', age: 12}
console.log(a === c); // true
  • 对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。

    dest = {
        set a(val) {
            console.log(`Invoked dest setter with param ${val}`);
        }
    };
    src = {
        get a() {
            console.log('Invoked src getter');
            return 'foo';
        }
    };
    Object.assign(dest, src);
    console.log(dest);
    

    截图.PNG

    解析以上代码:

    1、调用srcget()

    2、 调用dest的set()并传入参数foo

    3、因为这里的设置函数不执行赋值操作

    4、所以实际上并没有把值转移过来

  • 将每个源对象中可枚举(Object.propertyIsEnumerable() 返回 true)和自有属性复制到目标对象。如果属性重复,后面的会覆盖前面的属性值。继承属性和不可枚举属性时不能拷贝的

    const obj = Object.create({foo: 1}, { // foo 是个继承属性
        bar: {
            value: 2 // bar 是个不可枚举属性
        },
        baz: {
            value: 3,
            enumerable: true // baz是个自身可枚举属性
        }
    });
    
    const res = Object.assign({}, obj);
    console.log(res); // {baz: 3}
    
  • Object.assign()实际上对每个源对象执行的是浅复制(即只会深拷贝源对象的第一层属性,再深层的属性只会拷贝其引用),举例说明:

const dest = {
    a: 'a',
    obj: {
        b: 1
    }
};
let origin = {
    d: 66,
    f: {
        age: 12
    }
};
const res = Object.assign(dest, origin);
origin.d = 1;
console.log('res1', origin, res);

截图.PNG

修改深层属性值:

origin.f.age = 2;
console.log('res2', origin, res);

截图.PNG

  • 原始类型会被包装为对象, nullundefined 会被忽略
    const v1 = 'abc';
    const v2 = true;
    const v3 = 10;
    const v4 = Symbol('foo');
    
    const obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
    // 只有字符串的包装对象才可能有自身可枚举属性
    console.log(obj); // { "0": "a", "1": "b", "2": "c" }
    
  • 不能回滚,在赋值期间出错,则操作终止并退出,同时抛出错误。此时可能只会完成部分复制

进阶

创建复杂对象