深入理解JavaScript:每个开发者必备的编程概念

发表时间: 2023-08-21 20:37

JavaScript是构建在标准Web浏览器上工作的Web应用程序前端的唯一本地编程语言。每个流行的Web浏览器都遵循众所周知的ECMAScript标准,并允许Web开发人员运行可移植、兼容的JavaScript代码。作为另一种流行的编程语言,JavaScript生态系统提供了几个独特的编程概念,帮助开发人员编写自解释的、性能优先的源代码。大多数现代Web开发项目在其代码库中使用这些JavaScript概念。在某些开发需求中,使用其中一些概念是不可避免的。

因此,有一些必须了解的JavaScript概念,每个Web开发人员在他们的职业生涯中都应该知道。开发人员不需要深入理解这些概念来编写简单的实验性Web应用程序。但是,理解这些JavaScript概念可以让您充分发挥JavaScript的潜力,通过理解JavaScript内部机制,以高效地编写生产级Web代码库。理解JavaScript的核心概念有助于更快地发现错误,并支持您提升职业水平。

在本文中,我将解释几个作为经验丰富的Web开发人员应该了解的核心JavaScript概念。

闭包和Hoisting

在JavaScript源代码中,我们通常不使用全局变量和一个主要函数,而是通过作用域将代码执行模型分解为多个堆叠段。例如,一个函数可以调用另一个函数,该函数使用私有变量来计算特定的值。闭包是一种位于另一个函数内部的函数,它允许程序员创建内部作用域。在函数调用后,内部函数(闭包)可以访问主函数作用域。

让我们通过一个简单的例子来理解这个概念:

function createUnitAdder(unit) {  return (val) => `${val} ${unit}` }const cm = createUnitAdder('cm');const kg = createUnitAdder('m');console.log(cm(120));   console.log(kg(7.5));   

在这里,createUnitAdder函数返回一个内部函数,该内部函数使用主作用域中的unit变量。当我们调用内部函数时,它使用了在调用createUnitAdder函数时初始化的主作用域中的unit变量。即使cm和kg函数来自同一个主要函数,它们使用不同的词法环境。

在C/C++语言中,我们无法在声明之前调用函数,因此通常在头文件中预定义它们。JavaScript允许您在声明之前访问函数和变量,因为JavaScript通过提升概念将声明移到作用域的顶部:

sayHello();function sayHello() {  console.log(`Hello JavaScript!`);}

回调, 匿名方法, 和IIFE

现代JavaScript使用Promise和async/await关键字支持异步编程。但是,过去的JavaScript实现只能通过回调函数进行异步编程。回调通常是您传递给另一个函数的函数,该函数根据特定事件稍后调用它。尽管回调概念不再推荐用于异步编程,但基于事件的浏览器API通常会使用它。例如,您应该将回调函数传递给计时器和DOM API。

看下面的例子:

function sayHello() {  console.log(`Hello JavaScript!`);}setInterval(sayHello, 1000);

在这里,我们使用sayHello函数作为回调函数。在这里,我们也可以使用匿名函数:

setInterval(() => console.log(`Hello JavaScript!`), 1000);

如果您需要在创建匿名函数后立即运行它,那该怎么办?立即调用函数表达式(IIFE,Immediately Invoked Function Expression)的概念允许您这样做,如下面的示例所示:

((param) => {  let name = 'JS';  console.log('IIFE', param, name); })(2023);

如上所示,它通过不允许我们向全局命名空间添加新变量来隔离代码段。

防抖动和节流

标准的浏览器API为各种目的提供了基于事件的API,用于创建用户友好的现代Web应用程序前端。这些基于事件的API(特别是DOM API)在短时间内可能会生成大量事件。因此,如果您通过这么多传入事件触发了一个昂贵的操作(例如调用RESTful API、解析等),它可能会影响您的整个应用程序的性能。如何减少调用昂贵操作的事件数量?

JavaScript开发人员通常使用防抖动(debounce)和节流(throttling)的概念作为性能增强策略。防抖动会等待频繁事件,并在超时后没有新的频繁事件发生时执行昂贵的操作。例如,以下代码片段在用户在半秒钟内不再主动输入文本时触发模拟的RESTful API请求:

async function searchTags() {  return new Promise((resolve) => setTimeout(() => {    console.log('API called.');    resolve();  }, 500));}function debounce(cb, delay) {  let timer;  return (...args) => {     clearInterval(timer);     timer = setTimeout(() => {        cb(...args);     }, delay);  }}document  .getElementById('text')  .addEventListener('input', debounce(searchTags, 500));

另一方面,节流的概念根据预定义的时间间隔触发一个昂贵的函数。换句话说,在特定的时间间隔内只调用一次昂贵的操作。

