JS精进系列之你必须知道的对象操做(包含最新特性optional chaining)

2019年8月的已经进入stage3的提案 optional chaining很是的nice, 它改变了咱们访问深层次对象的方式。javascript

在咱们业务开发中, 遇到的最多见的复杂数据类型是 ___。java

答案: 对象 (plain object)react

不管是restful API获取服务端JSON数, 或者是配置, 再或者是初始化时候的optional属性, 都是一个复杂的对象, 里面能够有数组, 字符串, 也能够嵌套不少层。npm

const bigObject = {
  // ...
  prop1: {
    //...
    prop2: {
      // ...
      value: 'Some value'
    }
  }
};
复制代码

有这种对象时候, 开发起来最讨厌没有之一的事情是逐级检查属性是否是存在,redux

if (bigObject && 
    bigObject.prop1 != null && 
    bigObject.prop1.prop2 != null) {
  let result = bigObject.prop1.prop2.value;
}
复制代码

一个不检查就可能会致使 TypeError: Cannot read property 'name' of undefined. 尤为是服务端数据给的不许确时, 系统是很脆弱的。 但问题是这个代码很繁。数组

最新的JS特性optional chaining就是解决这个问题的, 下面这种判断是咱们目前的惯用手法浏览器

const movie = {
    director:{
        name: 'evle'
    }
}

const name = movie.director && movie.director.name;
复制代码

使用optional chaining特性后babel

const name = movie.director?.name
复制代码

还有一种很常见的嵌套结构是对象里面是个数组, 咱们不只要判断它是否是null还要判断length是否大于0来判断它是否是数组restful

const movie = {
    director:[{name: 'evle'}, {name:'max'}]
}

const name = movie.director &&  movie.director.length > 0 && movie.director[0].name
复制代码

简直可怕, 使用optional channing后事情变得简单了数据结构

const name = movie.director?.[0]?.name
复制代码

若是name咱们访问不到会返回undefined, 一般咱们会设置默认值, 好比

const name = movie.director &&  movie.director.length > 0 && movie.director[0].name || 'default name'
复制代码

||符号可读性很差, 引起咱们多余的逻辑思考的运算符都会致使代码可读性变差, optional channing提供了??操做符来明确的给定默认值

const name = movie.director?.[0]?.name ?? 'default name'
复制代码

介绍完 optioanl channing 的使用咱们来概括下它的使用场景

// 对象中的属性是基本类型
const object = null;
object?.property; 

// 对象中的属性是方法
const object = null;
object?.method('Some value'); 

// 对象中的属性是数组
const array = null;
array?.[0]; // => undefined

// 对象中的属性是动态属性
const object = null;
const name = 'property';
object?.[name]; // => undefined

// 对象中的属性有多层嵌套
const value = object.maybeUndefinedProp?.maybeNull()?.[propName];
复制代码

是否是想立马试试这个新特性? 赶快配置一个babel插件体验一下

// .babelrc
{
    "plugins": ["transform-optional-chaining"]
}
复制代码

插件地址: babel-plugin-transform-optional-chaining

除了最新的对象操做 optional chaining 外咱们来精进下已有的对象操做方法:

遍历 array-like object (类数组对象)

遍历array或者array-like object, 最多见的方式之一就是使用forEach(), 那么精通forEach的特性是咱们用好forEach遍历array-like 对象的关键。

forEach中的this

array.forEach(callback, thisArgument)
复制代码

从forEach的函数签名能够看出, 它第二个参数能够改变this的指向, 先来看一个简单的this指向问题

const letters = ['a', 'b', 'c'];

function iterate(letter) {
  console.log(this === window); 
}

letters.forEach(iterate); 
复制代码

log的信息必定是true, 由于iterate的调用是在浏览器环境下, this === window, 也就是说 forEach的callback中的this指向的是window 那么再看下面这个例子

class Unique {
  constructor(items) {
    this.items = items;
  }
  
