整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

从小白到专家:JavaScript 延展操作符的几个基本用法

文共5530字,预计学习时长11分钟



延展操作符首次于ES6中引入,并很快成为最受欢迎的功能之一。尽管事实上延展操作符只适用于数组,但仍有建议提出可以将其功能扩展到对象。最终ES9中引入了此功能。

本教程将说明为什么应该使用扩展运算符,以及它如何运作。



目录


1.为什么要使用延展操作符

2.克隆数组/对象

3.将类数组结构转换为数组

4.延展操作符作为参数

5.将元素添加到数组/对象

6.合并数组/对象


为什么要使用延展操作符


阅读了以上列表之后,你可能会想:“JavaScript就已经能够满足需求了,为什么还要使用延展操作符?”我们先来介绍下不变性。

牛津词典:不变性 - 随着时间的推移不变或无法改变。

作为软件开发的术语,不可变指状态不能随时间变化的值。实际上,通常使用的大多数值(原始值,如字符串,整数等)都是不可变的。

然而,JavaScript中非常特殊的一点是,其中的数组和对象实际上是可变的。这可能成为一个大问题。以下实例阐明了其中原因:

const mySquirtle={
name: 'Squirtle',
 type: 'Water',
 hp: 100
};
const anotherSquirtle = mySquirtle;
anotherSquirtle.hp = 0;
console.log(mySquirtle); //Result: { name: 'Squirtle', type: 'Water', hp: 0 }


从上述代码中可以看到,我们有一个变量Squirtle(杰尼龟)。因为刚刚访问了神奇宝贝中心,这只杰尼龟的HP值为100。

由于还想要另一只杰尼龟,因此声明变量为anotherSquirtle,将初始Squirtle指定为它的值。一场苦战后,另一只杰尼龟被击败了。因此,访问另一只杰尼龟的HP值并将其更改为0。下一步,检查初始Squirtle,输入console.log和...

等等,什么?初始Squirtle的HP降至0。这怎么可能?可怜的杰尼龟遭遇了什么?原来是发生了JavaScript变异。接下来将为你解释其中缘由。

当创建anotherSquirtle变量并将初始Squirtle指定为其值时,实际是给初始Squirtle对象的内存位置分配了一个引用。这是因为JavaScript数组和对象是引用数据类型。与基本数据类型不同,引用数据类型指向存储实际对象/数组的内存地址。

为了便于理解,可以将引用数据类型想象为全局变量的指针。更改引用数据类型的值实际上是在更改全局变量的值。

这意味着当将anotherSquirtle的HP值更改为0时,实际是将存储在内存中的Squirtle对象的HP值更改为0。这就是为什么mySquirtle的HP值为0 - 因为mySquirtle是对存储在内存中的对象的引用,可以通过anotherSquirtle变量被改变。谢谢JavaScript。

如何解决这个问题?

为了避免变量的变异,需要在要复制数组/对象时,创建数组/对象实例。如何实现这一操作?

使用延展操作符。


延展操作符如何运作


从MDN文档中可以查到:展开语法(spread syntax),可以在函数调用或数组构造时,将数组表达式或string等iterable在语法层面展开,还可以在构造字面量对象时,将对象表达式按键-值方式展开。

简而言之,延展操作符......延展iterable中的项(iterable指receiver中任何可循环的项,如字符串,数组,集等)。(receiver用于接收展开值。)为便于理解,以下是数组的简单示例:

const numbers = [1, 2, 3];
console.log(...numbers); //Result: 1 2 3
const pokemon = ['Squirtle', 'Bulbasur', 'Charmander'];
console.log(...pokemon); //Squirtle Bulbasur Charmander
const pokedex = [
 { name: 'Squirtle', type: 'Water' },
 { name: 'Bulbasur', type: 'Plant' },
 { name: 'Charmander', type: 'Fire' }
];
console.log(...pokedex); //{ name: 'Squirtle', type: 'Water' } { name: 'Bulbasur', type: 'Plant' } { name: 'Charmander', type: 'Fire' }
import pandas as pd


数组中使用延展操作符的三个示例


如上所示,当在数组上使用延展操作符时,可以获取数组中所含的每个单独的项。在上述所有示例中,receiver都是一个函数,即console.log函数。够简单吧?


