web前端入门到实战

web前端入门到实战

概览

“web前端入门到实战”是为后端、数据工程师量身定制的前端学习教程。通过本教程的学习,小伙伴们可以系统掌握一套web前端开发体系,满足日常开发需要,并且具备系统地分析和解决问题的能力。

学习会分为两个阶段进行:

  1. 系统学习(约占总学习时间的75%)
  2. 项目讲解

重点在系统学习,掌握整个体系后小伙伴们完全可以自主去理解项目。

系统学习路线

系统学习路线

每章学习时间跨度为1~3周,皆为周末自学。部分需要讲解的章节计划录制视频方便小伙伴们在家学习。

阶段一(2~3个月,重中之重,踏实学完即可快速上手项目开发)

  1. html
  2. css
  3. 作业:有赞官网首页首屏静态页面
  4. js(es5)
  5. react
  6. redux

阶段二(web前端技术栈必要知识补充)

  1. dom
  2. bom
  3. html5
  4. css3
  5. es6
  6. webpack

前期准备

  1. VS Code,代码编辑器
  2. nvm(Node Version Manager,可以自由切换node.js版本),安装完毕后再用nvm安装node.js v12.16.2。
  3. Chrome浏览器

html

初识html

html全称Hypertext Markup Language,超文本标记语言,网页中所有的文字图片和组织结构都是由html来编写的,当然html能够完成的工作不止这些。

html不是一种编程语言,它不像c/c++/java等编程语言那样拥有变量、函数等,它仅仅由标签组成,如<div></div>

html这个标签是根标签,其他的标签都要放在这个标签里面编写,次一级的两个结构是head标签和body标签<head></head>,<body></body>。head主要是给浏览器看的,body是页面的主体部分,我们展示出来的内容一般都放在body标签中。

下面让我们来看一个基础的html,打开VS Code编辑器,创建一个test.html文件,内容如下:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>hello world</title>
</head>
<body>
测试
</body>
</html>

打开浏览器就可以看到”测试”两字了。

其中:

doctype是文档类型,有混杂模式和标准模式两大类,不同的模式主要会影响CSS内容的呈现,一般使用HTML5标准模式。

1
<!DOCTYPE html>

lang属性是来告诉搜索引擎爬虫我们网站是使用什么语言的,例如<html lang="zh">表示中文,<html lang="en">表示英文。

<meta>标签可以设置编码格式的,这个标签不需要写闭合标签。编码集主要有gb2312、gbk、unicode、utf-8等等,一般使用utf-8。

<title>标签表示页面标题,每一个网页都有自己的名字,这个名字就是通过<title>标签设置的。

html中的标签

html中的标签主要分为两大类:

1.行级(或叫内联、行内)元素(标签),display: inline;
这一类元素(标签)的特点是:

  1. 不占满整行,元素所占空间完全由内容决定
  2. 不可以改变宽高

如:a em span strong

2.块级元素(标签),display: block;
这一类元素的特点是:

  1. 占满整行,无论内容多少
  2. 可以改变宽高

如:address div form h1-h6 p ul ol li

还有一类标签,既不属于行级元素也不属于块级元素,它们既不独占一行,又可以随意改变宽高,如imgselect

html的标签数量是很多的,并且在不断增加。这么多标签我们不必都记住,接下介绍一些HTML4中常见的标签,HTML5会在后面章节介绍。

p标签(块级)

<p></p>是段落标签,在<p></p>中写的内容会当做一个段落来处理。它的特点是独占一行,并且段落上下有一定间距。

标题标签h1-h6(块级)

标题标签的作用是着重显示文字,一般用于标题,它会将文字加粗放大并且独占一行。其中h4的默认大小是正常的文字大小,不过是加粗的。

strong标签(行级)

<strong></strong>标签的作用是将里面的文字加粗处理。

em标签(行级)

<em></em>的作用是将里面的文字变成斜体。

del标签(行级)

<del></del>是删除标签,它会在里面的文字的中间画一条横线。

address 标签(块级)

<address></address>是地址标签,它会将里面的内容变成斜体并且独占一行。

div、span

上面都是很基础的标签,作用也是显而易见。还有一种结构化标签,它们没有特殊的效果,而是用来当容器包裹其他标签的。

结构化标签的一个作用就是用来为里面的子元素设置样式。一般的元素如果某条属性没有被开发者设置样式的话,它会自动继承父级元素的相应属性的样式。

比如我们想让三个p标签里面的文字都变成红色,给三个p标签都写上color:red是非常麻烦的,最简单的方法是将三个p标签放到一个结构化标签里面,再给这个结构化标签设置color:red的样式,里面的三个p标签就都会有这个样式了。

我们在写一个页面之前,最先考虑的就是结构问题,因此一般先写结构化标签。

下面我们介绍最常见的两个结构化标签。

1.div

<div></div>标签是前端开发中很常见的容器标签。

2.span

<span></span>标签也是常见的容器标签,多数情况下盛放文字或者icon之类的内容。

ol、li (ol、li都是块级)

<ol><li></li></ol>是一组标签,它们二者都是成对出现的,单独出现没有意义。

这组标签叫作有序列表,ol是外面的列表框,li是里面的子项,并且每一个li子项的前面都会带有序号。

ol可以设置一些属性:

  1. type,这个属性的作用是用来设置每一个子项前面显示的内容的。默认情况下是按照数字来排序的,如果我们改成type=”a”,前面序号就会按照小写字母来排序。type的属性值还可以设置成A(按照大写字母来排序)、i(罗马数字、小写)、I(罗马数字、大写)来排序。此外,设置成其它值都是错误的,错误的情况下ol会按照默认的数字来排序。

  2. reversed,设置为reversed=”reversed”的时候,子项的序号会变成倒序排列。

  3. start,这个属性的意思是设置子项从第几个序号开始显示,当我们写start=”2”的时候,序号就会变成2、3、4,而不是默认的1、2、3,字母也是同样的道理。

然而,我们实际上很少在网页中看到文字前面有这些数字、字母序号,所以我们一般不使用ol、li,而是使用ul、li标签。

ul、li(ul、li都是块级)

<ul><li></li></ul>这一组标签是无序列表,前面的序号会变成点(• )。

ul同样有一个type属性,这个属性的值设置的是每一个子项前面显示的符号的形式,默认的值是disc(圆点),当值是square的时候,前面显示的就是方块,值是circle的时候显示的是空心圆圈。

同样的道理,我们也很少在网页中看到文字前面带圆点、方块之类的,所以我们在使用ul、li标签的时候,都会把ul的默认样式list-style改成none,这已经属于css的部分了。

无序列表一般用作导航栏之类,里面的结构样式都一样的部分。

a(行级元素)

<a></a>标签是一个非常重要的标签,它有一个必填的属性叫做href(hyperText reference,超文本链接)。

a标签的主要作用有两点:

  1. 定点跳转我们指定的id的元素位置。这个用法需要我们在href中写上id的值,如<a href="#test">点击我跳转</a>,这样点击a标签后页面就会定位到id=test的元素的位置

  2. 超链接。我们设置href的值为本地或者网上的链接,如https://www.baidu.com/ ,点击a标签就会跳转到这个网页。

  3. 协议限定符。在href中我们可以写javascript代码,如href=”javascript:while(1){alert(“你中毒了”)}”,点击这个a标签后浏览器会不断弹出对话框。还有发送邮件:<a href="mailto:rebell0003@gmail.com">rebell0003@gmail.com</a>,拨打电话:<a href="tel:13266419102">13266419102</a>

a标签默认是蓝色字体且带有下划线,我们一般会用css将a标签的默认样式覆盖掉。

img(行级块元素)

<img></img>标签是image图片的意思,它有一个必须的属性叫做src,src属性的值是我们图片的地址,可以是绝对地址也可以是相对地址。

图片标签还有两个属性。

  1. alt属性。这个属性为其设置图片占位符,当图片因为网速或者链接错误等原因加载不出来的时候,会显示alt设置的值。

  2. title属性。图片提示符,当我们鼠标移入图片的时候,在鼠标旁边会显示这个title属性设置的值。

table、tr、td标签(table是块级,tr、td是行级元素)

<table></table>需要搭配<tr><td></td></tr>一起使用。

table是表格的意思,tr是表格的行,td是表格的数据单元,我们可以理解为列。

1
2
3
4
5
6
7
8
9
10
<table>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</table>

table标签的大概结构是这个样子的,还有几个属性。

  1. cellpadding 内边距属性,可以为每一个单元格设置内边距,如cellpadding=”10px”(在table上设)
  2. cellspacing 设置单元之间的空间,当我们设置为0时就可以去掉边线了。(在table上设)
  3. colspan属性,设置一个td占几个单位,默认的一个td占一个单位,可以实现合并单元格的效果。(在td上加,值为数字)
1
2
3
4
5
6
7
8
9
10
11
12
<table border="1">
<tr>
<th>1</th>
<th>2</th>
</tr>
<tr>
<td colspan="2">3</td>
</tr>
<tr>
<td colspan="2">4</td>
</tr>
</table>

以前可以用这个标签给页面布局,现在我们不这么用了,而是使用div + css给页面布局。

原因是:

使用嵌套表格的方法来布局网页框架会使网页浏览的速度变慢,因为table中的内容是自适应的,为了自适应,它要计算嵌套最深的节点以满足自适应,所以有可能会有一断时间出现空白才显示。

使用div + css的方法布局网页框架的优点在于制作出来的页面速度较快,且内容和样式分离,便于维护扩展。缺点是相比table方式要复杂些。现在网页都倾向于使用div + css来布局了。

表单(form,块级元素)

这个元素可以让我们实现前端和后台的数据交互。

表单都是成组出现的,里面有各种各样的元素。

我们先介绍一下form表单元素拥有的属性:

  1. action属性 填写服务器地址,这个属性的意思是我们把数据发送到哪个服务器地址
  2. method属性 传输方法,最常见的是POST/GET

我们再介绍一下表单拥有的子元素:

input标签

这个标签是一个单标签,不需要闭合。这个标签有一个type属性,这个属性的值决定了这个input标签的类型是什么。

  1. text 输入框,我们可以输入文字信息。
  2. password 密码框,输入的文字信息都会以···形式展示。
  3. submit 如果type=”submit”,这个input标签就是一个提交按钮,我们点击这个提交按钮就会将整个表单数据发送到后台服务器。

我们发送数据一定要有数据名(key)和数据内容(value),数据内容就是我们给input标签设置(或输入)的value属性的值,而数据名就需要我们在input标签里面写一个name属性来表示我们这个数据的名字是什么。

这里我们写一个简单的表单:

1
2
3
4
5
<form action="https://www.baidu.com/" method="get">
<p>username: <input type="text" name="username" /></p>
<p>password: <input type="password" name="password" /></p>
<input type="submit" value="提交" />
</form>

当我们随便输入一个用户名和密码,点击提交按钮之后,浏览器显示的内容是这样的:

https://www.baidu.com/?username=test&password=test

网页地址后面出现了我们所传递的数据?username=test&password=test

input还有其他的数据形式:

  1. type=”radio”
  2. type=”checkbox”

radio是单选框的意思,当我们给一个input设置radio的type之后,它就会变成一个圆点。我们可以选择这个圆点,但是我们写很多单选框的时候,它们似乎都能被选中,并没有单选的作用,这是因为我们还没有为这一组单选框设置名字。当我们给几个radio都设置了同一个name的时候,它们就会变成只能选择一个的单选框了。

1
2
3
4
5
6
<form action="https://www.baidu.com/" method="get">
<input type="radio" name="sex" value="male" checked>Male
<br>
<input type="radio" name="sex" value="female">Female
<input type="submit" value="提交" />
</form>

checkbox是复选框的意思,我们可以同时选择很多个选项。

1
2
3
4
5
6
<form action="https://www.baidu.com/" method="get">
<input type="checkbox" name="vehicle" value="Bike">I have a bike
<br>
<input type="checkbox" name="vehicle" value="Car" checked>I have a car
<input type="submit" value="提交">
</form>

https://www.baidu.com/?vehicle=Bike&vehicle=Car

可以看到,我们可以使用checked属性为这两个选择框设置默认选中值。

select标签(行级块元素)

select标签是选择列表

1
2
3
4
5
6
7
8
<form action="https://www.baidu.com/" method="get">
<select name="province">
<option>山东</option>
<option>黑龙江</option>
<option>北京</option>
</select>
<input type="submit" value="提交">
</form>

下拉列表的name属性是写在<select>标签上的,option中间填写的内容就是默认的数据值,但是如果我们给每一个option都加一个value属性的话,那么option中间填写的内容则不作为数据值传递,value的值作为传递的数据值。

下拉列表默认选中的是第一个选项,如果我们可以添加属性selected=”selected”设置它的默认值。

文字分隔符与编码集

我们在body标签里加个div,为它设置样式,并在中间加一些文字。

<div style="width: 50px; height:50px; background-color: red">测试测试</div>

我们发现文字会在这个div标签的边界处自动换行,也就是说这个div标签圈定了一个范围,里面的文字或者其他标签都默认在这个范围里面显示。

但是当我们在中间书写的不是中文而是一串英文字符,如aaaaaaaaaaaa的时候,我们会发现这一串英文字符在div的边界处并没有换行,这是为什么呢?

原因是我们的每一个汉字,计算机都会认出来这是一个单独的词,每一个汉字都会默认地和其他汉字分隔开,但是英文字母却不会默认地分隔开,因为计算机不知道多少个英文字母才算是一个词,因此我们需要手动为其添加分隔符。

这个分隔符我们也不陌生,就是空格,只要我们在这一串字符中间加几个空格,那么被空格隔开的字符就会被当做是一个词而与其他词分隔开。

那么现在问题来了,既然空格的作用是当做分隔符来使用,并不是我们所想的那种”空白的一个格”,那么我们怎么在html中写真正的空白格呢?

这里我们就要提到一个名词叫做编码集了。我们在书写html的时候,很多特殊的符号是无法写出来的,这个时候我们只能用编码来让浏览器识别我们所想的符号,编码的格式是&编码;。空格的编码就是&nbsp;,在页面中我们就可以看到空格了。

其次,用来当做标签的尖括号<>也是无法正常通过符号来显示出来的,我们同样需要用编码集让浏览器识别出来。< 小于号的编码是&lt;,less than的意思,同理,> 大于号的编码是&gt;,great than的意思。我们只要在html中写这两个编码,大于号和小于号就可以正常显示出来了。

回车也属于分隔符,在html中回车是没有作用的,我们想要在网页上让文字显示出回车换行的效果的话,编码也是没有办法实现的,我们需要一个标签叫<br>标签,这个标签的作用就是换行。<br> 标签是空标签(意味着它没有结束标签,因此<br></br>是错误的),也可以把结束标签放在开始标签中,也就是 <br />

》》》》》》》》》

css

css全称Cascading Style Sheets,层叠样式表。它的主要作用是为html标签添加各种样式效果。

本章属于css2.0知识,css3.0知识会在后面章节中介绍。

如何引入css

css的引入方式一共有4种:

1.行间样式,直接在html标签上写style属性,<div style="color: red"><div>

2.页面级css,在head标签里面添加一个style标签

1
2
3
4
5
6
7
<head>
<style>
div {
color: red;
}
</style>
</head>

3.外部css文件(推荐),我们在外部创建一个.css后缀的文件,然后在html里面引入这个外部的css文件即可。

在head标签里面加上一个link标签。

1
2
3
<head>
<link rel="stylesheet" href="index.css">
</head>

4.import方式引入(已经弃用)