看下面的节流实现:

function throttle(cb, delay) {  let wait = false;  return (...args) => {     if(wait)        return;     cb(...args);     wait = true;     setTimeout(() => {        wait = false;     }, delay);  }}document  .getElementById('text')  .addEventListener('input', throttle(searchTags, 500));

上面的代码片段在半秒的时间内只触发了一次昂贵的函数调用,即使有很多传入事件发生。

异步编程

在过去,每个JavaScript开发人员都使用回调函数进行异步编程,并面临着回调地狱的问题。ES6引入了Promise的概念,并提供了一种方便的方式来进行异步编程。尽管Promise通过Promise链式调用解决了回调地狱的问题,但在异步操作调用过程中仍使用了回调函数。后来,ES8引入了async/await关键字,以比Promise更好的异步编程技术,但它们在内部仍然使用了Promise(带有async标记的函数只是返回一个Promise)。

在开发现代应用程序时使用async/await关键字是一个好主意,因为它们提高了代码的可读性,优于Promise和回调函数:

async function getTodos() {  let r = await fetch('https://jsonplaceholder.typicode.com/todos');  let todos = await r.json();  return todos;}(async function (){  let todos = await getTodos();  console.log(todos);})();

在这里,我们使用了带有await关键字的Fetch API。在JavaScript中,我们只能在异步函数内部使用await关键字,因此我们编写了一个IIFE。或者,您可以使用Promises API调用RESTful API,因为异步函数通常返回一个Promise:

getTodos().then((todos) => console.log(todos));

由于async/await关键字概念中的这种向后兼容特性,您甚至可以使用现代的await关键字调用旧的基于Promise的API:

function getItems() {   return Promise.resolve([1, 2, 4]);}(async function (){  let items = await getItems();  console.log(items);    })();

在JavaScript的事件系统中,使用回调是不可避免的,但有时我们可以使用async/await改进某些基于事件的API:

async function sleep(timeout) {  return new Promise((r) => setTimeout(r, timeout));}(async function (){  console.log('Please wait...');  await sleep(1000);  console.log('Done.');})();

在这里,我们部分地(没有检索返回值)将setTimeout函数转换为Promise。您可以使用Node.js内置的util.promisify函数或es6-promisify包来将任何老式的基于回调的代码转换为Promise。

事件系统

每个标准浏览器的JavaScript执行环境都是单线程的,但浏览器的功能通常是多线程的。例如,浏览器不会一次性排队和发送所有网络请求,而是可以并发发送网络请求。但是,浏览器使用事件循环的概念将多线程事件转换为单线程的JavaScript环境事件。从标准的Web API到高级库,事件无处不在。因此,掌握事件系统对于所有Web开发人员来说是必不可少的。

研究所有标准浏览器提供的有用事件。然后,您可以使用它们来构建用户友好且高效的Web应用程序。例如,您可以如下所示检测鼠标滚轮事件:

document.addEventListener('wheel', (e) => console.log(e.deltaY));

Web API标准积极实现新的浏览器事件,关注开发者的生产力和Web应用的可用性。浏览器甚至支持触发自定义事件以进行基于事件的编程。

函数式编程

在Web应用程序中的常规数据处理过程中,数组操作是一种经常使用的技术。过去的JavaScript实现提供了几种基本的数组方法,但由于缺乏便捷的数组处理方法,我们在许多场景中不得不使用循环结构。现代JavaScript现在提供了许多数组方法,以函数式的方式处理数据,避免了老式的循环结构。链式调用这些方法的可能性提高了开发者的生产力和源代码的简洁性。

看下面的例子,它显示了一个未排序整数数组中的最大的三个数字:

let marks = [50, 75, 80, 22, 81, 92];let topThree = marks                  .sort((a, b) => b - a)                  .slice(0, 3);console.log(topThree);   

这是另一个例子,它获取了两个单独数组中值的总和:

let costs1 = [20, 15, 5];let costs2 = [10, 200];let total = costs1.concat(costs2).reduce((acc, n) => acc + n);console.log(total);   

现代JavaScript拥有许多内置函数,以函数式的方式处理数组,因此在使用循环编写自己的算法之前,浏览MDN上现有的数组方法是一个明智的决策。

函数式的数据处理提高了代码的可读性,并激励您编写干净的代码。

结论

Web开发领域每天都在不断发展。许多协作者致力于与数百个W3C标准化草案合作,以改进基于JavaScript的Web API。与此同时,贡献者们致力于ECMAScript标准化草案,以改进标准JavaScript语言。这些新功能可以在Web开发领域引发新的概念。例如,WebAssembly和Workers引发了使用客户端计算能力来减少服务器工作负载的新概念,通过实现厚客户端。