对象

79 阅读13分钟

对象基础

ECMA-262将对对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此,可以把ECMAScript的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。

——《JavaScript高级程序设计(第四版)》

1.创建对象

创建对象通常使用字面量形式来创建,也可以通过对象的构造函数 Object 来创建一个实例。这两种创建对象的形式的等价的,属性和方法都一样都有自己的特征,而这些特征决定了它们在JavaScript中的行为。

// 分别用两种方式创建对象
// 通过 Object 构造函数创建对象实例
const instanceObj = new Object()
instanceObj.name = 'Hongyun'
instanceObj.age = 18
instanceObj.job = 'Frontend Engineer'
instanceObj.sayName = function () {
    console.log(this.name)
}
​
console.log(instanceObj)
// {
//   name: 'Hongyun',
//   age: 18,
//   job: 'Frontend Engineer',
//   sayName: [Function: sayName]
// }
instanceObj.sayName() // Hongyun// 通过对象字面量形式创建对象
const customObj = {
    name: 'Hongyun',
    age: 18,
    job: 'Frontend Engineer',
    sayName() {
        console.log(this.name)
    }
}
​
console.log(customObj)
// {
//   name: 'Hongyun',
//   age: 18,
//   job: 'Frontend Engineer',
//   sayName: [Function: sayName]
// }
customObj.sayName() // Hongyun

2.对象属性的类型

ECMA-262使用一些内部特性来描述属性的特征。这些特性由JavaScript实现引擎的规范定义的。所以开发者不能在JavaScript中 直接访问这些特性 。为了将某个特性标识为内部特性,规范会用 两个中括号把特性的名称括起来 ,比如 [[Enumerable]]

属性分为两种:数据属性和访问器属性

数据属性:数据属性包含一个保存数据值的位置。值会从这个位置读取也会写入这个位置。

  • [[Configurable]]: 表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,是否可以把它改为访问器属性。默认情况下直接定义的属性这个特性为 true
  • [[Enumerable]]: 属性是否可以通过 for-of 循环。默认情况下直接定义的属性这个特性为 true
  • [[Writable]]: 属性值是否可以修改。默认情况下直接定义的属性这个特性为 true
  • [[Value]]: 属性的实际的值,从这里读取,写入这个位置。默认为 undefined

访问器属性:访问器属性不包含数据值,它包含一个获取(getter)函数和一个设置(setter)函数,者两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数负责返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数负责决定对数据做出修改。

  • [[Configurable]]: 与数据属性 [[Configurable]] 表现形式一致。
  • [[Enumerable]]: 与数据属性 [[Enumerable]] 表现形式一致。
  • [[Get]]: 获取函数,在读取属性时调用。默认值为 undefined
  • [[Set]]: 设置函数,在写入属性时调用。默认值为 undefined

像上面创建对象那样创建一个对象数据属性 [[Configurable]][[Enumerable]][[Writable]] 都会设置为 true ,而 [[Value]] 会被设置为指定的值。可以通过 Object.defineProperty() 来修改数据属性;访问器属性必须使用 Object.defineProperty() 来定义。

2.1 Object.defineProperty()

要定义或修改数据属性和访问器属性只能通过 Object.defineProperty() 方法来修改。

此方法接收3个参数:1. 要添加属性的对象;2. 属性的名称;3. 描述对象的属性

描述对象的属性包含:

  • 数据属性:

    • [[Configurable]] => configurable: boolean
    • [[Enumerable]] => enumerable: boolean
    • [[Writable]] => writable: boolean
    • [[Value]] => value: any
  • 访问器属性:

    • [[Configurable]] => configurable: boolean
    • [[Enumerable]] => enumerable: boolean
    • [[Get]] => get: function
    • [[Set]] => set: function

一但将 configurable 属性设置为 false 后就不能将属性从对象上删除,而且就不能在设置为 true,也无法在数据属性和访问器属性之间转换,否则将报错。虽然可以对同一个属性多次调用此方法,但是将 configurable 设置为 false 之后就会受限制。