有两种写法:

1.在head标签里面写一个style标签,在第一行写上@import url(),url里面写上css文件的地址。

2.在css中直接使用

@import url(CSS文件路径地址)

这种引入方式有几种缺点导致它现在被弃用了:

1.import规则一定要写在除了@charset外的其他任何CSS规则之前,若有多个则一起写在最前面,否则失效。

下面这种写法会失效:

1
2
3
4
5
6
<style type="text/css">
div {
color: orange;
}
@import "import.css";
</style>

2.程序读到import的时候,会忽略掉import,等到html里面的所有内容包括图片在内的所有资源全都加载完毕之后才加载import的css文件。也就是说,import引入的css文件和html的加载是同步进行的。

一般推荐使用link标签引入外部css文件的方式

优点如下:

1.有利于SEO。引用外部css文件,使得html页面的源代码减少很多,搜索引擎蜘蛛爬行更快。

2.浏览器可以开启多线程同时下载html和css,加载速度一般会快些。

3.样式文件分离,修改样式更方便,特别是整站共用的css样式,只需修改公共css文件就行了。

css选择器

css选择器的作用是让我们找到想要修改样式的元素,然后为其修改样式。

选择器有很多种:

1.id选择器

我们给元素添加一个id属性,这个id是唯一标识,一个元素只能有一个id,一个id原则上也只能给一个元素。然后在css文件中,可以通过#id {}的方式选择这个元素。

1
2
3
4
5
6
7
<style>
#demo {
color: red;
}
</style>

<div id="demo"></div>

2.class类选择器

我们给元素添加一个class属性,这个属性为元素添加了一个类名,每一个元素可以有多个类名,同一个类名也可以赋给多个元素。然后我们在css文件中,可以通过.class {}的方式来选择添加了类名的元素。

1
2
3
4
5
6
7
<style>
.demo {
color: red;
}
</style>

<div class=”demo”></div>

3.标签选择器

通过标签名可以直接选择元素。

1
2
3
4
5
6
7
<style>
div {
color: red;
}
</style>

<div></div>

4.通配符选择器 *{}

所有标签都会被选择出来。

5.属性选择器

[id] {},这样有id属性的元素都会被选择出来,类似还有[class] {}[class="demo"] {}等。

6.父子选择器(派生选择器)

div p {}

给div下面的p加样式。

在实际开发中,因为要注意选择器的性能,一般父子选择器不超过4层。

像.wrapper .box .content .name这样不超过四层。

7.直接子元素选择器

div>strong,div直接子元素是strong才能被选择。

1
2
3
4
5
6
7
8
9
10
<div>
<strong></strong>
</div>

<div>
<em>
<strong></strong>
</em>
</div>
×

前面举的例子都是标签组合的父子选择器,id、class等也可以使用父子选择器哦。

8.并列选择器

1
2
<p class="select"></p>
<div class="select"></div>

怎么选择出类名是select的div呢?

我们可以使用div.select {}来选择

这种方式是只有div和.select同时作用在一个元素上才会被选择出来。

9.分组选择器

1
2
3
4
<p></p>
<strong></strong>
<em></em>
<div></div>

我们要想同时选择四个标签,把四个标签的样式都写一遍显然是不太合适的,这里可以用分组选择器div, p, em, strong {},这样四种标签就都被选择出来并且加上统一的样式了.

选择器的优先级

如果我们在单独的样式后面加上 !important,那么这个样式就会被赋予最高的优先级,一般后面无论怎么添加样式都不会覆盖或修改这个样式。比如:background-color: red !important;,后面怎么添加样式background-color都是红色的。

由此可见选择器是有优先级的,优先级高的选择器会覆盖优先级低的选择器的样式。

常用选择器的优先级为:!important > 行间样式(通过标签的style属性设置) > id > class|属性 > 标签 > *

我们还需要记住每一种选择器的权重值

CSS选择器 权重值

!important 无穷大

行间样式 1000

id 100

class、属性、伪类 10

标签、伪元素 1

通配符 0

和数学里不一样,在css选择器的权重值中,无穷大 + 1 > 无穷大。

有了选择器权重值,我们就可以计算父子选择器的权重值了。

1
2
3
div p {

}

这个选择器的权重值是div + p(两个标签选择器)相加后的结果。

最后需要补充一下:

  1. 我们一般不给标签加id,而是通过添加class类名来选择,因为id代表唯一标识,我们一般用id来做标记。
  2. 我们写类名的时候,一定要注意命名语义化,不要用abc之类无含义的类名。

简单的文字样式

css样式有很多 ,我们不可能一一都记住它们,我们建议记住一些常用的样式,其它的用到时再去查就可以了。

首先我们介绍一下样式的构成,它都是由属性名和属性值构成的,名和值之间用:(冒号)分隔,属性和属性之间用;(分号)分隔。

我们先来介绍几个字体相关的样式。

1
2
3
4
5
6
7
p {
font-size: 20px; // 字体大小
font-weight: bold; // 字体粗细
font-family: "Times New Roman", Georgia, Serif;// 字体系列
font-style: italic; // 字体风格
color: red; // 字体颜色
}

1.font-size

设置文字的大小,默认是16px,这条属性设置的其实是文字的高度而不是宽度。

2.font-weight

设置文字的粗细,默认值是normal,当我们设置成bold的时候,这个p标签就和strong标签没什么区别了,这就是为什么很多标签我们都不用,因为通过修改样式可以达到和其他标签一样的效果。

它的值有:

normal 默认值
bold 粗体字符。
bolder 更粗的字符。
lighter 更细的字符。
100、200、300、400、500、600、700、800、900 定义由细到粗的字符,400等同于normal,700等同于bold。

3.font-family

设置文字的样式,是黑体、宋体还是其他什么字体,默认是arial字体。

4.font-style

设置文字是否斜体,italic是斜体的意思,通过设置这个属性我们可以让p标签达到和em标签一样的效果。它可以使用斜体、倾斜或正常字体。

5.color属性

设置文字的颜色

颜色值可以有三种表达方式。

1.英文单词,如red、black、blue等等。

2.三位十六进制组成光学三原色红绿蓝,每一个的值都是00-ff,00代表空,ff代表满,例如#000000就是黑色的意思。其中,如果每连续的两位都是一样的,并且3组数字都是这种情况的时候,我们就可以两两合并只写一个数值就可以了,比如#ffffff —> #fff、 #55ffcc —> #5fc。

3.通过rgb(xx,xx,xx)属性值来设置颜色,其实和第二种是一个意思,只是把十六进制换成了十进制,rgb就是red、green、blue的缩写,三个数值每一项的范围都是0-255,例如红色就是rgb(255, 0, 0)。

接下来我们再介绍其他一些简单的样式。

6.text-indent

设置首行文字的缩进,值有两种单位,一种是px,一种是em。

这里就要提一下em和px的区别了。

px虽然不是一个绝对长度单位,不同设备上显示会有区别,但我们可以简单理解为它是一个差不多固定长度的单位。

em是一个相对长度单位,它相对的是当前元素内的文字的大小,也就是说1em = 1 * font-size,如果我们设置font-size是20px的话,那么1em就是20px,我们只要将text-indent设置成2em就可以实现首行缩进2个文字大小了。

7.border

border: 1px solid red;,设置元素边框样式。

这其实是一个复合属性,它由border-width、border-style、border-color三个属性复合而成,分别设置边框的宽度,边框的样式,边框的颜色。其中边框的样式有很多种,常用的有solid(实线)、dotted(短虚线)、dashed(长条虚线)。

border-width也是一个复合属性,它可以按照上右下左来分别设置四个边框的宽度值。

8.text-align

设置文字的位置,值有三种,分别是center(居中)、left(左对齐)、right(右对齐)。

9.line-height

设置一行文字所占的高度,默认和字体大小一样。

当我们想要让单行文字在容器内部上下居中的时候,我们只需要让height = line-height就行了。

10.text-decoration

文字装饰的意思,可以设置文字是否有下划线、上划线、中划线,分别对应的属性值是underline、overline、line-through。

11.cursor

设置我们的鼠标移入到这个元素上的时候,鼠标会变成什么样子。

它的值有很多,常用的是cursor: pointer;这条属性,它会让我们鼠标在移入这个元素的时候,变成可以点击的小手状态。

现在我们就可以来模拟一下a标签的样式了。

1
2
3
4
5
6
7
8
9
<style>
p {
color: rgb(0, 0, 238);
text-decoration: underline;
cursor: pointer;
}
</style>

<p>www.baidu.com</p>

伪类

下面介绍一下伪类。

我们可以通过在标签名后面添加伪类来达到一些效果。

1.hover伪类

这个伪类设置当鼠标移入元素的时候元素的样式。

1
2
3
4
5
6
7
8
<style>
a:hover {
font-size: 20px;
color: #424242;
}
</style>

<a href="www.baidu.com">www.baidu.com</a>

给a标签设置这样一个伪类之后,我们就可让鼠标移入a标签后,文字放大(颜色不会变)。

伪类当然不止hover一个,还有很多其他的伪类,这里就不一一介绍了。

伪类也是有权重值的,它的权重值和class一样是10。

前面鼠标移入只会有文字放大效果而不会有变色效果,就是因为行间样式的权重值是1000,而a+hover只有1+10=11。

盒模型

我们要知道,一个元素是由四部分组成的:margin、border、padding、content,分别是外边距、边框、内边距、内容区。其中content不是由属性构成的,而是由我们写的东西和width、height属性构成的。

margin:它设置的是这个元素距离外面靠近它的其他元素或者浏览器边框的距离。
这是一个复合属性,它其实是由margin-top、margin-right、margin-bottom、margin-left组成的,当然也可以分别设置每一个属性的属性值。

这个复合属性的值有4种写法:

1.margin: 10px 20px 30px 50px;分别按照上右下左的顺序设置四个外边距的大小。

2.margin: 10px 20px 30px;分别按照上、左右、下的顺序来设置四个外边距的大小,中间的那个属性值设置的是左右的外边距。

3.margin: 10px 20px;分别按照上下、左右的顺序来设置四个外边距的大小。

4.margin: 10px;四个方向都是这个值。

margin其实不属于一个盒子的模型部分,一个元素的盒子模型只有border、padding、content。

我们只需要在元素上鼠标右键点击检查或者查看,或是按住F12(windows系统),就可以查看元素设置的样式了,最下面也会有元素的盒模型。

margin合并与塌陷

css也有不完善的地方,存在或多或少的”bug”,有些我们可能从来不会遇到,有些我们可能会经常遇到,这节介绍的就是很经典的两个”bug”。

margin合并

我们写两个span标签,给它们两个分别加上margin-right和margin-left样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
<style>
.left {
margin-right: 10px;
background-color: red;
}
.right {
margin-left: 10px;
background-color: yellow;
}
</style>

<span class="left">left</span>
<span class="right">right</span>

这两个span之间的距离正是我们所想的那样是20px,

下面我们写两个div,分别给它们加上margin-bottom和margin-top的样式,我们再看看效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<style>
.top {
margin-bottom: 10px;
background-color: red;
}

.bottom {
margin-top: 10px;
background-color: yellow;
}
</style>

<div class="top">top</div>
<div class="bottom">bottom</div>

这次我们惊奇地发现,这两个div上下之间的距离,并不是我们所想的那样是相加的20px,而是只有10px。

这个现象就是margin合并。

我们尝试改变每一个div的margin-top或者margin-bottom的值,最后会发现:二者上下之间的距离取的是两个数值之中的最大值。

margin塌陷

当我们给父子结构的两个div分别设置margin-top的时候,就会出现这个bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<style>
.wrapper {
width: 100px;
height: 100px;
margin-top: 100px;
margin-left: 100px;
background-color: yellow;
}

.content {
width: 50px;
height: 50px;
margin-top: 50px;
margin-left: 50px;
background-color: red;
}
</style>

<div class=“wrapper”>
<div class=“content”></div>
</div>

我们写这段代码的原意是想要一个100100大小的div,然后里面有一个5050大小的子div。这个div在父级div的右下角,同时父级div距离浏览器的边框有100px的距离。

然而实际结果并不是这个样子的,子级div的margin-left正常显示了,但是margin-top的效果并不是我们所想的那样距离父级div的距离是50px。

由于子级的div距离浏览器边框的距离是50px,父级div本身有一个margin-top值,所以导致我们子级的margin-top的效果并没有显现出来。我们再改变一下子级div的margin-top值,改成200px,我们惊奇地发现,子级div不仅没有距离父级div有一段距离,反而带动了父级div一起向下移动了!这就是margin塌陷现象。

解法

答案是触发bfc。

bfc全称是block format context——块级格式化上下文,我们有一些css语法会触发bfc,触发bfc的元素的渲染规则和普通元素的渲染规则相比会变得不一样,从而可以解决塌陷问题。

我们可以利用overflow属性来触发bfc。

overflow是一个css属性,它可以设置当内容区超过了当前元素的区域的时候,我们采取怎样的处理方式,这个属性也可以触发bfc。

现在我们在父级div.wrapper里面加一条属性:overflow:hidden;

这条属性的意思是溢出隐藏。现在我们发现,在外观没有改变的同时,子级div和父级div解除了绑定,可能正常移动了!我们一般可以采用这种方式来解决margin塌陷的问题。

虽然overflow:hidden;的方式可以采用,但也不是没有缺点,一旦我们用js代码改变了子级div的位置,就会有导致子级一部分内容因为溢出而被隐藏。

理解了margin塌陷的解法之后,我们就很容易可以理解margin合并的解法了。

我们给每一个div分别加上一个父级包裹层,然后给父级包裹层都加上overflow:hidden;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<style>
.wrapper{
overflow: hidden;
}

.top {
margin-bottom: 100px;
background-color: red;
}

.bottom {
margin-top: 100px;
background-color: red;
}
</style>

<div class="wrapper">
<div class="top">top</div>
</div>
<div class="wrapper">
<div class="bottom">bottom</div>
</div>

这样通过父级div来触发bfc就解决了margin合并的问题。

定位与层模型

层模型

css中元素的层次模型主要是由position这个属性来决定的。

position的意思是定位,它一共有四个值,分别是static、absolute、relative、fixed。

1.static

static是默认值,当我们没有写position属性的时候,元素默认的定位就是static。

2.absolute

absolute是绝对定位的意思,它会使元素脱离本来的位置再进行定位,当元素脱离原来的位置之后,其他的元素就看不到这个元素了。同时,absolute也可以触发bfc。

当我们改变定位之后,这个元素就有四个属性可以使用了,分别是left、right、top、bottom。这四个属性分别可以设置当前元素距离左边、右边、上边和下边的距离为多少,这四个属性很少一起出现,一般都是两两一对出现,其中left和top是一对,right和bottom是一对。

1
2
3
4
5
6
7
8
9
10
<style>
div {
width: 100px;
height:100px;
backgroud-color: red;
position: absolute;
left: 100px;
top: 100px;
}
</style>

这个div就会脱离原来的位置,然后距离浏览器上边框和左边框分别100px的距离。

absolute的参照物是距离它最近的有定位(非static)的父级,当没有父级有定位时,元素会相对于浏览器边框进行定位。

3.relative

relative是相对定位的意思,它会让元素保留原来位置再进行定位,后面的元素可以看到它本来的位置。

当position改成relative之后,left、top、right、bottom进行的定位就会变成相对于自身的位置进行移动了。relative定位的参照物是元素自身。

