学习React前需要了解的顶级JavaScript概念

151 阅读18分钟

如果你想学习React--或者任何JavaScript框架--你首先需要了解基本的JavaScript方法和概念。

否则就像一个年轻人在学会走路之前先学会跑步。

许多开发人员在学习React时选择了 "边学边做 "的方法。但这往往不会带来生产力,反而会使他们的JavaScript知识的差距恶化。这种方法使吸收每个新功能的难度加倍(你可能开始混淆JavaScript和React)。

React是一个用于构建基于UI组件的用户界面的JavaScript框架。它的所有代码都是用JavaScript编写的,包括用JSX编写的HTML标记(这使开发人员能够轻松地将HTML和JavaScript写在一起)。

在这篇文章中,我们将采取一种实用的方法,复述你在学习React之前需要掌握的所有JS想法和技术。

React是使用现代JavaScript特性构建的,这些特性大多是在ES2015中引入的。所以这基本上就是我们在这篇文章中要讨论的内容。为了帮助你加深学习,我将把每个方法和概念的不同链接联系起来。

让我们开始吧...

学习React之前你需要知道的JavaScript

JavaScript中的回调函数

回调函数是一个在另一个函数执行完毕后执行的函数。它通常作为一个输入提供给另一个函数。

回调函数的理解至关重要,因为它们被用于数组方法(如map()filter() ,等等)、setTimeout() 、事件监听器(如点击、滚动,等等),以及其他许多地方。

下面是一个 "点击 "事件监听器的例子,它有一个回调函数,每当按钮被点击的时候就会运行:

//HTML
<button class="btn">Click Me</button>

//JavaScript
const btn = document.querySelector('.btn');

btn.addEventListener('click', () => {
  let name = 'John doe';
  console.log(name.toUpperCase())
})

注意:回调函数可以是一个普通的函数,也可以是一个箭头函数。

JavaScript中的承诺

如前所述,回调函数是在原始函数执行完毕后执行的。现在你可能开始考虑把这么多回调函数堆叠在一起,因为你不希望某个特定的函数在父函数运行完毕或特定的时间过去之前运行。

例如,让我们尝试在每个人2秒后在控制台显示5个名字--也就是说,2秒后出现第一个名字,4秒后出现第二个,以此类推...

setTimeout(() => {
    console.log("Joel");
    setTimeout(() => {
        console.log("Victoria");
        setTimeout(() => {
            console.log("John");
            setTimeout(() => {
                console.log("Doe");
                setTimeout(() => {
                    console.log("Sarah");
                }, 2000);
            }, 2000);
        }, 2000);
    }, 2000);
}, 2000);

上面这个例子可以工作,但很难理解、调试,甚至很难添加错误处理。这就是所谓的"回调地狱"。 回调地狱是一个由复杂的嵌套回调编码引起的大问题。

使用承诺的主要原因是为了防止回调地狱。通过承诺,我们可以用同步的方式编写异步代码。

Gotcha:你可以通过TAPAS ADHIKARY这篇文章了解JavaScript中同步和异步的含义。

承诺是一个对象,它返回一个你预期在未来会看到但现在看不到的值。

诺言的一个实际用途是在HTTP请求中,你提交了一个请求,但没有立即收到响应,因为这是一个异步活动。你只有在服务器响应时才能收到答案(数据或错误)。

JavaScript承诺的语法:

const myPromise = new Promise((resolve, reject) => {  
    // condition
});

承诺有两个参数,一个代表成功(resolve),一个代表失败(reject)。每个参数都有一个条件,必须满足这个条件才能使Promise得到解决--否则,它将被拒绝:

const promise = new Promise((resolve, reject) => {  
    let condition;
    
    if(condition is met) {    
        resolve('Promise is resolved successfully.');  
    } else {    
        reject('Promise is rejected');  
    }
});