访问器属性:虽然获取函数和设置函数不一定都要定义,但是如果只定义获取函数意味着改属性是 只读 的,尝试修改属性会抛出错误;如果只定义一个设置函数,那么读取改属性也会抛出错误。

const customObj = {
    name: 'Hongyun',
    age: 18,
    job: 'Frontend Engineer',
    sayName() {
        console.log(this.name)
    }
}
// 这里同时定义了数据属性和访问器属性,如果运行会抛出错误
Object.defineProperty(customObj, 'age', {
    writable: false,
    set(v) {
        return v
    }
})
// TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute,// configurable 将变成不可配置属性
Object.defineProperty(customObj, 'age', {
    configurable: false,
    
})
delete customObj.age
console.log(customObj)
// {
//   name: 'Hongyun',
//   age: 18,
//   job: 'Frontend Engineer',
//   sayName: [Function: sayName]
// }// 抛出错误 TypeError: Cannot redefine property: age
Object.defineProperty(customObj, 'age', {
    configurable: true
})
// TypeError: Cannot redefine property: age
Object.defineProperty(customObj, 'age', {
    get() {
        return 22
    }
})
// 经过我的测试如果在设置configurable为false的同时也设置其他描述对象属性,此时是可以使用的,但是下次再配置会报错
Object.defineProperty(customObj, 'age', {
    configurable: false,
    enumerable: false
})
for (const customObjKey in customObj) {
    console.log(customObjKey) // name job sayName
}
// TypeError: Cannot redefine property: age
Object.defineProperty(customObj, 'age', {
    enumerable: true
})

访问器属性这里就不放实例代码了,相信用过vue的多多少少都知道改如何使用

2.2 定义多个属性

ECMAScript 提供了方法 Object.defineProperties() 来一次性定义多个属性,它接收两个参数:1. 要添加属性的对象;2. 要修改的属性。

const people = {}
​
Object.defineProperties(people, {
    age_: {
        // 其他属性未设置默认未false
        value: 18
    },
    difference: {
        value: 0,
        writable: true
    },
    age: {
        get() {
            return this.age_ + this.difference
        },
        set(v) {
            if (v >= this.age_) {
                this.difference = v - this.age_
            } else {
                throw new Error('设置age必须大于当前age值')
            }
        }
    }
})
​
people.age = 20
console.log(people.age)

2.3 读取对象属性特性

使用 Object.getOwnPropertyDescriptor() 方法可以取得指定对象的指定属性的属性描述符。简单来说就是获取对象属性的特性,如 数据属性访问器属性

在ECMAScript 2017 中新增了 Object.getOwnPropertyDescriptors() 静态方法。这个方法会返回指定对象上所有属性的所有特性。这个方法实际上是在每个属性上调用 Object.getOwnPropertyDescriptor() 方法,并在一个对象上返回。

// 获取 people 对象 age_ 属性的所有特性
const age_Descriptor = Object.getOwnPropertyDescriptor(people, 'age_')
// { value: 18, writable: false, enumerable: false, configurable: false }
console.log(age_Descriptor)
// 获取 people 对象 age 属性的所有特性
const ageDescriptor = Object.getOwnPropertyDescriptor(people, 'age')
// { get: [Function: get], set: [Function: set], enumerable: false, configurable: false }
console.log(ageDescriptor)
​
// 获取 people 对象所有属性的所有特性
const ageDescriptors = Object.getOwnPropertyDescriptors(people)
console.log(ageDescriptors)
/* {
    age_: {
        value: 18,
            writable: false,
            enumerable: false,
            configurable: false
    },
    difference: { value: 2, writable: true, enumerable: false, configurable: false },
    age: {
        get: [Function: get],
        set: [Function: set],
        enumerable: false,
            configurable: false
    }
} */

3.合并对象及相等判定

3.1 合并对象

把源对象所有的本地属性一起复制到目标对象上,从而增强源对象的属性。合并(merge)有时也称为混入(mixin)。