当我们仅仅给元素设置position:relative,而没有设置left、right、top、bottom属性的时候,元素的定位是没有发生任何改变的,正因为这个特性,一般在开发中,relative都是用作设置参照物,一个absolute元素要相对于那个元素进行移动,就给那个元素设置relative定位就可以了。

我们通过例子来看一下absolute和relative的区别。

我们现在有这样一个结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<style>
.wrapper {
width: 200px;
height: 200px;
background-color: orange;
margin-top: 100px;
margin-left: 100px;
}
.box{
width:100px;
height: 100px;
background-color: black;
margin-left:100px;
}
.content{
width: 50px;
height: 50px;
background-color: yellow;
}
</style>

<div class="wrapper">
<div class="box">
<div class="content"></div>
</div>
</div>

现在我们给content加上绝对定位的样式。

1
2
3
4
5
6
7
8
9
<style>
.content{
position: absolute;
left: 50px;
width: 50px;
height: 50px;
background-color: yellow;
}
</style>

这个时候content那个黄色的小方块会跑到橘黄色的方块外面。

这是因为,当我们给content设置position:absolute,由于没有父级元素有定位,所以content会相对于浏览器边框定位,这时content的left属性就是相对于浏览器边框左边有50px的意思。

如果我们把content的定位换成relative,content黄色小方块在黑色方块的左上角,然后relative相对于自身的位置进行定位,这个时候left属性的意思就是相对于本来在黑色左上角的那个位置向右移动了50px。

因此,总结一下absolute和relative的特点:

absolute:

1.脱离原来位置进行定位

2.相对于最近的有定位的父级进行定位,如果没有那么相对于浏览器边框定位。

relative:

1.保留原来位置定位

2.相对于自己原来位置进行定位,一般被用来设置参照物

4.fixed

fixed定位是相对于视口定位,我们在网页上都见过左右两边不随着鼠标滚轮滚动而改变位置的广告栏,就是用fixed定位的。

1
2
3
4
5
6
7
8
9
10
11
12
<style>
.fixed {
position: fixed;
right: 0px;
top: 200px;
height: 200px;
width: 50px;
background-color: red;
}
</style>

<div class="fixed"></div>

.fixed这个元素会一直在视口的右边,不随着滚轮滚动而改变相对于视口的位置。

学习了定位之后,我们就可以实现元素水平垂直居中的效果了。

1
2
3
4
5
6
7
8
9
10
11
12
<style>
div {
width: 100px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -50px;
margin-top: -50px;
background-color: red;
}
</style>

这个div就会在有定位的父级里面水平垂直居中哦。

我们还可以实现多栏布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<style>
* {
margin: 0px;
padding: 0px;
}
div {
height: 100px;
}
.left {
position: absolute;
left: 0;
width: 100px;
background-color: yellow;
}
.right {
position: absolute;
right: 0;
width: 100px;
background-color: green;
}
.mid {
margin-left: 100px;
margin-right: 100px;
background-color: red;
}
</style>

<div class="left"></div>
<div class="right"></div>
<div class="mid"></div>

我们首先固定左侧和右侧的两个div,然后让中间的div分别给左侧和右侧留出一个固定宽度的margin之后,让自身自适应屏幕的大小即可实现三栏布局效果。

浮动模型

浮动模型

我们首先写一个二级结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
.wrapper {
width:400px;
height: 100px;
border: 1px solid red;
}
.content {
width: 100px;
height: 100px;
background-color: black;
color: white;
}
</style>

<div class="wrapper">
<div class="content">1</div>
<div class="content">2</div>
<div class="content">3</div>
</div>

现在我们给content加上一个特殊的属性:float

1
2
3
4
5
6
7
8
9
<style>
.content {
width: 100px;
height: 100px;
float: left;
background-color: black;
color: white;
}
</style>

.content元素就会像站队列一样排列了,这就是float属性的效果哦。

下面来正式介绍一下float属性

float属性可以让元素像站队列一样浮动起来,它会让本来占满整行的元素只按照内容和设置的大小在父级里面进行站队排列,当这一行剩余的空间不足以再放下一个元素的时候,元素就会自动换行,到下一行去进行浮动排列。当容器不够大的时候,虽然内容会超出容器的范围,但是超出之后仍然会按照相同的队形来进行站队。

浮动元素会像absolute的元素一样脱离文档流,但不会脱离文字流,这是什么意思呢?

我们来举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<style>
.top {
width: 100px;
height: 100px;
background-color: red;
float: left;
}
.bottom {
width: 200px;
height: 200px;
background-color: black;
color: white;
}
</style>

<div class="top"></div>
<div class="bottom">我是文字,我能看到文字流哟~</div>

脱离文档流的意思就是正常的元素看不到它了,这一点很类似absolute属性,不脱离文字流的意思则是display属性是inline或者inline-block的元素还是可以看到它的,文字本身是inline属性的。

接下来,我们讲.bottom这个div的display改成inline-block,再看一下效果。

1
2
3
4
5
6
7
8
9
<style>
.bottom {
width: 200px;
height: 200px;
background-color: black;
color: white;
display: inline-block;
}
</style>

我们发现下面的黑色方块并没有移动到下一行,而是紧跟着红色浮动方块进行了排列,但是我们并没有给黑色方块设置浮动效果啊?

这是因为,float属性会自动将这个元素的display给改成inline-block,也就是说,只要有float属性,那么这个元素的display就是inline-block,这就是为什么红色浮动方块没有独占一行的原因了。

float属性只有两个值:left和right,默认的状态是none。

right的效果就是从右边开始排列,left则相反。

下面我们来总结一下float属性

1.可以像absolute一样,让元素浮动起来,产生自己独有的浮动流。

2.脱离标准的文档流,但不会脱离文字流,正常的元素看不到它,但是有文字属性inline或inline-block的元素可以看到它。

3.在内部把改元素变成display: inline-block。

开发中,一般进行网状布局时可以使用float属性。当我们不知道容器里面会盛放多少个子元素,但是这些子元素又是按照一样的格式进行排列的,我们可以设置浮动来进行流式布局。浮动属性还可以让我们实现像报纸那样文字包围在图片四周的效果。

那么如何清除浮动流呢?

我们说了,元素浮动起来之后,正常的元素就看不到它了,包裹它的父级自然也看不到它了,我们想让父级根据里面浮动的子元素来自适应宽高,如果不清除浮动的话,父级就只剩下一条线了!

那么怎么来清除浮动呢?

1.我们先来一个不规范的写法解释下原理,我们在父级里面的内容区最后加一个p标签。

1
2
3
4
5
6
<div class="wrapper">
<div class="content">1</div>
<div class="content">2</div>
<div class="content">3</div>
<p class="clear"></p>
</div>

我们给这个p标签增加清除浮动的样式:

1
2
3
4
5
6
<style>
.clear {
/* clear属性专门用来清除浮动的,虽然有还有left和right值,但是我们清除浮动一般都写both值。 */
clear: both;
}
</style>

这时,父级的wrapper已经正常包裹住子元素了。

但实际上,并不是父级清除了浮动流,而是被p撑开了,p.clear标签能看到上面浮动的元素,wrapper能看到不浮动的p标签,因此就把p标签包裹进去了,仅此而已。

我们要知道html中的标签是做结构规划的,这个p标签只是清除浮动的功能,不具备p标签的段落语义,这种代码在html里是不规范的,因此不适用。

所以我们采用添加伪元素的方法

我们首先介绍一下伪元素。

伪元素是一种不能单独存在的元素,它必须要依附于其他元素标签使用,伪元素常用的一共有两个:after和before。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
span {
background-color: yellow;
}
span::before {
content: '前';
background-color: red;
}
span::after {
content:'后';
background-color: black;
}
</style>

<span>测试</span>

当我们不想改变html结构,又想增添一些东西的时候,伪元素after和before就非常实用。

要注意的是,写伪元素,即使内容是空的,也要加上content这个属性content:””。

既然是伪元素,那么自然也属于元素了,我们可以改变伪元素的display为block,从而可以改变宽高等块级元素才有的样式。

了解伪元素之后,我们根据第一种方式,为父级元素添加一个after伪元素,让这个伪元素专门实现清除浮动的功能:

1
2
3
4
5
6
7
<style>
.wrapper::after {
content: '';
clear: both;
display: block;
}
</style>

这就是我们通常使用的清除浮动的方法。

然而在ie6、ie7并没有伪元素这种东西,怎么办?

前面我们介绍了bfc,这里我们介绍ie6、ie7独有的一个东西——hasLayout。只要触发了hasLayout就和触发bfc有差不多的作用,我们可以用zoom属性来触发它。

1
2
3
4
.wrapper {
/* 视口同比例放大还是缩小,1就是不变 */
zoom: 1;
}

当我们写上这个元素的时候,ie6、ie7也可以清除浮动了。

不过我们其他的浏览器并不需要zoom这个属性,这个属性只是为了ie6和ie7准备的,这个时候我们需要一点点css hack,我们在zoom前面加一个号, zoom: 1; 这个符号只有ie6和ie7能够识别,其他的浏览器都不识别,这样就可以让ie6和ie7去读这一行属性,其他浏览器直接忽略。顺便一提属性前面加上”_”之后,就只有ie6可以识别了(_zoom: 1)。

另外我们还可以触发bfc来清除浮动

有很多属性都可以触发bfc:

① 给.wrapper添加overflow:hidden; 可以让父级包裹住浮动子元素。

② 给.wrapper添加display: inline-block; 可以让父级彻底包裹住浮动子元素。

③ 给.wrapper添加position:absolute; 可以让父级彻底包裹住浮动子元素。

虽然这三种方式都可以清除浮动,但都会改变样式,不符合开发规范,因此我们基本不使用。

我们学习了浮动模型之后,也可以用这种方式来实现三栏布局哦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<style>
.left {
width: 50px;
height: 50px;
background-color: red;
float: left;
}
.right {
width: 50px;
height: 50px;
background-color: yellow;
float: right;
}
.mid {
margin-left: 50px;
margin-right: 50px;
height: 50px;
background-color: green;
}
</style>

<div class="left"></div>
<div class="right"></div>
<div class="mid"></div>

css背景图片和其它

单行文字溢出打点

溢出打点是指当文字超过我们所规定的范围,后面的文字就以”…”的形式表现。

