# 对象

# 属性描述符

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。 Object.getOwnPropertyDescriptor 方法可以获取该属性的描述对象

let obj = { foo: 123, bar: {name: 'lan'} };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }
Object.getOwnPropertyDescriptor(obj.bar, 'name')
//  {
//    value: lan,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者

数据描述符还具有以下可选键值:

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为 undefined

writable

当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变

let obj = {}
Object.defineProperty(obj, 'b', {
    configurable: true,
    writable: false
})

Object.getOwnPropertyDescriptor(obj, 'b')
// configurable:true
// enumerable: false
// value: undefined
// writable: false

obj.b = 3 // 没有报错,看似没问题
conosle.log(obj.b) // undefined ,输出的时候发现其实没变化

// 因为configurable为true,所以可以通过修改属性描述的 value属性进行修改
Object.defineProperty(obj, 'b', {
    value: 3
})

console.log(obj.b) // 3  可以发现修改成功

存取描述符还具有以下可选键值:

get

属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值

默认为 undefined

set

属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。

默认为 undefined

var obj = {}
var obj2 = {}
Object.defineProperty(obj, 'b', {
    set: function(v){
         console.log('触发set');
         obj2.b = v 
    },
    get: function(b){
         console.log('触发get');
         return obj2.b 
    }
})
obj.b = 100 // 触发set
console.log(obj2) // {b: 100}
console.log(obj.b) // 触发get 100

数据描述符和属性描述符共有的属性:

可枚举属性(enumerable)

描述对象的 enumerable 属性,称为“可枚举性”,如果该属性为 false,就表示某些操作会忽略当前属性

目前,有四个操作会忽略 enumerablefalse 的属性:

  • for...in 循环:只遍历对象自身的和继承的可枚举的属性

  • Object.keys():返回对象自身的所有可枚举的属性的键名

  • JSON.stringify():只串行化对象自身的可枚举的属性

  • Object.assign(): 忽略 enumerablefalse 的属性,只拷贝对象自身的可枚举的属性

引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉 for...in 操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的 toString 方法,以及数组的 length 属性,就通过“可枚举性”,从而避免被 for...in 遍历到

Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false

Object.getOwnPropertyDescriptor([], 'length').enumerable
// false

ES6 规定,所有 Class 的原型的方法都是不可枚举的

Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false

configurable

当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。

let obj = {}
obj.a = 1
Object.defineProperty(obj, 'b', {
    configurable: false
})

Object.getOwnPropertyDescriptor(obj, 'a')
// configurable: true
// enumerable: true
// value: 1
// writable: true

Object.getOwnPropertyDescriptor(obj, 'b')
// configurable:false
// enumerable: false
// value: undefined
// writable: false

delete obj.b // false  删除失败 因为configurable为false

// Uncaught TypeError: Cannot redefine property: b ,也不能修改b属性的属性描述符了
Object.defineProperty(obj, 'b', {
    configurable: true
})

Object.defineProperty(obj, 'a', {
    value: 2
})
console.log(obj.a) // 2 
delete obj.a // true 删除成功
console.log(obj) // {b: undefined}

# 属性的遍历

ES6 一共有 5 种方法可以遍历对象的属性

  • for...infor...in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性

  • Object.keys(obj): Object.keys 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名

  • Object.getOwnPropertyNames(obj): Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名

  • Object.getOwnPropertySymbols(obj): 返回一个数组,包含对象自身的所有 Symbol 属性的键名

  • Reflect.ownKeys(obj): 返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举

var obj = { [Symbol]: 0, a: 1}
Object.defineProperty(obj, 'b', {value: 2})
console.log(obj) 
// a: 1
// Symbol(): 0
// b: 2

Object.keys(obj) // ["a"]
Object.getOwnPropertyNames(obj) //  ["a", "b"]
Object.getOwnPropertySymbols(obj) // [Symbol()]
Reflect.ownKeys(obj) //  ["a", "b", Symbol()]

# super 关键字

我们知道,this 关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字 super,指向当前对象的原型对象

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