ECMAScript 6 提供了一个 Object.assign() 方法来合并对象。将每个源对象中可枚举属性和属性的特性一起复制到目标对象上,这个方法会使用源对象的 [[Get]] 属性取得对象的值,使用目标对象的 [[Set]] 属性设置属性值。

注意事项:

  • 该方法对每个源对象执行浅复制,多个源对象有相同属性,后面对象的属性会覆盖前面的属性值;
  • 对于访问器属性,只会将属性的 [[Get]] 方法返回值复制到目标对象上,不会复制源对象的获取函数和设置函数;
  • 如果赋值期间出错,那么会终止操作,并抛出错误。将已经复制的属性以及属性值赋值给目标对象。
let targetObj = { baz: 'target' }
let sourceObj1 = { foo: 'foo1' }
const sourceObj2 = { foo: 'foo2', bar: 'bar2' }
Object.assign(targetObj, sourceObj1, sourceObj2)
// 执行了浅复制,后面属性覆盖前面属性值
// { baz: 'target', foo: 'foo2', bar: 'bar2' }
console.log(targetObj)
​
targetObj = {
    foo_: null,
    set foo(val) {
        console.log('targetObj 设置函数执行', val)
        this.foo_ = val
    },
    get foo() {
        return this.foo_
    }
}
​
sourceObj1 = {
    get foo() {
        console.log('执行了 sourceObj1 get 函数')
        return 'sourceObj1'
    },
​
    set foo(val) {
        console.log('执行了 sourceObj1 set 函数', val)
    }
}
​
Object.assign(targetObj, sourceObj1)
// 执行了 sourceObj1 get 函数
// targetObj 设置函数执行 sourceObj1
// sourceObj1
console.log(targetObj.foo)
​
targetObj = {}
sourceObj1 = {sourceObj1: 'sourceObj1'}
sourceObj2 = {
    sourceObj2: 'sourceObj2',
    get errorAttr() {
        throw new Error('get => errorAttr')
        return 'errorAttr'
    },
    errorAttrAfter: 'errorAttrAfter'
}
try {
    Object.assign(targetObj, sourceObj1, sourceObj2)
} catch (e) {
    // Error: get => errorAttr ......
    console.log(e)
}
// { sourceObj1: 'sourceObj1', sourceObj2: 'sourceObj2' }
console.log(targetObj)

3.2 对象相等的判定

对于一般情况 === 完全够用,但是对于判断 +0 -0 以及判定 NaN 是不可以的,这时可以使用 ECMAScript 6 新增的 Object.is() 方法来判断。

console.log(+0 === -0) // true
console.log(+0 === 0) // true
console.log(-0 === 0) // true
console.log(NaN === NaN) // false
console.log(isNaN(NaN) === isNaN(NaN)) // trueconsole.log(Object.is(+0, -0)) // false
console.log(Object.is(+0, 0)) // true
console.log(Object.is(-0, 0)) // false
console.log(Object.is(NaN, NaN)) // true

4.对象增强语法

ECMAScript 6 新增了很多极其有用的语法糖。这些新特性没有改变现有引擎的行为,但是极大提升了处理对象的方便程度。

4.1 对象属性值简写

只要对象的属性名和变量名称一致,那么只要在对象中使用变量名就会自动解释为 同名的属性键:变量值 。如果没有找到会抛出 ReferenceError 错误。

const name = 'Hongyun'
const programmer = {
    name
}
console.log(programmer) // { name: Hongyun }

4.2 可计算属性

可直接在定义对象的时候使用中括号语法来动态定义对象属性。在引入可计算属性之前如果想使用中括号语法只能在定义对象之后在添加。

const nameKey = 'name'
const programmer = {}
programmer[nameKey] = 'Hongyun'
console.log(programmer) // { name: 'Hongyun' }const programmer1 = {
    [nameKey]: 'Hongyun',
    [funKey('name')]: 'Hongyun'
}
console.log(programmer1) // { name: 'Hongyun', 'funKey-name': 'Hongyun' }