克隆数组/对象


现在已经知道了延展操作符的工作原理,可以利用它复制数组和对象而不改变其值。怎么做呢?延展内容然后使用数组[]或对象文字{}来生成数组/对象实例。

仍然以上文的杰尼龟为例,通过克隆mySquirtle变量解决上文中的问题:

const mySquirtle = {
 name: 'Squirtle',
 type: 'Water',
 hp: 100
};
const anotherSquirtle = { ...mySquirtle };
anotherSquirtle.hp = 0;
console.log(anotherSquirtle); //Result: { name: 'Squirtle', type: 'Water', hp: 0 }
console.log(mySquirtle); //Result: { name: 'Squirtle', type: 'Water', hp: 100 }

使用延展操作符复制对象


通过使用解延展操作符解构mySquirtle变量内容并使用对象字面量,创建了Squirtle对象的新实例。这样,就防止变量突然变异。

使用相同的语法复制数组:

const pokemon = ['Squirtle', 'Bulbasur', 'Charmander'];
const pokedex = [...pokemon];
pokedex.push('Cyndaquil');
console.log(pokemon); //[ 'Squirtle', 'Bulbasur', 'Charmander' ]
console.log(pokedex); //[ 'Squirtle', 'Bulbasur', 'Charmander', 'Cyndaquil' ]


使用延展操作符复制数组

注意:延展操作符只执行浅拷贝。这意味着若在数组/对象中存储了引用数据类型,则在使用延展操作符进行复制时,嵌套数组/对象将包含对原始的引用,因此其数值将是可变的。


将类数组对象转换为数组


类数组对象与数组非常相似。它们通常都有编号元素和长度属性。但是,两者有一个至关重要的区别:类数组对象没有任何数组函数。

类数组对象包含主要由DOM方法返回的HTML节点列表,和每个JS函数和少部分其他函数自动生成的参数变量。

使用与克隆数组相同的语法,可以使用延展操作符将类数组结构转换为数组,这可以代替使用Array.from的方法。以下是将nodeList转换为数组的示例:

const nodeList = document.getElementsByClassName("pokemon");
const array = [...nodeList];
 
console.log(nodeList); //Result: HTMLCollection [ div.pokemon, div.pokemon ]
console.log(array); //Result: Array [ div.pokemon, div.pokemon ]


将nodelist转换为数组

使用这种技术,可以将任何类数组结构转换为数组,从而访问所有数组函数。


延展操作符用作参数


某些函数接受可变数量的参数。其中一个典型列子就是Math集合中的函数。以Math.max()函数为例。它接受n个数字参数,并返回最大的参数。假设需要将一个数字数组传递给Math.max()函数。该怎么做呢?

可以这样做:

const numbers = [1, 4, 5];
const max = Math.max(numbers[0], numbers[1], numbers[2]);
console.log(max); //Result: 5


但是,这样做无疑是自寻死路。若是有20个值怎么办?1000个值呢?真的要通过索引访问每个值吗?当然不是。我们可以通过使用延展操作符提取数组中每个单独的值,如下所示:

const numbers = [1, 4, 5, 6, 9, 2, 3, 4, 5, 6];
const max = Math.max(...numbers);
console.log(max); //Result: 9


大救星:延展操作符。


添加新元素


将项添加到数组

向数组添加新元素,首先需要延展数组的内容并使用数字字面量[]创建数组实例,需要包含原始数组的内容以及要添加的值:

const pokemon = ['Squirtle', 'Bulbasur'];
const charmander = 'Charmander';
const cyndaquil = 'Cyndaquil';
const pokedex = [...pokemon, charmander, cyndaquil];
console.log(pokedex); //Result: [ 'Squirtle', 'Bulbasur', 'Charmander', 'Cyndaquil' ]


使用延展操作符将项添加到数组中


如你所见,可以任意添加新项。

向对象添加属性

通过使用与数组相同的语法,可以在克隆对象时轻松添加新属性。稍微转变一下,就有一个不同的语法来向对象添加属性(也可以用于数组):