上面代码中,对象 obj.find() 方法之中,通过 super.foo 引用了原型对象 protofoo 属性。

注意,super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错

// 报错
const obj = {
  foo: super.foo
}

// 报错
const obj = {
  foo: () => super.foo
}

// 报错
const obj = {
  foo: function () {
    return super.foo
  }
}

JavaScript 引擎内部,super.foo 等同于 Object.getPrototypeOf(this).foo (属性)或 Object.getPrototypeOf(this).foo.call(this) (方法)

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"

上面代码中,super.foo 指向原型对象 protofoo 方法,但是绑定的this却还是当前对象 obj ,因此输出的就是 world

# 扩展运算符

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

如果扩展运算符后面是一个空对象,则没有任何效果

{...{}, a: 1}
// { a: 1 }

上面代码中,扩展运算符后面是整数 1,会自动转为数值的包装对象 Number{1} 。由于该对象没有自身属性,所以返回一个空对象

// 等同于 {...Object(true)}
{...true} // {}

// 等同于 {...Object(undefined)}
{...undefined} // {}

// 等同于 {...Object(null)}
{...null} // {}

但是,如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象

{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

对象的扩展运算符等同于使用 Object.assign() 方法

let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);

上面的例子只是拷贝了对象实例的属性,如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法

// 写法一
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj
};

// 写法二
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
);

// 写法三
const clone3 = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)

与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式

const obj = {
  ...(x > 1 ? {a: 1} : {}),
  b: 2,
};

扩展运算符的参数对象之中,如果有取值函数 get ,这个函数是会执行的

let a = {
  get x() {
    throw new Error('not throw yet');
  }
}

let aWithXGetter = { ...a }; // 报错

上面例子中,取值函数 get 在扩展 a 对象时会自动执行,导致报错

# 链判断运算符

编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取 message.body.user.firstName ,安全的写法是写成下面这样

// 错误的写法
const  firstName = message.body.user.firstName;

// 正确的写法
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

上面例子中,firstName 属性在对象的第四层,所以需要判断四次,每一层是否有值。

三元运算符 ?: 也常用于判断对象是否存在

const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined

这样的层层判断非常麻烦,因此 ES2020 引入了“链判断运算符”(optional chaining operator) ?. ,简化上面的写法

const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value

上面代码使用了 ?. 运算符,直接在链式调用的时候判断,左侧的对象是否为 nullundefined 。如果是的,就不再往下运算,而是返回 undefined

下面是判断对象方法是否存在,如果存在就立即执行的例子

iterator.return?.()

上面代码中,iterator.return 如果有定义,就会调用该方法,否则 iterator.return 直接返回 undefined ,不再执行 ?. 后面的部分

对于那些可能没有实现的方法,这个运算符尤其有用

if (myForm.checkValidity?.() === false) {
  // 表单校验失败
  return;
}

上面代码中,老式浏览器的表单可能没有 checkValidity 这个方法,这时 ?. 运算符就会返回undefined,判断语句就变成了 undefined === false ,所以就会跳过下面的代码

链判断运算符有三种用法

  • obj?.prop // 对象属性
  • obj?.[expr] // 同上
  • func?.(...args) // 函数或对象方法的调用

下面是 obj?.[expr] 用法的一个例子