4.3 简写方法名

在 ECMAScript 6 之前,给对象定义方法名通常需要写一个方法名、冒号然后在加一个匿名函数表达式,在 ECMAScript 6 新增简写方法名,可以省略方法名以及冒号。

可用范围:

  • 对象的方法名;
  • 对象中的获取函数和设置函数;
  • 对象中可计算属性键;
  • 类同样适用。
const dynamicKey = 'dynamic'const obj = {
    name_: 'zhang',
​
    // 获取函数
    get name() {
        console.log('get => name', this.name_)
        return this.name_
    },
    // 设置函数
    set name(value) {
        console.log('set => name', value)
        this.name_ = value
    },
​
    // 方法名
    getName() {
        console.log('getName => ', this.name)
        return this.name
    },
​
    // 可计算属性键
    [dynamicKey]() {
        console.log(`${dynamicKey} => `, this.getName())
    }
}
​
console.log(obj.name)
// get => name zhang
// zhang
obj.name = 'hongyun'
// set => name hongyun
obj.getName()
// get => name hongyun
// getName =>  hongyun
// get => name hongyun
obj[dynamicKey]()
// get => name hongyun
// getName =>  hongyun
// get => name hongyun
// dynamic =>  hongyun

4.4 对象解构

ECMAScript 6 新增对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。使用与对象相匹配的键来完成赋值操作。

  • 在声明语句时,使用类似对象字面量方式来声明部分或者全部对象键,同时执行多个赋值操作,如果变量名与属性名相同则可以使用简写语法;
  • 如果解构的属性不存在则该变量就是 undefined ,可以定义变量默认值,如果对象有该属性则使用属性值,否则使用默认值;
  • 解构在内部使用函数 ToObject() ,把源数据结构转为对象。原始值也会被当成对象, null 和 undefined 不能被解构,否则会抛出错误;
  • 可以进行深层次嵌套解构;
  • 如果解构报错,那么赋值成功会成功赋值。
const obj = {
    attr1: 111,
    attr2: 222,
    attrObj: {
        attr3: 333,
    },
    attr4: 444
}
​
let attr, attr2, attr3, attr4, attr5, attr6, attrError
​
try {
    ({
        attr1: attr,
        attr2,
        attrObj: {
            attr3
        },
        attr5,
        attr6 = 666,
        attrObj: {
            attr33: {
                attrError
            }
        },
        attr4
    } = obj)
} catch (e) {
    console.log(e)
    // TypeError: Cannot read properties of undefined (reading 'attrError')
    console.log('attr: ', attr)
    // attr:  111
    console.log('attr2: ', attr2)
    // attr2:  222
    console.log('attr3: ', attr3) // 对象嵌套解构赋值
    // attr3:  333
    console.log('attr4: ', attr4) // 因为解构报错,导致无法进行下去
    // attr4:  undefined
    console.log('attr5: ', attr5) // 对象中没有此属性所以赋值为 undefined
    // attr5:  undefined
    console.log('attr6: ', attr6) // 因为没有此属性所以使用默认值
    // attr6:  666
    console.log('attrError: ', attrError) // 赋值报错
    // attrError:  undefined
}
​
const { _ } = null
// TypeError: Cannot destructure property '_' of 'null' as it is null.

创建对象

1.工厂模式

工厂方法模式(FACTORY METHOD)是一种常用的类创建型设计模式,此模式的核心精神是封装类中变化的部分,提取其中个性化善变的部分为独立类,通过依赖注入以达到解耦、复用和方便后期维护拓展的目的。它的核心结构有四个角色,分别是抽象工厂;具体工厂;抽象产品;具体产品

—— 百度百科

function personFactory(name, age, gender) {
    const obj = {}
    obj.name = name;
    obj.age = age
    obj.gender = gender
    obj.sayName = function () {
        console.log(this.name)
    }
    return obj
}
​
const person1 = personFactory('hongyun', 18, '男')
const person2 = personFactory('xue', 17, '女')
person1.sayName()
//hongyun
person2.sayName()
// xue
console.log(person1 instanceof personFactory) // 无法通过 instanceof 操作符判断实例属于哪个对象类型