它由三个属性组合来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<style>
div {
width: 300px;
height: 20px;
background-color: red;

overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>
<div>
单行文字溢出测试单行文字溢出测试单行文字溢出测试单行文字溢出测试单行文字溢出测试单行文字溢出测试单行文字溢出测试单行文字溢出测试
</div>

我们可以看到文字溢出打点了。

现在我们来介绍这三个属性

1.overflow: hidden; 让文字溢出容器的部分隐藏起来。

2.white-space: nowrap; 让文字不换行。文字默认是换行的。

3.text-overflow: ellipsis; 文字溢出之后,怎么处理。这里的处理方式是以点状显示。

这样三条属性配合使用,就可以实现单行文字溢出打点的功能了。

多行文字溢出打点

多行文字溢出打点的css兼容性不是很好,需要较高版本浏览器的支持,所以有的是通过计算文字宽高,然后手写”…”实现的。

如果我们想要以属性的方式实现,可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
<style>
div {
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
background-color: red;
color: black;
}
</style>

这里的-webkit-前缀的意思是webkit内核的浏览器私有属性。

背景图片

我们知道<img>标签可以展示图片,但是如果我们想要在图片上面写字的话,这个标签就不太好用了,这个时候就需要用到一个叫作background-image(背景图片)的属性了。

1
background-image: url();

url里面放图片地址,这样我们就可以给一个元素设置背景图片了,然后元素里面正常写文字就可以了。

还有一些其他的属性专门用来修饰背景图片样式的:

background-size

设置背景图片大小。

它有很多值:

1.cover(覆盖)

可以让浏览器用一张图片尽可能填充满我们的容器,当图片大的时候会截掉多余部分。

它的填充机制是:先等比例放大(或缩小)图片至宽度或高度等于元素的宽或高,看哪种方式可以让一张图片填充满整个元素,再裁减掉多余的部分。

2.contain(包含)

尽可能展示图片的全部信息,如果有剩余空间,就填充相同的图片。

它的填充机制是:固定一条边之后,让另外一条边可以全部显示,有剩余的部分就填充相同的图片。

3.设置x y 像素值

1
background-size: 50px 50px;

这样会让浏览器强行把图片展示成50*50的大小。

4.百分数

1
background-size: 50% 50%;

会让图片占容器宽高的50%,子级元素的百分比相对数值是父级里面这个属性的值,比如父级的高度是100px,那么子级的50%就是50px。

background-repeat

设置背景图片是否可以重复显示,默认是重复的repeat。也就说默认状态下,容器空余很多的时候,就会有相同的图片来填充容器剩余的空间。

而值为no-repeat时,不论容器剩余多少空间,都不会进行图片复制填充。

background-position

设置背景图片开始的位置,它的值同样有很多种:

1.像素

1
background-position: 50px 50px;

这样背景图片就会从容器的x轴50px y轴50px的位置开始显示。

2.百分比

1
.background-position: 50% 50%;

当写50% 50%的时候,图片刚好会在容器的正中央显示。

》》》》》》》》

js(ECMAScript 5)

初识JavaScript

JavaScript作为Netscape Navigator浏览器的一部分首次出现在1996年,它最初的设计目的是为了改善网页的用户体验。

ECMA为了统一js,推出了ECMA标准,因此js语言也可以被称为ECMAScript。本章我们介绍的是ECMAScript 5的知识,es6会在后面章节介绍。

js有两个特点

  1. 解释性语言,不需要代码编译,可以跨平台。

  2. 单线程

在浏览器中,js有三大部分:ECMAScript、DOM、BOM。

  1. ECMAScript:符合ECMA标准的javascript语言。

  2. DOM:文档对象模型(Document Object Model),可以操作网页。

  3. BOM:浏览器对象模型(Browser Object Model),可以操作浏览器。

引入JavaScript

在网页中引入JavaScript有两种方法。

1.页面内嵌script标签

我们可以在<head></head>标签或者<body></body>标签里面写一个<script></script>标签,这个标签有一个type属性,我们赋值为”text/javascript”,也可以不写type属性,浏览器默认就是这个。

写完标签后,我们就可以在这个标签里写js代码了。

2.引入外部js文件

script标签有个src属性可以引入外部js文件。

一个script标签只能在里面写代码或者引入外部js文件,不能既引入又在里面写代码。

我们一般采用第二种引入外部js文件的方法,结构(html)、样式(css)、行为(js)相分离,方便维护。

浏览器在加载html文件的时候,当遇到link标签会异步加载,但遇到script标签,会阻塞后面内容的加载,直到js文件下载并执行完毕。

这样就会导致两个问题

1.如果js操作DOM的话,页面的DOM还没有解析,js操作DOM就会出错。

2.如果js文件较大、执行较慢,会导致页面一直没有内容出现。

所以,我们一般会把script标签写在body标签中最后一行。

JavaScript基本语法

变量声明

js是弱数据类型语言,任何类型的变量都可以用关键字var来声明

1
2
3
var  arr = [1, 2, 3];
var num = 123;
var string = "abc";

也可以先声明,再赋值

1
2
var num;
num = 123;

当我们要声明多个变量的时候,一般采用下面这种写法节省代码量

1
2
3
var num1 = 123,
num1= 123,
num2 = 123;

变量命名规则

1.以英文字母、_、$符号开头。

2.变量名可以包括数字。

3.不可以使用关键字、保留字作为变量名。

关键字是被系统定义了语法的单词,如var、window、if、else等。

保留字是以后可能会变成关键字而做的保留词,如enum、abstract等。

值类型

js数据的值主要分为两大类:

1.原始值

Number、String、Boolean、undefined、null

undefined是未定义的意思,null是空值的意思。

1
2
3
4
var demo = null;
console.log(demo); //null
var a;
console.log(a); //undefined

2.引用值

主要有:数组array、对象object、函数function

1
2
3
4
5
6
7
var arr = [1, 2, 3, 4];
var object = {
name: "demo",
}
console.log(arr);// 1,2,3,4
console.log(object);// [Object Object]
console.log(object.name);// demo

这两种数据类型的区别在哪里呢?

举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var num = 123,
num1 = num;
num = 234;
console.log(num);// 234
console.log(num1);// 123

// num的改变对num1完全没有影响。

var arr = [1, 2, 3],
arr1 = arr;
arr.push(4);
arr.push(5);
console.log(arr);// 1,2,3,4,5
console.log(arr1);// 1,2,3,4,5

我们发现我们只是改变了arr的值,但是arr1也跟着改变了,这就是两种数据类型的第一个区别。

如果我们给arr重新赋值一个新的数组的话,那么arr1并不是这个新的数组。

1
2
arr = [1, 2, 3];// 重新开辟了一个堆空间
console.log(arr1);// [1, 2, 3, 4, 5]

因为原始值是存放在栈里面的,而引用值是存放在堆里面的,原始值的赋值是把值的内容赋值给另一个变量,但是引用值却不是这样。引用值的变量名存在栈里面,但是值却是存在堆里面的,栈里面的变量名只是指向了一个堆空间,这个堆空间存的是我们一开始赋的值,当我们写arr1 = arr的时候,其实是把arr1指向了和arr指向的同一个堆空间,这样当我们改变arr的内容的时候,其实就是改变这个堆空间的内容,自然同样指向这个堆空间的arr1的值也随着改变了。

栈内存不同,它被赋值后就不可以改变了,即使我们给num重新赋值为234,也是在栈里面重新开辟了一块空间赋值234,前面存放123的空间还存在,只是没有指针指向这里罢了。

两种数据类型的第二点区别是:原始值不可以被改变,引用值可以被改变。

1
2
3
4
5
6
var arr = [1, 2, 3, 4];
arr.length = 2;
console.log(arr);//1,2
var str = "1234";
str.length = 2;
console.log(str);//1234

这里涉及了一个包装类的概念,我们后面会提到。

算术运算符

1.+ 运算符

作用:

1.数学上相加的功能

2.拼接字符串

字符串和任何数据相加都会变成字符串

1
2
var num = 1 + 2 + "3";
console.log(num);//33

2.– 运算符

数学上的相减功能

3.* 运算符

数学上的相乘功能

4./ 运算符

数学上的相除功能

5.% 运算符

数学上的取余功能

6.= 运算符

赋值运算符

7.== 运算符

比较运算符中的等于,不属于严格等于,严格等于是”===”三个等号

8.() 运算符

和数学上一样,加括号的部分优先级高

1
2
var num = 1 + "2" + (1 + 1);
console.log(num);//122

9.++ 运算符

自加一运算,当写在变量前面的时候是先加1再执行运算,写在变量后面的时候是先运算再加1。

1
2
3
4
5
var num = 1;
console.log(num ++);//1
console.log(num);//2
console.log(++num);//3
console.log(num);//3

相类似的运算还有—- 运算符

10.+= 运算符

让变量加多少

1
2
3
var num = 1;
num += 10;
console.log(num);//11

相类似的还有 -=、/=、*-、%= 等等

比较运算符

比较运算符有 > 、< 、>= 、<= 、!= 、==、不严格等于、===严格等于。

下面介绍一下不严格等于和严格等于的区别

不严格等于是当我们比较两个数据的时候,是先转化成同一个类型的数据之后再进行比较的,而严格等于是两个数据不进行数据转化也相等。

1
2
"2" === 2//false
"2" == 2//true

NaN不等于任何数据,包括它本身,但undefined就等于它本身。

1
2
3
4
5
NaN == NaN//false
NaN === NaN//false
undefined == undefined//true
undefined === undefined//true
var demo = !!"abc";//true

这里的demo要进行取反运算,先进行了类型转换为布尔值false,再次取反之后为true,取反运算可以用来将数据转换成布尔值。

逻辑运算符

逻辑运算符主要有 && 和 || (与和或)

1
var demo = 1 < 2 && 3;//3

&&的作用是只有是true的时候,才会继续往后执行,一旦第一个表达式就错了,后面的第二个表达式根本不执行。如果前面表达式的返回结果都是true的话,那么&&的返回结果就是最后一个表达式的结果,如样例中的demo是3。

||的作用是只要有一个表达式是true,那么就结束,后面的就不走了,返回的结果就是这个正确的表达式的结果,如果前面的表达式都是false的话,那么返回结果就是最后一个表达式的结果。

1
var demo = 2 < 1 || (1 == 1);//true

&&有当做短路语句的作用,比如我们可以把简单的if语句用&&来写,因为只有第一个条件成立的时候,才会运行第二个表达式。

1
2
if(flag) { console.log("hello"); };
flag && console.log("hello");

||有赋初始值的作用,有时候我们希望函数参数有一个初始值,在不使用ES6的语法的情况下,可以使用或语句。

1
2
3
function demo (example) {
var example = example || 100;
}

不过这里有一个缺点,当我们传的参数是一个布尔值且为false时,会忽略掉我们传递的这个参数值而取默认初始值,ES6中的默认赋值方式可以避免这个问题。

1
2
function demo (example = 100) {
}

默认为false的值

所有的值都可以被转换成true或false,转换成true的值实在太多了,我们只需记住转换成false的值即可,其他的都是true。

默认为false:
false、undefined、null、””、NaN、0(+0、-0)。

1
2
3
4
5
6
console.log(Boolean(undefined));//false
console.log(Boolean(null));//false
console.log(Boolean(""));//false
console.log(Boolean(NaN));//false
console.log(Boolean(0));//false
console.log(Boolean(false));//false

显示类型转换

操作符typeof可以检测数据的类型。

1
console.log(typeof(123));//number

typeof能返回的类型一共只有6种:

number、string、boolean、undefined、object、function

• 数组和null类型都属于object,null通常用来作为对象占位符,所以归到了object里面。

• NaN属于number类型。非数也是数字的一种。

• typeof返回的结果是字符串

1.Number(mix)

这个方法可以把其他类型的数据转换成数字类型的数据

1
2
3
Number("123") // 123
Number(true) // 1
Number(undefined) // NaN

2.parseInt(string, radix)

这个方法可以将字符串转换成整型数字,第二个参数radix基底是可选参数,它的范围是2~36。

当参数string里面既包括数字字符串又包括其他字符串的时候,它看到其他字符串就不会继续转换后面的数字型字符串了。

1
2
3
4
5
parseInt("123abc345")//123
parseInt("abc123")//NaN
parseInt("123")//123
parseInt("abc")//NaN
parseInt(true)//NaN

第二个参数radix作用是,把第一个参数的数字当成几进制数字来转换成十进制。

1
2
var demo = 11;
console.log(parseInt(demo, 16));//17

3.parseFloat(string)

这个方法和parseInt方法类似,是将字符串转换成浮点类型的数字,同样是碰到第一个非数字型的字符停止,但是由于浮点型数据有小数点,所以它还会识别第一个小数点以及后面的数字,但是第二个小数点就无法识别了。

1
2
3
parseFloat("123.2.3")//123.2
parseFloat("123.2abc");//123.2
parseFloat("123.abc")//123

4.toString(radix)

这个方法是对象上的方法,绝大多数数据类型都可以使用(undefined和null没有toString方法),它将数据转换成字符串类型,涉及到包装类的一些知识。radix是可选参数。

1
2
3
var demo = 123;
demo.toString();//'123'
true.toString()//'true'

当写radix基底的时候,代表我们要将这个数字转化成几进制的数字型字符串。

1
2
var demo = 10;
demo.toString(16)//a

5.String(mix)

把任何类型转换成字符串类型。

1
typeof(String(123));//string

6.Boolean(mix)

把任何类型转换成布尔类型

1
2
3
4
5
Boolean(0);//false
Boolean(undefined);//false
Boolean(null);//false
Boolean("");//false
Boolean(NaN);//false

隐式类型转换

1.isNaN()

这个方法可以检测数据是不是非数类型。

1
2
3
isNaN(NaN);//true
isNaN("abc");//true
isNaN(123);//false

这中间隐含了隐式转换,它会先将你传的参数调用一下Number方法之后,再看看结果是不是NaN。

2.算术运算符

++会将数据调用一遍Number之后,再自加一。

1
2
3
4
5
6
var demo = "abc";
demo ++;//NaN
demo = "123";
++demo;//124
demo = "123"
demo++;//123 而非 "123"

++放在后面,虽然是执行完之后才加一,但是执行之前就会调用Number进行类型转换。

3.一目运算符

+、-、*、/在执行之前都会先进行类型转换,转换成数字类型再进行运算。

1
2
3
4
5
6
7
8
9
10
11
var num = false;
+num;//0
var demo = true;
-demo;//-1
var demo = "abc";
+demo;//NaN
1 * "2";//2
true * false;//0
false / false;//NaN
true / false;//infinity无穷大
-true / false;//-infinity

4.逻辑运算符

&&和||都是先把表达式调用Boolean,换成布尔值再进行判断是true还是false,不过返回的结果还是本身表达式的结果。

5.!取反操作符

返回的结果也是调用Boolean方法之后的结果

1
!"abc";//false

当然也有不发生类型转换的比较运算符

===严格等于

!==严格不等于

1
2
3
4
"123" === 123;//false
true === "true";// false
1 !== "1";// true
1 !== 1;// false

对象创建方法

对象的创建方法有三种:

1.对象字面量

1
var obj = {};

这样方式叫做字面量,是我们创建对象最简单、最常用的方法。

对象里面有属性,属性之间用逗号分隔,每一条属性都有属性名和属性值,属性名和属性值之间用冒号分隔。

2.构造函数

构造函数分两种:系统自带的构造函数和我们自定义的构造函数。

我们先介绍下系统自带的构造函数

创建对象的构造函数是Object

1
var obj = new Object();

通过这条语句,我们创建了一个空对象。

它和var obj = {};的作用是一样的。

系统自带的构造函数还有很多,比如Number、String、Boolean、Array。

我们常用的还是自定义构造函数

自定义构造函数也是正常的函数,我们为了区分它们,我们一般会把构造函数的首字母大写。

这里就声明了一个构造函数Person。

1
function Person() {}

有了构造函数后,我们就可以用new操作符来创建对象了。

1
2
var person = new Person();
typeof oPerson;//object

这里我们创建了一个对象person,不过现在这个对象是空对象,因为我们构造函数什么都没有写,我们也没有给这个对象添加任何属性。

另外,用new操作符创建出来的对象,尽管都是使用的是同一个构造函数,但是对象之间是没有关联的。

1
2
3
4
var person1 = new Person();
var person2 = new Person();
person1.name = "111";
console.log(person2.name);//undefined

person1和person2之间没有关联,它们是两个单独的对象。

我们在构造函数里面写一些对象默认就有的属性

1
2
3
4
5
6
function Person() {
this.name = "scarlett",
this.age = 17
}
var person = new Person();
person.name;//scarlett

当然构造函数既然是函数,那么就可以传参数。

1
2
3
4
5
6
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person("scarlett", 18);
person.age;//18

创建对象的时候,只有使用new操作符才会有this。

为什么我们通过new操作符可以创建互相独立的对象呢?

其实,当我们调用new操作符的时候,这个new在我们的构造函数里面隐式创建了一个this对象,并且最后返回了这个this对象,这就是为什么我们通过new可以创建一个对象的原因。

1
2
3
4
5
function Person(name) {
//var this = {};
this.name = name;
//retrun this;
}

如果我们在构造函数首行手动创建一个对象,比如that对象,然后最后返回that,那么里面的this就没有用了,这样我们为属性赋值就要用that了。

1
2
3
4
5
6
7
8
9
function Person (name) {
var that = {
name: "scarlett"
};
that.name = name;
return that;
}
var person = new Person("demo");
person.name;//demo

如果我们最后返回对象,那么this就失效。如果最后显示返回的是原始值,那么this还是有效的。

1
2
3
4
5
6
7
8
function Person() {
var that = {};
that.name = "that";
this.name = "this";
return 123;
}
var person = new Person();
person.name;//this

对象属性的增删改查

1.增

1
2
3
var obj = {};
obj.name = "scarlett";
obj.name;//scarlett

我们可以通过对象名+点+属性名的方式来给对象添加新的属性并赋值。

2.改

修改操作和增加操作其实是一样的,只要调用相同的属性名然后赋一个新值就可以了。

1
2
3
4
5
var obj = {
name: "scarlett"
}
obj.name = "demo";
obj.name;//demo

3.查

1
2
obj.name = 'demo';
console.log(obj.name);//demo

4.删

删除属性的操作我们需要借助delete操作符

1
2
3
4
5
6
var obj = {
name: "scarlett"
}
obj.name;//scarlett
delete obj.name;
obj.name;//undefined

对象的枚举

查看对象属性我们知道可以用obj.name这样的点操作符来查看,但是我们还有一种其它方式:

1
obj['name']

这样我们就可以用for-in操作符来遍历对象了。

1
2
3
4
5
6
7
8
var obj = {
name: "scarlett",
age: 18,
sex: "female"
}
for(var prop in obj) {
console.log(prop + ":" + obj[prop]);
}

我们for-in循环会按照属性的顺序取出属性名然后赋给prop,for-in循环会把原型里面的属性一起打印出来。

下面我们介绍三种操作符

1.hasOwnProperty

这个操作符的作用是查看当前这个属性是不是对象自身的属性,在原型链上的属性会被过滤掉。如果是自身的,就返回true,否则返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {
this.name = "scarlett";
}

Person.prototype = {
age: 18
}
var oPerson = new Person();
for(var prop in oPerson) {
if(oPerson.hasOwnProperty(prop)) {
console.log(oPerson[prop]);
}
}

这样,我们的for-in循环只会打印自身的name属性而不会打印原型上的age属性。

2.in操作符

这个操作符的作用是查看一个属性是不是在这个对象或者它的原型上

1
2
3
"name" in oPerson;//true
"age" in oPerson;//true
"sex" in oPerson;//false

3.instanceof操作符

这个操作符的作用是查看前面的对象是不是后面的构造函数构造出来的

1
2
3
4
oPerson instanceof Person;//true
oPerson instanceof Object;//true
{} instanceof Object;//true
{} instanceof Person;//false

也可以理解为:后面的构造器的原型是否在前面对象的原型链上。

包装类

我们前面提到过,原始值是不可以改变的,只有对象才有属性和方法,那么这个又是什么情况呢?

1
2
var str = "abcd";
console.log(str.length);//4

按理说字符串是原始值,是没有属性的,但是这里确实可以查看length这个属性。

这里就涉及到一个叫做包装类的知识了。

1
str.length

我们在调用执行这一行代码之前,程序会自动把str包装成一个字符串对象,在这一行代码执行完毕之后再销毁这个字符串对象。

1
2
3
4
var str = "abcd";
//var str1 = new String("abcd");
str.length;//str1.length
//销毁str1

这里的str.length的str其实是上一句包装好的str1。str1是对象,上面有属性和方法,然后就可以打印length属性了。在执行完str.length这一行代码后,str1这个对象就被销毁了。

这也就是为什么我们在执行str.length = 2这句话之后,再打印str.length还是4的原因。

1
2
3
4
5
var str = "abcd";
//var str1 = String("abcd");
str.length = 2;//str1.length = 2;
//销毁str1
console.log(str);//abcd长度还是4

其他类型的数据也是一样的,当我们给原始值加属性的时候,都是先隐式包装成对象,然后赋完属性值之后再销毁这个对象。

1
2
3
var bool = true;
bool.len = 4;
console.log(bool.len);

数组的声明

数组的声明方式一共有两种:

1.字面量方式声明数组。

1
var arr = [];

2.通过数组构造函数来声明数组。

1
2
var arr = new Array(1, 2, 3, 4);
console.log(arr);//1 2 3 4

这种方式和字面量方式没有区别,但是要注意的是,如果我们在构造函数里面只写一个数字new Array(5);这个时候这个数字就不是第一个值是5的意思了,而是我们创建一个长度为5的数组。

1
2
var arr = new Array(10);
console.log(arr);//一个长度为10的空数组

数组的读写

js的数组是弱数据类型的数组,不像其他语言那样严格。

溢出读为undefined

1
2
var arr = [1, 2];
console.log(arr[3]);//undefined

可以溢出写

1
2
arr[5] = 5;
console.log(arr);//1,2,,,5

数组的常用方法

数组的方法大致可以分类两类:不改变原数组的和改变原数组的。

1.改变原数组的

改变原数组的方法主要有:reverse,sort,push,pop,shift,unshift,splice

• reverse

reverse是使数组倒序。

• push

push是在数组最后的位置增加数据。

1
arr.push(4, 5, 1);

• pop

pop是从数组后面删除一位数据,同时返回这个被删除的数据,没有参数。

• shift

从数据的最前面删除一位数据,同时返回这个数据,没有参数。

• unshift

在数据的最前面添加数据,和push一样的用法。

• splice

这个方法是截取的意思,它有三个参数,第一个是截取开始的位置,第二个是截取的长度,第三个是参数是一组数据,代表我们要在截取的位置添加的数据。

1
2
3
var arr = [1, 2, 3, 4, 5];
arr.splice(1, 2, 100, 188);
console.log(arr);//1 100 188 4 5

如果我们不写要添加的数据的话,这个方法就变成了在数组中删除数据了。

如果我们截取的长度是0,然后添加数据的话,这个方法就变成了在数据的特定位置添加数据了。

• sort

这个方法是排序的意思。

它默认按字典序来排序。

1
2
3
4
arr --> 1 2 5 4 3
arr.sort --> 1 2 3 4 5
arr --> a c b d
arr.sort --> a b c d

我们还可以自定义排序规则

1
2
3
arr.sort(function(a, b) {
return a.age < b.age;
})

这里的a、b代表的是数组里面任意的两位数据。

无论中间的规则怎么写,系统只关注函数最后的返回值是正数还是负数。

负数的时候,表示a在前面,b在后面。

正数的时候,表示a在后面,b在前面。

如果我们这样写,就是乱序排序:

1
2
3
4
function (a, b) {
var num = Math.random() - 0.5;
return num;
}

不改变原数组的方法

不改变原数组的方法主要有:

concat,join

• concat

这个方法是连接数组的作用。

1
2
3
4
arr1 = [1, 2];
arr2 = [2, 3];
arr = arr1.concat(arr2);
console.log(arr, arr1, arr2);//[1, 2, 2, 3],[1, 2], [2, 3]

我们发现arr现在是arr1和arr2连接后的数组,并且arr1和arr2都没有改变。

当然我们如果要连接多个数组的话,那么concat里面的数组之间用逗号分隔即可。

1
2
arr3 = [5, 5];
arr = arr1.concat(arr2, arr3);

• join

这个方法是让数组的每一个数据以什么方式连接成字符串。

1
2
3
var arr = ["a", "b", "c"];
var str = arr.join("-");
console.log(str);//a-b-c

我们可以用这个方法来进行大量字符串的连接工作,它比用+运算符连接字符串性能好。

同时,字符串有一个split操作和join操作刚好相反。

split是把字符串以什么方式分割成数组。

1
2
3
var str = "a-b-c-d";
var arr = str.split("-");
console.log(arr);//a b c d

数组去重问题

1
2
3
4
5
6
7
8
9
10
11
12
13
Array.prototype.unique = function() {
var obj = {},
arr = [];
for(var i = 0, len = this.length; i < len; i ++) {
if(!obj[this[i]]) {
obj[this[i]] = true;
arr.push(this[i]);
}
}
return arr;
}
arr = [1, 1, 2, 3, 4, 2, 5, 6, undefined, undefined, null, null];
console.log(arr.unique());

这里我们运用了一个简单的哈希结构。当我们数组中的这个数据出现过一次之后,我们就在obj中将这个元素的值的位置标记成true,后面如果出现相同的属性值,因为这个位置已经是true了,就不会添加到新数组里面了,从而达到了去重的效果。

当然我们一般不建议直接改数组的原型,这里仅作演示之用。

ES6的数组方法

ES6提供了更丰富的数组方法,下面介绍几个经常会用到的

• forEach

这个方法可以改变原数组,它让数组中的元素从头到尾遍历一遍,每一个都调用一下我们在forEach里面传递的方法,中间不会停止。

1
2
3
4
5
var arr = [1, 2, 3, 4];
arr.forEach(function(item, index) {
arr[index] += 1;
});
console.log(arr); // [2, 3, 4, 5]

• map

这个方法和forEach很像,只不过map会返回一个新的数组,它也是传递一个指定的方法,让数组中的每一个元素都调用一遍这个方法。不过记得map方法最后有返回值。

1
2
3
4
5
6
var arr = [1, 2, 3];
var test = arr.map(function(item) {
return item * item;
});
console.log(test);// [1, 4, 9]
console.log(arr);// [1, 2, 3]

• filter

这个方法是过滤的作用,它返回一个原数组的子集。我们同样会传递一个方法,每一个元素都会调用一下这个方法,但是只有返回true的元素才会被添加到新数组里面,返回false的不会被添加到新数组里面。

1
2
3
4
5
var a = [1, 2, 3, 4, 5];
var b = a.filter(function(item) {
return item > 2;
});
console.log(b);// 3 4 5

同时,filter()会跳过稀疏数组里面缺少的元素,它的返回数组总是稠密的。

1
2
3
4
5
var arr = [1,,,,,,,,,,,,,3,4];
var b = arr.filter(function() {
return true;
});
console.log(b);//1 3 4

• every 和 some

这两个方法是数组的迭代方法,他们对数组应用指定函数进行判定,返回true或者false。

every是如果每一个元素经过传递的方法判定之后都返回true,最后才返回true。

some是只要有一个元素返回true,那么就返回true。

1
2
3
4
5
6
7
var arr = [1, 2, 3];
console.log(arr.some(function(item) {
return item < 3;
}));//true
console.log(arr.every(function(item) {
return item < 3;
}));//false

• reduce 和 reduceRight

reduce()和reduceRight()方法使用指定的函数将数组元素进行组合,最后变成一个值,reduce是从左向右,reduceRight是从右向左。有两个参数,第一个是方法,第二个是可选参数,即我们最后的这个值的初始值。

当我们没有设置初始值的时候,用数组的第一个元素的值作为初始值。不过当数组为空的时候,不带初始值就会报错。

当我们的数组只有一个元素并且没有指定初始值,或者有一个空数组并且指定一个初始值的情况下,reduce只是简单地返回那个值,而不会调用函数。

1
2
3
4
5
6
7
8
9
var arr = [1, 2, 3];
var sum = arr.reduce(function(x, y) {
return x + y;
}, 0);
console.log(sum);//0 + 1 + 2 + 3 = 6
var temp = [1];
var temoOut = temp.reduce(function(x, y) {
return x * x * y;
});//1,不会调用这个函数,因为数组只有一个值

数组类型的检测

在ES6中,我们有一个isArray()方法来检测是否是数组。

在ES5中,typeof运算符,数组和对象都会返回object,因此无法区分数组和对象。

constructor和instanceof操作符也可以用来判断,但是它们存在潜藏问题:

我们的web浏览器中可能有多个窗口或者窗体,每一个窗体都有自己的js环境,有自己的全局对象。并且,每个全局对象有自己的构造函数,因此一个窗体中的对象将不可能是另外窗体中的构造函数的实例,所以constructor和instanceof都不能真正可靠的检测数组类型。

这个时候我们就需要这样的代码来检测了:

1
Object.prototype.toString.call(arr) === '[object Array]'

这是ES5中最可靠的检测方法。

预编译

当我们在后面定义了一个函数,在定义函数之前使用这个函数也是可以的。

我们在后面使用var声明了一个变量,在前面调用这个变量并不会报错而是undefiend。

这两种现象在js中被称为函数声明提升和变量声明提升,函数声明提升是一种整体提升,它会把函数声明和函数体一起提升到前面。变量声明提升是一种局部提升,它仅仅将变量的声明提前了,但是并没有将赋值也一起提前。

它们都是在预编译阶段进行的。

js运行有三个阶段:

1.语法分析

2.预编译

3.解释执行

语法分析:js引擎在解析js代码之前,会先通篇扫描一下,找出低级的语法错误,比如写错大括号之类的。

解释执行:我们前面提到js是一种解释型语言,编译一行执行一行,当语法分析没有问题,并且已经完成预编译之后,就开始解释执行代码了。

这节我们着重介绍预编译

在介绍预编译之前,我们需要掌握两个重要概念。

1.隐示全局变量。

如果变量未经声明就赋值使用,此变量就会变成全局对象window的一个属性

1
2
window.a = 123;
window.a === a;//true

或者

1
2
a = 123;
window.a === a;

2.一切声明的全局变量,都是window的属性。

1
2
var a = 123;
console.log(window.a);//123

这两种情况有什么区别呢?

经过声明的全局变量不能通过delete操作来删除,但是未经声明的全局变量可以被删除。

1
2
3
4
5
6
7
8
a = 123;
delete window.a;
console.log(window.a);// undefined
console.log(a);// Uncaught ReferenceError: a is not defined
var b = 123;
delete window.b;
console.log(window.b);// 123
console.log(b);// 123

正是由于这一种特性,导致es5有一种弊端,我们总会在无形中声明一些全局变量。

1
2
3
function test() {
var a = b = 0;
}

这段代码,a经过了声明,但是b却没有声明,b会成为一个全局变量。

了解这两点后,我们开始正式介绍预编译的过程

预编译的过程分为以下四步:

1.创建AO对象(Activation Object)。

2.寻找形参和变量声明,将变量和形参作为AO对象的属性名添加到对象中,值为undefined。值得注意的是,函数声明的函数名不叫变量声明。

3.将实参值和形参值统一。

4.在函数体里面寻找函数声明,将函数名作为属性名,值为这个函数的函数体。

函数在执行前会产生一个上下文,这个上下文就是Activeaction Object对象,简称AO对象。

可以假想成

1
AO = {}

这个对象是空的,但是里面有一些我们看不到却存在的隐式属性,比如this、arguments等。

这个对象用来存放一些属性和方法,这些属性和方法就按照前面预编译的四步产生的。

接下来我们举个例子:

1
2
3
4
5
6
7
8
9
10
11
function test(a, b) {
console.log(a); // ƒ a () {}
function a () {};
a = 222;
console.log(a); // 222
function b () {}; // ƒ b () {}
console.log(b);
var b = 111;
var a;
}
test(1);

我们来按预编译的过程分析一下为什么打印出的是这些值。

首先第一步,创建一个AO对象。

1
var AO = {};

第二步,寻找形参值和变量声明,并且将值赋为undefined

1
2
3
4
AO = {
a: undefined,
b: undefined
}

第三步,将实参值和形参值相统一。属性a被赋值成了1。

1
2
3
4
AO = {
a: 1,
b: undefined
}

第四步,寻找函数声明,将函数体赋值给属性。

1
2
3
4
AO = {
a: function() {},
b: function() {}
}

这样在解释执行之前,我们预编译阶段创建的AO对象就是这个样子的

这时候我们就可以解释打印出来的值了

第一个console.log a –> function () {}

第二个console.log a –> 222,因为执行了a = 222这一行代码,所以被重新赋值了。

第三个console.log b –> function () {}

var b = function () {}这种不叫做函数声明,这个是函数赋值给b变量,b变量是声明。

例如:

1
2
3
4
5
6
7
8
9
10
11
function fn(a) {
a//function
d//function
var a = 123;
a//123
function a() {}
b//undefined
var b = function() {}
b//function
function d() {}
}

寻找变量声明的时候,不会管里面的代码到底会不会执行,只管寻找所有变量。

1
2
3
4
5
6
7
8
9
10
11
12
function test(b) {
console.log(a)//undefined
if(1 > 5) {
var a = 123;
function b() {}
}
console.log(a)//undefined
console.log(b)//function
var b = 234;
console.log(b)//234
}
test(2);

打印第一个a的时候并不会报错而是undefined,当a没有声明的时候才会报错,这里a是有声明的,if的条件对寻找变量声明没有关系。

原型与原型链

原型

原型的定义: 原型是function对象的一个属性,它定义了构造函数制造出来的对象的公共祖先。通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象。

1
function Person() {}

我们先定义一个构造函数,Person.prototype这个属性就是这个构造函数的原型,这个属性是天生就有的,并且这个属性的值也是一个对象。

我们可以在prototype上面添加属性和方法,每一个构造出来的对象都可以继承这些属性和方法。

1
2
3
4
5
Person.prototype.name  = "scarlett";
Person.prototype.age = 17;
var oPerson = new Person();
console.log(oPerson.name);//scarlett
console.log(oPerson.age);//17

虽然每一个对象都是独立的,但是他们有共同的祖先,当我们访问这个对象的属性的时候,如果它没有这个属性,就会向上找到它的原型,然后在原型上访问这个属性。

1
2
3
4
5
6
7
8
9
10
function Person() {
this.money = 100;
}
Person.prototype = {
money: 200
}
var oPerson = new Person();
console.log(oPerson.money);//100
delete oPerson.money;
console.log(oPerson.money);//200

这里我们oPerson对象因为自身有一个money属性,所以就不会到原型上去寻找money属性,因此打印的是100。但是当我们删除了自身的money属性之后,它就会到原型上去寻找money这个属性,因此就打印了200。

利用原型特点,我们可以提取公有属性。

我们可以把每一个对象都有的公有属性提取到原型上,这样当我们用构造函数构造大量的对象的时候就不需要走多次构造函数里面的赋值语句了,每一个对象调用属性的时候直接上原型上查找就可以了。

对象如何查看原型

前面我们提到了构造函数可以通过.prototype的方法来查看原型,那么我们怎么在对象上查看原型呢?

我们前面提到过用构造函数构造对象时,会隐式创建一个this对象,这个this对象里面有一个默认属性叫做proto,这个属性的值就指向这个对象的原型。

1
2
3
4
var this = {
//xxx
__proto__:Person.prototype;
}

当查找的属性是对象自身没有的属性时,会先查找__proto__这个属性,这个属性指向了原型,所以就可以在原型上继续查找属性了。

1
2
Person.money.prototype = 100;
oPerson.__proro__.money//100

总结一下就是:prototype是构造函数的属性,proto是对象的属性,它指向prototype。

对象如何查看自身的构造函数

在prototype里面,有一个隐式的属性叫做constructor,这个属性记录的就是对象构造函数。

1
console.log(oPerson.constructor); // Person();

原型链

有了原型,原型还是一个对象,那么这个名为原型的对象自然还有自己的原型,这样原型上还有原型的结构就构成了原型链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Gra.prototype.firstName = "scarlett";
function Gra () {
this.name = "grandfather";
this.sex = "male";
}
var grandfoo = new Gra();
grandfoo.word = "hello";
Foo.prototype = grandfoo;
function Foo() {
this.age = 18;
this.money = 100;
}
var father = new Foo();
function Son() {
this.name = "son";
}
Son.prototype = father;
var son = new Son();

console.log(son.firstName)// "scarlett"

这种链式结构就叫原型链。

原型链的终点

1
2
3
console.log(Gra.prototype);// Object{"name": "scarlett"}
console.log(Gra.prototype.__proto__);//Object.prototype
console.log(Gra.prototype.__proto__.__proto__);//null

从测试中可以看出,其实Gra.prototype上面还有原型,这个原型是Object.prototype,Object.prototype上面就没有原型了(null)。

其实,绝大部分对象最终都继承自Object.prototype这个对象

1
2
var obj = {};
console.log(obj.__proto__ === Object.prototype);
1
2
var obj = new Object();
console.log(obj.__proto__ === Object.prototype);

我们没有自定义原型的对象,它的原型就是Object.prototype。

由此可见,原型链的终点一般是Object.prototype;

但是并不是所有的对象都有原型。

我们可以使用Object.create方法创造没有原型的对象。

Object.create()方法需要写一个参数,这个参数传入的就是这个对象的原型。

如果我们想要构造和var obj = {}一样的对象,就可以这么写:

1
var obj = Object.create(Object.prototype);

我们也可以写一个自定义对象,让它成为原型。

1
2
var obj = Object.create({ name: "scarett" });
console.log(obj.name);//scarlett

当我们写参数为null的时候,我们就构造出来了一个没有原型的对象。

1
2
var obj = Object.create(null);
console.log(obj.__proto__);//undefined

原型链上属性的增删改查

1.新增

1
Person.prototype.a = 1

或者

1
person.__proto__.b = 2

2.删除

1
2
3
4
5
6
7
Person.prototype.name = "father";
function Person() {
this.name = "son";
}
var person = new Person();
delete person.name;
console.log(person.name);//father

这个时候person对象上面没有name属性了,我们再次删除这个属性是不是就可以删除原型上的属性了呢?

1
2
delete person.name;
console.log(person.name);//father

实际并没有,由此可见,对象并不能删除原型上的属性。

只有这样才能删除

1
delete person.__proto__.name

1
delete Person.prototype.name

3.修改

如果对象的属性只要原型上有,当我们通过一个对象改变了原型上属性的值时,所有对象的这个属性的值也会随之更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person () {}
Person.prototype.arr = [1, 2, 3];
var person1 = new Person();
var person2 = new Person();
person1.arr.push(4);
console.log(person2.arr);//[1, 2, 3, 4]

function Person () {}
Person.prototype.name = 'init';
var person1 = new Person();
var person2 = new Person();
console.log(person2.name);//init
Person.prototype.name = 'person1';
console.log(person2.name);//person1

function Person () {}
Person.prototype.name = 'init';
var person1 = new Person();
var person2 = new Person();
console.log(person2.name);//init
person1.__proto__.name = 'person1';
console.log(person2.name);//person1

4.查找

查找我们前面已经介绍过很多了。

最后,我们看个例子:

1
2
3
4
5
6
7
8
9
Person.prototype.name = "scarlett";
Person.prototype.sayName = function() {
console.log(this.name);
}
function Person() {
this.name = "son";
}
var oPerson = new Person();
oPerson.sayName();//son

有一点我们需要记住,谁调用这个方法,这个方法中的this就指向这个调用它的对象,所以打印的是son。

继承

1.传统形式

这个阶段使用的继承方式是我们前面介绍的原型链继承。

但是这种继承有一个缺点就是它会继承过多没有用的属性。

2.借用构造函数

1
2
3
4
5
6
7
8
9
function Foo(name, age) {
this.name = name;
this.age = age;
}
function Son(name, age) {
Foo.call(this, name, age);
}
var son = new Son("son", 123);
console.log(son.name);//son

这种方式就是利用了call和apply可以改变this指向的特点,通过构造函数来间接地构造子对象。

但是这种方式有两个缺点:

1.严格来说,这种方式不属于继承,也访问不了原型的原型。

2.每次构造一个对象都要走两个构造函数,效率很低。

3.共享原型。

1
Son.prototype = Foo.prototype;

这种方式就是让父子构造函数的原型都一样,虽然这种方法可以让子构造函数访问原型链,也不用走两个构造函数了,但是缺点也很明显:改变了子类的原型,父类原型也会改变,因为它们是同一个。

4.圣杯模式

这个阶段也是最终阶段,也是我们目前使用的方式。

1
2
3
4
5
function inherit(C, P) {
function F() {}
F.prototype = P.prototype;
C.prototype = new F();
}

这里我们利用了一个中间函数F来连接P和C的原型,这样我们改变C的原型的时候只会影响F而不会影响P。

但是这里还存在一个问题,当我们想查看Child构造出来的对象的构造函数的时候,它打印是Parent函数。

所以我们还需要记录子类的构造函数。

1
C.prototype.constructor = C;

最终的形式就变成了这个样子:

1
2
3
4
5
6
function inherit(C, P) {
function F() {};
F.prototype = P.prototype;
C.prototype = new F();
C.prototype.constructor = C;
}

函数与闭包

函数声明方式有两种:

1.function demo () {} 函数声明

2.var demo = function () {} 函数表达式

3.var demo = function xxx() {} 命名函数表达式

其实第2种和第3种相比较,第2种叫做匿名函数表达式,我们平时使用第1种和第2种居多,很少使用第3种。

因此第2种我们就简称为函数表达式了(第三种的xxx无法引用函数)。

每一个函数的AO对象里都有一个类数组属性arguments,这个属性里面存的就是实参。

1
2
3
4
function func (a) {
console.log(arguments[0]);// 1
}
func(1)

arguments[0]就可以查看我们传递的第一个实参了。

函数有一个length属性,这个length储存的是实参的数量。

每一个函数都会有一个return,如果不写的话函数也会自动加上一个return;

return的功能有两个:

1.返回这个函数的执行结果。

2.终止函数的执行。

arguments.callee和function.caller

arguments.callee指代函数本身。

当我们在一些匿名函数或者立即执行函数里面进行递归调用函数本身的时候,由于这个函数没有名字,我们不能用函数名的方式调用,就可以用arguments.callee来调用。

function.caller,这是函数本身自带的一个属性,可以指出当前函运行环境的函数引用,即这个函数是在哪个函数体里面执行的。

1
2
3
4
5
6
7
function test() {
console.log(test.caller);
}
function demo() {
test()
}
demo();//function demo(){}

作用域

定义:变量(变量作用域又称为上下文)和函数生效(能被访问)的区域。

javascript的函数,是可以产生作用域的!

es5中的作用域大概只有全局作用域和函数作用域两种,es6中新添加了块级作用域。

1
2
3
4
5
6
7
var demo = 123;//全局变量
function test() {
var demo = 234;//局部变量
console.log(demo);
}
test();//234
console.log(demo1);//报错

如果在函数作用域里面声明变量没有用var的话,那么就声明了一个全局变量。

两个不同作用域(除了全局作用域)之间是不能互相访问的。

1
2
3
4
5
6
7
8
9
function demo1() {
var str1 = "abc";
}

function demo2() {
console.log(str1);
}

demo2();//报错

作用域链

既然函数存在函数作用域,函数又可以嵌套,那么作用域之间自然就会产生嵌套关系,这个时候就产生了作用域链。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)来保证执行环境变量、函数的有权访问和有序访问。

