现代JS开发者的重要概念

Share

此资源包含一个现代, 由Toptal网络成员提供的重要JavaScript概念.

Still the de facto 作为一门网络语言,JavaScript有着漫长而零散的历史. 近年来,主要参与者一致努力使JavaScript符合ECMA-262或ECMA-262标准 ECMAScript.

In mid-2019, 主要里程碑,如ECMAScript 2015 (ES2015), (以前的ES6)仍然没有被任何前端或后端JavaScript引擎完全实现.

Since ES2015, 标准组决定每年发布一次规范, and ES2016, ES2017, ES2018, 和ES2019都添加了有趣的功能. 但是要掌握JavaScript中可用和有用的内容是很困难的 right now——无论是尖端的魔法还是久经考验的技巧.

因此这个社区驱动的项目, 汇集了关于现代JS开发人员应该知道的最重要的高级概念的专家建议. 我们欢迎您的贡献和反馈.

闭包和高阶函数

闭包和高阶函数是JavaScript中的两个重要概念, 还有一些是密切相关的. Let’s find out how.

Closures

当我们在JavaScript中创建一个函数到另一个函数中时, 内部函数既可以访问外部函数的作用域,也可以访问自己的作用域.

在下面的代码示例中, count function is able to access total,它在外部作用域中定义 tallyCounter.

function tallyCounter() {
  let total = 0;

  function count() {
    total++;
    return total;
  }
}

Here, the count 函数是闭包,并且可以继续访问 total variable even after tallyCounter has finished executing. 这是由于JavaScript中的词法作用域, 允许函数访问变量,即使它们是从最初创建它们的作用域之外调用的.

上面的代码不是很有用,因为我们不能调用 count from the outside. It was defined in the tallyCounter 作用域,外部不可用;

function tallyCounter() {
  let total = 0;

  function count() {
    total++;
    return total;
  }
}

console.log(count()); // Error: count is not defined

为了使闭包变得有用,我们需要返回它. 这样,我们就可以把它赋值给外部的一个局部变量 tallyCounter, and start using it:

function tallyCounter() {
  let total = 0;

  return function count() {
    total++;
    return total;
  }
}

let count = tallyCounter();

console.log(count()); // log: 1
console.log(count()); // log: 2
console.log(count()); // log: 3

在上面的例子中,我们无法访问 total variable from outside of the tallyCounter function. We’re able to call count and increment total 但是我们不能直接访问这个值. As such, 闭包在JavaScript中经常用于保护变量免受外部操作, 或者隐藏实现细节.

闭包的另一个常见用例是延迟代码执行. JavaScript中的回调就是一个典型的例子:

logTheDateLater(延迟){
  let date = new Date();

  function logDate() {
    console.log(date);
  }
  
  setTimeout(logDate, delay);
}

Here, we’re calling setTimeout 使用一个回调函数,该函数将输出当前日期和时间(调用外部函数时),但要经过一段延迟. Internally, setTimeout 延迟之后会调用我们的回调函数吗, 不知道它是做什么的,也不知道它是怎么工作的. 它也不能访问 date variable. 因为回调函数是闭包,所以它可以访问 date 在其词法作用域中,并且它可以将其记录到我们的控制台.

Higher-order Functions

Both tallyCounter and setTimeout are what we call higher-order functions in JavaScript. 这些函数要么返回另一个函数, 或者接受一个或多个函数作为输入参数.

They are called higher因为他们更关心高层概念而不是低层细节. 正如我们之前提到的,回调就是一个很好的例子:

function helloToptal() {
  console.log('Hello Totpal');
}

setTimeout (helloToptal, 1000);

Here, setTimeout 高阶函数是因为它接受回调函数吗, 并在给定的延迟后执行它. 它不需要知道JavaScript函数的作用.

现在,假设我们要确保不进行log运算 "Hello Toptal" more than once per second. 这里有一个高阶函数可以帮助我们实现这个目标:

Function throttle(fn, milliseconds) {
  let lastCallTime;

  return function() {
    let nowTime = Date.now();

    if(!lastCallTime || lastCallTime < nowTime - milliseconds) {
      lastCallTime = nowTime;
      fn();
    }
  };
}