Promise对象有3种状态:

  • Pending:默认情况下,这是初始状态,在Promise成功或失败之前。
  • 已解决:已完成的承诺
  • 拒绝的:失败的承诺

最后,让我们试着把回调地狱重新实现为一个承诺:

function addName (time, name){
  return new Promise ((resolve, reject) => {
    if(name){
      setTimeout(()=>{
        console.log(name)
        resolve();
      },time)
    }else{
      reject('No such name');
    }
  })
}

addName(2000, 'Joel')
  .then(()=>addName(2000, 'Victoria'))
  .then(()=>addName(2000, 'John'))
  .then(()=>addName(2000, 'Doe'))
  .then(()=>addName(2000, 'Sarah'))
  .catch((err)=>console.log(err))

你可以通过Cem Eygi这篇文章来更好地理解承诺。

JavaScript中的Map()

最常用的方法之一是Array.map() ,它允许你使用一个回调函数来迭代一个数组并修改其元素。回调函数将在每个数组元素上运行。

假设我们有一个包含用户信息的数组:

let users = [
  { firstName: "Susan", lastName: "Steward", age: 14, hobby: "Singing" },
  { firstName: "Daniel", lastName: "Longbottom", age: 16, hobby: "Football" },
  { firstName: "Jacob", lastName: "Black", age: 15, hobby: "Singing" }
];

我们可以使用map进行循环并修改它的输出

let singleUser = users.map((user)=>{
  //let's add the firstname and lastname together
  let fullName = user.firstName + ' ' + user.lastName;
  return `
    <h3 class='name'>${fullName}</h3>
    <p class="age">${user.age}</p>
  `
});

你应该注意到:

  • map() 总是返回一个新的数组,即使它是一个空数组。
  • 与过滤器方法相比,它不会改变原始数组的大小
  • 在制作一个新数组时,它总是利用你原来数组中的值。

小窍门:map方法的工作原理几乎和其他JavaScript迭代器一样,比如forEach() ,但是只要你要返回一个 值,就应该使用map方法。

image-83

这里是Simon Høiberg的一个完美描述

我们使用map的一个重要原因是,我们可以在一些HTML中封装我们的数据,而对于React来说,这只是使用JSX来完成。

你可以在这里阅读更多关于map()的信息。

JavaScript中的Filter()和Find()

Filter() 提供一个新的数组,取决于某些标准。与map()不同,它可以改变新数组的大小,而 ,只返回一个实例(这可能是一个对象或项目)。如果存在几个匹配项,它将返回第一个匹配项--否则,它将返回未定义项。find()

假设你有一个由不同年龄的注册用户组成的数组集合:

let users = [
  { firstName: "Susan", age: 14 },
  { firstName: "Daniel", age: 16 },
  { firstName: "Bruno", age: 56 },
  { firstName: "Jacob", age: 15 },
  { firstName: "Sam", age: 64 },
  { firstName: "Dave", age: 56 },
  { firstName: "Neils", age: 65 }
];

你可以选择按年龄组对这些数据进行排序,如年轻人(1-15岁)、老年人(50-70岁),等等......

在这种情况下,过滤器函数就派上用场了,因为它根据标准产生一个新的数组。让我们看一下它是如何工作的。

// for young people
const youngPeople = users.filter((person) => {
  return person.age <= 15;
});

//for senior people
const seniorPeople = users.filter((person) => person.age >= 50);

console.log(seniorPeople);
console.log(youngPeople); 

这产生了一个新的数组。如果条件不满足(不匹配),它会产生一个空数组。

你可以在这里阅读更多关于这个的信息。

查找()

find() 方法,像filter() 方法一样,在数组中迭代寻找符合指定条件的实例/项目。一旦找到它,它就会返回那个特定的数组项,并立即终止循环。如果没有发现匹配,该函数就会返回未定义。

比如说

const Bruno = users.find((person) => person.firstName === "Bruno");

console.log(Bruno);

你可以在这里阅读更多关于find()方法的信息。