let hex = "#C0FFEE".match(/#([A-Z]+)/i)?.[1];

上面例子中,字符串的 match() 方法,如果没有发现匹配会返回 null ,如果发现匹配会返回一个数组,?. 运算符起到了判断作用。

下面是 ?. 运算符常见形式,以及不使用该运算符时的等价形式。

a?.b
// 等同于
a == null ? undefined : a.b

a?.[x]
// 等同于
a == null ? undefined : a[x]

a?.b()
// 等同于
a == null ? undefined : a.b()

a?.()
// 等同于
a == null ? undefined : a()

上面代码中,特别注意后两种形式,如果 a?.b() 里面的 a.b 不是函数,不可调用,那么 a?.b() 是会报错的。 a?.() 也是如此,如果 a 不是 nullundefined ,但也不是函数,那么 a?.() 会报错

使用这个运算符,有几个注意点

短路机制

?. 运算符相当于一种短路机制,只要不满足条件,就不再往下执行

a?.[++x]
// 等同于
a == null ? undefined : a[++x]

上面代码中,如果 aundefinednull ,那么 x 不会进行递增运算。也就是说,链判断运算符一旦为真,右侧的表达式就不再求值

delete 运算符

delete a?.b
// 等同于
a == null ? undefined : delete a.b

上面代码中,如果 aundefinednull ,会直接返回 undefined ,而不会进行 delete 运算

括号的影响

如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响

(a?.b).c
// 等价于
(a == null ? undefined : a.b).c

上面代码中,?. 对圆括号外部没有影响,不管 a 对象是否存在,圆括号后面的 .c 总是会执行。

一般来说,使用 ?. 运算符的场合,不应该使用圆括号

报错场合

以下写法是禁止的,会报错

// 构造函数
new a?.()
new a?.b()

// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`

// 链判断运算符的左侧是 super
super?.()
super?.foo

// 链运算符用于赋值运算符左侧
a?.b = c

右侧不得为十进制数值

为了保证兼容以前的代码,允许 foo?.3:0 被解析成 foo ? .3 : 0 ,因此规定如果 ?. 后面紧跟一个十进制数字,那么 ?. 不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数

# Null 判断运算符

读取对象属性的时候,如果某个属性的值是 nullundefined ,有时候需要为它们指定默认值。常见做法是通过 || 运算符指定默认值

const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;

上面的三行代码都通过 || 运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为 nullundefined ,默认值就会生效,但是属性的值如果为空字符串或 false0 ,默认值也会生效

为了避免这种情况,ES2020 引入了一个新的 Null 判断运算符 ?? 。它的行为类似 || ,但是只有运算符左侧的值为 nullundefined 时,才会返回右侧的值

const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;

上面代码中,默认值只有在左侧属性值为 nullundefined 时,才会生效。

这个运算符的一个目的,就是跟链判断运算符 ?. 配合使用,为 nullundefined 的值设置默认值。

const animationDuration = response.settings?.animationDuration ?? 300;

上面代码中,response.settings 如果是 nullundefined ,就会返回默认值 300

这个运算符很适合判断函数参数是否赋值

function Component(props) {
  const enable = props.enabled ?? true;
  // …
}

上面代码判断 props 参数的 enabled 属性是否赋值,等同于下面的写法

function Component(props) {
  const {
    enabled: enable = true,
  } = props;
  // …
}

?? 有一个运算优先级问题,它与 &&|| 的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错

// 报错
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs

// 上面四个表达式都会报错,必须加入表明优先级的括号

(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);

(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);

(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);

(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);

# 对象的新增方法

# Object.is()

ES5 比较两个值是否相等,只有两个运算符:相等运算符( == )和严格相等运算符( === )。它们都有缺点,前者会自动转换数据类型,后者的 NaN 不等于自身,以及 +0 等于 -0 。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is 就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符( === )的行为基本一致。

Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

ES5 可以通过下面的代码,部署 Object.is

Object.defineProperty(Object, 'is', {
  value: function(x, y) {
    if (x === y) {
      // 针对+0 不等于 -0的情况
      return x !== 0 || 1 / x === 1 / y;
    }
    // 针对NaN的情况
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true
});

# Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }

使用Object.assign 的注意点:

  • 如果只有一个参数,Object.assign 会直接返回该参数

    const obj = {a: 1};
    Object.assign(obj) === obj // true
    
  • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性

  • 如果该参数不是对象,则会先转成对象,然后返回

    var a = Object.assign(2) // Number {2}
    
  • 关于在 Object.assign 使用 undefinednull

    // `undefined` 和 `null` 无法转成对象,所以如果它们放在参数首位,就会报错
    Object.assign(undefined) // 报错
    Object.assign(null) // 报错
    
    // 如果undefined和null 不在首参数, 就不会报错,而是直接略过过
    let obj = {a: 1};
    Object.assign(obj, undefined) === obj // true
    Object.assign(obj, null) === obj // true
    
    // 其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果
    const v1 = 'abc';
    const v2 = true;
    const v3 = 10;
    
    const obj = Object.assign({}, v1, v2, v3);
    console.log(obj); // { "0": "a", "1": "b", "2": "c" }
    

    上面代码中,v1v2v3 分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性

    Object(true) // {[[PrimitiveValue]]: true}
    Object(10)  //  {[[PrimitiveValue]]: 10}
    Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
    
  • 数组的处理

    Object.assign([1, 2, 3], [4, 5])
    // [4, 5, 3]
    // 等同于
    Object.assign({1: 1, 2: 2, 3: 3},  {1: 4, 2: 5})
    
  • 取值函数的处理

    Object.assign 只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制

    const source = {
      get foo() { return 1 }
    };
    const target = {};
    
    Object.assign(target, source)
    // { foo: 1 }
    

# Object.getOwnPropertyDescriptors()

ES5 的 Object.getOwnPropertyDescriptor() 方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors() 方法,返回指定对象所有自身属性(非继承属性)的描述对象

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: get bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

该方法的引入目的,主要是为了解决 Object.assign() 无法正确拷贝 get 属性和 set 属性的问题

const source = {
  set foo(value) {
    console.log(value);
  }
};

const target1 = {};
Object.assign(target1, source);

Object.getOwnPropertyDescriptor(target1, 'foo')
// { value: undefined,
//   writable: true,
//   enumerable: true,
//   configurable: true }

上面代码中,source 对象的 foo 属性的值是一个赋值函数,Object.assign方 法将这个属性拷贝给 target1 对象,结果该属性的值变成了 undefined 。这是因为 Object.assign 方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法

这时,Object.getOwnPropertyDescriptors() 方法配合 Object.defineProperties() 方法,就可以实现正确拷贝

const source = {
  set foo(value) {
    console.log(value);
  }
};

const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo')
// { get: undefined,
//   set: [Function: set foo],
//   enumerable: true,
//   configurable: true }

# __proto__属性

__proto__ 属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。目前,所有浏览器(包括 IE11)都部署了这个属性

// es5 的写法
const obj = {
  method: function() { ... }
};
obj.__proto__ = someOtherObj;

// es6 的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };

# Object.setPrototypeOf()

Object.setPrototypeOf 方法的作用与 __proto__ 相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法

// 格式
Object.setPrototypeOf(object, prototype)

// 用法
const o = Object.setPrototypeOf({}, null);

// 该方法等同于下面的函数
function setPrototypeOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果

Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf('foo', {}) === 'foo' // true
Object.setPrototypeOf(true, {}) === true // true

由于 undefinednull 无法转为对象,所以如果第一个参数是 undefinednull ,就会报错

Object.setPrototypeOf(undefined, {})
// TypeError: Object.setPrototypeOf called on null or undefined

Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined

# Object.getPrototypeOf()

该方法与 Object.setPrototypeOf 方法配套,用于读取一个对象的原型对象

function Rectangle() {
  // ...
}

const rec = new Rectangle();

Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false

如果参数不是对象,会被自动转为对象

// 等同于 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1)
// Number {[[PrimitiveValue]]: 0}

// 等同于 Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo')
// String {length: 0, [[PrimitiveValue]]: ""}

// 等同于 Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true)
// Boolean {[[PrimitiveValue]]: false}

Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true

如果参数是 undefinednull,它们无法转为对象,所以会报错

Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object

Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object

# Object.fromEntries()

Object.fromEntries() 方法是 Object.entries() 的逆操作,用于将一个键值对数组转为对象

Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }

该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象

// 例一
const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
]);

Object.fromEntries(entries)
// { foo: "bar", baz: 42 }

// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }

该方法的一个用处是配合 URLSearchParams 对象,将查询字符串转为对象

Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }