对象的扩展

200 阅读8分钟

属性的简洁写法

传统的js中,对象采用{key: value}的写法,但在es6中允许在大括号里面直接写入变量。

const name = 'Kimmy';
const age = 20;
const person = { name, age }; // { name: 'Kimmy', age: 20 }

// 等同于
const person = {
  name: name,
  age: age
};

上面例子中,在定义person对象时,变量名name作为了对象的属性名,它的值作为了属性值,因此只需要写一个{name}就可以表示{name: name}的含义。

除了属性可以简写,函数也可以简写,即省略关键字function

const person = {
  sayHello: function() {
    return 'hello';
  }
};

// 等同于
const person = {
  sayHello() {
    return 'hello';
  }
};

按照CommonJS写法,当需要输出一组模块变量,就非常合适使用对象简写的方法。

let person = {};

// 获取元素
function getItem(key) {
  return key in Person ? person[key] : null;
}

// 添加元素
function setItem(key, value) {
  person[key] = value;
}

// 清空元素
function clear() {
  person = {};
}

module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

链判断运算符

在js实际写法中,如果需要读取对象内部的某个属性,往往需要判断一下该对象是否存在(防止空对象取值报错的情况)

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

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

上面例子中,firstName属性在对象的第四层,所以要判断四次,每一层都需要判断是否空对象,这样的层层判断写法非常麻烦,代码不直观,es6中新增了一种新的写法?.用于判断对象是否存在。

const firstName = message?.body?.user?.firstName;

上面例子中,先判断?左侧的对象是否为nullundefined,如果是,就不再往下运算,返回undefined, 否则层层计算,即先判断message是否存在,然后判断message.body是否存在,再然后判断message.body.user是否存在,最后取值message.body.user.firstName

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

person.sayHello?.();

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

链判断运算符有三种用法:

-obj?.prop // 对象属性 -ojb?.[expr] // 对象属性 -function.(...args) // 函数或对象方法的调用

使用?.注意点: (1)短路机制 ?.相当于一种短路机制,如果不存在则不再往下继续执行。

```js
person?.name

// 相当于
person ? person.name : undefined
```

(2)delete运算符

```js
delete person?.name;

// 相当于
person ? delete person.name : undefined
```

(3)括号的影响

```js
(person?.name).firstName

// 相当于
(person ? person.name : undefined).firstName
```
`?.`只对括号内生效,括号外面的不受任何影响。所以在使用`?.`时,尽量不要与圆括号一起使用。

(4)报错场合

```js
// 构造函数
new person?.();
new person?.name();

// 链式运算符右侧是模板字符
person?.`${name}`;
person?.`f${name}`;

// 链式运算符左侧有super关键字
super?.();
super?.name;

// 链式运算符用于赋值
person?.name = 'Kimmy';
```

(5)右侧不得为十进制数值

若果`?.`后面紧跟十进制,会被当成是三元运算符计算

```js
true?.3:0 // 0.3
```

Null判断运算符

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

person.name || 'Kimmy'

上面例子中,person.namenullundefined时,默认值Kimmy就会生效,但是person.name的值为''false0时,默认值也会生效。

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

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

person?.name ?? 'Kimmy'

上面例子中personnullundefined,或者person.namenullundefined,就会返回默认值Kimmy。

??非常适合用来参数解构

function fn(props) {
  const fname = props.name ?? 'Kimmy';
}

// 等同于
function fn(props) {
  const {
    name: fname = 'Kimmy'
  } = props;
}

??||&&一起使用时,为了表明优先级需要使用括号,否则会报错。

// 报错
person && person.name ?? 'Kimmy'

// 正确写法
(person && person.name) ?? 'Kimmy'

属性的遍历

对象的属性遍历一共有5中方法可实现,分别是

- for...in
- Object.keys(obj)
- Object.getOwnPropertyNames(obj)
- Object.getOwnPropertySymbols(obj)
- Reflect.ownKeys(obj)

它们的区别请看下面的栗子:

// 定义父类
function Animal(name, type) {
  Object.assign(this, { name, type });
}

// 定义子类
function Cat(age, weight) {
  Object.assign(this, {
    age,
    weight,
    [Symbol('one')]: 'one'
  });
}

// 子类继承父类
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 生成子类的实例
let cat = new Cat(2, '2kg');

// 实例增加可枚举属性
Object.defineProperty(cat, 'color', {
  configurable: true,
  enumerable: true,
  value: 'orange',
  writable: true
});

// 实例增加不可枚举属性
Object.defineProperty(cat, 'height', {
  configurable: true,
  enumerable: true,
  value: '15cm',
  writable: true
});

实例cat的属性:

属性类型属性值
实例属性age、weight、Symbol('one')、color
继承属性name、type
可枚举属性age、weight、color
不可枚举属性height
Symbol属性Symbol('one')

(1)for...in for...in用于遍历对象自身和继承的可枚举属性(不包含Symbol属性)

for (let key in cat) {
  console.log(key); // age, weight, color, name, type
}

(2)Object.keys() Object.keys()返回一个数组,包含对象自身所有可枚举属性,不包含继承属性和Symbol属性

Object.keys(cat); // ["age", "weight", "color"]