  append(newItems) {
    newItems.forEach(function(newItem) {
      if (!this.items.includes(newItem)) {
        this.items.push(newItem);
      }
    });    
  }
}
复制代码

既然forEach中的this指向window, 那若是想实现咱们预期的功能, 咱们就可使用第二个参数, 改变forEach的callback中的this指向。

...
 newItems.forEach(function(newItem) {
      if (!this.items.includes(newItem)) {
        this.items.push(newItem);
      }
    }, this);  // 这里将callback中的this改变为Unique 
...
复制代码

除了改变this指向外, 还可使用 arrrow function

newItems.forEach(newItem => {
      if (!this.items.includes(newItem)) {
        this.items.push(newItem);
      }
    }); 
复制代码

forEach能够跳过空元素 可是不跳过empty元素

const array = [1, undefined, , , , , 3];

array.forEach(el => console.log(el)) // => 1, undefined, 3
复制代码

forEach遍历array-like对象

有这样一个类数组对象

const arrayLikeColors = {
  "0": "blue",
  "1": "green",
  "2": "white",
  "length": 3
};
复制代码

由于是对象, 因此不具备forEach的方法, 可是经过 不那么直接 的forEach调用方法能够遍历类数组。

function iterate(item) {
  console.log(item);
}

Array.prototype.forEach.call(arrayLikeColors, iterate);
// logs "blue"
// logs "green"
// logs "white"
复制代码

除了这个方法外, 最直接的方法就是将array-like对象转换成array, 使用Array.from()

Array.from(arrayLikeColors).forEach(iterate);
复制代码

forEach的缺点

不能终止

forEach的最佳实践是用来遍历数组中的每一项元素, 由于forEach是不支持循环终止的即:break无效, 经过强制抛出异常来中止循环太丑陋了。

这个例子能暴露forEach的缺点

let allEven = true;

const numbers = [22, 3, 4, 10];

numbers.forEach(function(number) {
  if (number % 2 === 1) {
    allEven = false;
    // 已经得出结果了 却不能中止 继续遍历损耗性能
  }
});

console.log(allEven); // => false
复制代码

若是须要遍历到某一项中止的话最佳的解决方案是for...of, 或者如下这些ES6中提供的现代方法:

  • array.map()
  • array.reduce()
  • array.every()
  • array.some()

下面让咱们使用every来改造一下上面的代码, 提升性能而且保持咱们代码的优雅。

const allEven = numbers.every(function(number) {
  return number % 2 === 0;
});
复制代码

反作用

forEach不会像array.map()或者array.filter()之类的返回一份拷贝, 而是能够直接操做元素

const inputs = document.querySelectorAll('input[type="text"]');

inputs.forEach(function(input) {
  input.value = '';
});
复制代码

input的值被意外的改变了, 也就是说forEach的callback会产生反作用, 违背了高阶函数 no-side-effects 的原则, 当确实须要使用它的side effects时必定要注意。此外forEach是没有返回值的(undefined)。

遍历对象 Object.entries

最先从2016年6月就提出了遍历对象的新方法:Object.values(), Object.entries(), 但使用率却不是很高, 下面让咱们来探索这两个新方法与for...of的结合产生的更优雅的遍历对象方式吧。

在介绍这两个后出的方法以前咱们 Object.keys 已经能够闭着眼睛写出

Object.keys(obj).forEach(key => {
    // obj[key]
})
复制代码

这样遍历对象的方式, 那Object.keys有什么特色呢? 在了解它的特色以前咱们先要明确一个概念: 遍历的是什么, Object.keys 仅仅返回自身的和可枚举属性, 弄个例子来讲明下:

let Cat = {
  color: 'black'
};

let Dog = {
  color: 'white',
  age: 15
};

Object.setPrototypeOf(Cat, Dog);
Object.keys(Cat); // => black'
复制代码

虽然白从狗上继承了白色 可是咱们使用Object.keys并无遍历出来, 可是若是使用 for...in会遍历出来继承的属性。