The throttle 函数接受一个函数参数,并向调用者返回一个新的经过throttated的函数. 我们现在可以使用它来创建控件的节流版本 helloToptal function:

let hellotoptalthrottated = throttle(helloToptal, 1000);
setInterval (helloToptalThrottled 10);

您会注意到,尽管间隔为10ms,但调用 helloToptal are throttled and "Hello Totpal" 每秒只记录一次吗.

Higher-order functions like throttle 允许我们从更小的组件组合JavaScript应用程序, 更容易测试的单职责代码单元, reusable, 当然也更容易维护.

Contributors

Merott Movahedi

自由JavaScript顾问

United Kingdom

Merott是一名对前端领域有浓厚兴趣的全栈开发人员. 他精通JavaScript,并且很好地掌握了JavaScript的核心概念和最佳实践. 他还擅长JavaScript框架,尤其是Angular. 除了web开发, 他对移动应用很感兴趣, 他在上面花了大量的时间. Merott非常喜欢使用他的编程技能来自动化和简化日常任务.

Show More

从Toptal获取最新的开发人员更新.

订阅意味着同意我们的 privacy policy

Rest和Spread操作符

ES6为rest和spread添加了新的语法,但操作符看起来像 ... in both cases. Rest在ES6之前就已经可行了,但它需要更多的代码. 它看起来也不那么漂亮.

为了说明这一点,让我们编写一个名为 getUser 它取一个包含a的对象 userId or username property, in ES5:

function omit() {
    if (arguments.length < 2) {
        抛出新的错误('省略需要至少2个参数')
    }

    var obj = arguments[0],
        keys = Array.prototype.slice.call(arguments, 1)

    var ret = {}

    for (var key in obj) {
        if (obj.hasOwnProperty(key) && keys.indexOf(key) === -1) {
            ret[key] = obj[key]
        }
    }

    return ret
}

function getUser(params) {
    var userId = params.userId,
        username = params.username,
        args =省略(params, 'userId', 'username')
    
    if (typeof userId !== 'undefined') {
        返回getUserById(userId, args)
    }

    if (typeof username !== 'undefined') {
        返回getUserByUsername(username, args)
    }

    抛出新的错误('需要传递userId或username ')
}

注意,我们必须自己编写 omit 函数不包含某些键. 我们还得复制 userId and username fields, once in the var 定义和一次调用 omit. 让我们看一下使用ES6 rest操作符编写的相同代码:

function getUser(params) {
    const { userId, username, ...args } = params
    
    if (typeof userId !== 'undefined') {
        返回getUserById(userId, args)
    }

    if (typeof username !== 'undefined') {
        返回getUserByUsername(username, args)
    }

    抛出新的错误('需要传递userId或username ')
}

As you can see, 这是更容易阅读和理解发生了什么在第一眼, 不需要额外的 omit helper function.


ES6的扩展操作符也非常有用. 例如,它可以用于将多个源传播到目的地:

函数mergeThreeObjects(obj1, obj2, obj3) {
    return { ...obj1, ...obj2, ...obj3 }
}