JavaScript中的数组和对象的去结构化

解构是ES6中引入的一个JavaScript特性,它允许更快更简单地访问和解压数组和对象中的变量。

在引入解构之前,如果我们有一个水果数组,想分别得到第一个、第二个和第三个水果,我们会得到这样的结果:

let fruits= ["Mango", "Pineapple" , "Orange", "Lemon", "Apple"];

let fruit1 = fruits[0];
let fruit2 = fruits[1];
let fruit3 = fruits[2];

console.log(fruit1, fruit2, fruit3); //"Mango" "Pineapple" "Orange"

这就像是在重复同样的事情,可能会变得很麻烦。让我们来看看如何调整结构以获得前三个水果:

let [fruit1, fruit2, fruit3] = fruits;

console.log(fruit1, fruit2, fruit3); //"Mango" "Pineapple" "Orange"

你可能想知道,如果你只想打印第一个和最后一个水果,或者第二个和第四个水果,你如何跳过数据?你可以使用逗号,如下所示:

const [fruit1 ,,,, fruit5] = fruits;
const [,fruit2 ,, fruit4,] = fruits;

对象重构

现在让我们看看如何对一个对象进行去结构化--因为在React中,你会做大量的对象去结构化。

假设我们有一个用户的对象,其中包含了他们的名字,姓氏,以及更多:

const Susan = {
  firstName: "Susan",
  lastName: "Steward",
  age: 14,
  hobbies: {
    hobby1: "singing",
    hobby2: "dancing"
  }
};

在以前的方法中,获取这些数据可能是很紧张的,而且充满了重复:

const firstName = Susan.firstName;
const age = Susan.age;
const hobby1 = Susan.hobbies.hobby1;

console.log(firstName, age, hobby1); //"Susan" 14 "singing"

但有了结构化,就容易多了:

const {firstName, age, hobbies:{hobby1}} = Susan;

console.log(firstName, age, hobby1); //"Susan" 14 "singing"

我们也可以在一个函数中这样做:

function individualData({firstName, age, hobbies:{hobby1}}){
  console.log(firstName, age, hobby1); //"Susan" 14 "singing"
}
individualData(Susan);

你可以在这里阅读更多关于重构数组和对象的信息。

JavaScript中的休止符和扩展符

JavaScript的传播和休息操作符使用三个点... 。 休息操作符收集了一些项目--它把一些特定的用户提供的数值的 "休息 "放到一个JavaScript数组/对象中。

假设你有一个水果的数组:

let fruits= ["Mango", "Pineapple" , "Orange", "Lemon", "Apple"];

我们可以取消结构,得到第一个和第二个水果,然后通过使用rest操作符将 "其余 "的水果放入一个数组:

const [firstFruit, secondFruit, ...rest] = fruits

console.log(firstFruit, secondFruit, rest); //"Mango" "Pineapple" ["Orange","Lemon","Apple"]

看一下结果,你会看到前两个项目,然后第三个项目是一个数组,由我们没有去结构化的剩余水果组成。现在我们可以对新生成的数组进行任何类型的处理,例如。

const chosenFruit = rest.find((fruit) => fruit === "Apple");

console.log(`This is an ${chosenFruit}`); //"This is an Apple"

重要的是要记住,这一点必须始终放在最后(位置是非常重要的)。

我们刚刚处理了数组--现在让我们来处理对象,它们是完全一样的。

假设我们有一个用户对象,其中有他们的名字、姓氏和很多其他信息。我们可以对它进行解构,然后提取剩余的数据:

const Susan = {
  firstName: "Susan",
  lastName: "Steward",
  age: 14,
  hobbies: {
    hobby1: "singing",
    hobby2: "dancing"
  }
};

const {age, ...rest} = Susan;
console.log(age, rest);

这将记录以下结果:

14
{
firstName: "Susan" ,
lastName: "Steward" ,
hobbies: {...}
}