let enumerableKeys = [];
for (let key in Cat) {
  enumerableKeys.push(key);
}
enumerableKeys; // => ['color', 'age']
复制代码

for...in把咱们从狗身上继承的age属性也遍历出来了, 因此你要清晰有清晰的认知:你要遍历这个对象自己的属性, 仍是要遍历它自己以及继承来的属性

还有个问题:Object.keys可遍历的还有可枚举属性, 那咱们试试

Object.defineProperty(Cat, 'name', {
  enumerable: false, // Make the property non-enumerable
  value: 'cheese'
});
复制代码

如今Cat具备了一个额外的自身属性name但它是不枚举的, 如今的Cat: {color: "black", name: "cheese"}, 接下来让咱们遍历一下:

Object.keys(Cat) // ['color']
复制代码

咱们能够看到name属性并无遍历出来, 当咱们明白了Object.keys()后其实Object.values()Object.entires()也是一个特性, 仅遍历出自身和可枚举的属性。

Object.entries() 的返回是属性的keyvalue:

[[key1, value1], [key1, value2]]
复制代码

第一眼看到就没啥用, 可是配合ES6的解构使用那就很舒服

let meals = {
  mealA: 'Breakfast',
  mealB: 'Lunch',
  mealC: 'Dinner'
};
for (let [key, value] of Object.entries(meals)) {
  console.log(key + ':' + value);
}
// 'mealA:Breakfast' 'mealB:Lunch' 'mealC:Dinner'
复制代码

之后遍历遍历对象的方式多了一种for...ofObject.entries的新标准对不对?

只是遍历这么简单吗? 新东西结合新东西才能发挥最大的做用, Object.entries()Map() 的组合是天生的搭档, 由于Map原本也是键值对, Object.entries()返回的也是键值对能够直接传入Map的构造函数生成Map

let greetings = {
  morning: 'Good morning',
  midday: 'Good day',
  evening: 'Good evening'
};
let greetingsMap = new Map(Object.entries(greetings));
greetingsMap.get('morning'); // => 'Good morning'
greetingsMap.get('midday');  // => 'Good day'
greetingsMap.get('evening'); // => 'Good evening'
复制代码

