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或者array-like object, 最多见的方式之一就是使用forEach()
, 那么精通forEach
的特性是咱们用好forEach
遍历array-like 对象的关键。
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);
}
});
复制代码
const array = [1, undefined, , , , , 3];
array.forEach(el => console.log(el)) // => 1, undefined, 3
复制代码
有这样一个类数组对象
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是不支持循环终止的即: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中提供的现代方法:
下面让咱们使用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
)。
最先从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()
的返回是属性的key
和value
:
[[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...of
和Object.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提供的
values和
entries就是
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...of
与Object.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 数据结构不产生反作用, 当一个对象在多处都要共享的时候, 最怕的就是不当心被直接改了致使的反作用, 因此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);
复制代码
咱们如今有一个对象 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}
{get name(){...}}
和 Setters {set name(val){...}}
那么若是要使用这个迭代协议就要定义这第三种属性, 长这个样子
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使用起来不陌生了, 去看它的原理也就更简单了, 但愿你们多动手敲敲, 悟一悟。
既然都看到这里了, 点个赞吧 💗