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使用起來不陌生了, 去看它的原理也就更簡單了, 但願你們多動手敲敲, 悟一悟。
既然都看到這裏了, 點個贊吧 💗