现在让我们了解一下传播操作符的工作原理,最后通过区分这两个操作符来进行总结。

传播操作符

展开运算符,顾名思义,是用来展开数组项目的。它使我们有能力从一个数组中获得一个参数列表。传播操作符的语法与其余操作符相似,只是它的操作方向相反。

注意: 扩散操作符只有在数组字面、函数调用或初始化属性对象中使用时才有效。

例如,假设你有不同类型的动物的数组:

let pets= ["cat", "dog" , "rabbits"];

let carnivorous = ["lion", "wolf", "leopard", "tiger"];

你可能想把这两个数组合并成一个动物数组。让我们来试一试:

let animals = [pets, carnivorous];

console.log(animals); //[["cat", "dog" , "rabbits"], ["lion", "wolf", "leopard", "tiger"]]

这不是我们想要的--我们希望所有的项目都只在一个数组中。而我们可以用spread操作符来实现这个目的:

let animals = [...pets, ...carnivorous];

console.log(animals); //["cat", "dog" , "rabbits", "lion", "wolf", "leopard", "tiger"]

这也适用于对象。值得注意的是,传播操作符不能扩展对象字面的值,因为属性对象不是一个可迭代的对象。但是我们可以用它来把一个对象的属性克隆到另一个对象中。

比如说:

let name = {firstName:"John", lastName:"Doe"};
let hobbies = { hobby1: "singing", hobby2: "dancing" }
let myInfo = {...name, ...hobbies};

console.log(myInfo); //{firstName:"John", lastName:"Doe", hobby1: "singing", hobby2: "dancing"}

你可以在这里阅读更多关于JavaScript Spread 和 Rest 操作符的信息。

唯一值--JavaScript中的Set()

最近,我试图为一个应用程序创建一个分类标签,我需要从一个数组中获取分类的值:

let animals = [
  {
    name:'Lion',
    category: 'carnivore'
  },
  {
    name:'dog',
    category:'pet'
  },
  {
    name:'cat',
    category:'pet'
  },
  {
    name:'wolf',
    category:'carnivore'
  }
]

第一件事是在数组中循环,但我得到了重复的值:

let category = animals.map((animal)=>animal.category);
console.log(category); //["carnivore" , "pet" , "pet" , "carnivore"]

这意味着我需要设置一个条件来避免重复。这有点棘手,直到我遇到了ES6提供的set() 构造函数/对象:)。

集合是一个项目的集合,它是唯一的,也就是说没有元素可以重复。让我们来看看我们如何能够轻松地实现这一点:

//wrap your iteration in the set method like this
let category = [...new Set(animals.map((animal)=>animal.category))];

console.log(category); ////["carnivore" , "pet"]

注意:我决定将这些值分散到一个数组中。你可以在这里阅读更多关于唯一值的内容。

JavaScript中的动态对象键

这使我们能够使用方括号符号来添加对象键。这可能对你现在没有意义,但当你继续学习React或开始与团队合作时,你可能会遇到它。

在JavaScript中,我们知道对象通常是由属性/键和值组成的,我们可以使用点符号来添加、编辑或访问一些值。作为一个例子:

let lion = {
  category: "carnivore"
};

console.log(lion); // { category: "carnivore" }
lion.baby = 'cub';
console.log(lion.category); // carnivore
console.log(lion); // { category: "carnivore" , baby: "cub" }

我们还可以选择使用方括号符号,当我们需要动态对象的键时,就可以利用方括号符号。

**我们所说的动态对象键是什么意思?**这些键可能不遵循对象中的属性/键的标准命名惯例。标准的命名惯例只允许CamelCase和snake_case,但通过使用方括号符号,我们可以解决这个问题。

例如,假设我们在命名我们的键时,在单词之间加了一个破折号,例如(lion-baby):

let lion = {
  'lion-baby' : "cub"
};

// dot notation
console.log(lion.lion-baby); // error: ReferenceError: baby is not defined
// bracket notation
console.log(lion['lion-baby']); // "cub"