console.log(mergeThreeObjects({x: 1}, {y: 1}, {y: 2, z: 3})

This logs { x: 1, y: 2, z: 3 }. 注意,后面的键将覆盖前面的键,就像 Object.assign() in ES5. (See how the y 属性覆盖了 y 属性.)

也有相反的语法:

addEmailToUser(email, user) {
    return { email, ...user }
}

Here, this addEmailToUser 的顶级属性 user 到一个只有 email property. 注意,这是一个浅拷贝. ES5中的等效代码为:

addEmailToUser(email, user) {
    return Object.Assign ({email: email}, user)
}

ES6的rest/spread操作符也可以与数组一起使用,这可能会使旧的语法(如使用 Array.prototype.concat and Array.prototype.slice a lot more readable:

ES5:

Var list = [0,1,2,3,4,5,6]
var first = list[0]
var rest = list.slice(1)
console.log(first, rest)

ES6:

const [ first, ...[0,1, 2, 3, 4, 5, 6]
console.log(first, rest)

These both log 0, [1, 2, 3, 4, 5, 6].

带有数组的扩展运算符也可以取代旧的ES5使用方式 Array.concat to combine arrays:

ES5:

var others = [1, 2, 3, 4]
var arr = [1, 2].concat(others)

ES6:

const others = [1, 2, 3, 4]
const arr = [1, 2, ...其他]// [1,2,1,2,3,4]

As you can see, ES6的rest和spread操作符有时只是提供了语法糖, 但其他时候比这强大得多. Enjoy!

Contributors

David Xu

自由JavaScript顾问

United States

大卫已经将几个移动应用程序从一个想法发展到全世界数百万用户, 作为城堡国际的首席建筑师, 在其他公司担任技术领导. 他从9岁开始编程,并在各种竞争性编程比赛中获得奖牌, 从澳大利亚信息奥林匹克竞赛到ACM-ICPC. 大卫在所有技术领域都有宝贵的经验, 从架构和设计到工程和开发运维.

Show More

Understanding Async/Await

理解异步编程对于现代软件开发和渐进式web应用程序的创建至关重要. 没有很好地理解它是如何工作的, 编写的代码很容易变成难以管理的混乱, 特别是当客户的期望在整个项目过程中不断增长和变化时.

功能更改还可能要求将代码从同步转换为异步. 这些在服务器端代码和客户端代码中同样重要.

Thankfully, in recent years, JavaScript极大地简化了这个过程, 尤其是有了 async and await.

Overview of Promises

JavaScript承诺只是将回调函数附加到一些异步事件的一种方式, 例如从数据库中获取记录的调用. 因为数据库服务器不一定会立即响应, 我们需要附加一个回调来等待响应,而不是阻止应用程序的其余部分继续.

承诺允许我们递归地或迭代地将事件链接在一起,并在每一步使用结果数据. It’s a very powerful tool, 但是没有明确的结构, 代码很快就会看起来非常难以读懂. 下面给出的示例从数据库读取员工记录,然后将其转储到控制台:

db.manyOrNone ('select * from employee')
.then (console.log); 

第一行向数据库发出调用并返回 Promise,它可以被认为是一个带有两个回调函数的线程, resolve and reject-尽管它不一定要在内部使用线程实现. 此线程中的代码将继续运行,而不会阻塞任何其他部分的代码, and when it’s done, the resolve 函数将被调用,并将结果数据传递给它. 如果遇到任何错误,则 reject 函数将被调用,并将错误代码传递给它.

在第二行代码中,我们调用 .then() method on the returned Promise 对象的处理程序 resolve part of our promise. 在这种情况下,我们只是连接 console.log 作为处理程序,它将返回的数据输出到控制台. 但是在回调函数中, 我们可以对数据做任何我们想做的事, 知道只有当数据库获取完成时才会调用它.

熟悉jQuery的开发人员习惯于这种相同类型的模式, although in that case, 回调函数通常作为参数或Settings对象的成员传递.

这段代码非常简单易读, 但是在下一节中我们会看到, 事情很容易变得相当混乱.

The Problem

在某些情况下,编写基于承诺的代码可能相当复杂. 其中之一是基于条件的承诺. Let’s say we have an employee object, 我们想查询他们的医疗保险信息, 但前提是他们是全职工作. 我们可以这样写代码:

getMedicalCoverage(员工)
.then (console.log);

getMedicalCoverage(雇员){
   if (!employee.isFullTime) {
      return (Promise.resolve (null));
   }
   else {
      return (db.manyOrNone ('select * from coverage where employee_id=' + employee.id));
   }
}

In this example, the getMedicalCoverage function will return a Promise 员工是否是全职员工. 如果雇员不是全职员工,则返回a Promise 它的值是 null. 如果它们是全职的,则执行一个数据库查询,该查询在获取数据后得到解析.

基于条件的数据获取通常需要我们以任何一种方式创建一个承诺, 这就是我们每次遇到这种情况都要写的代码. 可以使这些代码模式更易于管理 async and await那么让我们先来描述一下它是如何工作的.

Async/Await to the Rescue

In modern JavaScript, async and await 是否习惯于以更简洁的方式处理这个过程. 首先,让我们从理解什么开始 async does. 当我们编写一个函数的目的是在不阻塞其他代码或接口的情况下运行时, we’d define it as an async 函数,使用以下语法:

异步函数readEmployees () {
   var employees = ["Elsa", "Anna", "Kristoff"];
   return (employees);
}

下面是ES6箭头函数的语法:

const readEmployees = async () => {
   var employees = ["Elsa", "Anna", "Kristoff"];
   return (employees);
};

现在不要介意这个函数只是简单地返回一个预定义字符串数组. 关键是我们把它定义为一个异步函数, 这意味着JavaScript会自动将其包装在 Promise. This causes a Promise 要被创建,用这个函数作为那个函数的主体 Promise. 当我们从函数返回一个值时,它就像 resolve for the Promise.

现在,让我们看一个如何调用异步函数的例子:

var employees = await readEmployees ();

It’s really that simple! We just insert the await 关键字在函数调用本身之前. 不管底层发生了什么(API调用、数据库调用等).), 调用环境只需要调用函数并使用返回值就可以了.

记住,我们不能用 await 除非是在异步函数中. Defining a function as async exposes the await keyword inside its scope. 其思想是,启动整个流程的顶级函数需要被视为具有 .then() 处理程序等待它完成,但内部步骤可能被阻塞. 下面是一个演示整个过程的例子:

const readEmployees = async () => {
   var employees = await db.('select * from employee');
   for (let i=0; i

As you can see, 我在这里讨论的技巧使得用更少的代码行编写相同的含义成为可能. 现在想象一下,这段代码是一个巨大项目的一部分. 如果您可以查看整个代码, even with comments, without needing to scroll, 这样就更容易理解和修改了.

However, 我从来没有缩短过变量名, 也没有使它看起来像缩小的JS, 如果我们开始改变,会发生什么 element to e, or seconds to s. 使用这些变量名会让你停下来思考一下, In this context, what is e or s?

如果你怀疑你是否应该应用这些简洁技巧之一, 问自己以下问题:把事情弄成这样, 下次我以陌生人的眼光看代码的时候, 它是更容易理解还是更难理解? Happy coding!

Contributors

Juan Carlos Arias Ambriz

自由JavaScript顾问

Mexico

Juan拥有超过十年的自由用户体验经验. 他的项目范围很广,但都植根于他始终为用户提供最佳体验的承诺. Juan开发了一些知名客户使用的应用程序,因此他学会了在工作中追求细节的完美.

Show More

Tip Proposed by Jalik

Author Name: Jalik Author Email: jalik26@gmail.com

Thanks for sharing, 只是“当前时间是150”打错了,678 ms”, 应该以秒为单位,而不是毫秒.

Internationalization API

以人性化的方式呈现数据是任何优秀UX的特征, 但对于开发者来说,这一直是一个棘手的问题. 当一款应用支持多种语言或国家时尤其如此. 每个上下文都需要一套新的规则.

例如,用英语表示相对时间格式是相当简单的. 最多只能有两种形式:单数和复数. E.g.: 1 month ago and 2+ months ago.

在波兰,事情变得更加复杂. 名词根据数量的不同有多种形式: 1 miesiąc temu and 2-4 miesiące temu, but 5+ miesięcy temu.

有些公司需要考虑数百种语言和上下文的格式规则(想想Facebook或b谷歌的规模)。. 开发它会创建大量的样板代码.

值得庆幸的是,现代JavaScript拥有全局 Intl namespace with a set of ECMAScript国际化api. 这让事情变得非常简单.

Relative Time Format

Let’s tackle the case above. 我们将创建一个函数,以一种对人类友好的格式返回两个时间戳之间的差值. For this, we’ll leverage Intl.RelativeTimeFormat.

const getRelativeTimeString = (relativeDate, baseDate, locale = 'en') => {
   const rtf = new Intl.RelativeTimeFormat(语境,{
       //显示“昨天”而不是“1天前”
       numeric: 'auto',
   })
   //以毫秒为单位的时间差
   const deltaUnix = relativeDate.getTime() - baseDate.getTime()

   //将毫秒改为秒...
   const deltaSeconds = Math.round(deltaUnix / 1000)
   // ... 检查时间差是否小于60秒(1分钟)...
   if (Math.abs(deltaSeconds) < 60)
       // ... then return formatted date...
       return rtf.格式(deltaSeconds“秒”)
   // ... else increase the unit

   const deltaMinutes = Math.round(deltaSeconds / 60)
   if (Math.abs(deltaMinutes) < 60)
       return rtf.格式(deltaMinutes“分钟”)

   const deltaHours = Math.round(deltaMinutes / 60)
   if (Math.abs(deltaHours) < 24)
       return rtf.format(deltaHours, 'hours')

   const deltaDays = Math.round(deltaHours / 24)
   if (Math.abs(deltaDays) < 7)
       return rtf.format(deltaDays, 'days')

   const deltaWeeks = Math.round(deltaDays / 7)
   if (Math.abs(deltaWeeks) < 4)
       return rtf.format(deltaWeeks, 'weeks')

   const deltaMonths = Math.round(deltaWeeks / 4)
   if (Math.abs(deltaMonths) < 12)
       return rtf.格式(deltaMonths“月”)

   const deltaYears = Math.round(deltaMonths / 12)
   return rtf.format(deltaYears, 'years')
}

Now, let’s try it out:

const baseDate = new Date(日期.UTC(2019, 05, 13))

getRelativeTimeString(
   new Date(Date.UTC(2019, 04, 13)),
   baseDate,
   'en',
)
// => '1 month ago'

getRelativeTimeString(
   new Date(Date.UTC(2019, 04, 13)),
   baseDate,
   'pl',
)
// => '1 miesiąc temu'

getRelativeTimeString(
   new Date(Date.UTC(2019, 03, 13)),
   baseDate,
   'en',
)
// => '2 months ago'

getRelativeTimeString(
   new Date(Date.UTC(2019, 03, 13)),
   baseDate,
   'pl',
)
// => '2 miesiące temu'

getRelativeTimeString(
   new Date(Date.UTC(2018, 12, 13)),
   baseDate,
   'en',
)
// => '5 months ago'

getRelativeTimeString(
   new Date(Date.UTC(2018, 12, 13)),
   baseDate,
   'pl',
)
// => '5 miesięcy temu'

That was easy! 您不需要为新语言添加一套额外的规则. Just pass a different locale argument:

getRelativeTimeString(
   new Date(Date.UTC(2019, 03, 13)),
   baseDate,
   'de-AT',
)
// => 'vor 2 Monaten'

国际化API还能做什么?

这只是冰山一角. 冰山上充满了机遇!

The Intl 命名空间仍在开发中,希望将来会添加更多的api. 但是,对英国部分地区的支持已经很强烈 Intl 无论是在浏览器之间还是在Node的后端上下文中.js. 现在,这里是最广泛使用的.

Intl.Collator

Collator 允许开发人员以特定于语言环境的方式比较字符串. After all, alphabetical 是一个比一些程序员可能意识到的更相对的概念吗. So Collator 在排序数组时非常有用:

['z', 'ä', 'c'].sort(new Intl.Collator('de').compare)
// -> ['ä', 'c', 'z']

['z', 'ä', 'c'].sort(new Intl.Collator('sv').compare)
// -> ['c', 'z', 'ä']

Intl.DateTimeFormat

DateTimeFormat 执行语言敏感的日期和时间格式. 它有很多选项,但这里有一个基本的初学者的例子:

const date = new Date(Date.UTC(2019, 05, 13))

new Intl.DateTimeFormat('en-US').format(date)
// -> '6/13/2019'

new Intl.DateTimeFormat('en-GB').format(date)
// -> '13/06/2019'

Intl.ListFormat (Experimental)

ListFormat 仍处于实验模式,但这绝对是我最喜欢的部分 Intl. 它有助于将数组显示为对人类友好的列表. For example, transforming ['A', 'B', 'C'] into 'A, B, and C' (or 'A, B and C' by default in en-GB):

const list = ['A', 'B', 'C']

new Intl.ListFormat('en-US', {
   style: 'long',
   type: 'conjunction',
}).format(list)
// -> 'A, B, and C'

new Intl.ListFormat('en-GB', {
   style: 'long',
   type: 'conjunction',
}).format(list)
// -> 'A, B and C'

Intl.NumberFormat

NumberFormat, as its name implies, does number formatting, 在处理货币时,哪一个特别方便, among other uses.

Dot or comma? 金额之前或之后的货币符号? 实际上,是哪个货币符号? Now it’s easy:

new Intl.NumberFormat('en-US', {style: 'currency',货币:'USD'}).format(123.45)
// -> '$123.45'

new Intl.NumberFormat('en-GB', {style: 'currency',货币:'USD'}).format(123.45)
// -> 'US$123.45'

new Intl.NumberFormat('pl-PL', {style: '货币',货币:'美元'}).format(123.45)
// -> '123,45 USD

Intl.PluralRules

PluralRules 是否复数敏感格式和处理复数语言规则. 就像上面的例子一样 RelativeTimeFormat,但更加通用和低级. 它在定义您自己的国际化规则和api时非常有用.


It’s worth checking out 的完整文档 Intl namespace and taking it for a spin. Happy coding!

Contributors

Patryk Pawłowski

自由JavaScript顾问

Poland

Patryk是一位经验丰富的全栈开发人员,擅长所有类型的现代JavaScript实现——从构建后端和api到构建像素完美的web和移动应用程序. 这要归功于他经营自己公司的经验和设计背景, 他是业务团队和产品团队之间的伟大促进者. 帕特里克也喜欢在会议上发言.

Show More

Template Strings

ES6增加了一个名为“模板字符串”的新特性.” At first glance, 它们可能看起来类似于其他语言(如Ruby和PHP)中的字符串插值. 是的,它们可以用来实现相同的行为:

const name = 'David'
const myStr = ' Hello, ${name}!`
console.log(myStr)

This logs Hello, David! to the console. Anything between ${ ... } 插入到字符串中.

但实际上,模板字符串比这强大得多. Behold:

const name = 'David'

//模板字符串函数接受第一个参数为
//一个字符串数组,它是模板字符串的片段
//和一个可变数量的参数后面的表示
//在每个位置插入标记
const escape = (parts, ...tokens) => {
    let result = ''

    // iterate the parts array
    for (let i = 0; i < parts.length; i++) {
        result += parts[i]

        //如果我们还没有到达零件数组的末尾... 
        if (i < parts.length - 1) {
            // ... 用引号将标记括起来
            Result += '"' + token [i] + '"'        
        }
    }

    return result
}

const mystrautoquotes = escape 'Hello, ${name}!`
console.log(myStrAutoQuoted)

This will log Hello, "David"! to the console. The syntax here is important; see how we simply put the name of the template function right before the template string without parentheses. Note how the variable name was automatically quoted! 这只是模板字符串真正强大的开始. 它们为JavaScript编程的可能性增加了另一个维度.

这个特性在许多新的JavaScript库中都有使用,包括 styled-components,它允许开发者以一种非常干净的方式用JS编写CSS:

从styles -components中导入style
从react-dom中导入{render}

const RedSquare = styled.div`
    width: ${props => props.size}px;
    height: ${props => props.size}px;
    background-color: red;
`

render(, document.getElementById(容器))

这将在屏幕上呈现一个大小为20像素的红色方框.

Contributors

David Xu

自由JavaScript顾问

United States

大卫已经将几个移动应用程序从一个想法发展到全世界数百万用户, 作为城堡国际的首席建筑师, 在其他公司担任技术领导. 他从9岁开始编程,并在各种竞争性编程比赛中获得奖牌, 从澳大利亚信息奥林匹克竞赛到ACM-ICPC. 大卫在所有技术领域都有宝贵的经验, 从架构和设计到工程和开发运维.

Show More

Submit a tip

提交的问题和答案将被审查和编辑, 并可能会或可能不会选择张贴, 由Toptal全权决定, LLC.

* All fields are required

Toptal Connects the Top 3% 世界各地的自由职业人才.

Join the Toptal community.