作用域链的顶层对象始终是当前执行代码所在环境的变量对象。

1
2
3
4
5
6
7
8
9
function demo() {
var demo_a = 1;
function test() {
var demo_a = 2;
console.log(demo_a);
}
test();
}
demo();

在这个例子中,demo运行的时候,首先创建了一个demo的作用域,但是window本身还有一个全局作用域,这就让demo产生了一个作用域链。本着对执行环境的有权和有序访问,每个函数自身的作用域总是在作用域链的最顶层,下一层是这个函数的父级函数的作用域,再下面是父级的父级作用域,直到全局作用域。因此这个例子打印的值是2.

闭包

什么是闭包?

闭包是能够读取其他函数内部变量的函数

不同作用域之间是不能够互相访问的,但是我们如果在一个函数内部再定义一个函数,并且这个内部函数与外部函数的变量有关联,那么我们就可以通过返回这个内部函数,然后来访问外部函数里面的变量。

闭包是将函数内部和函数外部连接起来的一座桥梁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function func () {
var demo = 1;
return {
add: function() {
demo ++;
},
get: function() {
return demo;
}
}
}

var obj = func();
obj.add();
obj.add();
console.log(obj.get())// 3

当函数执行完之后,函数的执行上下文就会被销毁,我们自然就无法访问里面的变量了。但是我们这个函数返回了一个依赖于这个函数的新函数,也就是说这个没有被销毁的新函数的作用域链中还存在着对原本函数的作用域的引用,就导致我们原本的函数的上下文不会被销毁,我们称返回的这个新函数是原本函数的闭包函数。

