es javascript
使您JavaScript技能保持精明的重要部分是紧跟JavaScript的最新功能。 因此,我认为构建一个包含JavaScript ES2020中所有七个新功能的应用程序会很有趣。
最近,我在好市多(Costco)进行了一些大宗购物,以购买一些食品必需品。 像大多数商店一样,它们的价格标签显示每件商品的单价,因此您可以评估和比较每笔交易的质量。 您会选择小袋子还是大袋子? (我在跟谁开玩笑?是Costco。放开!)
但是,如果不显示单价怎么办?
在本文中,我将使用香草JavaScript作为前端,使用Node.js和Express.js作为后端来构建单价计算器应用程序。 我将在Heroku上部署该应用程序,这是快速部署node.js应用程序的简便位置。
JavaScript编程语言符合称为ECMAScript的规范。 从ES2015(或ES6)的发布开始,每年都会发布一个新版本JavaScript。 截至目前,最新版本为ES2020(ES11)。 ES2020包含七个令人兴奋的新功能,JavaScript开发人员一直在等待很长一段时间才能看到。 新功能包括:
1. Promise.allSettled()
2.可选链接
3.空位合并
4. globalThis
5.动态导入
6. String.prototype.matchAll()
7. BigInt
您应该注意,并非所有浏览器都支持这些功能。 如果要立即开始使用这些功能,请确保提供适当的polyfill或使用Babel这样的编译器,以确保您的代码与旧版浏览器兼容。
如果要遵循自己的代码副本,请首先创建一个Heroku帐户,然后在计算机上安装Heroku CLI。 有关安装说明,请参见此Heroku指南 。
完成此操作后,您可以使用CLI轻松创建和部署项目。 运行此示例应用程序所需的所有源代码都可以在GitHub上找到 。
以下是有关如何克隆存储库并将其部署到Heroku的分步说明:
gitclone https://github.com/thawkin3/unit-price-calculator.git
cd unit-price-calculator
heroku create
git push heroku master
heroku open
我的单价计算器应用程序非常简单:它可以让您比较虚构产品的各种价格和重量选项,然后计算单价。 页面加载后,它将通过点击两个API端点从服务器获取产品数据。
然后,您可以选择产品,首选的度量单位以及价格/重量组合。 点击提交按钮后,即完成单价计算。
单价计算器应用
既然您已经看到了该应用程序,那么让我们看一下我如何使用所有这七个ES2020功能。 对于每个功能,我将确切讨论它的含义,它的有用性以及使用方式。
当用户首次访问计算器应用程序时,将启动三个API请求,以从服务器获取产品数据。 我们使用Promise.allSettled()
等待所有三个请求完成:
const fetchProductsPromise = fetch( '/api/products' )
.then( response => response.json())
const fetchPricesPromise = fetch( '/api/prices' )
.then( response => response.json())
const fetchDescriptionsPromise = fetch( '/api/descriptions' )
.then( response => response.json())
Promise .allSettled([fetchProductsPromise, fetchPricesPromise, fetchDescriptionsPromise])
.then( data => {
// handle the response
})
.catch( err => {
// handle any errors
})
Promise.allSettled()
是一项新功能,对现有Promise.all()
功能进行了改进。 这两种方法都允许您提供一个promise数组作为参数,并且这两种方法都返回一个promise。
区别在于Promise.all()
将短路并在任何承诺被拒绝的情况下尽早拒绝自己。 另一方面, Promise.allSettled()
等待所有诺言得到解决,而不管它们是被解决还是被拒绝,然后再自行解决。
因此,如果您希望获得所有承诺的结果,即使某些承诺被拒绝,也可以开始使用Promise.allSettled()
。
让我们看一下Promise.all()
另一个示例:
// promises 1-3 all will be resolved
const promise1 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 1 resolved!' ), 100 ))
const promise2 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 2 resolved!' ), 200 ))
const promise3 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 3 resolved!' ), 300 ))
// promise 4 and 6 will be resolved, but promise 5 will be rejected
const promise4 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 4 resolved!' ), 1100 ))
const promise5 = new Promise ( ( resolve, reject ) => setTimeout( () => reject( 'promise 5 rejected!' ), 1200 ))
const promise6 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 6 resolved!' ), 1300 ))
// Promise.all() with no rejections
Promise .all([promise1, promise2, promise3])
.then( data => console .log( 'all resolved! here are the resolve values:' , data))
.catch( err => console .log( 'got rejected! reason:' , err))
// all resolved! here are the resolve values: ["promise 1 resolved!", "promise 2 resolved!", "promise 3 resolved!"]
// Promise.all() with a rejection
Promise .all([promise4, promise5, promise6])
.then( data => console .log( 'all resolved! here are the resolve values:' , data))
.catch( err => console .log( 'got rejected! reason:' , err))
// got rejected! reason: promise 5 rejected!
现在,让我们看一下Promise.allSettled()
另一个示例,以记录诺言被拒绝时行为的不同:
// promises 1-3 all will be resolved
const promise1 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 1 resolved!' ), 100 ))
const promise2 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 2 resolved!' ), 200 ))
const promise3 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 3 resolved!' ), 300 ))
// promise 4 and 6 will be resolved, but promise 5 will be rejected
const promise4 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 4 resolved!' ), 1100 ))
const promise5 = new Promise ( ( resolve, reject ) => setTimeout( () => reject( 'promise 5 rejected!' ), 1200 ))
const promise6 = new Promise ( ( resolve, reject ) => setTimeout( () => resolve( 'promise 6 resolved!' ), 1300 ))
// Promise.allSettled() with no rejections
Promise .allSettled([promise1, promise2, promise3])
.then( data => console .log( 'all settled! here are the results:' , data))
.catch( err => console .log( 'oh no, error! reason:' , err))
// all settled! here are the results: [
// { status: "fulfilled", value: "promise 1 resolved!" },
// { status: "fulfilled", value: "promise 2 resolved!" },
// { status: "fulfilled", value: "promise 3 resolved!" },
// ]
// Promise.allSettled() with a rejection
Promise .allSettled([promise4, promise5, promise6])
.then( data => console .log( 'all settled! here are the results:' , data))
.catch( err => console .log( 'oh no, error! reason:' , err))
// all settled! here are the results: [
// { status: "fulfilled", value: "promise 4 resolved!" },
// { status: "rejected", reason: "promise 5 rejected!" },
// { status: "fulfilled", value: "promise 6 resolved!" },
// ]
提取产品数据后,我们将处理响应。 从服务器返回的数据包含具有深层嵌套属性的对象数组。 为了安全地访问这些属性,我们使用了新的可选链接运算符:
if (data?.[ 0 ]?.status === 'fulfilled' && data?.[ 1 ]?.status === 'fulfilled' ) {
const products = data[ 0 ].value?.products
const prices = data[ 1 ].value?.prices
const descriptions = data[ 2 ].value?.descriptions
populateProductDropdown(products, descriptions)
saveDataToAppState(products, prices, descriptions)
return
}
可选链接是我在ES2020中最兴奋的功能。 可选的链接运算符- ?.
—使您可以安全地访问对象的深层属性,而无需检查每个属性的存在。
例如,在ES2020之前,您可以编写如下所示的代码以访问某些user
对象的street
属性:
const user = {
firstName : 'John' ,
lastName : 'Doe' ,
address : {
street : '123 Anywhere Lane' ,
city : 'Some Town' ,
state : 'NY' ,
zip : 12345 ,
},
}
const street = user && user.address && user.address.street
// '123 Anywhere Lane'
const badProp = user && user.fakeProp && user.fakeProp.fakePropChild
// undefined
为了安全地访问street
属性,首先必须确保user
对象存在并且address
属性存在,然后才能尝试访问street
属性。
使用可选的链接,访问嵌套属性的代码要短得多:
const user = {
firstName : 'John' ,
lastName : 'Doe' ,
address : {
street : '123 Anywhere Lane' ,
city : 'Some Town' ,
state : 'NY' ,
zip : 12345 ,
},
}
const street = user?.address?.street
// '123 Anywhere Lane'
const badProp = user?.fakeProp?.fakePropChild
// undefined
如果链中的任何点不存在值,则将返回undefined
。 否则,返回值将是您想要访问的属性的值,这与预期的一样。
应用加载时,我们还将获取用户的计量单位首选项:千克或磅。 偏好设置存储在本地存储中,因此首次访问者尚不存在该偏好设置。 要使用本地存储中的值或默认使用千克来处理,我们使用无效的合并运算符:
appState.doesPreferKilograms =JSON .parse(doesPreferKilograms ?? 'true' )
空合并运算符- ??
—是方便的运算符,用于当您明确想要使用变量的值(只要它不是undefined
或null
。 您应该使用此运算符,而不是简单的OR- ||
—运算符,如果指定的变量是布尔值,并且即使它为false
也要使用其值。
例如,假设您有一个用于某些功能设置的开关。 如果用户专门为该功能设置设置了一个值,则您要尊重他或她的选择。 如果他们没有指定设置,那么您想默认为他们的帐户启用该功能。
在ES2020之前,您可能会这样写:
const useCoolFeature1 = true
const useCoolFeature2 = false
const useCoolFeature3 = undefined
const useCoolFeature4 = null
const getUserFeaturePreference = ( featurePreference ) => {
if (featurePreference || featurePreference === false ) {
return featurePreference
}
return true
}
getUserFeaturePreference(useCoolFeature1) // true
getUserFeaturePreference(useCoolFeature2) // false
getUserFeaturePreference(useCoolFeature3) // true
getUserFeaturePreference(useCoolFeature4) // true
使用无效的合并运算符,您的代码将更短,更容易理解:
const useCoolFeature1 = true
const useCoolFeature2 = false
const useCoolFeature3 = undefined
const useCoolFeature4 = null
const getUserFeaturePreference = ( featurePreference ) => {
return featurePreference ?? true
}
getUserFeaturePreference(useCoolFeature1) // true
getUserFeaturePreference(useCoolFeature2) // false
getUserFeaturePreference(useCoolFeature3) // true
getUserFeaturePreference(useCoolFeature4) // true
如上所述,为了获取和设置用户对度量单位的偏好,我们使用本地存储。 对于浏览器,本地存储对象是window
对象的属性。 虽然您可以直接调用localStorage
,但是也可以使用window.localStorage
来调用它。 在ES2020中,我们还可以通过globalThis
对象访问它(还请注意,再次使用可选链接来进行某些功能检测,以确保浏览器支持本地存储):
const doesPreferKilograms = globalThis.localStorage?.getItem?.( 'prefersKg' )
globalThis
功能非常简单,但是它解决了许多有时会咬你的矛盾之处。 简而言之, globalThis
包含对全局对象的引用。 在浏览器中,全局对象是window
对象。
在节点环境中,全局对象从字面上称为global
。 使用globalThis
可以确保无论代码运行在什么环境下,您始终对全局对象始终具有有效的引用。
这样,您可以编写可移植JavaScript模块,这些模块将在浏览器的主线程,Web Worker或节点环境中正确运行。
// assuming you're in a browser
window .alert( 'Hi from the window!' )
globalThis.alert( 'Hi from globalThis, which is also the window!' )
一旦用户选择了产品,计量单位以及重量和价格组合,他或她就可以单击“提交”按钮来查找单位价格。 单击该按钮时,将延迟加载用于计算单价JavaScript模块。
您可以在浏览器的开发工具中检查网络请求,以确保在您单击按钮之前未加载第二个文件:
import ( './calculate.js' )
.then( module => {
// use a method exported by the module
})
.catch( err => {
// handle any errors loading the module or any subsequent errors
})
在ES2020之前,在JavaScript中使用import
语句意味着在请求父文件时,导入的文件会自动包含在父文件中。
诸如webpack的捆绑软件已经普及了“代码拆分”的概念,该功能使您可以将JavaScript捆绑软件拆分为多个文件,然后按需加载。 React还通过其React.lazy()
方法实现了此功能。
代码拆分对于单页应用程序(SPA)极为有用。 您可以将代码分为每个页面单独的捆绑包,因此仅下载当前视图所需的代码。 这显着加快了初始页面加载时间,从而使最终用户不必预先下载整个应用程序。
代码拆分对于大部分很少使用的代码也很有帮助。 例如,假设您在应用程序的页面上有一个“导出PDF”按钮。 PDF下载代码很大,在页面加载时包含它可以减少总体加载时间。
但是,并非每个访问此页面的用户都需要或想要导出PDF。 为了提高性能,您可以延迟加载PDF下载代码,以便仅在用户单击“导出PDF”按钮时才下载其他JavaScript捆绑包。
在ES2020中,将动态导入直接纳入JavaScript规范中!
让我们看一下没有动态导入的“导出PDF”功能的示例设置:
import { exportPdf } from './pdf-download.js'
const exportPdfButton = document .querySelector( '.exportPdfButton' )
exportPdfButton.addEventListener( 'click' , exportPdf)
// this code is short, but the 'pdf-download.js' module is loaded on page load rather than when the button is clicked
现在让我们看看如何使用动态导入来延迟加载大型PDF下载模块:
const exportPdfButton = document .querySelector( '.exportPdfButton' )
exportPdfButton.addEventListener( 'click' , () => {
import ( './pdf-download.js' )
.then( module => {
// call some exported method in the module
module .exportPdf()
})
.catch( err => {
// handle the error if the module fails to load
})
})
// the 'pdf-download.js' module is only imported once the user click the "Export PDF" button
当调用calculateUnitPrice
方法时,我们传递产品名称和价格/重量组合。 价格/重量组合是一个字符串,看起来像“ 10公斤200美元”。 我们需要解析该字符串以获得价格,重量和度量单位。 (肯定有一种更好的方法来构造此应用程序,以避免解析这样的字符串,但是为了演示此下一个功能,我将以此方式进行设置。)要提取必要的数据,可以使用String.prototype.matchAll()
:
const matchResults = [...weightAndPrice.matchAll( /\d+|lb|kg/g )]
一行代码中发生了很多事情。 我们基于正则表达式在字符串中寻找匹配项,该正则表达式搜索数字和字符串“ lb”或“ kg”。 它返回一个迭代器,然后我们可以将其扩展为一个数组。 该数组最后包含三个元素,每个匹配项一个元素(200、10和“ kg”)。
此功能可能是最难理解的,特别是如果您不熟悉正则表达式。 对String.prototype.matchAll()
的简短解释是,它对String.prototype.match()
和RegExp.prototype.exec()
的功能进行了改进。 此新方法使您可以将字符串与正则表达式进行匹配,并返回所有匹配结果(包括捕获组)的迭代器。
你明白了吗? 让我们看另一个有助于巩固概念的示例:
const regexp = /t(e)(st(\d?))/
const regexpWithGlobalFlag = /t(e)(st(\d?))/g
const str = 'test1test2'
// Using `RegExp.prototype.exec()`
const matchFromExec = regexp.exec(str)
console .log(matchFromExec)
// ["test1", "e", "st1", "1", index: 0, input: "test1test2", groups: undefined]
// Using `String.prototype.match()` on a regular expression WITHOUT a global flag returns the capture groups
const matchFromMatch = str.match(regexp)
console .log(matchFromMatch)
// ["test1", "e", "st1", "1", index: 0, input: "test1test2", groups: undefined]
// Using `String.prototype.match()` on a regular expression WITH a global flag does NOT return the capture groups :(
const matchesFromMatchWithGlobalFlag = str.match(regexpWithGlobalFlag)
for ( const match of matchesFromMatchWithGlobalFlag) {
console .log(match)
}
// test1
// test2
// Using `String.prototype.matchAll()` correctly returns even the capture groups when the global flag is used :)
const matchesFromMatchAll = str.matchAll(regexpWithGlobalFlag)
for ( const match of matchesFromMatchAll) {
console .log(match)
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2", groups: undefined]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2", groups: undefined]
最后,我们将价格除以权重就可以得出单价。 您可以使用普通数来执行此操作,但是在处理大数时,ES2020引入了BigInt
,它使您可以在不损失精度的情况下对大整数进行计算。 对于我们的应用程序,使用BigInt
是过大的,但是谁知道,也许我们的API端点会发生变化,包括一些疯狂的批量交易!
const price = BigInt(matchResults[ 0 ][ 0 ])
const priceInPennies = BigInt(matchResults[ 0 ][ 0 ] * 100 )
const weight = BigInt(matchResults[ 1 ][ 0 ])
const unit = matchResults[ 2 ][ 0 ]
const unitPriceInPennies = Number (priceInPennies / weight)
const unitPriceInDollars = unitPriceInPennies / 100
const unitPriceFormatted = unitPriceInDollars.toFixed( 2 )
如果您曾经使用过包含非常大数字的数据,那么您将知道在执行JavaScript数学运算时确保数字数据的完整性可能会很痛苦。 在ES2020之前,可以安全存储的最大整数由Number.MAX_SAFE_INTEGER
表示,即2 ^ Number.MAX_SAFE_INTEGER
。
如果您尝试将大于该值的数字存储在变量中,则有时该数字将无法正确存储:
const biggestNumber = Number .MAX_SAFE_INTEGER // 9007199254740991
const incorrectLargerNumber = biggestNumber + 10
// should be: 9007199254741001
// actually stored as: 9007199254741000
新的BigInt
数据类型有助于解决此问题,并允许您使用大得多的整数。 要使整数成为BigInt
,您只需将字母n附加到整数的末尾或在整数上调用函数BigInt()
:
const biggestNumber = BigInt( Number .MAX_SAFE_INTEGER) // 9007199254740991n
const correctLargerNumber = biggestNumber + 10 n
// should be: 9007199254741001n
// actually stored as: 9007199254741001n
而已! 现在您已经了解了ES2020的新功能,您还等什么呢? 赶快去那里,立即开始编写新JavaScript!
翻译自: https://hackernoon.com/building-an-app-that-uses-all-7-new-javascript-es2020-features-6c6t3232
es javascript