你可以看到点符号和方括号符号的区别。让我们来看看其他的例子:

let category = 'carnivore';
let lion = {
  'lion-baby' : "cub",
  [category] : true,
};

console.log(lion); // { lion-baby: "cub" , carnivore: true }

你还可以通过在方括号内使用条件来进行更复杂的操作,比如说:

const number = 5;
const gavebirth = true;

let animal = {
  name: 'lion',
  age: 6,
  [gavebirth && 'babies']: number
};

console.log(animal); // { name: "lion" , age: 6 , babies: 5 }

你可以在这里阅读更多关于这个的内容。

在JavaScript中的reduce()

这可以说是最强大的数组函数。它可以取代filter()find() 方法,在对大量数据进行map()filter() 方法时也相当方便。

当你把map和filter方法连在一起时,你就会做两次工作--先过滤每一个值,然后再映射剩余的值。另一方面,reduce() ,允许你在一个单一的过程中过滤和映射。这种方法很强大,但它也更复杂和棘手。

我们对我们的数组进行迭代,然后获得一个回调函数,这与map(),filter(),find(), 和其他的函数类似。主要的区别是,它把我们的数组减少到一个单一的值,这个值可能是一个数字、数组或对象。

关于reduce()方法,需要记住的另一件事是,我们要传入两个参数,从你开始阅读本教程开始,就不是这样了。

第一个参数是所有计算的总和/总数,第二个参数是当前的迭代值(你很快就会明白)。

例如,假设我们有一个员工的工资列表:

let staffs = [
  { name: "Susan", age: 14, salary: 100 },
  { name: "Daniel", age: 16, salary: 120 },
  { name: "Bruno", age: 56, salary: 400 },
  { name: "Jacob", age: 15, salary: 110 },
  { name: "Sam", age: 64, salary: 500 },
  { name: "Dave", age: 56, salary: 380 },
  { name: "Neils", age: 65, salary: 540 }
];

而我们想为所有员工计算10%的什一税。我们可以很容易地用减少方法做到这一点,但在这之前,让我们做一些更简单的事情:让我们先计算总工资:

const totalSalary = staffs.reduce((total, staff) => {
  total += staff.salary;
  return total;
},0)
console.log(totalSalary); // 2150

注意:我们传递的第二个参数是总数,它可以是任何东西--例如一个数字或对象。

现在让我们计算一下所有员工的10%的什一税,然后得到总数。我们可以直接从总数中得到10%,或者先从每个人的工资中得到,再把它们加起来:

const salaryInfo = staffs.reduce(
  (total, staff) => {
    let staffTithe = staff.salary * 0.1;
    total.totalTithe += staffTithe;
    total['totalSalary'] += staff.salary;
    return total;
  },
  { totalSalary: 0, totalTithe: 0 }
);

console.log(salaryInfo); // { totalSalary: 2150 , totalTithe: 215 }

**陷阱:**我们使用了一个对象作为第二个参数,而且我们还使用了动态的对象键。你可以在这里阅读更多关于减少方法的信息。

JavaScript中的可选链

可选链是在JavaScript中访问嵌套对象属性的一种安全方式,而不是在访问一长串对象属性时必须进行多次空值检查。它是ES2020中引入的一个新特性。

举例来说:

let users = [
{
    name: "Sam",
    age: 64,
    hobby: "cooking",
    hobbies: {
      hobb1: "cooking",
      hobby2: "sleeping"
    }
  },
  { name: "Bruno", age: 56 },
  { name: "Dave", age: 56, hobby: "Football" },
  {
    name: "Jacob",
    age: 65,
    hobbies: {
      hobb1: "driving",
      hobby2: "sleeping"
    }
  }
];

假设你想从上面的数组中获取爱好,让我们来试试:

users.forEach((user) => {
  console.log(user.hobbies.hobby2);
});