const basicSquirtle = { name: 'Squirtle', type: 'Water' };
const fullSquirtle = {
 ...basicSquirtle,
 species: 'Tiny Turtle Pokemon',
 evolution: 'Wartortle'
};
console.log(fullSquirtle); //Result: { name: 'Squirtle', type: 'Water', species: 'Tiny Turtle Pokemon', evolution: 'Wartortle' }


如你所见,可以在对象字面量中而不是在外部直接声明和初始化新变量。


合并数组/对象


数组

如上述例子所示,可以通过延展数组并使用数组字面量来合并两个数组。但是,这一部分要讲的不是简单地添加新元素,而是添加另一个(延展)数组:

const pokemon = ['Squirtle', 'Bulbasur', 'Charmander'];
const morePokemon = ['Totodile', 'Chikorita', 'Cyndaquil'];
const pokedex = [...pokemon, ...morePokemon];
console.log(pokedex); //Result: [ 'Squirtle', 'Bulbasur', 'Charmander', 'Totodile', 'Chikorita', 'Cyndaquil' ]

使用延展操作符合并数组

这也适用于数组对象:

const pokemon = [ { name: 'Squirtle', type: 'Water' }, { name: 'Bulbasur', type: 'Plant' }];const morePokemon = [{ name: 'Charmander', type: 'Fire' }]; const pokedex = [...pokemon, ...morePokemon]; console.log(pokedex); //Result: [ { name: 'Squirtle', type: 'Water' }, { name: 'Bulbasur', type: 'Plant' }, { name: 'Charmander', type: 'Fire' } ]Merging two arrays of objects with the spread operator


对象

可以使用与之前相同的语法将两个(或更多)对象合并到一个对象中(你可能已经留意到,扩展运算符在数组和对象中的使用方式非常相似):

const baseSquirtle = {
 name: 'Squirtle',
 type: 'Water'
};
const squirtleDetails = {
 species: 'Tiny Turtle Pokemon',
 evolution: 'Wartortle'
};
const squirtle = { ...baseSquirtle, ...squirtleDetails };
console.log(squirtle); //Result: { name: 'Squirtle', type: 'Water', species: 'Tiny Turtle Pokemon', evolution: 'Wartortle' }


使用延展操作符合并对象

本教程说明了为什么应该使用扩展运算符(重点强调不变性!),它是如何工作的以及几个基本用法。


留言 点赞 关注

我们一起分享AI学习与发展的干货

如需转载,请后台留言,遵守转载规范

篇文章JavaScript基础——你真的清楚JavaScript是什么吗?,我们明白了JavaScript是一个单线程、非阻塞、异步、解释性语言,清楚了什么是单线程、进程、阻塞、调用堆栈、异步回调、任务循环等感念,没看的或者不清楚的建议点击《JavaScript基础——你真的清楚JavaScript是什么吗?再看一遍,只有理解了,才能轻松阅读理解本篇文章内容。

什么是callback?

JavaScript 是单线程工作,这意味着两段脚本不能同时运行,而是必须一个接一个地运行。我们人类是多线程工作。您可以使用多个手指打字,可以一边开车一边与人交谈。唯一一个会妨碍我们的是打喷嚏,因为当我们打喷嚏的时候,所有当前进行的活动都必须暂停。这真是非常讨厌,尤其是当您在开车并想与人交谈时。您可不想编写像打喷嚏似的代码。JavaScript由于单线程限制,防止阻塞,只能通过异步函数的调用方式,把需要延迟处理的事件放入事件循环队列。到目前为止,回调是编写和处理JavaScript程序异步逻辑的最常用方式。说了这么多,既然回调这么重要,到底什么是回调(callback)呢?

简单的定义:回调就是一个在另外一个函数执行完后要执行的函数

复杂的定义:在JavaScript中,函数是对象。因此函数可以将函数作为参数,并且可以由其他函数进行返回。执行此操作的函数称为高阶函数。任何作为参数传递的函数都称为回调函数。

为什么需要回调?

开篇已经介绍了JavaScript是单线程的,需要通回调函数处理异步相关的逻辑,理论还是过于生硬,我们还是来看段代码吧:

function first(){
console.log(1);
}
function second(){
console.log(2);
}
first();
second();

正如你所料,先执行first函数,再执行second函数,控制台将输出以下内容:

1
2

目前看来没什么问题,如果first()函数中含有某种无法立即执行的函数呢?例如,我们必须发送请求然后等待结果响应的API请求?为了模拟API请求,我们可以使用setTimeout函数模拟。我们将函数延迟500毫秒来模拟请求,我们更改后代码如下:

function first(){
// Simulate a code delay
setTimeout( function(){
console.log(1);
}, 500 );
}
function second(){
console.log(2);
}
first();
second();

我们将 console.log(1) 延迟500毫秒输出,这段代码会怎么输出呢?

2
1

我们希望的顺序先执行first,再执行second,但是由于JavaScript是异步的,所有的延迟处理都要放入循环队列里,因此事与愿违,不能按照我们的希望顺序输出。如果希望这段代码按照我们的意愿输出,我们可以使用回调函数,确保某些代码执行完了,在循序执行另外一段代码。

创建回调

说了这么多,让我们创建一个简单的回调!

我们打开编辑器,先输入如下代码:

function doHomework(subject) {
alert(`Starting my ${subject} homework.`);
}

上面我们创建了doHomeWork的函数,我们接受一个变量,通过控制台调用,将得到下面的提示:

doHomework('math');
// Alerts: Starting my math homework.

接着,我们开始添加回调,在doHomework函数中添加一个参数callback,然后在第二个参数中回调我们定义的函数。代码如下:

function doHomework(subject, callback) {
alert(`Starting my ${subject} homework.`);
callback();
}
doHomework('math', function() {
alert('Finished my homework');
});

正如你希望的,我们在控制台里运行上述代码,将会受到两个连续的alert,Starting my math homework,然后弹出 Finished my homework。

但是回调函数并不是非得在调用函数中定义,我们可以单独定义,修改后的代码如下:

function doHomework(subject, callback) {
alert(`Starting my ${subject} homework.`);
callback();
}
function alertFinished(){
alert('Finished my homework');
}
doHomework('math', alertFinished);

此示例的输出结果和上段代码的结果一致,我们实现了在doHomework函数中调用alertFinished,实现了函数作为参数进行传递,实现了回调函数的创建。

用回调写一段真实业务场景的代码!

例如我们有一个需求,用NodeJs实现从论坛帖子列表中显示其中的一个帖子的信息及留言列表信息,代码如下:

DB/posts.json(帖子列表数据)

[
{
"id": "001",
"title": "Greeting",
"text": "Hello World",
"author": "Jane Doe"
},
{
"id": "002",
"title": "JavaScript 101",
"text": "The fundamentals of programming.",
"author": "Alberta Williams"
},
{
"id": "003",
"title": "Async Programming",
"text": "Callbacks, Promises and Async/Await.",
"author": "Alberta Williams"
}
]

DB/comments.json(评论列表)

[
{
"id": "phx732",
"postId": "003",
"text": "I don't get this callback stuff."
},
{
"id": "avj9438",
"postId": "003",
"text": "This is really useful info."
},
{
"id": "gnk368",
"postId": "001",
"text": "This is a test comment."
}
]

Index.js

const fs = require('fs');
const path = require('path');
const postsUrl = path.join(__dirname, 'db/posts.json');
const commentsUrl = path.join(__dirname, 'db/comments.json');
//return the data from our file
function loadCollection(url, callback) {
fs.readFile(url, 'utf8', function(error, data) {
if (error) {
console.log(error);
} else {
return callback(JSON.parse(data));
}
});
}
//return an object by id
function getRecord(collection, id, callback) {
var collectobj=collection.find(function(element){
return element.id == id;
});
callback(collectobj);
return collectobj;
}
//return an array of comments for a post
function getCommentsByPost(comments, postId) {
return comments.filter(function(comment){
return comment.postId == postId;
});
}
loadCollection(postsUrl, function(posts){
loadCollection(commentsUrl, function(comments){
getRecord(posts, "001", function(post){
const postComments = getCommentsByPost(comments, post.id);
console.log(post);
console.log(postComments);
});
});
});

大家请注意,我们在loadCollection函数中我们没有使用try/catch,使用的是if/else,因为catch无法从readFile方法中获取错误。上述代码还需要完善,我没有包含任何错误处理。如果在任何步骤中发生错误,程序将无法继续。