在上面的例子中,func函数内部有一个局部变量demo,我们把这个函数的返回值赋值给了一个全局变量obj,但是由于这个返回值中有新函数依赖于本来的func函数,这就导致func函数的上下文不会被销毁。

这里我们一共运行了两次obj.add(),最后打印出来的值是3,说明func函数的a变量在函数执行完之后并没有被销毁而是存到了内存中。

这个里的add函数相当于一个setter累加器,可以在函数外部对函数内部的变量进行操作。

闭包会使得函数中的变量被保存在内存中,所以也不能滥用闭包。

立即执行函数

立即执行函数是处理闭包的一个重要方法。但是注意闭包是没有办法解除的,我们只是通过另一个新闭包来消除上一个闭包的影响。

定义:立即执行函数不需要被定义,直接执行,执行完毕之后直接释放。

经常被用作初始化。

1.(function (a) {})(num);

2.(function (a) {} (num));

传递的形参是a,a的实参值是num,num是我们在外面定义的变量。

这两种写法功能完全一样,但是标准一点的写法是第二种。

函数声明不能被执行,但是函数表达式可以。

1
2
function test() {} ();//错误
var test = function () {} ();//表达式也可以写成立即执行函数的形式。

第一种加一个括号变成立即执行函数就可以执行了。

使用立即执行函数处理闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
function returnB() {
var arr = [];
for (var i = 0; i < 10; i ++) {
arr[i] = function() {
console.log(i);
}
}
return arr;
}
var save = returnB();
for(var i = 0; i < 10; i ++) {
save[i]();
}

最后会输出10个10,并不是我们想要的0-9。

这是因为我们打印i的这一行代码所在的函数的自身作用域中不存在i这个变量,而在它的父级函数returnB中才有i这个变量,而arr[0]-arr[9]这10个值都是闭包函数,当for循环执行完i变成10之后,我们后面依次触发每一个save[i](arr[0]-arr[9])函数的时候,函数寻找的i是父级函数returnB作用域中的i,而这个作用域中的i此时已经变成10了。

那么如何更改可以让它输出0-9呢?这里就需要利用立即执行函数来产生新的闭包以消除我们共用一个闭包值导致的问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function returnB() {
var arr = [];
for(var i = 0; i < 10; i ++) {
(function(n) {
arr[n] = function() {
console.log(n);
};
}(i));
}
return arr;
}
var save = returnB();
for(var i = 0; i < save.length; i ++) {
save[i]()
}

立即执行函数执行之后,会产生一个新的作用域,我们把i的具体的0-9这10个数分别作为参数传进去,也就是说每一个作用域里面的的n都是不一样的,所以可以正常打印出0-9。

call/apply

使用call/apply可以改变this指向。call/apply的区别是传参形式不同。

1
2
3
4
5
function person() {
this.name = "scarlett";
console.log(this);
}
person();//window

此时this打印的是window。

我们尝试使用一下call

1
2
var obj = {};
person.call(obj);//{name: "scarlett"}

当使用了call方法之后,person函数内部的this就指向了我们传递的obj对象了。

还需要传参数的话,我们只要把实参值写在call后面并用逗号间隔开就可以了。

1
2
3
4
5
6
7
function person(name, age) {
this.name = name;
this.age = age;
}
var obj = {};
person.call(obj, "scarlett", 18);
console.log(obj.name);//scarlett

apply和call基本没有区别,唯一的区别是call后面的参数是一个一个传的,而apply后面的参数是放在一个数组里然后传进去的。

还是上面那个例子,如果用apply来传递实参的话,将是下面这种形式。

1
person.apply(obj, ["scarett", 17]);

this指向

js中this指向只需要记住这4点

1.预编译过程中 this–>window

2.全局作用域里面 this–>window

3.call/apply可以改变this指向

4.obj.func() func()里面的this指向obj

我们举几个例子:

1
2
3
4
5
6
7
8
var obj = {
height: 190,
eat: function() {
this.height++;
}
}
obj.eat();//this指向obj,谁调用的this,this就指向谁。
eat.call(obj);//this指向obj

理解了下面这段代码,你就基本能掌握this指向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var name = "222";
var a = {
name: "111",
say: function() {
console.log(this.name);
}
}
var fun = a.say;
fun();//222
a.say();//111
var b = {
name: "333",
say: function(fun) {
fun();
}
}
b.say(a.say);//222
b.say = a.say;
b.say();//333

第一处打印调用fun(),这里其实就是把a.say这个函数的函数体赋给了fun这个函数,相当于在全局空间下写了一个func函数,里面的代码就是a.say里面的代码,所以this指向window,因此打印222。

第二处打印调用了a.say(),按照我们前面说的谁调用函数,函数里面的this就指向谁,因此这里的this指向a,所以打印111。

第三处比较复杂,它调用了b.say(a.say))。这里其实是把a.say这个函数体替换了原本b中fun的位置,我们在调用b.say()这个方法的时候,里面的this是指向b的,但是这个this并不在fun里面而是在say里面,fun里面的this是在预编译阶段指向window的,因此打印222。

第四个b.say = a.say,其实和第二种是一模一样的意思,因此打印333。

克隆

克隆的概念

克隆和我们前面所讲的继承有一些区别,克隆是复制出来一个一模一样的目标对象,克隆分为浅层克隆和深层克隆。

浅层克隆

大致就是我们的源对象里面有什么属性,目标对象就有什么属性:

1
2
3
4
5
6
7
8
9
function clone(src, tar) {
var tar = tar || {};
for(var prop in src) {
if(src.hasOwnProperty(prop)) {
tar[prop] = src[prop];
}
}
return tar;
}

当我们有一个属性值是引用值(数组或者对象)的时候,按照这种克隆方式,如果改变了源对象或者目标对象的引用值属性值,另一个也会跟着改变,这一点就是浅层克隆的缺点。

深层克隆

深层克隆的原理很简单,我们只要不克隆引用值的引用,而是把引用值也当做一个源对象,把里面的值一个一个克隆进目标对象里面就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function deepClone(src, tar) {
var tar = tar || {};
for(var prop in src) {
if (src.hasOwnProperty(prop)) {
if (typeof(src[prop]) === "object") {
tar[prop] = (src[prop] instanceof Array) ? [] : {};
deepClone(src[prop], tar[prop]);
} else {
tar[prop] = src[prop];
}
}
}
return tar;
}
var src = {
a: [1, 2, 3],
b: {
age: "18",
sex: "male"
}
}
var tar = {};
var tar1 = deepClone(src, tar);
src.a.push(4);
src.b.name = "jiang";
console.log(src);
console.log(tar1)