除了能够直接像把``Object.entires的值传给Map的构造函数外, 其实Map提供的valuesentries就是Object.values()Object.entries()`, 他们是同一个东西。

// ...
[...greetingsMap.values()];
// => ['Good morning', 'Good day', 'Good evening']
[...greetingsMap.entries()];
// => [ ['morning', 'Good morning'], ['midday', 'Good day'], 
// ['evening', 'Good evening'] ]
// 可用熟悉的for...of + 解构来遍历
复制代码

接下来讲一下Object.values(), 在之前使用for...ofObject.keys()的组合遍历对象时

let meals = {
  mealA: 'Breakfast',
  mealB: 'Lunch',
  mealC: 'Dinner'
};
for (let key of Object.keys(meals)) {
  let mealName = meals[key];
  // ... do something with mealName
  console.log(mealName);
}
复制代码

咱们须要使用meals[key]这样的方式获取到属性的值, 可是使用Object.values()的话咱们直接就能够取到值了

let meals = {
  mealA: 'Breakfast',
  mealB: 'Lunch',
  mealC: 'Dinner'
};
for (let mealName of Object.values(meals)) {
  console.log(mealName);
}
// 'Breakfast' 'Lunch' 'Dinner'
复制代码

多句嘴: JS对象中你不能保证对象属性的顺序, 别依靠遍历顺序写任何逻辑代码, 请使用数组或者Set代替

对象合并

之前对象合并咱们通常会使用extend工具函数, 好比使用lodash里面的

_.extend(target, [sources]) 
复制代码

此外咱们还会使用Object.assign(), 早期Redux应用写reducer合并对象时候全是Object.assign()方法。

可是出现了 spread 展开符合并简单就像呼吸同样简单了, 可是咱们也要精通它的特性, 使用起来才游刃有余。

合并的规则

** latter property wins ** ** latter property wins ** ** latter property wins **

重要的事情说三遍, 记住这个规则就能够合并时候不会纠结谁覆盖谁了, 谁在后谁厉害, 好比

const max = {name: 'max', age: 27}
const evle = {name: 'evle', age: 27}

{...evle, ...max} // max在后面 因此当合并对象重名的时, max的属性的值会覆盖其余对象的

// => {name: "max", age: 27}
复制代码

拷贝枚举属性

spread 这个操做符不能拷贝不可枚举属性

let person = {name: 'evle'}

Object.defineProperty(person, 'age', {
  enumerable: false, // Make the property non-enumerable
  value: 25
});
console.log(person['age']); // => 25
复制代码

下面让咱们试试

const clone = {
  ...person
};
console.log(clone);  // {name: "evle"}
复制代码

实践证实, 不可枚举的属性是没法拷贝到的

拷贝对象(深浅拷贝?)

使用 ... 拷贝对象很简单

let clone = {...person}
复制代码

可是使用 spread 操做符拷贝的对象只有自身的属性被拷贝了, 其内部嵌入的对象没有拷贝一份新的, 还仅仅是拷贝对象的引用而已, 所以属于 浅拷贝

const laptop = {
  name: 'MacBook Pro',
  screen: {
    size: 17,
    isRetina: true
  }
};
const laptopClone = {
  ...laptop
};

console.log(laptop === laptopClone);               // => false
console.log(laptop.screen === laptopClone.screen); // => true
复制代码

那么若是想完全拷贝一个新的出来怎么办, 包括它的子对象? 那就继续使用 ... 拷贝嵌套的内容

const laptopDeepClone = {
  ...laptop,
  screen: {
     ...laptop.screen
  }
};

console.log(laptop === laptopDeepClone);               // => false
console.log(laptop.screen === laptopDeepClone.screen); // => false
复制代码

拷贝丢失

基本数据类型咱们能够拷贝, 对象也能够拷贝, 可是函数缺没办法拷贝, 会丢失的

class Game {
  constructor(name) {
    this.name = name;
  }

  getMessage() {
    return `I like ${this.name}!`;
  }
}

const doom = new Game('Doom');
console.log(doom instanceof Game); // => true
console.log(doom.name);            // => "Doom"
console.log(doom.getMessage());    // => "I like Doom!"

const doomClone = {
  ...doom
};

console.log(doomClone instanceof Game); // => false
console.log(doomClone.name);            // => "Doom"
console.log(doomClone.getMessage());
// TypeError: doomClone.getMessage is not a function
复制代码

有没有想过为何拷贝不到函数?

由于doomClone是一个普通的JS plain object, 它是继承自Object.prototype的, 若是想拥有getMessage()方法的话要继承的是Game.prototype。因此要手动改变它prototype的指向。

const doomFullClone = {
  ...doom,
  __proto__: Game.prototype
};

console.log(doomFullClone instanceof Game); // => true
console.log(doomFullClone.name);            // => "Doom"
console.log(doomFullClone.getMessage());    // => "I like Doom!"
复制代码

但文档中__proto__已是个降级的东西了,之后可能不用了, 因此仍是推荐如下方法 克隆一个class的方法

const doomFullClone = Object.assign(new Game(), doom);

console.log(doomFullClone instanceof Game); // => true
console.log(doomFullClone.name);            // => "Doom"
console.log(doomFullClone.getMessage());    // => "I like Doom!"
复制代码

immutable 对象更新

immutable 数据结构不产生反作用, 当一个对象在多处都要共享的时候, 最怕的就是不当心被直接改了致使的反作用, 因此redux 之类各类状态管理的解决方案变得颇有必要, 追踪改动是件很重要的事情。

在咱们写程序时候也有一个好的实践是 使操做immutable, 这样的话即便很复杂的应用场景, 也不会出现意外原始变量被改的状况。

使用 spread 操做符就很方便的使操做immtable

const book = {
  name: 'JavaScript: The Definitive Guide',
  author: 'David Flanagan',
  edition: 5,
  year: 2008
};
复制代码

这书第5版, 你想改为第6版用, 但又不想改动原始数据

const newerBook = {
  ...book,
  edition: 6,  // <----- Overwrites book.edition
  year: 2011   // <----- Overwrites book.year
};

复制代码

拷贝基本数据类型会发生什么

通常都是用来拷贝对象和数组的, 好奇心做怪试试拷贝基本类型的结果是什么?

const nothing = undefined;
const missingObject = null;
const two = 2;
const hello = 'hello';

console.log({ ...nothing });       // => { }
console.log({ ...missingObject }); // => { }
console.log({ ...two });           // => { }
console.log({ ...hello });         // => {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
复制代码

结论:别拷贝基本类型

给对象默认的值

有些对象都是运行时候生成的, 你也不知道最终参数是啥, 好比配置, 用户只要指定一下核心属性, 没指定的用默认值就行了, 这个太常见了在各类框架或者库中, 编写个multiline(str, config)实践一下这个经常使用手法。

multiline('Hello World!');
// => 'Hello Worl\nd!'

multiline('Hello World!', { width: 6 });
// => 'Hello \nWorld!'

multiline('Hello World!', { width: 6, newLine: '*' });
// => 'Hello *World!'

multiline('Hello World!', { width: 6, newLine: '*', indent: '_' });
// => '_Hello *_World!'
复制代码
function multiline(str, config = {}) {
  const defaultConfig = {
    width: 10,
    newLine: '\n',
    indent: ''
  };
  const safeConfig = {
    ...defaultConfig,
    ...config
  };
  let result = '';
  // Implementation of multiline() using
  // safeConfig.width, safeConfig.newLine, safeConfig.indent
  // ...
  return result;
}
复制代码

在合并对象时候 ...Object.assign()更优的地方 在于 它更新大的对象方便, 由于能够局部更新

const box = {
  color: 'red',
  size: {
    width: 200, 
    height: 100 
  },
  items: ['pencil', 'notebook']
};

好比我只想改size
const biggerBox = {
  ...box,
  size: {
    ...box.size,
    height: 200
  }
};
console.log(biggerBox);
复制代码

对象的继承 (super call的使用)

咱们如今有一个对象 myProto, 并包含一个方法propertyExists()

var myProto = {
  propertyExists: function(name) {
    return name in this;
  },
};
复制代码

若是想继承这个myProto, 咱们须要借助一个函数: Object.create()

var myNumbers = Object.create(myProto);
myNumbers['array'] = [1, 6, 7];
myNumbers.propertyExists('array'); // => true
myNumbers.propertyExists('collection'); // => false

myProto.isPrototypeOf(myNumbers); // true
复制代码

myNumbers继承自myProto, 而且定义了自身属性array, 如今咱们能够根据官方建议的另外一个语义化更明确的函数 setPrototypeOf() 来指定prototype了。

var obj = {};

Object.setPrototypeOf(obj, myProto) // oo具备myProto的属性和方法

// 还有一种降级的使用方法(官方不推荐)
var myNumbers = {
  __proto__: myProto,  // 直接设置__proto__属性值
  array: [1, 6, 7],
};
复制代码

继承后, 可使用super来访问继承的属性

var calc = {
  numbers: null,
  sumElements() {
    return this.numbers.reduce(function(a, b) {
      return a + b;
    });
  },
};

var numbers = {
  __proto__: calc,
  numbers: [4, 6, 7],
  sumElements() {
    // Verify if numbers is not null or empty
    if (this.numbers == null || this.numbers.length === 0) {
      return 0;
    }
    return super.sumElements();
  },
};
numbers.sumElements(); // => 17
复制代码

计算属性

计算属性就是动态属性, 即: 在写这个对象时候并不知道对象的属性叫什么

function prefix(prefStr, name) {
  return prefStr + '_' + name;
}
var object = {};
object[prefix('number', 'pi')] = 3.14;
object[prefix('bool', 'false')] = false;
object; // => { number_pi: 3.14, bool_false: false }
复制代码

好比上面咱们就动态生成了2个属性, 这就叫作动态属性, 如今ES标准给了咱们更优雅的解决方案

function prefix(prefStr, name) {
  return prefStr + '_' + name;
}
var object = {
  [prefix('number', 'pi')]: 3.14,
  [prefix('bool', 'false')]: false,
};
object; // => { number_pi: 3.14, bool_false: false }
复制代码

咱们没必要经过定义对象后再设置属性的方式添加动态属性, 能够在定义对象的时候添加动态属性。

添加动态属性咱们掌握了, 也要学会解构动态属性, 静态属性解构很简单

const movie = { title: 'Heat' };

const { title } = movie;

title; // => 'Heat'
复制代码

由于咱们知道movie里面有个属性叫作title, 因此咱们直接解出来title就行了, 可是对于动态属性, 咱们不知道咱们要解出来属性的名字, 咱们只须要这样: 不须要管名字叫啥, 给它来个别名代替。

function greet(obj, nameProp) {
 // 配合别名 + 默认值 代码数量比之前不知道少了多少行!
 const { [nameProp]: name = 'Unknown' } = obj;
 return `Hello, ${name}!`;
}

greet({ name: 'Batman' }, 'name'); // => 'Hello, Batman!'
greet({ }, 'name'); // => 'Hello, Unknown!'
复制代码

回归 ... 操做符的本质

spread operator 也就是...能展开对象, 数组实际上是依靠迭代协议, ...使用了迭代协议去遍历了对象或者数组, 迭代协议要求对象必须包含一个特殊属性Symbol.iterator而且它的值好比为一个函数, 返回一个迭代对象。

先看一下js对象能够定义3种属性:

  • 键值对 {name: value}
  • Getters {get name(){...}} 和 Setters {set name(val){...}}
  • Symbol

那么若是要使用这个迭代协议就要定义这第三种属性, 长这个样子

interface Iterable {
  [Symbol.iterator]() {
    //...
    return Iterator;
  }
}

interface Iterator {
  next() {
     //...
     return {
        value: <value>,
        done: <boolean>
     };
  };
}

复制代码

那上面代码中的迭代器(Iterator)又是什么?举一个最简单的例子

const str = 'hi';
const iterator = str[Symbol.iterator]();
iterator.toString(); // => '[object String Iterator]'
iterator.next();     // => { value: 'h', done: false }
iterator.next();     // => { value: 'i', done: false }
iterator.next();     // => { value: undefined, done: true }
[...str];            // => ['h', 'i']
复制代码

使用str[Symbol.iterator]()将str转为一个迭代器, 能够经过next()来一项一项迭代str的内容, 还可使用...访问str的值。

前面咱们说过遍历array-like的方法, 但迭代器给咱们提供了一个新的思路

const arrayLike = {
  0: 'Cat',
  1: 'Bird',
  length: 2
};
// 应用迭代协议
arrayLike[Symbol.iterator] = iterator;
const array = [...arrayLike];
console.log(array); // => ['Cat', 'Bird']
复制代码

综合以上知识来给对象应用一个迭代协议: 好比咱们只想使用 ...获取对象的keys

var object = {
   number1: 14,
   number2: 15,
   string1: 'hello',
   string2: 'world',
   [Symbol.iterator]: function *() {
     var own = Object.getOwnPropertyNames(this),
       prop;
     while(prop = own.pop()) {
       yield prop;
     }
   }
}

[...object]; // => ['number1', 'number2', 'string1', 'string2']
复制代码

迭代器太经常使用了, 掌握了迭代器就对于react saga的API使用起来不陌生了, 去看它的原理也就更简单了, 但愿你们多动手敲敲, 悟一悟。

既然都看到这里了, 点个赞吧 💗

相关文章
相关标签/搜索