错误处理是很重要的事情,我们写代码时要严格对待,比如我们要编写一个用户登录的功能。涉及从网页表单里获取用户名和密码,查询我们的数据库,确认用户信息是否正确,验证通过后,将用户引导到用户中心页面。如果用户名密码格式不正确,用户名密码不正确,我们应该将错误信息返回给用户,并引导用户重新登录。

总结

很好!我们一起把回调的内容学完了,理解了什么是回调,异步编程是我们的代码中使用的一种方法,用于推迟事件以便以后执行。当您处理异步任务时,回调是一种解决方案,以便它们按顺序执行。

如果我们有多个任务依赖于前几个任务的结果,那我们就要使用多个嵌套回调,但是就会引发“回调地域”(过多的回调嵌套会使得代码变得难以理解与维护),还好Promise解决了“回调地狱”的问题,让我们以同步的方式编写代码,小编将会再下篇文章里进行详细介绍,敬请期待!

更多精彩内容,请微信关注”前端达人”公众号

JavaScript、CSS 相比,HTML 经过三十多年的发展,似乎逐渐走进无人问津的角落,如何才能让 HTML 再次回到人们视野的中心。

作者 | Yaser Adel Mehraban

译者 | 谭开朗,责编 | 屠敏

出品 | CSDN(ID:CSDNnews)

以下为译文:

有多少次,身为开发者的你编写了一个HTML块而没有意识到可能编码得并不理想?

为什么

HTML一直处于无人问津的角落,因为JavaScript和CSS总是吸引人们的注意力。

请在脑海里先保留这种印象,因为我要用一些简单的技巧来发挥作用,让HTML再次回到人们视野的中心。

以下是创建一目了然、可维护和可扩展的代码的一些方法,其很好的应用了HTML5的语义标记元素,并将在支持的浏览器中正确呈现。

其缘由就不赘述了,让我们来看看具体是什么吧。

文档类型

在index.html的顶部位置,请确保声明了DOCTYPE。这将在所有浏览器中激活标准模式,并告知浏览器该如何编译文档。请记住DOCTYPE不是HTML元素。

HTML5是这样的:

<!DOCTYPE html>

注意:如果应用了框架,这已预先写好。如果没有,我强烈建议使用像Emmet这样的代码片段,它在VS代码中可用。

了解更多关于其他文档类型的信息吗?点击这里查看参考文件:https://html.com/tags/doctype/。

可选标签

有些标签在HTML5中是可选的,主要是因为元素是隐式呈现的。信不信由你,你可以省略<html>标签,而页面呈现得也很好。

<!DOCTYPE HTML>

<head>
<title>Hello</title>
</head>
<body>
<p>Welcome to this example.</p>
</body>
</html>

上面是一个有效的HTML,但在某些情况下就不能这样做了。例如标签后面跟着注释:

<!DOCTYPE HTML>
<!-- where is this comment in the DOM? -->

<head>
<title>Hello</title>
</head>
<body>
<p>Welcome to this example.</p>
</body>
</html>

上面是无效的,因为注释位于<thml>标签之外,解析树发生了更改。

结束标签

应始终记得结束标签,否则某些浏览器在呈现页面时会出现问题。出于可读性和其他原因,建议保留这些内容,稍后我会详细介绍。

<div id="example">
<img src="example.jpg" alt="example" />
<a href="#" title="test">example</a>
<p>example</p>
</div>

以上都是有效的标签,但也有一些特例,如下。

自闭合标签是有效的,但不是必需的。这些元素包括:

<br>, <hr>, <img>, <input>, <link>, <meta>,
<area>, <base>, <col>, <command>, <embed>, <keygen>, <param>, <source>, <track>, <wbr>

注意:普通元素永远不能有自闭合标签。

<title />

上面显然是无效的。

字符集

预先定义字符集。最好是将它放在顶部元素中。

<head>
<title>This is a super duper cool title, right ?</title>
<meta charset="utf-8">
</head>

上面是无效的,标题无法正确呈现。正确写法是将字符集移到顶部位置。