当你看控制台时,你会发现第一次迭代完成了,但第二次迭代却没有爱好。所以它不得不抛出一个错误并脱离迭代--这意味着它不能从数组中的其他对象获取数据。

输出

"sleeping"
error: Uncaught TypeError: user.hobbies is undefined

这个错误可以用可选链来解决,尽管有几种方法可以解决这个问题(例如,使用条件)。让我们看看我们如何用条件和可选链来做这件事。

条件渲染法

users.forEach((user) => {
  console.log(user.hobbies && user.hobbies.hobby2);
});

可选链

users.forEach((user) => {
  console.log(user ?.hobbies ?.hobby2);
});

输出

"sleeping"
undefined
undefined
"sleeping"

这对你来说现在可能没有什么意义,但到你将来做更大的事情时,它就会落到实处了你可以在这里阅读更多信息。

JavaScript中的Fetch API和错误

fetch API,顾名思义,是用来从API获取数据的。它是一个浏览器API,允许你使用JavaScript来进行基本的AJAX(异步JavaScript和XML)请求。

因为它是由浏览器提供的,所以你可以使用它而不需要安装或导入任何包或依赖关系(如axios)。它的配置相当简单易懂。fetch API默认提供了一个承诺(我在本文前面讲过承诺)。

让我们看看如何通过fetch API来获取数据。我们将使用一个免费的API,其中包含成千上万的随机报价。

fetch("https://type.fit/api/quotes")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

我们在这里所做的是:

  • 1行:我们从API获取数据,它返回一个承诺
  • 第2行:然后我们得到数据的.json() 格式,这也是一个承诺
  • 第3行:我们得到了我们的数据,现在返回JSON
  • 第4行:我们得到了错误,如果有的话

我们将在下一节中看到如何用async/await来完成这个任务。你可以在这里阅读更多关于fetch API的信息。

如何处理Fetch API中的错误

现在让我们来看看我们如何处理来自fetch API的错误而不需要依赖catch关键字。fetch() 函数会对网络错误自动抛出错误,但对HTTP错误,如400至5xx响应,则不会。

好消息是,fetch 提供了一个简单的response.ok 标志,表明请求是否失败或HTTP响应的状态码是否在成功范围内。

这一点实现起来非常简单:

fetch("https://type.fit/api/quotes")
  .then((response) => {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response.json();
  })
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

你可以在这里阅读更多关于Fetch API错误的信息。

JavaScript中的异步/等待

Async/Await允许我们以同步的方式编写异步代码。这意味着你不需要继续嵌套回调。

一个异步函数 总是 返回一个承诺。

你可能会绞尽脑汁想知道同步和异步之间的区别意味着什么。简单地说,同步意味着工作是一个接一个地完成的。异步的意思是,任务是独立完成的。

请注意,我们总是在函数前面加上async,而且只有在有async时才能使用await。你很快就会明白的!

现在让我们用async/await来实现我们之前研究的Fetch API代码:

const fetchData = async () =>{
  const quotes = await fetch("https://type.fit/api/quotes");
  const response = await quotes.json();
  console.log(response);
}

fetchData();

这就更容易阅读了,对吗?

你可能想知道我们如何用async/await来处理错误。是的!你使用 try 和 catch 关键字:

const fetchData = async () => {
  try {
    const quotes = await fetch("https://type.fit/api/quotes");
    const response = await quotes.json();
    console.log(response);
  } catch (error) {
    console.log(error);
  }
};

fetchData();

你可以在这里阅读更多关于async/await的信息。

总结

在这篇文章中,我们已经学习了超过10个JavaScript方法和概念,每个人在学习React之前都应该彻底了解。

还有很多其他的方法和概念你应该知道,但这些是你在学习JavaScript时可能没有真正注意到的。在你学习React之前,这些是很重要的。

假设你刚刚开始学习JavaScript--我在这里策划了一个很棒的资源清单,可以帮助你学习JavaScript概念和主题。不要忘了加星和分享!:).