这里我们运用了递归调用的方法,当我们检测到源对象里面的这个属性值是引用类型,那么就在目标对象里面也创建一个引用类型的属性。如果原来是数组就创建数组,是对象就创建对象,然后分别将源对象里面的这个引用值和目标对象里面的引用值分别当做新的源对象和目标对象进行克隆,这样就是克隆的里面每一个值了,而不是把整个引用都克隆过去。

类数组

我们知道有两种数据叫做数组和对象,但是我们其实可以用对象来模拟出数组的效果,我们把这种对象叫做类数组。

我们前面提到的arguments实参列表就是一个类数组。

类数组并不是一个数组,但是它可以表现出数组的特性。

1
2
3
4
5
6
7
8
9
10
11
var arrObj = {
"0": 1,
"1": 2,
"2": 3,
"length": 3,
"push": Array.prototype.push

}
arrObj.push(4);

console.log(arrObj);//{0: 1, 1: 2, 2: 3, 3: 4, length: 4}

这样我们就创造了一个类数组,它表现了出数组的特性。

我们会发现它自动改变了length值,这就非常神奇了。

其实这个类数组的关键就在这个length属性上,如果没有length属性,那么就是一个普通的对象,即使有push方法也不能使用。

我们来模拟一下数组的push方法的实现:

1
2
3
Array.prototype.push = function(num) {
this[this.length++] = num;
}

try…catch

1
2
3
4
5
6
7
try {

} catch (e) {

} finally {

}

一般是用来检测可能出错的代码的。

我们把可能出错的代码块放入try里面,然后把如果出错后的处理代码放到catch里面,finally就是最后都会走的代码。

catch的参数e一定要写上,系统会自动将错误信息传进去,错误信息一般有6种:

1.EvalError eval(),使用与定义不一致
2.RangeError,数值越界
3.ReferenceError,非法或不能识别的引用数值
4.SyntaxError,发生语法解析错误
5.TypeError,操作数类型错误
6.URIError,URI处理函数使用不当

其中3和4比较常见。

当try里面的代码出错了,try里面出错代码后面的代码就不会执行了,但是在try外面的代码还是正常执行的。

1
2
3
4
5
6
try {
console.log(a);
}catch (e) {
console.log(e);// ReferenceError: a is not defined
}
console.log(1);// 1

with

with () {}的作用是改变作用域链,它可以把括号里面的执行期上下文或者作用域放在自己的作用域链的最顶端。

1
2
3
4
5
6
7
8
9
10
var obj = { a: 123 };
function test() {
var a = 111;
var b = 222;
with(obj) {
console.log(a);// 123
console.log(b);// 222
}
}
test();

如果没有with,在test函数里面作用域链的最顶端应该是自身,但是使用with之后,我们把obj放在了作用域链的最顶端。

with改变作用域链,很影响性能,一般不建议使用。

ES5严格模式

es5的严格模式是一种全新的es5规范,在这个模式下,有一些es3的不标准的规则就不能使用了。

我们只要在代码的第一行写上”use strict;”这一行字符串就可以进入严格模式了,不会对不兼容严格模式的浏览器产生影响。

严格模式还可以分为:

1.全局严格模式

2.局部严格模式

全局模式就是我们在整个js代码的第一行写上字符串,而局部模式就是在函数里面的第一行写上字符串。

使用严格模式可以强制使我们的代码避免使用一些不推荐的语法,例如:

1.不允许使用with函数、arguments.callee方法、func.caller属性。

2.变量赋值之前必须声明。

3.局部的this使用前必须被赋值,除了全局的this默认指向window,其他的默认都是undefiend。而且在非严格模式下,Person.call(null/undefined)之后,里面的this还是指向window。但是如果是严格模式的话,那么传递null,this就指向null,传递undefiend,this就指向undefiend。

4.拒绝重复属性和参数。不过有一些浏览器的属性名可以重复。

》》》》》》》》》》》》》》》》》

react

React起源于Facebook内部项目,于2013年5月开源。

react的几个重要特点

1.虚拟DOM

传统web页面一般是直接操作DOM的,但是DOM操作是很耗性能的。react引入了虚拟DOM的概念,在需要更新DOM时,利用diff算法计算出虚拟DOM的最小更新操作,然后再进行更新,因此具有很好的性能。

由于DOM章节我们还没有学,这里你只需要把它理解为是html里的标签(如div、a等)。

2.组件化

整个页面是由一个个组件组合而成的,从而可以很方便地实现功能单元的解耦和复用。

3.单向数据流

数据是由外层组件向内层组件单向进行传递和更新的,所以让组件之间的关系变得简单、可预测。

环境搭建

本章所有示例的源码都可以在react-demos中查看、调试。

它只需要将仓库克隆下来,在根目录下先执行cnpm install安装依赖包,再执行npm run start即可调试。

执行npm、cnpm命令需要安装node.js环境

node.js安装非常简单,去官网首页下载长期支持版安装最新版本即可。

不过我们推荐大家安装课前概览中所介绍的nvm(Node Version Manager,它可以自由切换node.js版本)。

安装完node.js后,你就可以使用它自带的包管理npm了。

直接用npm下载依赖包非常耗时(因为连接的是国外的服务器),所以建议你切换到淘宝 NPM 镜像,执行npm install -g cnpm --registry=https://registry.npm.taobao.org,这样你就可以使用cnpm命令安装依赖包了。

简易工程搭建

环境搭建好后,接下来让我们看看react-demos简易工程是怎么搭建的。

首先新建一个空文件夹,我们命名为react-demos

以该文件夹为根目录执行npm init,一路回车即可。

再执行依赖包安装命令:

1
2
3
cnpm install babel-preset-react@6.24.1 babel-core@6.26.3 babel-loader@7.1.5 babel-preset-env@1.7.0 babel-preset-stage-2@^6.24.1 web-webpack-plugin@4.6.7 webpack@4.44.1 webpack-cli@3.3.12 webpack-dev-server@3.11.0 --save-dev

cnpm install react@16.13.1 react-dom@16.13.1 --save

创建一个webpack.config.js文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const path = require("path");
const { WebPlugin } = require("web-webpack-plugin");

module.exports = {
mode: "development",
entry: {
// 修改入口文件即可调试不同文件下的代码
app: "./src/01/index.js"
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name]_[contenthash:8].js"
},
devServer: {
open: true
},
module: {
rules: [
{
test: /\.jsx?$/,
include: [path.resolve(__dirname, "src")],
loader: "babel-loader",
options: {
presets: ["react", "es2015"]
}
}
]
},
plugins: [
new WebPlugin({
template: "./src/index.html",
filename: "index.html"
})
]
};

关于webpack的配置我们会在后面章节中介绍,这里大家拷贝内容即可。

编写你的第一个react页面

工程也搭建完毕了,接下来我们就可以编写react代码了。

我们创建代码目录结构如下:

1
2
3
4
5
6
7
8
9
src
01
index.jsx
02
index.jsx
03
index.jsx
...
index.html

我们主要在index.jsx文件中编写代码。01、02、03…目录是为了方便管理本章不同知识点的示例代码,这样你就可以通过修改webpack.config.js的入口文件地址来切换各个示例代码了。

其中,index.html文件我们不会去改动它,它的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>react demos</title>
</head>
<body>
<div id="root"></div>
<script src="app"></script>
</body>
</html>

接下来在index.jsx文件中编写react代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 引入所需模块
import React, { Component } from "react";
import ReactDom from "react-dom";

// 编写一个组件
class App extends Component {
render() {
// const和var类似,它是ES6中的变量命名关键字,它命名的是一个常量。
const text = 'test'
// jsx语法
return <div>{text}</div>;
}
}

// 将组件渲染到页面中
ReactDom.render(<App />, document.getElementById("root"));

class App extends Component是react编写组件的方式,我们的页面就是由一个个组件组成的。

直接在JavaScript语言写html的语法叫JSX,它的基本语法规则是:遇到HTML标签(以 < 开头),就用HTML规则解析;遇到代码块(以 { 开头),就用JavaScript规则解析。

ReactDom.render可以将组件实例(<App />)转为HTML,并渲染到指定的DOM节点中。

此时我们在根目录下执行npm run start,就可以看到浏览器自动打开我们第一个react页面,页面中出现了test。

行间样式和className

react中元素的style属性可以添加行间样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from "react";
import ReactDom from "react-dom";

class App extends Component {
render() {
const style = {
color: "red"
};
return (
<span className="text" style={style}>
test
</span>
);
}
}

ReactDom.render(<App />, document.getElementById("root"));

在开发中我们一般是为元素设置类名(由于class是保留字,需要使用className设置类名),然后在单独的样式文件中通过css的class选择器为元素添加样式,而非采用style属性,这样可以实现样式文件的解耦。

this.props

在组件中,通过this.props属性可以获取向组件传递的参数,props改变会触发组件重新渲染(执行render函数)。

原生的数组map方法可以渲染列表,需要为列表中每一个子项添加一个唯一标识(react性能优化需要),否则会抛Warning: Each child in a list should have a unique "key" prop.警告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from "react";
import ReactDom from "react-dom";

class App extends Component {
render() {
const { arr } = this.props;

return (
<ul>
{arr.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}
}

const arr = [1, 2, 3];

ReactDom.render(<App arr={arr} />, document.getElementById("root"));

此时页面中渲染出了1、2、3。

this.state

state是组件的内部状态,react将组件看成是一个状态机。通过调用this.setState方法可以改变state,state的改变会触发组件重新渲染(执行render函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { Component } from "react";
import ReactDom from "react-dom";

class App extends Component {
constructor(props) {
super(props);
this.state = {
value: ""
};
this.ref = React.createRef();
}

onChange = () => {
this.setState({
value: this.ref.current.value
});
};

render() {
const { value } = this.state;
return (
<div>
<input ref={this.ref} onChange={this.onChange}></input>
<span>{value}</span>
</div>
);
}
}

ReactDom.render(<App />, document.getElementById("root"));

组件中标签并不是真实的DOM节点,而是虚拟DOM(virtual DOM)。只有当它插入html文档后,才会变成真实的DOM。

这个例子我们还附带演示了如何从组件获取真实的DOM节点,也就是refs转发的使用方式。

组件生命周期

组件生命周期

组件是个状态机,它的生命周期分成三大阶段:

1.Mounting(装载组件,上侧虚线框)

依次会经历:

getDefaultProps 初始化props

getInitialState 初始化state

componentWillMount 将要装载

render 渲染

componentDidMount 装载完毕,此时可以获取到更新后的DOM,AJAX请求也在这个生命周期调用

2.Updating(正在更新,左下角虚线框)

组件装载完毕后,就处于运行阶段了。

当state或props改变,组件就会更新,重新渲染(执行render函数)。

此时会走下面的生命周期:

shouldComponentUpdate(nextProps, nextState) 返回一个boolean值,用于判断是否重新渲染,默认是true。

componentWillUpdate(nextProps, nextState) 将要render

render

componentDidUpdate(prevProps, prevState) render完成,此时能取到更新后的DOM。

我们可以注意到props改变时,会多走一个生命周期函数:

componentWillReceiveProps(nextProps) 将要接收新的props,这里你可以拿到nextProps的值(最终的props值)

3.Unmounting(卸载,右下角虚线框)

componentWillUnmount 组件将要卸载,这里一般会做一些清除操作,如清除定时器等。

在代码中你可以打印一下生命周期看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from "react";
import ReactDom from "react-dom";

class App extends Component {
static defaultProps = {
value: ""
};
constructor(props) {
super(props);
this.state = {};
}

componentWillMount() {
console.log("componentWillMount");
}

componentDidMount() {
console.log("componentDidMount");
}

render() {
console.log("render");
return null;
}
}

ReactDom.render(<App />, document.getElementById("root"));

控制台中会依次打印出

1
2
3
componentWillMount
render
componentDidMount

Ajax

组件的数据来源,通常是通过Ajax请求从服务器获取的,我们一般在componentDidMount生命周期方法中调用Ajax请求,等到请求成功,再用this.setState方法将数据保存到组件的state中,同时state改变会触发UI重新渲染,页面展示出数据。

子组件向父组件传递数据(通信)

由于react数据流是单向的,父组件向子组件传递数据只需要直接传参,再通过this.props获取就行了,而子组件向父组件传递数据则需要借助回调函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { Component } from "react";
import ReactDom from "react-dom";

class Child extends Component {
render() {
const { onClick } = this.props;

return (
<div>
<button
onClick={() => {
onClick(Math.random().toString());
}}
>
child button
</button>
</div>
);
}
}

class Parent extends Component {
constructor(props) {
super(props);
this.state = {
text: ""
};
}

onClick = text => {
this.setState({
text
});
};

render() {
const { text } = this.state;

return (
<div>
<Child onClick={this.onClick} />
<span>{text}</span>
</div>
);
}
}

ReactDom.render(<Parent />, document.getElementById("root"));

例子中在父组件定义点击事件的回调函数,并通过props传递个子组件,子组件的点击事件调用了该回调函数并将数据传递出去。

this.props.children

它表示组件的所有子节点。有时候我们需要为组件的定义预留一些内容时会用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from "react";
import ReactDom from "react-dom";

class Child extends Component {
render() {
return (
<div>
<div>child</div>
<div>{this.props.children}</div>
</div>
);
}
}

class App extends Component {
render() {
return <Child>parent</Child>;
}
}

ReactDom.render(<App />, document.getElementById("root"));

页面中child和parent都会显示。

》》》》》》》

Redux

示例代码

什么是Redux

Redux是JavaScript状态容器,提供可预测的状态管理。

为什么要用Redux

React其实是一个视图框架,随着JavaScript应用开发日趋复杂,需要管理的状态也越来越多,同时也要保证应用行为的可预测,仅用React难以支撑大型应用的开发,所以需要使用Redux。

Redux并不是非用不可,有些项目你用React其实也够了,有些项目可能你也会选用其它状态管理框架如Mobx。不过React+Redux仍是目前最热门、最经典的组合,它足以支撑任何大型中后台应用的开发。

Redux的数据流

我们先来总体介绍一下Redux的数据流,这里你不要完全理解,因为数据流中涉及的各个概念后面我们都会详细介绍。

Redux的数据流

从图中我们可以看到,Redux的数据流是单向的,你只能通过改变数据的方式来改变视图,这样也让数据流变得清晰、可预测。

Redux的一般流程是:视图触发事件,Action构成函数生成一个action,action其实就是普通对象,它有一个表示此action类型的必填属性type,容器Store会将action作为参数,调用dispatch方法分发,然后就会走相应的reducer函数,reducer函数定义了action是如何改变Store的状态(state)的。最后Store的状态改变,视图自动刷新。

Store

Store是保存应用状态(state)的地方,你可以把它看成一个容器,改变Store中state的唯一途径就是dispatch(action)。应用中应有且仅有一个Store。

1
const Store = createStore(reducer)// 创建Store

Store上有几个方法

1.getState():获得state

2.dispatch(action): 分发一个action

3.subscribe(listener): 添加监听函数,每次分发action都会把监听函数执行一遍

action

dispatch(action)是唯一可以改变state的方式,action其实就是JavaScript普通对象,action必须有一个字符串类型的type字段来表示执行动作的类型。

例如:

1
2
3
4
const action = {
type: "ADD",
...
}

reducer

reducer用于描述Store是如何根据action来改变state的。它本质是一个纯函数。

那么什么是纯函数呢?

1.不对外界产生影响

2.不对输入产生影响

3.同样的输入,必定得到同样的输出。

例如:

1
2
3
function add(x, y) {
return x + y;
}

1.不对外界产生影响

反例

1
2
3
4
5
function add(x, y) {
const sum = x + y;
updateSql(sum);// 对数据库有操作
return sum;
}

2.不对输入产生影响

反例

1
2
3
4
function addComponent(arr, ele) {
arr.push(ele);
return arr;
}

3.同样的输入,必定得到同样的输出。

反例

1
2
3
function getTime() {
return new Date().getTime();
}

redux的常用知识点就是这些,是不是很简单呢?接下来我们就通过实际例子来熟练掌握它。

实现一个计数器

首先我们编写一个计数器reducer

1
2
3
4
5
6
7
8
9
10
11
// reducer是一个纯函数,用于描述action如何改变state
function counter(state = 0, action) {
switch (action.type) {
case "INCREASE":
return state + 1;
case "DECREASE":
return state - 1;
default:
return state;
}
}

然后用redux的createStore api创建一个Store

1
2
// 使用redux中的createStore api创建Store
const Store = createStore(counter);

我们来测试一下这个Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Store = createStore(counter);

// Store提供了getState获取state
console.log(Store.getState()); //0

// 通过Store的dispatch方法分发一个action
Store.dispatch({
type: "INCREASE"
});

console.log(Store.getState()); //1

Store.dispatch({
type: "DECREASE"
});

console.log(Store.getState()); //0

可以发现它已经具备一个计数器的功能了,并且我们前面介绍的redux知识点也基本囊括其中了。没错,redux的使用就是这么简单!

当然,我们也想在页面中直观地看到效果

我们做一些DOM处理就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文档元素绑定上点击事件,会在DOM章节中介绍
document.addEventListener(
"click",
function() {
Store.dispatch({
type: "INCREASE"
});
},
false
);

// 将html内容写入body元素中,会在DOM章节中介绍
function render() {
document.body.innerHTML = "<h1>" + Store.getState() + "</h1>";
}

// 初始化页面
render();

// subscribe用于添加监听函数,每次分发action都会把监听函数执行一遍
Store.subscribe(render);

这样你点击页面数字就会累加了。

简单结合react的计数器

下面是react和redux的简单结合,涉及的知识点我们前面都介绍过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { Component } from "react";
import ReactDom from "react-dom";
import { createStore } from "redux";

function counter(state = 0, action) {
switch (action.type) {
case "INCREASE":
return state + 1;
case "DECREASE":
return state - 1;
default:
return state;
}
}

const Store = createStore(counter);

class Counter extends Component {
render() {
return (
<div>
<h1>{Store.getState()}</h1>
<button
onClick={() => {
Store.dispatch({
type: "INCREASE"
});
}}
>
+
</button>
<button
onClick={() => {
Store.dispatch({
type: "DECREASE"
});
}}
>
-
</button>
</div>
);
}
}

function render() {
ReactDom.render(<Counter />, document.getElementById("root"));
}

render();
Store.subscribe(render);

combineReducers

reducer是描述action如何改变state的函数,我们发现如果按照前面的写法,reducer逻辑只能写在一个函数里,无法拆分。

这时候我们就需要用到combineReducers api了,它可以将多个reducer组合起来。这样你就可以编写多个reducer函数,将不同功能写在不同的reducer里了。

示例:todolist

我们看一个todoList的例子,看看combineReducers是怎么使用的。

这个例子里有两个reducer,todos和visiableFilter。todos是存储清单列表数据的,visiableFilter是存储过滤条件的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ./reducers/todos.js

let gid = 0;

const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: ++gid,
text: action.text,
completed: false
}
];
case "TOGGLE_TODO":
return state.map(todo => {
if (todo.id === action.id) {
return {
...todo,
completed: !todo.completed
};
}
return todo;
});
default:
return state;
}
};