这里工厂模式根据几个参数创建了一个包含 Person 信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含三个属性一个方法的对象。虽然可以解决创建多个类似对象的问题,但是没有解决对象标识问题。

2.构造函数模式

在 ECMAScript 中构造函数用于创建特定类型的对象。例如 ObjectArray ,也可以自定义构造函数,用函数形式为自己的对象类型定义属性和方法。

根据构造函数将工厂模式创建对象进行改写:

function Person(name, age, gender) {
    this.name = name
    this.age = age
    this.gender = gender
    this.sayName = function () {
        console.log(this.name)
    }
}
​
const person1 = new Person('hongyun', 18, '男')
const person2 = new Person('xue', 17, '女')
​
person1.sayName()
person2.sayName()
​
console.log(person1 instanceof Person)
console.log(person2 instanceof Person)
// 因为该实例是在内存创建的一个新对象,所以也是 Object 的实例
console.log(person1 instanceof Object)

通过 new 操作符,实例化一个构造函数,创建一个新实例,使得实例的 constructor 指向 Person 构造函数,用来标识对象类型。

通过 new 操作符调用构造函数会执行如下操作:

  1. 在内存中创建一个新对象;
  2. 这个对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性;
  3. 构造函数内部的 this 被赋值为这个新对象( this 指向新对象);
  4. 执行构造函数内部代码;
  5. 如果 构造函数返回非空对象,则返回该对象 ;否则返回刚创建的新对象。

2.1 构造函数与函数区别

构造函数与普通函数的唯一区别就是 调用方式不同 。任何函数加上 new 操作符都可以称之为 构造函数 ,并不是说首字母大写的就是构造函数,只是说建议这样书写,方便阅读。

例如前面的可以想这样调用:

// 通过 new 操作符调用
const person = new Person('hongyun', 18, '男')
person.sayName() // hongyun// 作为函数直接调用
Person('hongyun function', 18, '男')
// 因为我这是在 node 环境运行的,所以全局对象是 global ,在浏览器中运行全局对象是 window
global.sayName() // hongyun function// 通过 call 改变 Person 作用域,作为对象方法来调用
const p = {}
Person.call(p, 'hongyun call()', 18, '男')
p.sayName() // hongyun call()

简单来说就是函数内部 this 指向问题。

  • new 操作符会改变普通函数的 this 指向;
  • 直接调用函数 this 指向全局对象,在浏览器中就是 window 对象;
  • 如果作为对象的方法来调用,this 指向调用的对象;
  • 如果使用了 call() 或者 apply() 则内部 this 指向特定的作用域。

2.2 构造函数模式带来的问题

这种方法相比较工厂模式来说

  • 解决了对象标识的问题;
  • 没有显示的创建对象
  • 属性和方法直接赋值给 this
  • 没有 return 返回值。

虽然构造函数解决了工厂模式的问题,但是在构造函数内部 sayName() 方法每次创建一个实例都会初始化一次函数实例,因为该函数都是做的同一件事,所以没必要每次都要额外再实例化一个 Function 实例,所以可以把该函数提取出来,放到构造函数外部。

function Person(name, age, gender) {
    this.name = name
    this.age = age
    this.gender = gender
    this.sayName = sayName
}
​
function sayName() {
    console.log(this.name)
}
​
const person1 = new Person('hongyun', 18, '男')
const person2 = new Person('xue', 17, '女')
person1.sayName()
person2.sayName()
​
global.name = 'test'
sayName()

这样好处是只要实例化一个 Function 实例即可给多个 Person 实例使用,但全局作用域也被搞乱了。如果有多个构造函数,或者一个构造函数有多个方法,那么全局作用域会混乱不堪。

这个问题可以通过原型模式来解决。

3.原型模式

对象迭代