这些天,React是最受欢迎的JavaScript库之一。它可以用来创建动态和响应式的应用程序,允许更好的性能,并且可以很容易地扩展。底层逻辑是基于可以在不同情况下重复使用的组件,减少了多次编写相同代码的需要。简而言之,使用React你可以创建高效和强大的应用程序。
因此,现在是学习如何创建React应用程序的最佳时机。
然而,如果没有对一些关键的JavaScript功能的扎实了解,构建React应用程序可能是困难的,甚至是不可能的。
出于这个原因,我们汇编了一份在开始使用React之前需要了解的JavaScript特性和概念清单。你越了解这些概念,你就越容易构建专业的React应用程序。
既然如此,下面是我们将在本文中讨论的内容:
- JavaScript和ECMAScript
- 语句与表达式
- React中的不可变性(Immutability)
- 模板字符串
- 箭头函数
- 类
- 关键词this
- 三元运算符
- 短路求值(最小化求值)
- 展开语法
- 解构赋值
- filter(), map(), and reduce()
- 导出和导入
JavaScript和ECMAScript
JavaScript是一种流行的脚本语言,与HTML和CSS一起用于建立动态网页。HTML用于创建网页的结构,CSS用于创建其元素的样式和布局,而JavaScript则是用于向网页添加行为的语言,即创建功能和互动性。
✍️ JavaScript是由Netscape Communications的Brendan Eich在1995年开发的,旨在为Netscape Navigator浏览器的网页增加互动性。
此后,该语言被各大浏览器采用,并编写了一份文件来描述JavaScript的工作方式:ECMAScript标准。
从2015年起,ECMAScript标准的更新每年都会发布,因此每年都会有新的功能加入到JavaScript中。
ECMAScript 2015是该标准的第六个版本,因此也被称为ES6。之后的版本都是按进度标注的,所以我们把ECMAScript 2016称为ES7,ECMAScript 2017称为ES8,以此类推。
由于标准中添加新功能的频率,有些功能可能不被所有浏览器支持。那么,你怎样才能确保你添加到JS应用中的最新JavaScript功能在所有的网络浏览器中都能如期工作呢?
你有三个选择:
- 等到所有主要的浏览器都提供对新功能的支持。但如果你的应用程序绝对需要那个令人惊奇的新JS功能,这不是一个选择。
- 使用Polyfill,它是 “一段代码(通常是网络上的JavaScript),用于在不支持现代功能的旧浏览器上提供现代功能”(另见MDN网络文档)。
- 使用JavaScript转码器,如Babel或Traceur,将ECMAScript 2015+代码转换成所有浏览器都支持的JavaScript版本。
语句与表达式
在构建React应用程序时,理解语句和表达式之间的区别是至关重要的。因此,让我们暂时回到编程的基本概念。
一个计算机程序是一个由计算机执行的指令列表。这些指令被称为语句。
与语句不同,表达式是产生一个值的代码片段。在语句中,表达式是返回一个值的部分,我们通常在等号的右边看到它。
✍️ 语句是一个做某事的代码块。
而:
✍️ 一个表达式是产生一个值的代码片段。
JavaScript语句可以是代码块或代码行,通常以分号结尾或用大括号括起来。
下面是一个JavaScript语句的简单例子:
document.getElementById("hello").innerHTML = "Hello World!";
上面的语句在一个 id="hello"
的DOM元素中写下了 "Hello World!"
。
正如我们已经提到的,expessions产生一个值或者本身就是一个值。请看下面的例子:
msg = document.getElementById("hello").value;
document.getElementById("hello").value
是一个表达式,因为它返回一个值。
一个额外的例子应该有助于澄清表达式和语句之间的区别:
const msg = "Hello World!"; function sayHello( msg ) { console.log( msg ); }
在上面的例子中
- 第一行是一个语句,其中
"Hello World!"
是一个表达式、 - 函数声明是一个语句,其中传递给函数的参数
msg
是一个表达式、 - 在控制台中打印消息的那一行是一个语句,其中参数
msg
也是一个表达式。
为什么表达式在React中很重要
在构建React应用程序时,你可以在JSX代码中注入JavaScript表达式。例如,你可以传递一个变量,写一个事件处理器或一个条件。要做到这一点,你需要在大括号中包含你的JS代码。
例如,你可以传递一个变量:
const Message = () => { const name = "Carlo"; return <p>Welcome {name}!</p>; }
简而言之,大括号告诉你的转码器将大括号中的代码作为JS代码来处理。在开头 <p>
标签之前和结尾 </p>
标签之后的所有内容都是正常的JavaScript代码。开头 <p>
和结尾 </p>
标签内的所有内容都被当作JSX代码处理。
下面是另一个例子:
const Message = () => { const name = "Ann"; const heading = <h3>Welcome {name}</h3>; return ( <div> {heading} <p>This is your dashboard.</p> </div> ); }
你也可以传递一个对象:
render(){ const person = { name: 'Carlo', avatar: 'https://en.gravatar.com/userimage/954861/fc68a728946aac04f8531c3a8742ac22', description: 'Content Writer' } return ( <div> <h2>Welcome {person.name}</h2> <img className="card" src={person.avatar} alt={person.name} /> <p>Description: {person.description}.</p> </div> ); }
而下面是一个更全面的例子:
render(){ const person = { name: 'Carlo', avatar: 'https://en.gravatar.com/userimage/954861/fc68a728946aac04f8531c3a8742ac22?size=original', description: 'Content Writer', theme: { boxShadow: '0 4px 8px 0 rgba(0,0,0,0.2)', width: '200px' } } return ( <div style={person.theme}> <img src={person.avatar} alt={person.name} style={ { width: '100%' } } /> <div style={ { padding: '2px 16px' } }> <h3>{person.name}</h3> <p>{person.description}.</p> </div> </div> ); }
注意 img
和 div
这两个元素的 style
属性中的双大括号。我们用双括号来传递两个包含卡片和图像样式的对象。
一个用React构建的卡片实例
你应该注意到,在上面的所有例子中,我们在JSX中包含了JavaScript表达式。
✍️ JSX只接受大括号中的JavaScript表达式。你不允许在你的JSX代码中写语句。
这包括:
- 变量
- 带引号的字符串
- 函数调用
- 对象
- 条件性表达式
React中的不可变性(Immutability)
可变性和不变性是面向对象和函数式编程的两个关键概念。
不可变性是指一个值在被创建后不能被改变。当然,可变性的意思正好相反。
在Javascript中,基元值是不可变的,这意味着一旦一个基元值被创建,它就不能被改变。相反,数组和对象是可变的,因为它们的属性和元素可以被改变,而不需要重新分配一个新值。
在JavaScript中使用不可变的对象有几个原因:
- 提高性能
- 减少了内存消耗
- 线程安全
- 更容易编码和调试
遵循不可变性的模式,一旦一个变量或对象被分配,它就不能被重新分配或改变。当你需要修改数据时,你应该创建它的副本并修改其内容,而不改变原来的内容。
不变性(Immutability)也是React的一个关键概念。
React文档指出:
类组件的状态可以用 this.state
来表示。状态字段必须是一个对象。请不要直接改变状态。如果你想改变状态,用新的状态调用 setState
。
每当一个组件的状态改变时,React会计算是否重新渲染该组件并更新虚拟DOM。如果React没有跟踪之前的状态,它就不能确定是否重新渲染组件。React文档对此提供了一个很好的例子。
我们可以使用哪些JavaScript特性来保证React中状态对象的不变性?
声明变量
在JavaScript中,你有三种方法来声明变量: var
、 let
和 const
。
var
语句从JavaScript的开始就存在。它被用来声明一个函数范围或全局范围的变量,可以选择将其初始化为一个值。
当你使用 var
声明一个变量时,你可以在全局和局部范围内重新声明和更新该变量。下面的代码是允许的:
// Declare a variable var msg = "Hello!"; // Redeclare the same variable var msg = "Goodbye!" // Update the variable msg = "Hello again!"
var
的声明在任何代码执行之前都会被处理。因此,在代码中的任何地方声明一个变量都等同于在顶部声明它。这种行为被称为变量提升(hoisting)。
值得注意的是,只有变量声明被提升,而不是初始化,初始化只发生在控制流到达赋值语句的时候。在那之前,该变量是 undefined
的:
console.log(msg); // undefined var msg = "Hello!"; console.log(msg); // Hello!
在JS函数中声明的 var
的范围是该函数的整个主体。
这意味着该变量不是在块级定义的,而是在整个函数级定义的。这导致了一些问题,可能会使你的JavaScript代码出现错误并难以维护。
为了解决这些问题,ES6引入了 let
关键字。
let
declaration声明了一个block-scoped的局部变量,可以选择将其初始化为一个值。
与 var
相比, let
的优点是什么?这里有一些:
let
声明一个变量到一个块语句的范围内,而var
声明一个变量到整个函数的全局或局部,与块范围无关。- 全局
let
变量不是window
对象的属性。你不能用window.variableName
访问它们。 -
let
只能在其声明到达后被访问。在控制流到达它被声明的那行代码之前,该变量不会被初始化(let声明是non-hoisted)。 - 用
let
重新声明一个变量会产生一个SyntaxError
。
由于使用 var
声明的变量不能被block-scoped,如果你在一个循环中或 if
语句中使用 var
定义一个变量,它可以从block之外被访问,这可能导致代码的bug。
第一个例子中的代码执行起来没有错误。现在在上面看到的代码块中用 let
代替 var
:
console.log(msg); let msg = "Hello!"; console.log(msg);
在第二个例子中,使用 let
而不是 var
产生了一个 Uncaught ReferenceError
:
Chrome浏览器中未捕获的引用错误
✍️ 因此,作为一般规则,你应该总是使用 let
而不是 var
。
ES6还引入了第三个关键字: const
。
const
与 let
非常相似,但有一个关键区别:
✍️ 用 const
声明的变量不能被赋值,除非是在它们被声明的地方。
请看下面的例子:
const MAX_VALUE = 1000; MAX_VALUE = 2000;
上述代码将产生以下类型错误:
谷歌浏览器中的Uncaught TypeError: Assignment to constant variable
此外:
✍️ 你不能声明一个 const
而不给它一个值。
声明一个 const
而不给它一个值,会产生以下 SyntaxError
(参见ES6 in Depth:let和const):
谷歌浏览器中的Uncaught SyntaxError: Missing initializer in const declaration
✍️ 未发现的语法错误: 在Chrome中常量声明中缺少初始化器
但是如果一个常量是一个数组或一个对象,你就可以编辑该数组或对象中的属性或项目。
例如,你可以改变、增加和删除数组项目:
// Declare a constant array const cities = ["London", "New York", "Sydney"]; // Change an item cities[0] = "Madrid"; // Add an item cities.push("Paris"); // Remove an item cities.pop(); console.log(cities); // Array(3) // 0: "Madrid" // 1: "New York" // 2: "Sydney"
但你不允许重新分配数组:
const cities = ["London", "New York", "Sydney"]; cities = ["Athens", "Barcelona", "Naples"];
上面的代码会导致一个TypeError。
谷歌浏览器中的Uncaught TypeError: Assignment to constant variable
你可以添加、重新分配和删除对象的属性和方法:
// Declare a constant obj const post = { id: 1, name: 'JavaScript is awesome', excerpt: 'JavaScript is an awesome scripting language', content: 'JavaScript is a scripting language that enables you to create dynamically updating content.' }; // add a new property post.slug = "javascript-is-awesome"; // Reassign property post.id = 5; // Delete a property delete post.excerpt; console.log(post); // {id: 5, name: 'JavaScript is awesome', content: 'JavaScript is a scripting language that enables you to create dynamically updating content.', slug: 'javascript-is-awesome'}
但你不允许重新分配对象本身。下面的代码会经历一个 Uncaught TypeError
:
// Declare a constant obj const post = { id: 1, name: 'JavaScript is awesome', excerpt: 'JavaScript is an awesome scripting language' }; post = { id: 1, name: 'React is powerful', excerpt: 'React lets you build user interfaces' };
✍️ 在React中,用 const
声明变量是默认的。 当 const
不合适时,应该使用 let
。非常不鼓励使用 var
。
Object.freeze()
我们现在同意,使用 const
并不总是保证强大的不变性(尤其是在处理对象和数组时)。那么,你如何在你的React应用程序中实现不变性模式呢?
首先,当你想防止数组的元素或对象的属性被修改时,你可以使用静态方法 Object.freeze()
。
冻结一个对象可以防止扩展,并使现有的属性不可写入和不可配置。一个被冻结的对象不能再被改变:新的属性不能被添加,现有的属性不能被删除,它们的可枚举性、可配置性、可写性或值不能被改变,而且对象的原型不能被重新分配。freeze()
返回与传入的对象相同。
任何试图添加、改变或删除一个属性的行为都会失败,要么是无声无息,要么是抛出一个 TypeError
,最常见的是在严格模式下。
你可以这样使用 Object.freeze()
:
'use strict' // Declare a constant obj const post = { id: 1, name: 'JavaScript is awesome', excerpt: 'JavaScript is an awesome scripting language' }; // Freeze the object Object.freeze(post);
如果你现在尝试添加一个属性,你会收到一个 Uncaught TypeError
:
// Add a new property post.slug = "javascript-is-awesome"; // Uncaught TypeError
火狐浏览器中的Uncaught TypeError: can’t define property “slug” : Object is not extensible
当你试图重新分配一个属性时,你会得到另一种 TypeError
:
// Reassign property post.id = 5; // Uncaught TypeError
重新分配一个只读属性会产生一个未发现的类型错误
谷歌浏览器中的Uncaught TypeError: Cannot assign to read only property ‘id’ of object ‘#<Object>’
你也可以尝试删除一个属性。其结果将是另一个 TypeError
:
// Delete a property delete post.excerpt; // Uncaught TypeError
火狐浏览器中的Uncaught TypeError: property “excerpt” is non-configurable and can’t be deleted
模板字符串
当你需要在JavaScript中把字符串和表达式的输出结合起来时,你通常使用加法运算符 +
。然而,你也可以使用一个JavaScript特性,它允许你在字符串中包含表达式而不使用加法运算符:模板字符串。
模板字符串是一种特殊的字符串,以回车键( `
)字符为界。
在模板文字中,你可以包括占位符,它是由美元字符限定的嵌入式表达式,用大括号包裹。
下面是一个例子:
const align = 'left'; console.log(`This string is ${ align }-aligned`);
字符串和占位符被传递给一个默认函数,该函数执行字符串插值,以替代占位符,并将各部分连接成一个字符串。你也可以用一个自定义函数来替换默认函数。
你可以在以下情况下使用模板字符串:
多行字符串:换行符是模板字符串的一部分。
console.log(`Twinkle, twinkle, little bat! How I wonder what you’re at!`);
字符串插值:如果没有模板字符串,你只能使用加法运算符将表达式的输出与字符串相结合。请看下面的例子:
const a = 3; const b = 7; console.log("The result of " + a + " + " + b + " is " + (a + b));
这有点令人困惑,不是吗?但你可以用模板字符串的方式来写这段代码,使其更易读和可维护:
const a = 3; const b = 7; console.log(`The result of ${ a } + ${ b } is ${ a + b }`);
但请记住,这两种语法之间是有区别的:
✍️ 模板字符串直接将其表达式胁迫为字符串,而加法则先将其操作数胁迫为基数。
模板字符串可以有多种用途。在下面的例子中,我们使用一个三元运算符来给一个 class
属性赋值。
const page = 'archive'; console.log(`class=${ page === 'archive' ? 'archive' : 'single' }`);
下面,我们进行一个简单的计算:
const price = 100; const VAT = 0.22; console.log(`Total price: ${ (price * (1 + VAT)).toFixed(2) }`);
也可以通过在 ${expression}
占位符中包含模板字样来嵌套模板字样(但要谨慎使用嵌套模板,因为复杂的字符串结构可能难以阅读和维护)。
标签模板:正如我们上面提到的,也可以定义一个自定义函数来执行字符串连接。这种模板字面意义被称为标签模板。
标签允许你用一个函数来解析模板字面。标签函数的第一个参数包含一个字符串值的数组。其余的参数与表达式有关。
标签允许你用一个自定义函数来解析模板字面。这个函数的第一个参数是一个包含在模板字面意义中的字符串数组,其他参数是表达式。
你可以创建一个自定义函数,对模板参数进行任何形式的操作,并返回所操作的字符串。下面是一个非常基本的标签模板的例子:
const name = "Carlo"; const role = "student"; const organization = "North Pole University"; const age = 25; function customFunc(strings, ...tags) { console.log(strings); // ['My name is ', ", I'm ", ', and I am ', ' at ', '', raw: Array(5)] console.log(tags); // ['Carlo', 25, 'student', 'North Pole University'] let string = ''; for ( let i = 0; i < strings.length - 1; i++ ){ console.log(i + "" + strings[i] + "" + tags[i]); string += strings[i] + tags[i]; } return string.toUpperCase(); } const output = customFunc`My name is ${name}, I'm ${age}, and I am ${role} at ${organization}`; console.log(output);
上面的代码打印了 strings
和 tags
数组元素,然后在浏览器控制台中打印输出之前将字符串字符大写。
箭头函数
箭头函数是JavaScript中匿名函数(没有名字的函数)的替代品,但有一些区别和限制。
下面的声明都是有效的箭头函数例子:
// Arrow function without parameters const myFunction = () => expression; // Arrow function with one parameter const myFunction = param => expression; // Arrow function with one parameter const myFunction = (param) => expression; // Arrow function with more parameters const myFunction = (param1, param2) => expression; // Arrow function without parameters const myFunction = () => { statements } // Arrow function with one parameter const myFunction = param => { statements } // Arrow function with more parameters const myFunction = (param1, param2) => { statements }
如果你只向函数传递一个参数,你可以省略圆括号。如果你传递两个或更多的参数,你必须用圆括号把它们括起来。下面是一个例子:
const render = ( id, title, category ) => `${id}: ${title} - ${category}`; console.log( render ( 5, 'Hello World!', "JavaScript" ) );
单行箭头函数默认会返回一个值。如果你使用多行语法,你将不得不手动返回一个值:
const render = ( id, title, category ) => { console.log( `Post title: ${ title }` ); return `${ id }: ${ title } - ${ category }`; } console.log( `Post details: ${ render ( 5, 'Hello World!', "JavaScript" ) }` );
✍️ 你通常会在React应用程序中使用Arrow Function,除非有特殊原因不使用它们。
要记住普通函数和Arrow函数的一个关键区别是,Arrow函数没有自己的关键字 this
的绑定。如果你试图在Arrow函数中使用 this
,它将超出函数范围。
关于Arrow函数的更深入的描述和使用实例,请阅读mdn web docs。
类
JavaScript中的类是一种特殊类型的函数,用于创建使用原型继承机制的对象。
根据mdn web docs:
说到继承,JavaScript只有一种结构:对象。每个对象都有一个私有属性,它持有一个指向另一个对象的链接,称为它的原型。该原型对象有一个自己的原型,以此类推,直到达到一个以 null
为原型的对象。
和函数一样,你有两种方法来定义一个类:
- 一个类的表达式
- 一个类的声明
你可以使用 class
关键字在表达式中定义一个类,如下面的例子中所示:
const Circle = class { constructor(radius) { this.radius = Number(radius); } area() { return Math.PI * Math.pow(this.radius, 2); } circumference() { return Math.PI * this.radius * 2; } } console.log('Circumference: ' + new Circle(10).circumference()); // 62.83185307179586 console.log('Area: ' + new Circle(10).area()); // 314.1592653589793
一个类有一个主体,就是包含在大括号中的代码。在这里你将定义构造函数和方法,它们也被称为类成员。即使不使用 'strict mode'
指令,类的主体也会以严格模式执行。
constructor
方法用于创建和初始化用类创建的对象,在类被实例化时自动执行。如果你没有在你的类中定义一个构造方法,JavaScript会自动使用一个默认的构造方法。
一个类可以用 extends
关键字来扩展。
class Book { constructor(title, author) { this.booktitle = title; this.authorname = author; } present() { return this.booktitle + ' is a great book from ' + this.authorname; } } class BookDetails extends Book { constructor(title, author, cat) { super(title, author); this.category = cat; } show() { return this.present() + ', it is a ' + this.category + ' book'; } } const bookInfo = new BookDetails("The Fellowship of the Ring", "J. R. R. Tolkien", "Fantasy"); console.log(bookInfo.show());
一个构造函数可以使用 super
关键字来调用父构造函数。如果你向 super()
方法传递了一个参数,这个参数也会在父级构造函数类中出现。
关于对JavaScript类的深入研究和几个使用实例,也可以参见mdn web docs。
类经常被用来创建React组件。通常情况下,你不会创建你自己的类,而是扩展内置的React类。
React中的所有类都有一个 render()
方法,可以返回一个React元素:
class Animal extends React.Component { render() { return <h2>Hey, I am a {this.props.name}!</h2>; } }
在上面的例子中, Animal
是一个类组件。请记住
- 组件的名称必须以大写字母开头
- 该组件必须包括表达式
extends React.Component
。这样就可以访问React.Component
的方法。 -
render()
方法返回HTML,这是必须的。
一旦你创建了你的类组件,你就可以在页面上渲染HTML了:
const root = ReactDOM.createRoot(document.getElementById('root')); const element = <Animal name="Rabbit" />; root.render(element);
下面的图片显示了页面上的结果(你可以在CodePen上看到它的操作)。
一个简单的React类组件
但是请注意,不建议在React中使用类组件,最好是将组件定义为函数。
关键词this
在JavaScript中, this
关键字是一个通用的占位符,通常在对象、类和函数中使用,它根据上下文或范围指代不同的元素。
this
可以在全局范围内使用。如果你在浏览器的控制台中挖出 this
,你会得到:
Window {window: Window, self: Window, document: document, name: '', location: Location, ...}
你可以访问 Window
对象的任何方法和属性。因此,如果你在浏览器的控制台中运行 this.location
,你会得到以下输出:
Location {ancestorOrigins: DOMStringList, href: 'https://www.wbolt.com/', origin: 'https://www.wbolt.com', protocol: 'https:', host: 'www.wbolt.com', ...}
当你在一个对象中使用 this
时,它指的是对象本身。通过这种方式,你可以在对象本身的方法中引用对象的值:
const post = { id: 5, getSlug: function(){ return `post-${this.id}`; }, title: 'Awesome post', category: 'JavaScript' }; console.log( post.getSlug );
现在让我们试着在一个函数中使用 this
:
const useThis = function () { return this; } console.log( useThis() );
如果你不是在严格的模式下,你会得到:
Window {window: Window, self: Window, document: document, name: '', location: Location, ...}
但如果你调用严格模式,你会得到一个不同的结果:
const doSomething = function () { 'use strict'; return this; } console.log( doSomething() );
在这种情况下,该函数返回 undefined
。这是因为在一个函数中 this
指的是它的显式值。
那么,如何在一个函数中显式地设置 this
呢?
首先,你可以手动分配属性和方法给函数:
function doSomething( post ) { this.id = post.id; this.title = post.title; console.log( `${this.id} - ${this.title}` ); } new doSomething( { id: 5, title: 'Awesome post' } );
但你也可以使用 call
, apply
, 和 bind
方法,以及箭头函数。
✍️ 一个函数的 call()
方法接受 this
所指的对象。
const doSomething = function() { console.log( `${this.id} - ${this.title}` ); } doSomething.call( { id: 5, title: 'Awesome post' } );
call()
方法可以在任何函数上使用,并且完全按照它所说的做:调用该函数。
此外, call()
还接受在函数中定义的任何其他参数:
const doSomething = function( cat ) { console.log( `${this.id} - ${this.title} - Category: ${cat}` ); } doSomething.call( { id: 5, title: 'Awesome post' }, 'JavaScript' );
✍️ apply()
方法接受一个对象, this
将被引用,并接受一个函数参数数组。
const doSomething = function( cat1, cat2 ) { console.log( `${this.id} - ${this.title} - Categories: ${cat1}, ${cat2}` ); } doSomething.apply( { id: 5, title: 'Awesome post' }, ['JavaScript', 'React'] );
✍️ bind()
方法将一个对象与一个函数联系起来,这样,每当你调用这个函数时,this
指代这个对象。
const post = { id: 5, title: 'Awesome post', category: 'JavaScript' }; const doSomething = function() { return `${this.id} - ${this.title} - ${this.category}`; } const bindRender = doSomething.bind( post ); console.log( bindRender() );
除了上面讨论的选项外,还有一种方法是使用箭头函数。
箭头函数表达式应该只用于非方法函数,因为它们没有自己的 this
。
这使得箭头函数对事件处理程序特别有用。
这是因为 “当代码从内联事件处理程序属性中调用时,其 this
被设置为监听器所在的DOM元素”(见cdn web docs)。
但箭头函数的情况就不同了,因为…
… 箭头函数是 this
根据箭头函数定义的范围来建立的,而 this
值不会因为函数的调用方式而改变。
✍️ 使用箭头函数允许你直接将上下文绑定到事件处理程序。
将 “this” 绑定到React的事件处理程序上
当涉及到React时,你有几种方法来确保事件处理程序不会丢失其上下文:
1. 在渲染方法中使用 bind()
:
import React, { Component } from 'react'; class MyComponent extends Component { state = { message: 'Hello World!' }; showMessage(){ console.log( 'This refers to: ', this ); console.log( 'The message is: ', this.state.message ); } render(){ return( <button onClick={ this.showMessage.bind( this ) }>Show message from state!</button> ); } } export default MyComponent;
2. 在构造函数中将上下文与事件处理程序绑定:
import React, { Component } from 'react'; class MyComponent extends Component { state = { message: 'Hello World!' }; constructor(props) { super(props); this.showMessage = this.showMessage.bind( this ); } showMessage(){ console.log( 'This refers to: ', this ); console.log( 'The message is: ', this.state.message ); } render(){ return( <button onClick={ this.showMessage }>Show message from state!</button> ); } } export default MyComponent;
3. 使用箭头函数定义事件处理程序:
import React, { Component } from 'react'; class MyComponent extends Component { state = { message: 'Hello World!' }; showMessage = () => { console.log( 'This refers to: ', this ); console.log( 'The message is: ', this.state.message ); } render(){ return( <button onClick={this.showMessage}>Show message from state!</button> ); } } export default MyComponent;
4. 在渲染方法中使用箭头函数:
import React, { Component } from 'react'; class MyComponent extends Component { state = { message: 'Hello World!' }; showMessage() { console.log( 'This refers to: ', this ); console.log( 'The message is: ', this.state.message ); } render(){ return( <button onClick={()=>{this.showMessage()}}>Show message from state!</button> ); } } export default MyComponent;
无论你选择哪种方法,当你点击按钮时,浏览器控制台显示以下输出:
This refers to: MyComponent {props: {…}, context: {…}, refs: {…}, updater: {…}, state: {…}, …} The message is: Hello World!
三元运算符
条件运算符(或三元运算符)允许你在JavaScript中编写简单的条件表达式。它需要三个操作数:
- 一个条件,后面跟着一个问号(
?
) - 如果条件是真实的,则执行一个表达式,后面是一个分号(
:
) - 第二个表达式,如果条件是falsy,则执行。
const drink = personAge >= 18 ? "Wine" : "Juice";
也可以将多个表达式连在一起:
const drink = personAge >= 18 ? "Wine" : personAge >= 6 ? "Juice" : "Milk";
但要小心,因为连锁多个表达式会导致混乱的代码,难以维护。
三元运算符在React中特别有用,尤其是在你的JSX代码中,它只接受大括号中的表达式。
例如,你可以使用三元运算符根据一个特定的条件来设置一个属性的值:
render(){ const person = { name: 'Carlo', avatar: 'https://en.gravatar.com/...', description: 'Content Writer', theme: 'light' } return ( <div className='card' style={ person.theme === 'dark' ? { background: 'black', color: 'white' } : { background: 'white', color: 'black'} }> <img src={person.avatar} alt={person.name} style={ { width: '100%' } } /> <div style={ { padding: '2px 16px' } }> <h3>{person.name}</h3> <p>{person.description}.</p> </div> </div> ); }
在上面的代码中,我们检查条件 person.theme === 'dark'
,以设置容器 div
的 style
属性的值。
短路求值(最小化求值)
逻辑与( &&
)运算符从左到右评估操作数,当且仅当所有操作数为 true
时返回 true
。
逻辑与是一个短路运算符。每个操作数都被转换为布尔值,如果转换的结果是 false
,AND运算符就会停止并返回假操作数的原始值。如果所有的值都是 true
,它就返回最后一个操作数的原始值。
✍️ 在JavaScript中,true && expression
总是返回 expression
,而 false && expression
总是返回 false
。
短路求值是React中常用的JavaScript功能,因为它允许你根据特定条件输出代码块。下面是一个例子:
{ displayExcerpt && post.excerpt.rendered && ( <p> <RawHTML> { post.excerpt.rendered } </RawHTML> </p> ) }
在上面的代码中,如果 displayExcerpt
和 post.excerpt.rendered
求值为true
,React会返回最后的JSX块。
简而言之,”如果条件为 true
,紧随 &&
之后的元素将出现在输出中。如果是 false
,React会忽略并跳过它”。
展开语法
在JavaScript中,展开语法允许你将一个可迭代的元素,如数组或对象,扩展为函数参数、数组字面,或对象字面。
在下面的例子中,我们在一个函数调用中对一个数组进行解包:
function doSomething( x, y, z ){ return `First: ${x} - Second: ${y} - Third: ${z} - Sum: ${x+y+z}`; } const numbers = [3, 4, 7]; console.log( doSomething( ...numbers ) );
你可以使用spread语法来复制一个数组(甚至是多维数组)或连接数组。在下面的例子中,我们以两种不同的方式串联两个数组:
const firstArray = [1, 2, 3]; const secondArray = [4, 5, 6]; firstArray.push( ...secondArray ); console.log( firstArray );
另外,还可以选择:
let firstArray = [1, 2, 3]; const secondArray = [4, 5, 6]; firstArray = [ ...firstArray, ...secondArray]; console.log( firstArray );
你也可以使用展开语法来克隆或合并两个对象:
const firstObj = { id: '1', title: 'JS is awesome' }; const secondObj = { cat: 'React', description: 'React is easy' }; // clone object const thirdObj = { ...firstObj }; // merge objects const fourthObj = { ...firstObj, ...secondObj } console.log( { ...thirdObj } ); console.log( { ...fourthObj } );
解构赋值
你会发现React中经常使用的另一种语法结构是解构赋值语法。
✍️ 解构赋值语法允许你将数组中的值或对象中的属性解包到单独的变量中。
在下面的例子中,我们从一个数组中解包数值:
const user = ['Carlo', 'Content writer', 'Kinsta']; const [name, description, company] = user; console.log( `${name} is ${description} at ${company}` );
而这里是一个用对象进行解构赋值的简单例子:
const user = { name: 'Carlo', description: 'Content writer', company: 'Kinsta' } const { name, description, company } = user; console.log( `${name} is ${description} at ${company}` );
但我们可以做得更多。在下面的例子中,我们把一个对象的一些属性拆开,用展开语法把剩余的属性分配给另一个对象:
const user = { name: 'Carlo', family: 'Daniele', description: 'Content writer', company: 'Kinsta', power: 'swimming' } const { name, description, company, ...rest } = user; console.log( rest ); // {family: 'Daniele', power: 'swimming'}
你也可以给一个数组赋值:
const user = []; const object = { name: 'Carlo', company: 'Kinsta' }; ( { name: user[0], company: user[1] } = object ); console.log( user ); // (2) ['Carlo', 'Kinsta']
注意,当使用没有声明的对象字面解构赋值时,赋值语句周围的括号是必须的。
关于解构赋值的更深入分析,以及几个使用实例,请参考 mdn web docs。
filter(), map()和reduce()
JavaScript提供了几个有用的方法,你会发现在React中经常使用。
filter()
✍️ filter()
方法创建一个浅拷贝,将给定数组的一部分过滤到符合所提供函数条件的元素。
在下面的例子中,我们将过滤器应用于 numbers
数组,得到一个元素大于5的数组:
const numbers = [2, 6, 8, 2, 5, 9, 23]; const result = numbers.filter( number => number > 5); console.log(result); // (4) [6, 8, 9, 23]
在下面的例子中,我们得到一个标题中包含 “JavaScript “一词的posts数组:
const posts = [ {id: 0, title: 'JavaScript is awesome', content: 'your content'}, {id: 1, title: 'WordPress is easy', content: 'your content'}, {id: 2, title: 'React is cool', content: 'your content'}, {id: 3, title: 'With JavaScript to the moon', content: 'your content'}, ]; const jsPosts = posts.filter( post => post.title.includes( 'JavaScript' ) ); console.log( jsPosts );
标题中包含 “JavaScript” 的posts数组
map()
✍️ map()
方法对数组中的每个元素执行所提供的函数,并返回一个由回调函数产生的每个元素填充的新数组。
const numbers = [2, 6, 8, 2, 5, 9, 23]; const result = numbers.map( number => number * 5 ); console.log(result); // (7) [10, 30, 40, 10, 25, 45, 115]
在React组件中,你会经常发现 map()
方法被用来建立列表。在下面的例子中,我们正在映射WordPress的 posts
对象来建立一个文章的列表:
<ul> { posts && posts.map( ( post ) => { return ( <li key={ post.id }> <h5> <a href={ post.link }> { post.title.rendered ? post.title.rendered : __( 'Default title', 'author-plugin' ) } </a> </h5> </li> ) })} </ul>
reduce()
✍️ reduce()
方法在数组的每个元素上执行一个回调函数(reducer),并将返回的值传递给下一次迭代。简而言之,还原器将一个数组的所有元素 “reduces” 为一个单一的值。
reduce()
接受两个参数:
- 一个回调函数,为数组中的每个元素执行。它返回一个值,在下次调用时成为累加器参数的值。在最后一次调用时,该函数返回的值将成为
reduce()
的返回值。 - 一个初始值,是传递给回调函数的累加器的第一个值。
回调函数需要几个参数:
- 一个累加器(accumulator):从上一次调用回调函数返回的值。在第一次调用时,如果指定的话,它会被设置为一个初始值。否则,它取数组中第一项的值。
- 当前元素的值:如果已经设置了一个初始值,该值被设置为数组的第一个元素(
array[0]
),否则它取第二个元素的值(array[1]
)。 - 当前的索引是当前元素的索引位置。
下面额例子会让一切变得更清晰。
const numbers = [1, 2, 3, 4, 5]; const initialValue = 0; const sumElements = numbers.reduce( ( accumulator, currentValue ) => accumulator + currentValue, initialValue ); console.log( numbers ); // (5) [1, 2, 3, 4, 5] console.log( sumElements ); // 15
让我们详细了解一下每次迭代时发生的情况。回到前面的例子中,改变 initialValue
:
const numbers = [1, 2, 3, 4, 5]; const initialValue = 5; const sumElements = numbers.reduce( ( accumulator, currentValue, index ) => { console.log('Accumulator: ' + accumulator + ' - currentValue: ' + currentValue + ' - index: ' + index); return accumulator + currentValue; }, initialValue ); console.log( sumElements );
下图显示了浏览器控制台的输出:
使用reduce(),初始值设置为5
现在让我们来看看没有 initialValue
参数会发生什么:
const numbers = [1, 2, 3, 4, 5]; const sumElements = numbers.reduce( ( accumulator, currentValue, index ) => { console.log( 'Accumulator: ' + accumulator + ' - currentValue: ' + currentValue + ' - index: ' + index ); return accumulator + currentValue; } ); console.log( sumElements );
使用reduce()而不使用初始值
更多的例子和用例在mdn web docs网站上讨论。
导出和导入
从ECMAScript 2015(ES6)开始,可以从一个JavaScript模块中导出值,并将其导入另一个脚本中。你将在你的React应用程序中广泛使用导入和导出,因此,对它们的工作原理有一个良好的理解是很重要的。
下面的代码创建了一个功能组件。第一行是导入React库:
import React from 'react'; function MyComponent() { const person = { name: 'Carlo', avatar: 'https://en.gravatar.com/userimage/954861/fc68a728946aac04f8531c3a8742ac22?size=original', description: 'Content Writer', theme: 'dark' } return ( <div className = 'card' style = { person.theme === 'dark' ? { background: 'black', color: 'white' } : { background: 'white', color: 'black'} }> <img src = { person.avatar } alt = { person.name } style = { { width: '100%' } } /> <div style = { { padding: '2px 16px' } } > <h3>{ person.name }</h3> <p>{ person.description }.</p> </div> </div> ); } export default MyComponent;
我们使用了 import
关键字,后面是我们要分配给我们要导入的东西的名字,然后是我们要安装的包的名字,因为它在package.json文件中被提及。
✍️ import
声明是用来导入由其他模块导出的只读的活体绑定的。
注意,在上面的 MyComponent()
函数中,我们使用了前几节讨论的一些JavaScript特性。我们在大括号中包含了属性值,并使用条件运算符语法为 style
属性赋值。
脚本以导出我们的自定义组件结束。
现在我们对导入和导出有了一些了解,让我们仔细看看它们是如何工作的。
Export
✍️ export
声明用于从一个JavaScript模块导出值。
每个React模块可以有两种不同类型的导出:命名导出和默认导出。
✍️ 每个模块可以有多个命名导出,但只有一个默认导出。
例如,你可以用一个 export
语句同时导出几个功能:
export { MyComponent, MyVariable };
你也可以导出单个特征(function
, class
, const
, let
):
export function MyComponent() { ... }; export let myVariable = x + y;
但你只能有一个单一的默认导出:
export default MyComponent;
你也可以对个别功能使用默认导出:
export default function() { ... } export default class { ... }
Import
一旦组件被导出,它就可以和其他模块一起被导入另一个文件,例如 index.js
文件:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import MyComponent from './MyComponent'; const root = ReactDOM.createRoot( document.getElementById( 'root' ) ); root.render( <React.StrictMode> <MyComponent /> </React.StrictMode> );
在上面的代码中,我们以几种方式使用了导入声明。
在前两行中,我们为导入的资源指定了一个名称,在第三行中,我们没有指定一个名称,只是导入了./index.css文件。最后一条 import
语句导入了./MyComponent文件并指定了一个名称。
让我们来看看这些导入的区别。
总的来说,有四种类型的导入:
Named import
import { MyFunction, MyVariable } from "./my-module";
Default import
import MyComponent from "./MyComponent";
Namespace import
import * as name from "my-module";
Side effect import
import "module-name";
✍️ 不带大括号的 import
语句用于导入默认 export
。带大括号的 import
语句是用来导入一个命名的 export
。
一旦你在index.css中添加了一些样式,你的卡片应该像下面的图片一样,你也可以看到相应的HTML代码:
一个简单的React组件
注意, import
声明只能在顶层的模块中使用(不能在函数、类等内部使用)。
对于 import
和 export
语句的更全面的概述,你可能还想查看以下资源:
- export (mdn web docs)
- import (mdn web docs)
- 导入和导出组件 (React dev)
- 什么时候应该在ES6导入时使用大括号? (Stack Overflow)
小结
React是当今最流行的JavaScript库之一,是网络开发世界中要求最多的技能之一。
有了React,就可以创建动态的网络应用程序和高级界面。由于其可重复使用的组件,创建大型、动态和互动的应用程序可以很容易。
但React是一个JavaScript库,充分了解JavaScript的主要功能对于开始你的React之旅至关重要。这就是为什么我们在一个地方收集了一些你会发现在React中最常使用的JavaScript功能。掌握这些功能会让你在React学习之旅中占得先机。
而当涉及到网络开发时,从JS/React转移到WordPress只需要很少的努力。
现在轮到你了,你认为哪些JavaScript功能在React开发中是最有用的?我们有没有遗漏任何你希望在我们的列表中看到的重要功能?请在下面的评论中与我们分享你的想法。
评论留言