(3)Object.getOwnPropertyNames() Object.getOwnPropertyNames()返回一个数组,包含对象自身所有可枚举属性和不可枚举属性,不包含继承属性和Symbol属性

Object.getOwnPropertyNames(cat); // ["age", "weight", "color", "height"]

(4)Object.getOwnPropertySymbols() Object.getOwnPropertySymbols返回一个数组,包含对象自身所有Symbol属性,不包含其他属性

Object.getOwnPropertySymbols(cat); // [Symbol(one)]

(5)Reflect.ownKeys() Reflect.ownKeys()返回一个数组,包含可枚举属性、不可枚举属性及Symbol属性,不包含继承属性

Reflect.ownKeys(cat); // ["age", "weight", "color", "height", Symbol(one)]

结论:

遍历方法可继承自身可枚举自身不可枚举Symbol属性
for...inYesYesNoNo
Object.keys()NoYesNoNo
Object.getOwnPropertyNames()NoYesYesNo
Object.getOwnPerpertySymbols()NoNoNoYes
Reflect.ownKeys()NoYesYesYes

Object.assign()

Object.assign()用于将一个或者多个对象的可枚举属性复制到目标对象,然后返回目标对象。 当多个源对象具有相同属性时,后面的属性会覆盖前面的属性。 先看个简单的栗子:

const target = {a: 1}; // 目标对象
const target1 = {b: 2}; // 源对象1
const target2 = {c: 3}; // 源对象2
const target3 = {c: 4}; // 源对象3,和源对象2有共同的属性名c

Object.assign(target, targe1, target2, target3); // {a: 1, b: 2, c: 4}

target2和target3对象中同时出现了属性c,target3对象覆盖了target2对象的属性,所以c的属性值为“4”。

:Object.assign()函数无法复制对象的不可枚举属性和继承属性,但是可复制可枚举的Symbol属性。

让我们来看个例子: 首先创建一个同时拥有可枚举属性、不可枚举属性、继承属性、Symbol属性的对象。

const obj = Object.create({
  a: 1 // 继承属性
}, {
  b: {
    value: 2 // 不可枚举属性
  },
  c: {
    value: 3,
    enumerable: true // 可枚举属性
  },
  [Symbol('one')]: {
    value: 'one',
    enumerable: true // Symbol属性
  }
});

调用Object.assign()将obj对象属性复制到一个空对象,并输出结果

Object.assign({}, obj); // {c: 3, Symbol(one): "one"}

从结果可以看出被复制的属性中包含了可枚举属性c和Symbol属性,不包含继承属性a和不可枚举属性b。如果想要保持继承链,可以这样处理:

function clone(origin) {
  let originProto = Object.getPrototypeOf(origin); // 对象的原型链
  return Object.assign(Object.create(originProto), origin);
}

Object.assign()常见的用途: (1)克隆对象 Object.assign()可以复制源对象的属性到目标对象中,所以使用Object.assign()可以实现对象克隆。

function clone(origin) {
  return Object.assign({}, origin);
}

let origin = {
  name: 'Kimmy',
  age: 20
};

clone(origin); // {name: "Kimmy", age: 20}

需要注意的是,使用Object.assign()进行克隆时,进行的是浅克隆。如果属性时基本数据类型,则会复制它的值;如果属性时引用数据类型,则会复制它的引用。

let target = {};
let source1 = {a: 1, b: {c: 2}};
let result = Object.assign(target, source1);

result.a; // 1,
reuslt.b.c; // 2
source1.b.c = 3;
result.b.c; // 3

从上面的栗子中可以看出,将source1对象复制得到result,既可访问到基本数据类型a,又可访问到引用数据类型c。因为Object.assign()使用的是浅克隆,对源对象属性值进行的修改会影响到目标对象的属性值,两者实际共享同一个对象的引用。因此,在涉及对象深克隆的问题时,不能使用Object.assign()

(2)给对象添加属性 当我们采用传统的方法为对象添加实例属性时,我们会将属性添加到this上。当属性非常多时,这样的写法比较繁琐,但是通过Object.assign()可以省略很多繁琐的代码。

// 传统的写法
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

// Object.assign()写法
class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

上面的方法通过Object.assign(),将x属性和y属性添加到Point类的对象实例。

(3)给对象添加方法 当我们采用传统的写法为对象添加共同方法时,会扩展其prototype属性,使用Object.assign()可以简化代码编写方式。

// 传统写法
Point.prototype.getWidth = function() {
  return this.x;
}

Point.prototype.getHeight = function() {
  return this.y;
}

// Object.assign()写法
Object.assign(Point.prototype, {
  getWidth() {
    return this.x;
  },
  getHeight() {
    return this.y;
  }
});

(4)合并对象 使用Object.assign()可以将多个对象合并到某个对象中,也可以将多个对象合并到一个新对象,只需要将target设置为空对象{}即可。

// 多个对象合并到某个对象
const merge = (...target, ...sources) => Object.assign(target, ...sources);

// 多个对象合并为一个新对象并返回
const merge = (...sources) => Object.assign({}, ...sources);

参考文档

对象的扩展
对象的新增方法
Object