<head>
<meta charset="utf-8">
<title>This is a super duper cool title, right ?</title>
</head>

语言

不忽略可选标签的另一个原因是在使用属性时。在这种情况下,我们应该定义web页面的语言,这对于可访问性和搜索非常重要。

<html lang="fr-CA">
...
</html>

标题

永远不要忽略标题标签,否则可访问性太差了。我个人就永远不会使用这样的网站,因为我刚打开它即刻在20多个页面后就找不到了(浏览器选项卡不会有任何显示)。

base标签

这是一个非常有用的标签,应该谨慎使用。它将设置应用程序的基本URL。一旦设置好,所有链接都将相对于这个基本URL,这可能会导致一些不必要的行为:

<base href="http://www.example.com/" />

通过以上设置,href="#internal"将被编译为href=http://www.example.com/#internal。或者href="example.org"将被编译为href="http://www.example.com/example.org"。

描述

这个meta标签非常有用,尽管严格来说它不是最佳写法。但在搜索引擎时,这是超级有用的。

<meta name="description" content="HTML best practices">

这有一个帖子“搜索引擎优化正盛行”:https://yashints.dev/blog/2019/06/11/seo-tips。

语义标签

虽然可以使用div创建UX工程师的线框,但这并不意味着必须这样做。语义HTML为页面提供了意义,而不单纯是内容显示。像p、section、h{1-6}、main、nav等标签都是语义标签。如果使用p标签,用户将知道这表示一段文本,浏览器也知道如何展示它们。

语义HTML超出了本文的范围。但是我们应该进行检查,就好比写作所用的笔,而我们有鼠标。

hr不应该用于格式化

<hr>不是格式化元素,所以不要用它来格式化内容。在HTML5中,这个标签代表了内容的主题分离。正确的用法是这样的:

<p>Paragraph about puppies</p>
<p>Paragraph about puppies' favourite foods</p>
<p>Paragraph about puppies' breeds</p>
<hr>
<p>Paragraph about why I am shaving my head </p>

使用title属性时要小心

title属性是一个功能强大的工具,它可以帮助阐明页面上元素的操作或目的,比如工具提示。但是,它不能与图像上的alt等其他属性互换。

HTML 5 规范道:

目前不鼓励依赖title属性,因为很多用户代理不按照规范的访问方式来暴露该属性(例如,使用鼠标等设备来唤出提示框,而不包括只用键盘或触控键盘的用户,或者现代手机或平板电脑)。

请阅读有关如何正确使用此属性的更多信息:https://html.spec.whatwg.org/multipage/dom.html#the-title-attribute。

单引号或双引号

我见过的许多代码库,他们的标记中混合了这两种形式。这并不好,特别是当你使用一个依赖于单引号的框架时,比如php,当你在一个句子中使用单引号时,就像我现在做的一样。另一个原因是保持一致,这总是好的。不要这样写:

<img alt="super funny meme" src='/img/meme.jpg'>

而写为:

<img alt="super funny meme" src="/img/meme.jpg">

省略布尔值

当涉及到属性的布尔值时,建议省略,因为它们不添加任何值,还会增加标记的权重。

public class MyActivity extends AppCompatActivity {
<audio autoplay="autoplay" src="podcast.mp3">

<!-- instead -->

<audio autoplay src="podcast.mp3">

省略类型属性

不需要向scriptand样式标签添加type属性。某些服务(如W3C的标记验证工具)还会出现验证错误。

验证标记

可以使用W3C的标记验证等服务以确保有效的标记。

拒绝内联样式

HTML中写的是内容,其如何展示取决于样式。将展示形式留给CSS吧,不要使用内联样式,这将有利于开发人员和浏览器理解你的标记。

总结

这些只是编写标签时要记住的冰山一角。还有很多很好的资源可以让你深入了解,强烈建议你重复阅读。

  • 《GitHub HTML最佳实践》:https://github.com/hail2u/html-best-practices

  • 《W3C School HTML样式指南》:https://www.w3schools.com/html/html5_syntax.asp

希望你喜欢本文,并能写出优雅的标签。

原文:https://dev.to/yashints/let-s-write-html-like-a-pro-28h5

本文为 CSDN 翻译,转载请注明来源出处。

【END】