export default todos;
1
2
3
4
5
6
7
8
9
10
11
12
// ./reducers/visiableFilter.js

const visiableFilter = (state = "SHOW_ALL", action) => {
switch (action.type) {
case "SET_VISIABLE_FILTER":
return action.filter;
default:
return state;
}
};

export default visiableFilter;

我们使用combineReducer将两个reducer组合起来。

1
2
3
4
5
6
7
8
9
10
// ./reducers/index.js
import { combineReducers } from "redux";

import todos from "./todos.js";
import visiableFilter from "./visiableFilter.js";

export default combineReducers({
todos,
visiableFilter
});

接下来我们就可以在应用中使用这个rootReducer了。

由于最终要展示的列表是todos和visiableFilter计算得出的结果,所以我们编写了一个getFilterTodos做这部分计算操作。

我们还增加了筛选功能,就是底部的三个FilterLink,它们对应的是visiableFilter reducer的操作。

所以将todos和visiableFilter两个独立的状态拆分开了,一个状态专门负责一个功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// ./index.jsx

import React, { Component } from "react";
import ReactDom from "react-dom";
import { createStore } from "redux";

import rootReducer from "./reducers/index.js";
import FilterLink from "./FilterLink.jsx";

const Store = createStore(rootReducer);

const FILTER_TYPE_MAP = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE"
};

const getFilterTodos = (todos, visiableFilter) => {
switch (visiableFilter) {
case FILTER_TYPE_MAP.SHOW_ALL:
return todos;
case FILTER_TYPE_MAP.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case FILTER_TYPE_MAP.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
default:
throw new Error("unknow filter type");
}
};

class App extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}

render() {
const state = Store.getState();
let { todos, visiableFilter } = state;

todos = getFilterTodos(todos, visiableFilter);

return (
<div>
<input type="text" ref={this.inputRef} />
<button
onClick={() => {
Store.dispatch({
type: "ADD_TODO",
text: this.inputRef.current.value
});
}}
>
add
</button>
<ul>
{todos.map(todo => (
<li
style={{
textDecoration: todo.completed ? "line-through" : "none"
}}
key={todo.id}
onClick={() => {
Store.dispatch({
type: "TOGGLE_TODO",
id: todo.id
});
}}
>
{todo.text}
</li>
))}
</ul>
{Object.keys(FILTER_TYPE_MAP).map(key => {
const filter = FILTER_TYPE_MAP[key];
return (
<FilterLink
key={filter}
filter={filter}
visiableFilter={visiableFilter}
handleClick={() => {
Store.dispatch({
type: "SET_VISIABLE_FILTER",
filter
});
}}
/>
);
})}
</div>
);
}
}

const render = () => {
ReactDom.render(<App />, document.getElementById("root"));
};

render();
Store.subscribe(render);

FilterLink是我们为了实现代码复用编写的一个组件,这样我们就不需要重复写三个类似的标签和行为了。当它自身的filter和当前用户选中的visiableFilter相等时,组件文本的颜色是红色的,同时点击组件可以实现过滤操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ./FilterLink.jsx

import React, { Component } from "react";

class FilterLink extends Component {
render() {
const { filter, visiableFilter, handleClick } = this.props;

const style = {};
if (filter === visiableFilter) {
style.color = "red";
}

return (
<div style={style} onClick={handleClick}>
{filter}
</div>
);
}
}

export default FilterLink;

react-redux

在实际项目中redux和react结合有更好的方式,那就是react-redux库。

它主要作用是用Provider组件将根组件包裹,将Store从Provider组件注入,这样使用connect方法就可以让任意子组件访问Store中state和disptach了(也就是状态和改变状态的方法)。当state更新时,相关子组件也会自动更新(状态和视图保持一致)。

首先我们还是先编写redux部分

reducer部分和上一节是相似的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ./redux/reducers/todos.js

import { ADD_TODO, TOGGLE_TODO } from "../actions";

let gid = 0;

const todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
id: ++gid,
text: action.text,
completed: false
}
];
case TOGGLE_TODO:
return state.map(todo => {
if (todo.id === action.id) {
return {
...todo,
completed: !todo.completed
};
}
return todo;
});
default:
return state;
}
};

export default todos;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ./redux/reducers/visiableFilter.js

import { SET_VISIABLE_FILTER } from "../actions";

const visiableFilter = (state = "SHOW_ALL", action) => {
switch (action.type) {
case SET_VISIABLE_FILTER:
return action.filter;
default:
return state;
}
};

export default visiableFilter;
1
2
3
4
5
6
7
8
9
10
11
// ./redux/reducers/index.js

import { combineReducers } from "redux";

import todos from "./todos.js";
import visiableFilter from "./visiableFilter.js";

export default combineReducers({
todos,
visiableFilter
});

我们应用所有的action都写actions目录,这样我们就能很方便地查找到整个应用触发了哪些行为操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ./redux/actions/index.js

export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const SET_VISIABLE_FILTER = "SET_VISIABLE_FILTER";

export const addTodo = todo => {
return {
type: ADD_TODO,
text: todo
};
};

export const toggleTodo = id => {
return {
type: TOGGLE_TODO,
id
};
};

export const setVisiableFilter = filter => {
return {
type: SET_VISIABLE_FILTER,
filter
};
};

redux代码编写部分介绍完了,我们再看看用react-redux库如何将redux和react结合起来。

可以看到我们使用Provider组件将应用的根组件App包裹,再将store注入进去。

同时我们做了进一步的优化,整个应用被拆成了AddTodo、TodoList、FilterLink三个组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ./index.jsx

import React, { Component } from "react";
import ReactDom from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";

import rootReducer from "./redux/reducers/index.js";
import AddTodo from "./components/AddTodo.jsx";
import TodoList from "./components/TodoList.jsx";
import FilterLink from "./components/FilterLink.jsx";
import { FILTER_TYPE_MAP } from "./constants";

const store = createStore(rootReducer);

class App extends Component {
render() {
return (
<div>
<AddTodo />
<TodoList />
{Object.keys(FILTER_TYPE_MAP).map(key => {
const filter = FILTER_TYPE_MAP[key];
return <FilterLink key={filter} filter={filter} />;
})}
</div>
);
}
}

ReactDom.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);

接下来我们只需要编写这三个组件,整个应用就搭建完成了。

AddTodo组件的功能是增加todo项,实现和前一节介绍的类似。区别是我们使用了connect方法,mapDispatchToProps负责将用户对组件的操作映射成Action。

这里我们将addTodo action注入到AddTodo中了。这样在AddTodo组件里就可以很方便地触发addTodo action,从而改变state了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ./components/AddTodo.js

import React, { Component } from "react";
import { connect } from "react-redux";

import { addTodo } from "../redux/actions";

class AddTodo extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
const { addTodo } = this.props;

return (
<div>
<input type="text" ref={this.inputRef} />
<button
onClick={() => {
addTodo(this.inputRef.current.value);
}}
>
add
</button>
</div>
);
}
}

const mapDispatchToProps = dispatch => {
return {
addTodo: todo => {
dispatch(addTodo(todo));
}
};
};

export default connect(null, mapDispatchToProps)(AddTodo);

FilterLink组件也使用了connect方法,它将setVisiableFilter action注入,从而可以很方便地改变state中的visiableFilter。

mapStateToProps负责将state映射到组件的props中,connect会将state作为mapStateToProps的参数传入,你只需要通过对象取值的形式获取visiableFilter即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ./components/FilterLink.js

import React, { Component } from "react";
import { connect } from "react-redux";

import { setVisiableFilter } from "../redux/actions";

class FilterLink extends Component {
render() {
const { filter, visiableFilter, setVisiableFilter } = this.props;

const style = {};
if (filter === visiableFilter) {
style.color = "red";
}

return (
<div
style={style}
onClick={() => {
setVisiableFilter(filter);
}}
>
{filter}
</div>
);
}
}

const mapStateToProps = state => {
const { visiableFilter } = state;
return {
visiableFilter
};
};

const mapDispatchToProps = dispatch => {
return {
setVisiableFilter: filter => {
dispatch(setVisiableFilter(filter));
}
};
};

export default connect(mapStateToProps, mapDispatchToProps)(FilterLink);

前面介绍了connect方法的相关知识,TodoList组件就很容易理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// ./components/TodoList.js

import React, { Component } from "react";
import { connect } from "react-redux";

import { toggleTodo } from "../redux/actions";
import { FILTER_TYPE_MAP } from "../constants";

class TodoList extends Component {
render() {
const { todos, toggleTodo } = this.props;

return (
<ul>
{todos.map(todo => (
<li
style={{
textDecoration: todo.completed ? "line-through" : "none"
}}
key={todo.id}
onClick={() => {
toggleTodo(todo.id);
}}
>
{todo.text}
</li>
))}
</ul>
);
}
}

const getFilterTodos = (todos, visiableFilter) => {
switch (visiableFilter) {
case FILTER_TYPE_MAP.SHOW_ALL:
return todos;
case FILTER_TYPE_MAP.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case FILTER_TYPE_MAP.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
default:
throw new Error("unknow filter type");
}
};

const mapStateToProps = state => {
const { todos, visiableFilter } = state;
return {
todos: getFilterTodos(todos, visiableFilter)
};
};

const mapDispatchToProps = dispatch => {
return {
toggleTodo: id => {
dispatch(toggleTodo(id));
}
};
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

react-router-dom

顾名思义,这个类库可以让我们的应用集成路由功能。

它的使用方法也很容易理解,基本就是根据路径去匹配相应的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
import { Route, HashRouter } from "react-router-dom";

import BaseLayout from "./layouts/BaseLayout.jsx";
import Todo from "./pages/Todo/index.jsx";

function AppRouter() {
return (
<HashRouter>
<Route exac path="/" component={BaseLayout} />
<Route exac path="/:filter" component={Todo} />
</HashRouter>
);
}

export default AppRouter;

通过withRouter高阶组件包裹可以让组件很方便地获取到路由参数(match.params),同时它提供了Link组件,该组件点击后可进行路由跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { Component } from "react";
import { Link, withRouter } from "react-router-dom";

class FilterLink extends Component {
render() {
const { filter, match } = this.props;
const { params } = match;

const style = {};
if (params.filter === filter) {
style.color = "red";
}

return (
<div>
<Link to={filter} style={style}>
{filter}
</Link>
</div>
);
}
}

export default withRouter(FilterLink);

完整代码详见:react-router-dom

redux中间件

这里我们介绍两个很有用的redux中间件:redux-thunk、redux-logger

redux-thunk可以让redux处理诸如ajax请求之类的异步操作。

redux-logger可以将redux的状态变化通过控制台日志打印出来,方便调试。

引入它们是很简单的。

1
2
3
4
5
6
7
8
9
10
11
// ./redux/index.js

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { createLogger } from "redux-logger";

import rootReducer from "./reducers";

const store = createStore(rootReducer, applyMiddleware(thunk, createLogger()));

export default store;

这样你就能使用这两个中间件了。

redux-logger不需要什么操作,它会自动将redux日志打印到控制台中。

redux-thunk需要你修改action的写法,以支持异步操作,此时我们的action是一个函数。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import * as api from "../../apiMock";

export const SET_TODOS = "SET_TODOS";
export const SET_LOADING = "SET_LOADING";

export const setTodos = todos => {
return {
type: SET_TODOS,
todos
};
};

export const setLoading = loading => {
return {
type: SET_LOADING,
loading
};
};

export const fetchTodos = () => {
return dispatch => {
dispatch(setLoading(true));
api
.fetchTodoList()
.then(res => {
dispatch(setTodos(res));
})
.finally(() => {
dispatch(setLoading(false));
});
};
};

完整代码详见:redux中间件

0%