HTML5和CSS3

  1. contentEditable设置为true时,该元素就变成了可编辑的,且该元素可继承。

  2. designMode属性用来指定整个页面是否可编辑。document.designMode="on"

爬虫

爬虫基础

爬虫就是获取网页并提取和保存信息的自动化程序。

Cookies里面保存了登录的凭证,有了它,只需在下次请求携带Cookies发送请求而不必重新输入用户名和密码。因此在爬虫中,有时候需要登录才能访问页面时,我们一般会直接将登录成功后截取的Cookies放在请求头里面直接请求,而不必重新模拟登录。

请求头

Accept:请求报文域,用于指定客户端可接受哪些类型的信息。

Accept-Language:指定客户端可接受的语言类型。

Accept-Encoding:指定客户端可接受的内容编码。

Host:用于指定请求资源的主机IP和端口号,其内容为请求URL的原始服务器或网关的位置。从HTTP1.1版本开始,请求必须包含此内容。

Cookie:也常用复数形式Cookies,这是网站为了辨别用户进行会话跟踪而存储在用户本地的数据。它的主要功能是维持当前会话。如:我们输入用户名和密码成功登陆到某个网站后,服务器会用会话保存登录状态信息,后面我们每次刷新或请求该站点的其他页面时,会发现都是登陆状态,这就是Cookies的功劳。Cookies里面有信息标识了我们所对应的服务器的会话,每次浏览器在请求该站点的页面时,都会在请求头中加上Cookies并将其发送给服务器,服务器通过Cookies识别出是我们自己,并且查出当前状态是登录状态,所以返回的是登陆后才能看到的网页内容。

Referer:此内容用来标识这个请求是从哪个页面发过来的,服务器可以拿到这一信息并作相应的处理,如来源统计,防盗链处理等。

User-Agent:简称UA,它是一个特殊的字符串头,可以是服务器识别客户使用的操作系统和版本,浏览器及版本等信息。在做爬虫的时候加上此信息可以伪装为浏览器;如果不加,很可能会被识别为爬虫。

Content-Type:也叫互联网媒体类型或者MIME类型,在HTTP协议消息头中,它用来表示具体请求中的媒体类型信息。如:text/html表示HTML格式,image/gif表示GIF图片,application/json表示JSON类型。

Content-Type 提交数据的方式
application/x-www-form-urlencoded 表单数据
multipart/form-data 表单文件上传
application/data 序列化JSON数据
text/xml XML数据

在爬虫中,若要构造POST请求,需要使用正确的Content-Type,并了解各种请求库的各个参数设置时使用的是哪种Content-Type,不然可能导致POST提交后无法正常响应。

请求头是请求的重要组成部分,再写爬虫时大部分情况下都需要设定请求头。

请求主体

请求体一般承载的内容是POST请求中的表单数据,而对于GET请求,请求体则为空。

响应三部分:响应状态码,响应头,响应体。

响应头

Date:标识响应产生的时间。

Last-Modified:指定资源的最后修改时间。

Content-Encoding:指定响应内容的编码。

Server:包含服务器的信息,比如名称和版本号等。

Content-Type:文档类型,指定返回的数据类型是什么,如text/html代表返回的是HTML文档,application/x-javascript则代表返回JavaScript文件,image/jpeg则代表返回图片。

Set-Cookie:设置Cookies,响应头中的Set-Cookie告诉浏览器需要将此内容放在Cookies中,下次请求携带Cookies请求。

Expires:指定响应的过期时间,可以使代理服务器或浏览器将加载的内容更新到缓存中。如果再次访问时,就可以直接从缓存中加载,降低服务器负载,缩短加载时间。

响应体

响应的正文数据都在响应体中,比如请求网页时,它的响应体就是网页的 HTML代码;请求一张图片时,它的响应体就是图片的二进制数据。我们做爬虫请求网页后,要解析的内容就是响应体。

会话

在Web中,会话对象用来存储特定用户会话所需的属性及配置信息,这样,当用户在应用程序的Web页之间跳转时,存储在会话对象中的变量将不会丢失,而是在整个用户会话中一直村下去。当用户请求来自应用程序的Web页时,如果该用户还没有会话,则Web服务器将自动创建一个会话对象。当会话过期或放弃后,服务器将终止该会话。

会话维持

当客户端第一次请求服务器时,服务器会返回一个请求头中带有Set-Cookie字段的响应给客户端,用来标记是哪一个用户,客户端浏览器就会把此Cookies放到请求头一起提交给服务器,Cookies携带了会话ID信息,服务器检查该Cookies即可找到对应的会话是什么,然后再判断会话来以此来辨认用户状态。

在成功登录某个网站时,服务器就会告诉客户端设置哪些Cookies信息,在后续访问页面时客户端会把Cookies发送给服务器,服务器再找到对应的会话加以判断。如果会话中某些设置登录状态的变量是有效的,那就证明用户处于登录状态,此时返回登陆之后才可以查看的网页内容,浏览器解析便可以看到了。

反之,如果传给服务器的Cookies是无效的,或者会话已经过期了,我们将不能继续访问页面,此时可能会收到错误的响应或者跳转到登录页面重新登陆。

所以,Cookies和会话需要配合,一个处于客户端,一个处于服务端,两者共同协作就实现会话控制。

会话Cookie与持久Cookie

从表面意思来说,会话Cookie就是把Cookie放在浏览器内存中,浏览器在关闭之后该Cookie即失效;持久Cookie则会保存到客户端的硬盘中,方便下一次使用,用于长久保持用户登录状态。

因此一些持久化登陆的网站其实就是把Cookie的有效时间和会话有效期设置得较长,下次我们再访问页面时仍然携带之前的Cookie,就可以直接保持登录状态。

会话误区

在谈论会话机制时,人们认为“只要关闭浏览器,会话就会消失”。可以想象一下会员卡的例子除非顾客主动对店家提出销卡,否则店家绝不会轻易删除顾客的资料。对会话来说,也是一样,除非程序通知服务器删除一个会话,否则服务器会一直保留。比如,程序一般都是在我们做注销操作时才去删除会话。

但是当我们关闭浏览器时,浏览器不会主动在关闭之前通知服务器他将关闭,所以服务器根本不会有机会知道浏览器已经关闭。之所以会有这种错觉是因为大部分会话机制都使用会话Cookie来保存会话ID信息,而关闭浏览器后Cookies就消失了,再次连接服务器时,也就无法找到原来的会话了。如果服务器设置的Cookies保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的Cookies发送给服务器,则再次打开浏览器能找到原来的会话ID,依旧还是可以保持登录状态的。

而恰恰是由于关闭浏览器不会导致会话被删除,这就需要服务器为会话设置一个失效的时间,当距离客户端上一次使用会话的时间时,服务器就可以认为客户端已经停止了活动,才会把会话删除以节省存储空间。

代理

代理的基本原理

我们在做爬虫时经常会遇到这种情况,最初爬虫正常运行,但过一会就报403Forbidden错误,可能会看到“您的IP访问频率太高”这样的提示,原因是网站采取了反爬虫措施。比如,服务器会检测某个IP在单位时间内的请求次数,如果超过了这个阈值就会直接拒绝服务,返回一些错误信息,这种情况称为封IP。这时就可以采用代理来实现IP伪装。

代理实际是代理服务器,proxy server,功能是代理网络用户去取得网络信息,形象地说是网络信息的中转站,在我们正常请求一个网站时,是发送了请求给Web服务器,WEb服务器把响应传回给我们。如果设置了代理服务器,实际上就是在本机和服务器之间架一座桥,此时本机不是直接向Web服务器发起请求,而是向代理服务器发起请求,请求会发送给代理服务器,然后由代理服务器再发送给Web服务器,接着由代理服务器再把Web服务器返回的响应转发给本机,这样就可正常访问网页了,但这个过程中Web服务器识别出的真实IP就不再是我们本机的IP了,就成功实现了IP伪装,这就是代理的原理。

使用代理隐藏真实的IP,让服务器误以为是代理服务器在请求自己,这样在爬取过程中不断更换代理就不会被封锁,可达到很好的效果

代理作用:
  1. 突破自身限制,访问一些平时不能访问的站点。

  2. 访问一些单位或团体内部资源:比如使用教育网内地址段访问免费代理服务器,就可以实现对教育网开放的各类FTP下载上传,以及各类资料查询共享等服务。

  3. 提高访问速度:通常代理服务器都设置了一个较大的硬盘缓存区,当有外界的信息通过时,同时也将其保存到缓存区中,当其他用户再访问相同的信息时,则直接由缓存区中取出信息,传给用户,以提高访问速度。

  4. 隐藏真实的IP:上网者也可以通过这种方法隐藏自己的IP免受攻击。对于爬虫来说,我们用代理就是为了隐藏自身IP,防止自身的IP被封锁。

代理分类
  1. 根据协议区分:

    FTP 代理服务器: 主要用于访问 TP 服务器, 般有上传 下载以及 存功能,端口一般为21 212

    HTTP 代理服务器: 主要用于访问网页,一般有内容过滤和缓存功能,端口 般为 80 8080 3128

    SSL LS 代理: 主要用于访问加密网站, 般有 SSL TLS 加密功能(最高支持 128 位加密强度),端口一般为 443

    RTSP 代理: 主要用于访问 Real 流媒体服务器, 般有缓存功能,端口 般为 554

    Telnet 代理: 主要用于 telnet 远程控制(黑客人侵计算机时常用于隐藏身份),端口 般为 23

    POP3/SMTP 代理: 主要用于 POP3 SMTP 方式收发邮件, 般有缓存功能,端口 般为 110 25

    SOCKS 代理: 只是单纯传递数据包,不关心具体协议和用法,所以速度快很 般有存功能,端口一般为 1080 SOCKS 代理协议又分为 SOCKS4 SOCKS5 ,前者只支持 TCP,而后者支持 TC UDP ,还支持各种身份验证机制、服务器端域名解析 简单来说,SOCKS4 能做到的 SOCKS5 都可以做到,但 SOCKS5 能做到的 SOCKS4 定能做到

  2. 根据匿名程度区分

    高度匿名代理: 会将数据包原封不动地转发,在服务端看来就好像真的是 个普通客户端访问,而记录的 IP 是代理服务器的 IP

    普通匿名代理: 会在数据包上做一些改动 服务端上有可能发现这是个代理服务器,也有一定几率追查到客户端的真实 代理服务器通常会加入的 Hπ?头有 HTTP VIA HTTP X FOR DED FOR

    透明代理: 不但改动了数据包 还会告诉服务器客户端的真实 IP 这种代理除了能用缓存技术提高浏览速度,能用内容过滤提高安全性之外,并无其他显著作用,最常见的例子是内网的硬件防火墙

    间谍代理: 指组织或个人创建的用于记录用户传输的数据,然后进行研究 监控 目的的代理服务器。

基本库

使用urllib

urllib库是Python内置的HTTP请求库,不需安装即可使用。包含4个模块:

  1. request:它是最基本的 HTTP 请求模块,可以用来模拟发送请求 就像在浏览器里输入网址然后回车一样,只需要给库方法传入 RL 及额外的 数,就可以模拟实现这个过程了
  2. error 异常处理模块,如果出现请求错误 可以捕获这些异常,然后进行重试或作以保证程序不会意外终止
  3. parse 个工具模块,提供了许多 URL 处理方法,比如拆分、解析 合并
  4. robot parser :主要是用来识别网站的 robots.txt 文件,然后判断哪些网站可以爬,它其实用得 较少。
发送请求

使用urllib的request模块方便实现请求的发送并得到响应。利用它可模拟浏览器的一个请求发起过程,同时他还带有处理授权验证(authentication),重定向(redirection),浏览器的Cookies以及其他内容。

1
2
3
import urllib.request
response = urllib.request.urlopen('https://www.python.org')
print(response);
  1. 利用最基本的urllib.request.urlopen(url,data=None,[timeout,]*,cafile=None,capath=None,cadefault-False,context=None)方法,可以完成最基本的简单网页的GET请求抓取。

  2. 1
    class urllib.request.Request(url,data=None,headers={},origin_req_host=None,unveriable,method=Node)

    url为必传参数,data要传时必须传bytes(字节流)类型,若为字典,可用urllib.parse模块的urlencode()编码。

    headers是一个字典,它是一个请求头,我们可以在构造函数请求时通过headers参数直接构造,也可以通过调用请求实例的add_header()方法添加。添加请求头最常用的方法就是通过修改User-Agent来伪装浏览器,默认的User-Agent是Python-urllib,我们可以通过修改它来伪装浏览器。比如伪装成火狐浏览器,可将它设置为:Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0

    origin_req_host指的是请求方的host名称或者IP地址。

    unverifiable表示这个请求是否是无法通过验证的,默认是False,意思是说用户没有足够的权限来选择接受这个请求的结果。如:我们请求一个HTML文档中的图片,但是我们没有自动抓取图像的权限,这是unverifiable的值就是true。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from urllib import request,parse
    url = 'http://httpbin.org/post'
    headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0',
    'Host': 'httpbin.org'
    }
    dict = {
    'name': 'Germy'
    }
    data = bytes(parse.urlencode(dict),encoding='utf8')
    req = request.Request(url=url,data=data,headers=headers,method='POST')
    response = request.urlopen(req)
    print(response.read().decode('utf8'))

python的学习

准备

rmdir 删除目录

ipconfig 查看IP地址

ping 测试网络连接

python 进入python环境

exit() 退出环境

cd 指定目录

dir 浏览目录

mkdir 创建新的目录

pip包管理命令

pip list 列出匹配管理的包有哪些

pip install 包名

pip install redis == 3.0.2

pip uninstall 包名 卸载

pip -v 查看版本

pip freeze > requirements.txt 将项目依赖的包输出到指定的requirements.txt中

pip install -r requirements.txt 使用pip安装requirements.txt中依赖的文件,其中-r指requirements.txt文件

安装requests

requests是第三方库,python默认不会自带这个库。

  1. git clone https ://github.com/kennethreitz/requests
  2. cd requests
  3. python setup.py install
安装Selenium

Selenium是一个自动化测试工具,利用它我们可以驱动浏览器执行特定的动作,如点击,下拉等操作。对于一些js渲染的页面来说,这种抓取方式很有效。

print

print(value, …, sep=’ ‘,end=’\n’, file=sys.stdout, flush=False)

sep默认的分隔符是空格

python基础

数与表达式

乘方运算符的优先级比求负(单目减)高,因此-3**2等价于-(3**2)。如果要计算的是(-3)**2,必须明确指出。

十六进制:0xAF

八进制:010

二进制:0b101011

2 / 4 # 除法,得到一个浮点数

2 // 4 # 除法,得到一个整数

2 ** 5 # 乘方 32

用a + bj,或者complex(a,b)表示复数。

注意

1、Python可以同时为多个变量赋值,如a, b = 1, 2。

2、一个变量可以通过赋值指向不同类型的对象。

3、数值的除法包含两个运算符:/ 返回一个浮点数,// 返回一个整数。

4、在混合计算时,Python会把整型转换成为浮点数。

标准数据类型
  • Number数字
  • String字符串
  • List列表
  • Tuple元组
  • Set集合
  • Dictionary字典

有四种数字类型

  1. int整数
  2. bool布尔
  3. float浮点数
  4. complex复数

判断类型

type(a) :判断a的数据类型,不会认为子类是一种父类类型。

isinstance(a,int):判断a是否为int类型,会认为子类是一种父类类型。

del val_a, var_b 删除单个或多个对象。

字符串截取

str=’Runoob’ print(str) # 输出字符串

print(str[0:-1]) # 输出第一个到倒数第二个的所有字符

print(str[0]) # 输出字符串第一个字符

print(str[2:5]) # 输出从第三个开始到第五个的字符

print(str[2:]) # 输出从第三个开始后的所有字符

print(str * 2) # 输出字符串两次

print(str + ‘你好’) # 连接字符串

print(‘hello\nrunoob’) # 使用反斜杠()+n转义特殊字符

print(r’hello\nrunoob’) # 在字符串前面添加一个 r,表示原始字符串,不会发生转义

print 默认输出是换行的,如果要实现不换行需要在变量末尾加上 end=””

print(x) # 换行输出

print(x,end=” “) # 不换行输出

模块

import somemodule 导入整个模块

from somemodule import firstfunc, secondfunc, thirdfunc 从某个模块中导入多个函数

保留字

不可用作任何标识符名称。Python的标准库提供了一个keyword模块,可输出当前版本的所有关键字。

注释

单行以#开头,多行用三个单或双引号包裹。

urllib库

python内置的HTTP请求库

request解析库

urlopen:打开网址

bs4解析库

bs4: BeautifulSoup

用于解析网页,提取指定数据的。接口简单,较人性化,但bs4只能解析html格式的数据。

BeautifulSoup类

文件解析库:

‘html.parser’:是python自带的一个文件解析库。

‘lxml’:是一个第三方文件解析库,需安装。

根据html标签查找信息

1
content = soup.find('div',itemprop='acticleBody').text

find() 方法:返回第一个匹配的内容,为字符串类型

find(name,attrs,recursive,text,**kwargs)

这五个参数都可以充当过滤器,提高匹配结果的精确度。

find_all() 方法:返回所有匹配的项,为一个列表类型

find(name,attrs,recursive, text,limit,**kwargs)

实例

二维码
1
2
3
4
5
6
7
pip install MyQR

from MyQR Simport myqr
words: 文本,链接地址或字符串
picture:二维码的背景图片
colorized:True,表示生成彩图
save_name: 表示生成的二维码的名字
1
2
3
4
5
6
7
from MyQR import myqr
myqr.run(
words="https://www.icourse163.org/learn/NHDX-1449955168",
picture="1.png",
colorized=True,
save_name="ORMooc.png"
)
个人名片
1
2
3
4
5
6
7
8
9
10
pip install Segno

from segno import helpers
qr = helpers.make_mecard(
name='Erin',
city='湖南省衡阳市南华大学',
phone='1581529632',
email='3511564414@qq.com'
)
qr.save('Erin名片.png',scale=10)
爬取小说
1
2
3
4
import urllib.request

response = urllib.request.urlopen('http://www.39shubao.com/')
print(response.read().decode('utf-8'))

报错UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte

所以去掉解码格式utf-8,输出的字节码可以看到它是以”b’\x1f\x8b\x08”开头的 ,说明它是gzip压缩过的数据,这也是报错的原因,所以我们需要对我们接收的字节码进行一个解码操作。

1
2
3
4
5
6
7
8
9
import urllib.request
from io import BytesIO
import gzip

response = urllib.request.urlopen('http://www.39shubao.com/')
buff = BytesIO(response.read())
f = gzip.GzipFile(fileobj=buff)
htmls = f.read().decode('utf-8')
print(htmls)

解决乱码问题

1
2
3
4
import requests
url = 'http://www.shuquge.com/txt/63542/9645082.html'
response = requests.get(url)
print(response.text)

通过response.text拿到的数据出现乱码是因为该网页对数据进行加密,所以直接拿到的数据乱码了。

解决:

方法一:使用较老的方法response.encoding='utf-8',只能解决utf-8的乱码,其他格式的不能解决。

方法二:response.encoding=response.apparent_encoding,自动解决乱码格式,不仅utf-8格式的可解决,gbk格式的也可解决。

未封装的爬一章小说

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 requests
# 使用parsel来解析网页内容
import parsel
url = 'http://www.shuquge.com/txt/63542/9645082.html'
response = requests.get(url)
# 自动解决乱码问题
response.encoding = response.apparent_encoding
# print(response.text)
# 把网页数据结构化
sel = parsel.Selector(response.text)
# 根据css选择器提取标题
title = sel.css('.content > h1::text').get()
# 根据css路径提取内容
content = sel.css('#content::text').getall()
# 根据xPath提取内容,getall是提取全部的内容,也有re方法,用于匹配正则里面的内容
# print(sel.xpath('//div[@id="content"]/text()').getall())
# 保存数据
with open(title + '.txt',mode='w', encoding='utf-8') as f:
f.write(title)
# 用切片去掉最后三行
for con in content[:-3]:
# str使用replace去除空格
f.write(con.replace('\xa0',''))
# f.write(con.strip('\xa0'))
动态爬取数据

数据信息不是存放在网页源代码中,它是一个动态更新的网站,需要实时更新的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import json
import pandas as pd

url = 'https://api.inews.qq.com/newsqa/v1/automation/foreign/country/ranklist'
response = requests.get(url)
content = json.loads(response.text)
print(content['data'])
df = pd.DataFrame(columns=['国家及地区','确诊人数','死亡病例','治愈病例'])
for i in range(len(content['data'])):
df.loc[i + 1] = [content['data'][i]['name'],
content['data'][i]['confirm'],
content['data'][i]['dead'],
content['data'][i]['heal']]
df.to_csv('F://data.csv',index=0,encoding='utf-8')
print('爬取完成')

快速入门

AJAX学习

AJAX

传统网站存在的问题
  1. 网速慢的情况下,页面加载时间长,用户只能等待。
  2. 表单提交后,如果一项内容不合格,需要重新填写所有表单内容。
  3. 页面跳转,重新加载页面,造成资源浪费,增加用户等待时间。
Ajax概述

它是浏览器提供的一套方法,可以实现页面无刷新更新数据,提高用户浏览网站应用的体验。

应用场景
  1. 页面上拉加载更多数据,当下拉到底部并点击加载更多时才会向服务器发起请求并响应给用户。
  2. 列表数据无刷新分页,当从第一页切换到第二页时,只有列表中的数据才会变,头部和底部是不会发生变化的。
  3. 表单项离开焦点数据验证,当用户输入数据且该表单项失去焦点后,网页会自动将数据传给服务器进行验证,若已有该记录提示给用户。
  4. 搜索框提示文字下拉列表。
AJAX优缺点

浏览器可以从服务器同时请求多项内容,利用异步请求可以确保浏览器在后台工作,避免因完全页面刷新而中断用户的工作。

浏览器请求返回的速度会快得多,只是在有些情况下如此而已,请求和响应的速度取决于服务器返回的内容。AJAX页面完全有可能比传统页面的速度更慢。

不能够更真实的渲染颜色,因为颜色渲染由用户监视器控制而不是由用户支配。

只有页面真正改变部分会得到更新。

会减少服务器数据流量,利用AJAX可以建立更小更集中的请求。不过要当心……这也很容易导致建立更多的请求以至于增大数据流量,因为可能要一部建立所有这些请求。

页面的不兼容问题会更严重,由于除了XHTML外,AJAX页面还依赖于其它一些技术,所以使用AJAX后的兼容性问题实际会更严重。

Ajax运行原理及实现

原理

Ajax相当于浏览器发送请求与接收响应的代理人以实现在不影响用户浏览页面的情况下,局部更新数据,从而提高用户体验。

Ajax实现步骤

  1. 创建Ajax对象

    var xhr = new XMLHttpRequest();

  2. 告诉Ajax请求地址以及请求方式

    xhr.open('get','http://www.example.com')

  3. 发送请求

    xhr.send();

  4. 获取服务器端给与客户端的响应数据。

    1
    2
    3
    xhr.onload = function(){
    console.log(xhr.responseText);
    }

为避免ajax跨域请求,故用express开启一个后端服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//index.js文件
const express = require('express');
const path = require('path');
const app = express();

// 利用app.use拦截了所有请求,将请求交给express.static处理
// 也就是当网页请求该服务器时,请求全部转向public文件夹进行访问。
app.use(express.static(path.join(__dirname,'public')));

app.get('/first', (req,res) => {
res.send('hello ajax')
})

app.listen(3000);

console.log('服务器启动成功');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public/index.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
let xhr = new XMLHttpRequest();
xhr.open('GET','http://localhost:3000/first');
xhr.send();
xhr.onload = function(){
console.log(xhr.responseText);
}
</script>
</body>
</html>

服务器端相应的数据格式

在真实的项目中,服务器端大多数情况下会以JSON对象作为响应数据的格式。

在http请求与响应过程中,无论是请求参数还是响应内容,如果是对象类型最终都会被转换为对象字符串进行传输。

JSON.parse() //将json字符串转换为json对象

请求参数传递

传统网站表单提交

1
2
3
4
5
<form method="get" action="http://www.example.com">
<input type="text" name="username" />
<input type="password" name="password">
</form>
<!-- http://www.example.com?username=zhangsan&passwor=123456 -->

Ajax中的get请求

xhr.open('GET','http://www.example.com?name=zhangsan&age=20');

post请求

1
2
xhr.setResquestHeader('Content-Type','application/x-www-form-urlencoded')
xhr.send('name=zhangsan&age=20');

application/json

{name:’zhangsan’ , age: ‘20’ , sex: ‘男 ‘}

在请求头中指定Content-Type属性的值是application/json,告诉服务器当前请求参数的格式是json。

JSON.stringify() // 将json对象转换为json字符串

Ajax状态码

在创建ajax对象,配置ajax对象,发送请求以及接受完服务器端响应数据,这个过程中的每一个步骤都会对应一个数值,这个数值就是ajax状态码。

0:请求未初始化,还没调用open()

1:请求已经建立,但是还没有发送(还没调用send())

2:请求已经发送

3:请求正在处理中,通常响应中已经有部分数据可以用了。

4:响应已经完成,可以获取并使用服务器的响应了。

xhr.readyState //获取ajax状态码

onreadystatechange事件,当ajax状态码发生变化时将自动触发该事件。不过推荐使用xhr.onload方法。

Ajax错误处理
  1. 网络畅通,服务端能接收到请求,服务器返回的结果不是预期的结果。

    可以判断服务器端返回的状态码,分别进行处理。xhr.status获取http状态码。

  2. 网络畅通,服务器没有接收到请求,返回404状态码。

    检查请求地址是否错误。

  3. 网络畅通,服务器端能接受收到请求,服务器端返回500状态码。

    服务器端错误,找后端程序员进行沟通。

  4. 网络中断,请求无法发送到服务器端。(模拟断网,可在浏览器中的network中将offline勾选即可)

    会触发xhr对象下面的onerror事件,但不会会触发onload事件,在onerror事件处理函数中对错误进行处理。

封装Ajax函数
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
function ajax(options){
// 存储的是默认值
var default = {
type: 'get',
url: '',
data: {},
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
success: function(){},
error: function(){}

};
// 使用options对象中的属性覆盖default对象中的属性
Object.assign(default,options);
// 创建ajax对象
var xhr = new XMLHttpRequest();
// 拼接请求参数的变量
var params = '';
// 循环用户传递进来的对象格式参数
for(var attr in default.data){
// 将参数转换为字符串
params += attr + '=' + default.data[attr] + '&';
}
// 将参数最后的&截取掉
// 将截取的结果重新赋值给params变量
params = params.substr(0,params.length - 1);

// 判断请求方式
if(default.type == 'get'){
default.url = default.url + '?' + params;
}
// 配置ajax对象
xhr.open(default.type,default.url);
// 如果是post请求
if(default.type == 'post'){
var contentType = default.header['Content-Type'];
// 设置请求参数格式的类型
xhr.setRequestHeader('Content-Type',contentType);
if(contentType == 'application/json'){
xhr.send(JSON.stringify(default.data))
}else{
xhr.send(params);
}
}else{
// 发送请求
xhr.send();
}
// 监听xhr对象上的onload事件
// 当xhr对象接收完响应数据后触发
xhr.onload = function(){
// 获取响应头中的数据
var contentType = xhr.getResponseHeader('Content-Type');
// 服务器端返回的数据
var responseText = xhr.responseText;
// 如果响应类型中包含application/json
if(contentType.includes('application/json')){
responseText = JSON.parse(responseText);
}
if(xhr.status == 200){
default.success(xhr.responseText,xhr);
}else{
default.error(xhr.responseText,xhr);
}
}
}

ajax({
type: 'get',
url: 'http://lcalhost:3000/first',
data: {
name: 'zhangsan',
age:20
},
header:{
'Content-Type': 'json'
},
sucess: function(data){
console.log(data);
},
error:function(err){
console.log(err);
}
})
验证邮箱地址的唯一性
  1. 获取文本框并为其添加离开焦点事件
  2. 离开焦点时,检测用户输入的邮箱地址是否符合规则。
  3. 如果不符合规则,组织程序向下执行并给出提示信息。
  4. 向服务端发送请求,检测邮箱地址是否被别人注册。
  5. 根据服务端返回值决定客户端显示何种提示信息。

常用元字符

代码 说明
. 匹配换行符以外的任意字符
\w 匹配字母或数字或下划线
\s 匹配任意的空白符
\d 匹配数字
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

常用限定符

代码/语法 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

以此举个例子如邮箱:Yuan.Yiuri691@ningmeng.com.cn
正则表达式为:^[A-Za-z0-9-._]+@[A-Za-z0-9-]+(\.[A-Za-z0-9]+)\*(\.[A-Za-z]{2,6})$

创建过程:

  1. 开头和结尾:^$;
  2. 加入@:^@$;
  3. @前的内容匹配:[A-Za-z0-9-._] 大小写字母,数字,”.”,下划线 随机出现;”+” 号表示当前规则中的字符可以重复;
  4. @后的内容受顶级域名的限制,按点号分割后的规则略有不同:(.[A-Za-z]{2,6}) 必须是字母出现,比如.com,.cn等
1
2


node进阶

单线程的弱点

  1. 无法利用多核CPU。
  2. 错误会引起整个应用退出,应用的健壮性值得考虑。
  3. 大量计算占用CPU导致无法继续调用异步I/O。

像浏览器中JavaScript与UI共用一个线程一样,JavaScript长时间执行会导致UI渲染和响应被中断。在Node中,长时间的CPU占用也会导致后续异步I/O发不出调用,已完成的异步I/O的回调函数也会得不到及时执行。

Node采用了与Web Workers相同的思路来解决单线程中大计算量的问题:child_process。

子进程的出现,意味着Node可以从容地应对单线程在健壮性和无法利用多核CPU方面的问题。通过计算分发到各个子进程,可以将大量计算分解掉,然后再通过进程之间的事件消息来传递结果,这可以很好的保持应用模型的简单和地低依赖。通过Master-Worker的管理方式,也可以很好地管理各个工作进程,以达到更高的的健壮性。

node简介

  • Node.js是一个js运行环境,实际上它是对Google v8引擎的封装。V8引擎执行js的速度快,性能好。node.js对一些特殊用例进行了优化,提供了替代的API,使得V8在非浏览器环境上运行的更好。
  • node.js是一个基于chrome javascript运行建立的平台,用于方便的搭建响应速度快,易于拓展的网络应用。node.js使用事件驱动,非阻塞I/O模型得以轻量和高效,非常适合在分布式设备上运行数据密集型的实时应用。
  • node采用一系列“非阻塞”库来支持事件循环的方式。本质上就是为文件系统,数据库之类的资源提供接口。向文件系统发送一个请求时,无需等待硬盘(寻址并检索文件),硬盘准备好的时候非阻塞接口会通知node。该模型以可拓展的方式简化了对慢资源的访问,直观易懂。
  • node.js可以在不新增额外线程的情况下,依然可以对任务进行并行处理——node.js是单线程的。它通过事件轮询(event loop)来实现并行操作,因此,我们应该要充分的利用这一点——尽可能地避免阻塞操作

node.js组成部分

  • 引入required模块:我们可使用required指令载入node.js模块 var http = require(“http”);
  • 创建服务器:服务器可监听客户端请求,类似于Apache,Nginx等http服务器 。使用http.createServer()方法创建服务器,并使用listen方法绑定端口。
  • 接收请求和响应请求:服务器很容易创建,客户端可以使用浏览器或终端发送http请求,服务器接收请求并返回响应数据。使用request和response参数来接收和响应数据。

npm包管理器

  • 允许用户从NPM服务器下载别人编写的第三方包到本地使用。
  • 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用
  • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用

NPM常用命令

NPM提供了很多命令,例如install和publish,使用npm help可查看所有命令。

  • NPM提供了很多命令,例如installpublish,使用npm help可查看所有命令。

  • 使用npm help <command>可查看某条命令的详细帮助,例如npm help install

  • package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。

  • 使用npm update <package>可以把当前目录下node_modules子目录里边的对应模块更新至最新版本。

  • 使用npm update <package> -g可以把全局安装的对应命令行程序更新至最新版。

  • 使用npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。

  • 使用npm unpublish <package>@<version>可以撤销发布自己发布过的某个版本代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    npm -v  						查看版本
    npm version 查看所有模块的版本
    npm search 包名/部分包名 搜索包
    npm init 初始化package.json文件
    npm install/i 安装包
    npm remove/r包名 删除包
    npm install/i 包名 --save 安装包并添加到依赖中
    npm install 根据package.json下载当前项目所依赖的包
    npm install 包名 -g 全局安装包,用于一些编译根据,比如:gulp,webpack
  • 注意:如果想在页面中引入node_module中某个模块,优先从当前目录引入,如果没有,则从上一级目录中找,直到根目录。

REPL交互式解释器

  • REPL(Read Eval Print Loop:交互式解释器)表示一个电脑的环境,类似于终端,可接受系统的响应。node自带了交互式解释器,可执行以下任务
    • 读取,读取用户输入,解析输入js数据结构并存储于内存中。
    • 执行,执行输入的数据结构。
    • 打印,输出结果。
    • 循环,循环操作以上步骤直到用户两次按下ctrl+c按钮退出。

node知识

node.js与其它语言的区别
  • node.js不是一门独立的语言。php,jsp即使语言,又是平台。node.js用js进行编程,运行平台是封装后的js引擎V8

  • 轻量级架构

    • java,php,net,需要运行在服务器上,apache,tomcat
    • node.js不用架设在任何服务器软件之上。
    • 用最低的硬件成本,达到更高的开发,更优的处理函数。
  • node.js没有web容器,就是安装配置完成之后,没有根目录(php的根目录是www)

node.js的特点(追求极致性能)
  • 单线程
    • 优势:减少内存消耗(操作系统不再有创建线程,销毁线程的开销。
      • 在php,jsp等服务器语言中,会为每个用户创建一个线程,而每个线程大约需要2M内存,每创建一个线程就要占用内存空间。
      • 当有客户链接时,就会触发一个内部事件,通过非阻塞I/O,事件驱动机制,让node.js宏观上是并发的。可同时处理4万用户的请求。即当张三连接着时,李四请求连接,引擎就会停止张三语法的执行,转而将李四加入到时间栈中。
      • node.js不为每个用户创建一个线程,仅仅使用同一个线程。
    • 劣势:
      • 如果线程遭遇I/O阻塞,整个线程便阻塞了。
      • 如果有人将node.js搞崩溃了,则会全部奔溃。
  • 非阻塞
    • node.js采用非阻塞I/O机制,因此在执行完访问数据库操作后,会立即执行后面的代码(其他非阻塞事件不会,他们会等数据库操作完毕并返回结果才执行后面的代码),把数据库的处理代码放入回调函数中,从而提高效率。
    • 当某个I/O执行完毕后,将以事件的形式通过执行I/O操作的线程,线程执行完这个事件的回调函数。为了处理异步I/O,线程必须有事件循环,不断检查有没有未处理的事件,并依次予以处理。
    • 阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远执行计算操作,这个线程的CPU核心利用率永远是100%;
  • 事件驱动
    • 不管用户的请求,还是老用户的I/O完成,都将以事件的形式加入事件环,等待调度。
  • node.js的iI/O都是异步的,都是回调函数调回调函数
node事件触发
  • 继承EventEmitter,大多数情况下不会直接使用EventEmitter,而是在对象中继承它。包括 fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。
  • 原因:
    • 具有某个实体功能的对象实现时间符合语义,事件的监听和发射应该是一个对象的方法。
    • js对象是基于原型的,支持多重继承,继承EventEmitter不会打乱对象原有的继承关系。
node.js应用方向
  1. 特点
    • 善于I/O,不善于计算。
      • 因为node.js最擅长任务的调度,如果你的任务有很多CPU计算,实际上相当于这个计算阻塞了这个单线程,就不适合node开发。
      • 当应用程序需要处理大量并发的I/O,而在向客户端发出响应后,应用程序内部并不需要进行复杂的处理时,node也非常适合与websocket配合,开发长连接的实时交互应用程序。
    • 天生异步
      • callback,trunk(参数的求值策略),promise,generator(es6的生成器,用于计算),asynac函数

node模块化

  • CommonJS规范为JS能够在任何地方执行,这是一个愿景。
  • 从文件角度看,每个JS文件就是一个个模块,从结构上看,多个JS文件之间可以相互require共同实现一个功能,这整体功能就是一个模块。
  • 在node.js中,一个模块定义的变量只能在该文件中使用,当需要另一文件中的变量时,需使用exports进行暴漏,然后使用require引入。
  • 引入模块时,如果是非核心模块,且在同级目录时,require需要加上./,核心模块则不需要,直接写名字即可
node核心模块
  • 全局变量是global。

  • 每个node都在外面给我们套了一个函数。

    function(exports,require,module,_filename,_dirname){//里面是你写的内容}

    • exports:该对象用来将函数内部的局部函数暴漏到外部函数中。
    • require:用来引入外部模块。
    • module:代表当前模块本身,exports就是module的属性。我们可以使用exports或modile.exports导出。
    • _filename:当前模块的完整路径
    • _dirname:当前模块所在的文件的完整路径。
  • exports与module.exports的区别

  • json文件不能加注释。

Buffer缓存区

  • 可以理解为是一个存放二进制的容器,专门用于数据的存放。
  • node自带的,不需要引入就可使用。一个字节占8bit
  • 8bit = 1B 1024B = 1KB 1024KB = 1MKB 1GB=1024KB 1TB=1024GB
  • Buffer.from(str,编码格式);
  • Buffer.alloc(size[,fill[,encoding]])
    • size:新建的Buffer期望的长度,不能动态改变,溢出的数据不做处理。int值
    • fill:用来预填充新建的Buffer的值,默认为0. String Buffer int
    • encoding:如果fill为字符串,则该值就是它的字符编码,默认为“utf-8”

fs文件处理

  • fs.open(path,flag[,mode],callback)打开文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //1.引入模块
    let fs = require('fs')
    //2.创建写入流,相当于在it666.txt与服务器之间建立一个通道,使数据可以源源不断从it666.txt中上传至服务器中
    let ws = fs.createWriteStream("it666.txt");
    //3.打开通道,监听打开事件,有on,once等方法,因只需监听一次,故用once
    stream.once('open',() => {
    console.log('通道已经打开');
    })
    stream.once('close', ()=> {
    console.log('通道已经关闭');
    });
    //写入东西
    stream.write('dfsadc');
    stream.write('dfsadc');
    //关闭通道,当发现还有东西未写入时,不会关闭
    stream.end()
    1
    2
    3
    4
    5
    let fs = require('fs')
    let ws = fs.createWriteStream("it666.txt");
    let rs = fs.createReadStream("sp.mp4");
    //创建管道,该语句就会自动将数据存入sp.mp4中,同时监听打开关闭等事件
    rs.pipe(ws);

数据库

  • 数据库就是按照一定的数据结构来组织,存储和管理数据的仓库。
  • 我们写的程序都是在计算机上运行的,一旦计算机断电或程序运行完毕,程序数据就会全部丢失,所以我们需要将一些程序运行的数据持久性保存到硬盘中,以确保数据的安全性。
  • 选择数据库的原因:
    • 数据库是有结构的,数据与数据之间可以建立各种关系,类似于网状拓扑图。
    • 数据库提供各种接口,让各种操作(增删改查)变得快捷简单。
    • 给个种语言(php,jsp,java)提供了完善的接口。
  • 数据库分类
    • RDBMS(关系型数据库):MySQL,SQL Server,ORACLE,DB2….
    • NoSQL(非关系型数据库 Not only SQL):MongoDB,CouchDB,HBase,Redis…
      • 没有行列的概念,用JSON来存储数据,集合就相当于“表”,文档就相当于“行”。
      • 非关系型数据库为非标准化的数据库。
      • 特征:键值存储数据库,列存储数据库,文档存储数据库,图形数据库。
    • 两者区别:关系型数据库比较结构化,操作不是很灵活;非关系型数据库操作灵活,但不适合大型数据存储,比较适合微架构….两者相辅相成。
MongoDB
  • MongoDB是为快速开发互联网Web应用而设计的数据库系统。他的数据类型是面向文档的,类似于JSON的结构。
  • 基本组成
    • 数据库(database):数据库是一个仓库,在仓库中可以存放集合。
    • 集合(collection):集合类似于数组,在集合中可以存放文档。
    • 文档(document):文档数据库中的最小单位,我们存储和操作的内容都是文档。
  • mongoDB的基本指令:
    • show dbs :显示当前所有的数据库
    • use 数据库名 :进入到指定的数据库中
    • db :显示当前数据库
    • show collections :显示数据库中的所有集合
  • 命令进行CRUD
    • 插入:db..insert(doc); 如:db.student.insert({id:001,name:’nikita’});
    • 查询:db..find();

同步与异步,阻塞与非阻塞

  • 同步,当发起一个调用时,在没有获取结果前,调用不会返回,直到获取结果。事一件一件做,做完一件在做一件。

  • 异步:当发起一个调用时,在没有获取结果之前,调用就返回了,调用者并不会立即得到结果,而是被调用者通知调用调用者,通过回调函数处理结果。

  • 阻塞,在等待结果时,不能干其他事,线程被挂起,直到结果返回。

  • 非阻塞:在等待结果中,还能干其他事,线程不会被阻塞。

url相关操作

  • 将url解析为一个url对象:url.parse(urlString[,parseQueryString[,slashDenotHost]])
  • 将一个url对象反解析为一个url地址:url.format(urlObject)
  • 将部分url拼接为一个完整url地址url.resolve(from,to)
  • response对象有一个方法:write可以用来给客户端发送响应数据。write可以使用多次,但最后需要用end来结束响应,否则客户端会一直等待
  • req.url是地址栏中localhost后面的所有内容,除了hash的内容。
  • querystring.parse将字符串转为一个对象。

上传文件

使用第三方插件

需要先安装formidable包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let http = require('http');
let url = require('url');
let formidable = require('formidable');
let util = require('util');//用于将object转为字符串
http.createServer((req,res) => {
if(req.url === '/postmsg' && req.methods.tolowerCase() === "post"){
//实例化对象
let form = new formidable.IncomingForm();
//设置上传文件路径
form.uploadDir = './uploads';
//获取表单内容
form.parse(req,(err,fileds,files) => {
res.writeHead(200,{"content-Type":"text/plain;charset=UTF-8"});
res.end(util.inspect({fields:fields,files:files}));
})
}
}).listen(80,'127.0.0.1');

模块编译

在Node中,每个文件模块都是一个对象,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
function Module(id, parent){
this.id = id;
this.exports = {};
this.parent = parent;
if(parent && parent.children){
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}

编译和执行是引入文件模块的最后一个阶段。定位到具体文件后,Node会新建一个模块对象,然后通过路径载入并编译。对于不同的文件扩展名,其载入的方式也有所不同,具体如下。

1
2
3
4
5
6
7
.js文件。通过fs模块同步读取文件后编译执行。

.node文件。这是用C/C++编写的扩展文件,通过dlopen()方法加载最后的编译生成的文件。

.json文件。通过fs模块同步读取文件后,用JSON.parse()解析返回结果。

其余扩展名文件。它们都被当作.js文件解析。(因为node只能解析js文件,其他文件最后都会被转化成js文件,故当其余扩展名文件出现时,node无法识别,故将其认为是默认扩展名进行解析,即.js)

每一个编译成功的模块都会将文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。

根据不同文件扩展名,node会调用不同的读取方式,如:.json文件调用如下:

1
2
3
4
5
6
7
8
9
Module._extensions['.json'] = function(module,filename){
var content = NativeModule.require('fs').readFileSync(filename,'utf8');
try{
module.exports = JSON.parse(strinpBOM(cntent));
}catch(err){
err.message = filename + ':' + err.message;
throw err;
}
}

其中,Module._extensions会被赋值给 require() 的extensions属性,所以通过在代码中访问require.extensions可知道系统中已有的加载方式。编写代码测试一下:

1
2
3
4
//新建index.js文件
console.log(require.extensions);
//运行node index.js,输出
//[Object: null prototype] { '.js': [Function], '.json': [Function], '.node': [Function] }

如果想对自定义的扩展名进行特殊的加载,可以通过类似require.extensions[‘.ext’]的方式实现。早期的CoffeeScript文件就是通过添加require.extensions[‘.coffee’]扩展方式来加载的。但是从v0.10.6版本开始,官方不鼓励通过这种方式进行自定义扩展名的加载,而是期望先将其它语言或文件先编译成js文件后再进行加载,这样做的好处是不将繁琐的编译加载等过程引入node的执行过程。

在确定文件的扩展名后,node将调用具体的编译方式来将文件执行后返回给调用者。

注:我们都知道CommonJS模块规范中,每个模块文件都存在require、exports、module、_filename、_dirname这5个变量却不知其从何而来。若是把直接定义模块的过程放在浏览器端,势必会存在污染全局变量的情况,故其不可能。

事实上,在编译过程中,node会对获取的js文件内容进行包装,具体如下:

1
2
3
4
5
6
(function(require、exports、module、_filename、_dirname){
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius;
}
})

这样每个模块文件之间都进行了作用于隔离。包装之后的代码会通过vm原生模块的runInThisContext() 方法执行,类似于eval,只是具有明确上下文,不污染全局,返回一个体的function对象。最后,将当前模块对象的exports属性,require()方法,module(模块对象本身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。

这就是为什么这些变量并没有在每个模块声明却可以使用的原因。在执行后,模块的exports属性被返回给了调用方。exports属性上的任何方法都可以被外部调用到,但是模块中的其余变量和属性则不可直接被调用。

至此,require、exports、module的流程已经完整,这就是Node对CommonJS模块规范的实现。

JSON文件的编译

json文件的编译是三种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用。

JSON文件在用作项目的配置文件时比较有用。如果你定义了JSON文件作为配置,那就不用调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响。

这里我们提到的模块编译都是指文件编译,即用户自己编写的模块。

内建模块

内建模块的优势在于:首先,他们本身由C/C++编写,性能上优于脚本语言;其次,其次,在进行文件编译时,他们会被编译进二进制文件。一旦node开始执行,他们会被直接加载进内存中,无需再次坐标识符定位、文件定位、编译等过程,直接就可执行。

Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助加载内建模块。

在加载内建模块时,我们会先创建一个exports空对象,然后调用get_builtin_module()方法取出内建模块对象,通过执行register_func()填充exports对象,最后将exports对象按模块名缓存,并返回给调用方完成导出。

http进阶

Telnet实例

由于Http使用的是TCP/IP传输协议,而且它是基于文本的,没有使用哪些难以理解的二进制格式,因此很容易直接1与Web服务器进行对话。

Telnet程序可以将键盘连接到某个目标TCP端口,并将此TCP端口的输出回送到显示屏上。Telnet常用于远程终端会话,但它几乎可以连接所有的TCP服务器,包括HTTP服务器。

Web结构组件

  1. 代理:位于客户端和服务器之间的HTTP中间实体。
  2. 缓存:HTTP的仓库,是常用页面的副本可以存在离客户端更近的地方。
  3. 网关:连接应用程序的特殊Web服务器。
  4. 隧道:对HTTP通信报文进行盲转发的特殊代理。
  5. Agent代理:发起自动HTTP请求的半智能Web客户端。
代理

HTTP代理服务器是Web安全,应用集成以及性能优化的重要组成模块。

代理位于客户端与服务器之间,接收所有客户端的HTTP请求,并将这些请求转发给服务器(可能会对请求进行修改之后转发)。对用户来说,这些应用程序就是一个代理,代表用户访问服务器。

出于安全考虑,通常会将代理作为转发所有Web流量的可信任中间节点使用。代理还可以对请求和响应进行过滤。比如,在企业中对下载的应用程序进行病毒检测,或者对小学生屏蔽一些成人才能看的内容。

缓存

WEb缓存(Web cache)或代理缓存(proxy cache)是一种特殊的HTTP代理服务器,可以将经过代理传送的常用文档复制保存起来。下一个请求同一文档的客户端就可以享受缓存的私有副本所提供的服务了。

网关

网关(gateway)是一种特殊的服务器,作为其他服务器的中间实体使用,通常用于将HTTP流量转换成其他的协议。网关接受时就好像自己是咨议员的源端服务器一样。客户端可能并不知道自己正在与一个网关进行通信。

如:一个HTTP/FTP网关会通过HTTP请求接受对FTP URI的请求,但通过FTP协议来获取文档。得到的文档会被封装成一条HTTP报文发给客户端。

隧道

隧道(tunnel)是建立起来之后就会在两条连接之间对原始数据进行盲转发的HTTP应用程序。HTTP隧道通常用来在一条或多条HTTP连接上进行转发非HTTP数据,转发时不会窥探数据。

HTTP隧道的一种常见用途是通过HTTP连接承载加密的安全套接字层(SSL,Secure Socket Layout)流量,这样ssl流量就可以穿过只允许Web流量通过的防火墙了。

隧道

如:HTTP/SSL隧道收到一条HTTP请求,要求建立一条到目的的地址和端口的输出连接,然后在HTTP信道上通过隧道传输加密的SSL流量,这样就可以将其转发到目的服务器上。

Agent代理

用户代理是代表用户发起HTTP请求的客户端程序。所有发布Web请求的应用程序都是HTTP Agent代理。到目前为止,我们只提过一种HTTP Agent代理:WEb浏览器,但用户Agent代理还有很多类型。

如:有些自己会在Web上闲逛的自动用户Agent代理,可以在无人监视的情况下发布HTTP事务并获取内容。这些自动代理的名字很生动,比如“网络蜘蛛”或“web机器人”。网络蜘蛛会在WEb上闲逛,搜集信息以构建有效的Web内容档案,比如一个搜索引擎的数据库或者为比较购物机器人生成的产品目录。

image-20200430094207000

HTTP报文

请求报文格式

1
2
3
<method>     <request-URL>  <version>
<headers>
<entrity-body>

响应报文

1
2
3
<version>   <status>   <response-phrase>
<headers>
<entrity-body>
方法(method)

客户端希望服务器对资源执行的动作,是一个单独的词,比如GET,HEAD或POST.

方法 描述 是否包含主体
GET 从服务器获取一份文档
HEAD 只从服务器获取文档的首部
POST 向服务器发送需要处理的数据
PUT 将请求的主体部分存储在服务器上
TRACE 对可能经过代理服务器传送到服务器上去的报文进行追踪
OPTIONS 决定可以在服务器上执行哪些方法
DELETE 从服务器上删除一份文档

扩展方法:指的是没有在HTTP/1.1规范中定义的方法,服务器会为它所管理的资源实现一些二HTTP服务,这些方法为开发者提供了一种扩展这些HTTP服务能力的手段。

方法 描述
LOCK 允许用户“锁定”资源——比如,可以在编辑某个资源的时候将其锁定,以防别人对其进行修改。
MKCOL 允许用户创建资源
COPY 便于在服务器上复制资源
MOVE 在服务器上移动资源

惯例:对所发送的内容要求严一点,对接收的内容宽容一些处理扩展方法。

  1. 请求URL(request-URL)

    命名了所有请求资源,或者URL路径组件的完整URL。如果直接与服务器进行对话,只要URL的路径组件是资源的绝对路径,通常就不会有什么问题——服务器可以假定自己是URL的主机/端口。

  2. 版本(version)

  3. 状态码(status-code)

    这三位数字描述了请求过程中所发生的情况。每个状态码的第一位数字都用于描述状态的一般类型(“成功”、“出错”)。

    状态码分类

    整体范围 已定义范围 分类
    100-199 100-101 信息提示
    200-299 200-206 成功
    300-399 300-305 重定向
    400-499 400-415 客户端错误
    500-599 500-505 服务器错误

    常见状态码

    状态码 原因短语 含义
    200 OK 成功。请求的所有数据都在响应主体中。
    401 Unauthorized(未授权) 需要输入用户名和密码
    404 Not Found(未找到) 服务器无法找到所请求URL对应的资源
    501 Not Implemented(无法实现)

    信息性状态码

    状态码 原因短语 含义
    100 Continue 说明受到了请求的初始部分,请客户端继续,发送了这个状态码后,服务器在收到请求后必须进行响应。
    101 Switching Protocols 说明服务器正在根据客户端的指定,将协议切换成Update’首部所列的协议。

    100 Continue是一种优化,客户端应用程序只有在避免向服务器发送一个服务器无法处理或使用的大实体时才会使用100 Continue。当服务端超时一定时间后,客户端直接将实体发出去。

    成功状态码

    状态码 原因短语 原因短语
    200 请求没问题,实体的主体部分包含了所请求的资源 OK
    201 用于创建对象的请求(PUT)。响应的主体部分包含各种已创建资源的URL,location首部包含则是最具体的引用。 Created
    202 请求已被接受,但服务器还未对其执行任何动作,不能保证服务器会完成这个请求。 Accepted
    203 实体部分包含的资源不是来自源服务器,而是来自资源的一份副本。如果中间节点上有一份资源副本,但无法对它所发送的与资源有关的元信息(首部)进行验证,就会出现这种情况。 Non-Authoritative-Information
    204 响应报文中包含若干首部和一个状态行,但没有实体部分,主要用于在浏览器不转为显示新文档的情况下,对其将进行更新(如刷新一个表单页面) No Content
    205 另一个主要用于浏览器的代码,负责告诉浏览器清除当前页面中所有HTML表单元素。 Reset Content
    206 成功执行一部分请求。稍后会看到,客户端可以通过一些特殊的首部来获取部分或某个范围内的文档——这个状态码就说明请求成功了。 Partial Content

    重定向状态码

    状态码 原因短语 描述
    300 Multiple Choices 客户端请求一个实际指向多个资源的URL时会返回这个状态码,比如服务器上有某个HTML文档的英语和法语版本。返回这个代码时会带有一个选项列表,这样用户就可以选择他希望使用的那一项了。有多个版本1可用时,客户端需要沟通解决。
    301 Moved Permananently 在请求的URL已被移除时使用。响应的Location首部中应该包含资源现在所处的URL。
    302 Found 与301状态码相似,但是客户端应该使用Location首部给出的URL来临时定位资源。将来的请求仍应使用老的URL。
    303 See Other 告知客户端应该用另一个URL来获取资源。新的URL位于响应报文的Location首部。其主要目的是允许POST请求的响应将客户端定向到某个资源上去。
    304 Not Modified 客户端可以通过所包含的请求首部,使其请求变成有条件的。如果客户端发起一个GET请求,而最近资源未被修改,就可用这个状态码来说明资源未被修改,带有这个状态吗的响应不应该包含实体的主体部分。
    305 Use Proxy 用来说明必须通过一个代理来访问资源,代理的位置由Location首部给出。很重要一点是,客户端是相对某个特定资源来解析这个响应的,不能假定所有请求,甚至所有对持有所请求资源的服务器的请求都通过这个代理进行。如果客户端错误的让代理介入了某条请求,可能会引发破环性的行为,而且会造成安全漏洞。
    306 (未使用) 当前未使用
    307 Temporary Redirect 与301状态码相似,但是客户端应该使用Location首部给出的URL来临时定位资源。将来的请求仍应使用老的URL。

    客户端错误

    状态码 原因短语 描述
    400 Bad Request 用于告诉客户端他发送了一个错误的请求
    401 Unauthorized 与适当的首部一同返回,在这些首部中请求客户端在获取对资源的访问权之前,对自己进行认证。
    402 Payment Required 现在这个状态码还未使用,但已经被保留,以作未来之用。
    403 Forbidden 用以说明请求被服务器拒绝了,如果服务器想说明为什么拒绝请求,可以包含实体的主体部分来对原因进行描述。但这个状态通常是在服务器不想说明拒绝原因的时候使用的。
    404 Not Found 用于说明服务器无法找到所请求的URL。通常会包含一个实体以便客户端应用程序显示给用户看。
    405 Method Not Allowed 发起的请求中带所请求的URL不支持的方法时,使用此状态码。应该在响应中包含Allow首部,以告知客户端对请求的资源可以使用哪些方法。
    406 Not Acceptable 客户端可以指定参数来说明他们愿意接受什么类型的实体。服务器没有与客户端可接受的URL相匹配的资源时,使用此代码。通常,服务器会包含一些首部以便客户端弄清楚为什么请求无法满足。
    407 Proxy Authentication Required 与401状态码类似,但用于要求对资源进行认证的代理服务器。
    408 Request Timeout 如果客户端完成请求所花的时间太长,服务器可以回送此状态码,并关闭连接。超时时长随服务器的不同而不同,但通常对所有合法请求来说都是够长的。
    409 Conflict 用于说明请求可能在资源上引发的一些冲突,服务器担心请求会引发冲突时可发送此状态码。响应中应该包含描述冲突的主体。
    410 Gone 与404相似,只要服务曾经拥有过此资源。主要用于Web站点的维护,这样服务器的管理者就可以在资源被移除的情况下通知客户端了。
    411 Length Required 服务器要求在请求报文中包含Content-Length首部时使用。
    412 Precondition Failed 客户端发起了条件请求且其中一个条件失败时使用。
    413 Request Entrity Too Large 客户端发送实体主体部分比服务器能够或希望处理的要大时发送此状态码。
    414 Request URL Too Long 客户端发送请求中的URL比服务器希望处理的要大时发送此状态码。
    415 Unsupported Media Type 服务器无法理解或无法支持客户端所发送实体的内容类型时
    416 Unsupported Media Type 请求报文所请求的是指定资源的某个范围,而此范围无效或无法满足时使用此状态码。
    417 Expectation Failed 请求的Expect请求首部包含了一个期望,但服务器无法满足此期望时,使用此状态码。如果代理或其他中间应用程序与确切证据说明源端服务器会为某请求产生一个失败的期望就可发送此状态码。

    服务器错误

    状态码 原因短语 含义
    500 Internal Server Error 服务器遇到一个妨碍它为请求提供服务的错误时使用此状态码。
    501 Not Implemented 客户端发起的请求超出服务器的能力范围。
    502 Bad Gateway 作为代理或网关使用的服务器从请求响应链的下一条链路上收到了一条伪响应(如,他无法链接到其父网关时)。
    503 Service Unavailable 用来说明服务器现在不能为请求提供服务,但将来可以。如果服务器知道什么时候资源会变为可用的,可在响应中包含一个Retry-After首部。
    504 Gateway Timeout 与状态码408相似,只是这里的响应来自一个网关或代理,他们在等待另一服务器对其请求进行响应时超时了。
    505 HTTP Version Not Supported 服务器收到的请求使用了它无法或不愿支持的协议版本。有些服务器应用程序会选择不支持协议的早期版本。
  1. 原因短语(reason-phrase)

    数字状态码的可读版本,包含行终止序列的所有文本。

  2. 首部(header)

    可以有零个或多个首部,每个首部都包含一个名字,后面跟着一个冒号,然后是一个可选的空格,接着是一个值,最后是一个CRLF。首部是由一个空行结束的,表示了首部列表的结束和实体主体部分的开始。

    首部分类

    • 通用首部:既可以出现在请求报文中,又可以出现在响应报文中。
    • 请求首部:提供更多有关请求的信息。
    • 响应首部:提供更多有关响应的信息。
    • 实体首部:描述主体的长度和内容,或者资源自身。
    • 扩展首部:规范中没有定义的新首部。

    常见首部实例

    首部实例 描述
    Date:Tue,3 Oct 1997 02:16:03 GMT 服务器产生响应的日期
    Content-length:15040 实体的主体部分包含了
    Content-type:image/gif 实体的主体部分是一个GIF图片
    Accept:image/gif, image/jpeg, text/html 客户端可以接收
    Connection 允许客户端和服务器指定与请求或响应连接有关的选项。
    MIME-Version 给定发送端使用的MIME版本。
    Trailer 如果报文采用了分块传输编码(chunked transfer encoding)方式就可以用这个首部列出位于报文拖挂(trailer)部分的首部集合。
    Transfer-Encoding 告知接收端为了保证报文可靠传输,对报文采取什么编码方式。
    Update 给出了发送端可能想要“升级”使用的新版本或协议。
    Via 显示了报文经过的中间节点(代理,网关)
  3. 实体的主体部分(entrity-body)

    实体的主体部分包含一个由任意数据组成的数据块。并不是所有的报文都包含实体的主体部分。

通用首部

通用缓存首部:HTTP/1.0引入了第一个允许HTTP应用程序缓存对象本地副本的首部,这样就不需要总是直接从源端服务器获取了。

首部 描述
Cache-Control 用于随报文传送缓存指示。
Pragma 另一种随报文传送指示的方式,但并不专用于缓存。

请求首部:是只在请求报文中有意义的首部,用于说明是谁或什么在发送请求,请求源自何处,或者客户端的喜好和能力。服务器可以根据请求首部给出的客户端信息,试着为客户端提供更好的响应。

image-20200502215359440

Accept首部为客户端提供一种将其喜好和能力告知服务器的方式,包括它们想要什么,可以使用什么,以及最重要的,他们不想要什么,这样,服务器可以根据这些额外的信息,对要发送的内容做出更明智的决定。Accept首部会使连接的两端都受益。客户端会得到它们想要的内容,服务器则不会浪费时间和带宽来发送客户端无法使用的东西。

image-20200502220337984

条件请求首部:有时客户端希望为请求加上某些限制。比如,如果客户端已经有了一份文件副本就希望只在服务器上的文档与客户端拥有的副本有所区别时才请求服务器传输文档。通过条件请求首部,客户端就可以为请求加上这种限制,要求服务器在对请求进行响应前,确保某个条件为真。

image-20200502220845355

安全请求首部

HTTP本身就支持一种简单的机制,可以对请求进行质询/响应认证。这种机制要求客户端在获取特定资源之前,先对自身进行认证,这样就可以使事务稍微安全一些。

image-20200502221420633

代理请求首部

image-20200502221552479

响应报文

响应报文有自己的响应首部集。响应首部为客户端提供了一些额外信息,比如谁发送响应,响应者的功能,甚至与响应相关的一些特殊指令,这些首部有助于客户端处理响应,并在将来发起更好的请求。

image-20200503080735091

协商首部

如果资源有多种表示方法——如,若服务器上有某文档的法语和德语译稿,HTTP/1.1可以为服务器和客户端提供对资源进行协商的能力。

image-20200503082210004

安全响应首部

首部 描述
Proxy-Authenticate 来自代理的对客户端的质询列表
Set-Cookie 不是真正的安全首部,但隐含有安全功能,可以在客户端设置一个令牌,以便服务器对客户端进行标识。
Set-Cookie2 与Set-Cookie类似
www-Authenticate 来自服务器的对客户端的质询列表
HTTP事务产生时延的原因
  1. 客户端首先需要根据URI确定Web服务器的IP地址和端口号。如果最近没有对URI中的主机名进行访问,通过DNS解析系统将URI中的主机名转换成一个IP地址可能要花费数十秒的时间。
  2. 接下来,客户端会向服务器发送一条TCP连接请求,并等待服务器回送一个请求经接受应答。每条新的TCP连接都会有连接建立时延。这个值通常最多只有一两秒钟,但如果有数百个HTTP事务的话,这个值就会快速叠加上去。
  3. 一旦连接建立起来,客户端就会通过新建立的TCP管道来发送HTTP请求。数据到达时,Web服务器会从TCP链接中读取请求报文,并对请求进行处理。因特网传输请求报文,以及服务器请求报文都需要时间。
  4. 然后,Web服务器会回送HTTP响应,这也需要花费时间。

注:大多数HTTP客户端都有一个小的DNS缓存,用来保存近期所访问站点的IP地址。如果已经在本地“缓存”了IP地址,查询就可立即完成,因为大多数Web浏览器浏览的是少数常用站点,所以通常可以很快将主机名解析出来。

TCP网络时延的大小取决于硬件速度,网络和服务器的负载,请求和响应报文的尺寸,以及客户端和服务器之间的距离。

TCP连接握手步骤
  1. 请求新的TCP连接时,客户端要向服务器发送一个小的TCP分组(通常是40-60字节)。这个分组中设置了一个特殊的SYN标记,说明这是一个连接请求。
  2. 如果服务器接受了连接,就会对连接参数进行计算,并向客户端回送一个TCP分组,这个分组中的SYN和ACK标记都被置位,说明连接请求已被接受。
  3. 最后,客户端向服务器回送一条确认消息,通知他连接已成功建立,现代的TCP栈都允许客户端在这个确认分组中发送消息。

小的HTTP事务可能会在TCP建立上花费50%,或更多的时间

延迟确认

由于因特网自身无法确保可靠的分组传输(因特网路由器超负荷的话,可以随意丢弃分组),所以TCP实现了自己的确认机制来确保数据的成功传输。

每个TCP段都有一个序列号和数据完整性校验和。每个段的接收者收到完好的段时,都会向发送者回送小的确认分组。如果发送者,欸有在指定的窗口时间内收到确认信息,发送者就会认为分组已被破环或损毁,并重发数据。

由于u企鹅人保温很小,所以TCP允许在发往相同方向的输出数据分组中对其进行“捎带”。TCP将将返回的确认信息与输出的数据分组结合在一起,可以更有效利用网络。为了增加确认报文找到同向传输数据分组的可能性,很多TCP栈都实现了一种延迟确认算法。延迟确认算法会在一个特定的窗口时间内将输出确认缓存放在缓存区中,以寻找能够捎带他的输出数据分组。如果在那个时间段内没有输出数据分组,就将确认信息放在单独的分组中传送。

但是,HTTP具有双峰特征的请求——应答行为就降低了捎带信息的可能。当希望有相反方向回传分组的时候,偏偏没有那么多。通常,延迟确认算法会引起相当大的时延。根据所使用的操作系统的不同,可以调整或禁止延迟确认算法。

在对TCP栈的任何参数进行修改之前,一定要对袭击在做什么会有清晰的了解。TCP中引入这些算法的目的是为了防止设计欠佳的应用程序对因特网造成破环。对TCP配置进行任意修改都要绝对确保应用程序不会引发这些i算法所要避免的问题。

TCP慢启动

TCP数据传输的性能还取决于TCP连接的使用期。TCP连接会随着时间进行自我“调谐”,起初还会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度,这种调谐被称为TCP慢启动,用于防止因特网的突然过载和拥塞。

TCP慢启动限制了一个TCP端点在任意时刻可以传输的分组数。简单来讲是每成功接收一个分组,发送端就有了发送另外两个分组的权限。如果某个HTTP事务有大量的数据要发送,是不能依次将所有分组都发送出去的,必须发送一个分组,确认后才可以再发。这种方式是”打开拥塞窗口“。

由于存在这种拥塞控制特性,所以新连接的传输速度会比已经交换过一定数据量的,“已调谐”连接慢一些。由于已调谐连接要更快一些,所以HTTP中有一些可以重用现存连接的工具。即HTTP“持久连接”。

提高HTTP连接性能的四类方法
  1. 并行连接
  2. 持久连接
  3. 管道化连接
  4. 复用连接

ES6的学习

ES6变量声明

let命令
  • 声明变量,只在所在的代码块有效,处于暂时性死区
  • for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
  • 变量不提升,不允许重复声明
  • 允许在块级作用域内声明函数,函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
const命令
  • const声明一个只读的常量。一旦声明,常量的值就不能改变。且必须立即初始化,不能留到以后赋值。只声明不赋值,就会报错。
  • const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
将对象彻底冻结
  • 简单冻结 :const foo = Object.freeze({});

  • 对象及属性都冻结:

    var constantize = (obj) => {

    Object.freeze(obj);

    Object.keys(obj).forEach((key,i) =>{

    if( typeof obj[key] === ‘object’) {

    constantize(obj[key]);}

    });};

import命令
class命令
顶层对象
  • 浏览器环境中的顶层对象指window对象,node指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
    • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
    • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
    • Node 里面,顶层对象是global,但其他环境都不支持。
  • 一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。
  • 同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。
    • 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
    • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
    • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么evalnew Function这些方法都可能无法使用。

数组解构

  • 只需模式匹配即可用数组方式为各种数据类型赋值
  • 解构赋值允许指定默认值。只有当一个数组成员严格等于undefined,默认值才会生效,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined

this

  • 全局环境中,this会返回顶层对象(浏览器是window,Node是global)。但是,Node模块和ES6模块中,this返回当前块
  • 函数里面的this,如果函数不是作为对象的方法执行,而是单纯作为函数运行,this会指向顶层对象。但是在严格模式下,这时的this会返回undefined
  • 不管是严格模式还是普通模式,new Function(‘return this’)(),总是返回全局对象。但是,如果浏览器用了CSP(Content Security Policy,内容安全策略),那么eval,new Function这些方法都可能不能使用。

字符串

  • 字符串的遍历器接口

    • for(let 变量名 of ‘遍历对象’){}使得字符串如字符串数组般被遍历。

    • 除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点

      let text = String.fromCodePoint(0x20BB7);

      for(let i = 0;i < text.length; i++){

      ​ console.log(text[i]);

      }

      //“ “

      // ” “

      //for循环认为其包含两个字符

      for(let i of text){

      ​ console.log(text[i]);

      }

      //”吉“

      //for…of循环会正确识别出这一个字符

  • 为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD8000xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

    1
    2
    JSON.stringify('\u{D834}') // ""\\uD834""
    JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""
  • 模板字符串

    • 模板字符串(template string)是增强版的字符串,用反引号(~)标识。他可以当作普通字符串使用,也可以用来定义多行字符串,或在字符串中嵌入变量。

      1
      2
      3
      4
      5
      6
      7
      > $('#result').append(
      > 'There are <b>' + basket.count + '</b> ' +
      > 'items in your basket, ' +
      > '<em>' + basket.onSale +
      > '</em> are on sale!'
      > );
      >
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      > // 普通字符串
      > `In JavaScript '\n' is a line-feed.`
      >
      > // 多行字符串
      > `In JavaScript this is
      > not legal.`
      >
      > console.log(`string text line 1
      > string text line 2`);
      >
      > // 字符串中嵌入变量
      > let name = "Bob", time = "today";
      > `Hello ${name}, how are you ${time}?`
      >
    • 上面的代码都是用反引号表示,如果在模板字符串中需要使用反引号,则前面要用反斜杠转义

      1
      2
      > let greeting = `\`Yo\` World!`;
      >
    • 如果使用模板字符串表示多行字符串,所有空格和缩进都会被保留在输出之中。如不想这个换行,可使用trim方法消除他。

      1
      2
      3
      4
      5
      6
      7
      > $('#list').html(`
      > <ul>
      > <li>first</li>
      > <li>second</li>
      > </ul>
      > `.trim());
      >
    • 模板字符串嵌入变量,需要将变量名写在${}之中。还可以调用函数大括号内部可以放入任意的js表达式,可进行运算,以及引用对象属性。

    • 如果大括号内的值不是字符串,将按照一般规则转为字符串。若是字符串则会调用对象的toString方法。若模板字符串的变量没有声明,将会报错。

  • 标签模板

    • 它可紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。

      1
      2
      3
      alert `123`
      //等价于
      alert(123);
    • 模板字符串里面如果有变量,就不是简单调用,而是将模板字符串先处理成多个参数,在调用函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      let a = 5;
      let b = 10;

      tag`Hello ${ a + b } world ${ a * b }`;
      // 等同于
      tag(['Hello ', ' world ', ''], 15, 50);
      function tag(stringArr, ...values){
      // ...
      }

字符串新增方法

  • String.fromCodePoint()

    • 可以识别大于0xFFFF的字符,弥补了String.fromCharCode()方法的不足。
  • String.raw()

    • 该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。
  • codePointAt()

  • normalize()

    • 用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。
确定一个字符串是否包含在另一个字符串中
  • indexof()

  • includes():返回布尔值,表示是否找到了参数字符串。

  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。

  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

  • repeat():返回一个新字符串,表示将原字符串重复n次。
    • 其参数会先进行往0方向取整再代值,如是负数或Infinity会报错。参数NaN等同于 0
1
2
3
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
  • padStart()用于头部补全,padEnd()用于尾部补全。如果某个字符串不够指定长度,会在头部或尾部补全。
1
2
3
4
5
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
  • trimStart()trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
  • matchAll()方法返回一个正则表达式在当前字符串的所有匹配

正则表达式

  • 如果正则构造函数的第一个参数是一个正则对象,那么就可以使用第二个参数指定修饰符。且返回的正则表达式会忽略原有的正则表达式修饰符,只使用新指定的修饰符。

    1
    2
    new RegExp(/abc/ig,'i').flags
    //"i"
  • 字符串对象共有 4 个方法,可以使用正则表达式:match()replace()search()split()

    ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

    • String.prototype.match 调用 RegExp.prototype[Symbol.match]
    • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
    • String.prototype.search 调用 RegExp.prototype[Symbol.search]
    • String.prototype.split 调用 RegExp.prototype[Symbol.split]
具名组匹配
1
2
3
4
5
6
7
> const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
>
> const matchObj = RE_DATE.exec('1999-12-31');
> const year = matchObj[1]; // 1999
> const month = matchObj[2]; // 12
> const day = matchObj[3]; // 31
>
  • 允许为每一个组匹配指定一个名字。模式的头部添加“问号 + 尖括号 + 组名”,然后就可以在exec方法返回结果的groups属性上引用该组名。

    1
    2
    3
    4
    5
    6
    7
    > const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
    >
    > const matchObj = RE_DATE.exec('1999-12-31');
    > const year = matchObj.groups.year; // 1999
    > const month = matchObj.groups.month; // 12
    > const day = matchObj.groups.day; // 31
    >
  • RegExp.prototype.flags 属性返回正则表达式的修饰符。

先行断言

“先行断言”指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。“先行否定断言”指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/

1
2
/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them') // ["44"]
后行断言

“后行断言”正好与“先行断言”相反,x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=\$)\d+/。“后行否定断言”则与“先行否定断言”相反,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!\$)\d+/

1
2
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"]
属性
  • RegExp.prototype.unicode属性表示是否设置了u修饰符。
  • y修饰符隐含了头部匹配的标志^

函数的拓展

  • ES6允许为函数的参数设置默认值,即直接写在参数定义的后面

    function log(x,y=’world’){

    ​ console.log(x,y);

    }

    • 好处:阅读代码的人可立即意识到哪些参数可以省略,不用查看函数体或文档。其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数也不会导致以前的代码无法运行

    • 参数变量是默认声明的,故不能用let和const再次声明。否则会报错

    • 使用参数默认值时,函数不能有同名参数

    • 参数默认值不是传值的,而是每次都重新计算默认值表达式的值,即参数默认值是惰性求值的。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      > let x = 99;
      > function foo(p = x + 1) {
      > console.log(p);
      > }
      >
      > foo() // 100
      >
      > x = 100;
      > foo() // 101
      > //参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。
      >
  • 函数与解构默认值结合使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    > function foo({x, y = 5}) {
    > console.log(x, y);
    > }
    >
    > foo({}) // undefined 5
    > foo({x: 1}) // 1 5
    > foo({x: 1, y: 2}) // 1 2
    > foo() // TypeError: Cannot read property 'x' of undefined
    >
    • 上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值就可避免这种情况。

      function foo({x,y=5} = {}){

      ​ console.log(x,y);

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      > // 函数没有参数的情况
      > m1() // [0, 0]
      > m2() // [0, 0]
      >
      > // x 和 y 都有值的情况
      > m1({x: 3, y: 8}) // [3, 8]
      > m2({x: 3, y: 8}) // [3, 8]
      >
      > // x 有值,y 无值的情况
      > m1({x: 3}) // [3, 0]
      > m2({x: 3}) // [3, undefined]
      >
      > // x 和 y 都无值的情况
      > m1({}) // [0, 0];
      > m2({}) // [undefined, undefined]
      >
      > m1({z: 3}) // [0, 0]
      > m2({z: 3}) // [undefined, undefined]
      >
    • 参数默认值一般是尾参数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      > function f(x, y = 5, z) {
      > return [x, y, z];
      > }
      >
      > f() // [undefined, 5, undefined]
      > f(1) // [1, 5, undefined]
      > f(1, ,2) // 报错
      > f(1, undefined, 2) // [1, 5, 2]
      >
  • 函数的length属性

    • 指定了默认值后,函数的length属性将返回没有指定默认值的参数个数。也就是说指定了默认值后,length属性将失真。

    • length属性的含义是该函数预期传入的参数个数,某个指定了默认之后,预期传入的参数个数就不包括这个参数了。

    • 如果设置默认值的参数不是尾参数,那么length属性将不再计入后面的参数了。

      1
      2
      3
      4
      5
      6
      7
      > (function(a){}).length  //1
      > (function(a=5){}).length //0
      > (function(a,b,c=9){}).length //2
      > (function(...args){}).length //0
      > (function(a=0,b,c){}).length //0
      > (function(a,b=2,c){}).length //1
      >
  • 作用域

    • 一旦设置了参数默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用于就会消失。这种情况在没有默认参数值是不会出现的。

      var x = 1;

      function f(x, y = x){

      ​ console.log(y);

      }

      f(2) //2

      //上面代码中,参数y的默认值等于变量x。调用函数时,参数会形成一个单独的作用域。在这个作用域里,默认变量x指向第一个参数x,而不是全局变量x,所以输出的是2

      let x = 1;

      function f(y=x){

      ​ let x = 2;

      ​ console.log(y);

      }

      f() //1

      //函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。若全局变量x不存在,就会报错。

    • 参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。

      1
      2
      3
      4
      5
      6
      7
      8
      > var x = 1;
      >
      > function foo(x = x) {
      > // ...
      > }
      >
      > foo() // ReferenceError: x is not defined
      >
    • 函数的参数默认值是一个匿名函数,当函数参数形成的单独作用域里面并没有定义该变量时,该变量就会指向外层的全局变量。若函数外层并无该变量,则会报错。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      > var x = 1;
      > function foo(x, y = function() { x = 2; }) {
      > var x = 3;
      > y();
      > console.log(x);
      > }
      >
      > foo() // 3
      > x // 1
      >
  • rest参数

    ​ function 函数名(…变量名){}

    • rest参数搭配的变量是一个数组,该变量会将多余的参数放入数组中。
    • 与arguments对象不同,arguments对象是一个类数组。
    • rest参数之后不能有其他参数(及只能是是最后一个参数),否则会报错。当然,函数的length属性也不包括rest参数。
  • 严格模式下函数参数不能使用默认值,解构赋值或拓展运算符。两种解决方法:

    • 设定全局式的严格模式。

      ‘use strict’;

      function (a,b=a){

      //code

      }

    • 把函数包在一个无参数的立即执行函数中。

      const doSomething = (function(){

      ‘use strict’;

      return function(value = 42){

      ​ return value;

      };

      }());

  • name属性返回函数的函数名

    • 如果将一个匿名函数赋值给一个变量,Es5的name属性会返回空字符串,ES6返回实际函数名。

    • 如果将一个具名函数赋值给一个变量,两者都返回这个具名函数原本的名字。

      const bar = function baz(){};

      //ES5

      bar.name //“baz”

      //ES6

      bar.name //“baz”

    • Function构造函数返回的函数实例,name属性的值为anonymous

      (new Function).name //“anonymous”

    • bind返回的函数,name属性值会加上bound前缀

      1
      2
      3
      4
      5
      > function foo() {};
      > foo.bind({}).name // "bound foo"
      >
      > (function(){}).bind({}).name // "bound "
      >
箭头函数(=>)

var f= v => v;

//等同于

var f = function(v){

return v;

};

  • 由于大括号被解释为代码块,故如果箭头函数直接返回一个对象会报错,必须在对象外面加上括号;

    //报错

    let gettempItem = id => { id: id, name: “temp” };

    //不报错

    let gettempItem = id => ({ id: id, name: “temp”})

  • 使用箭头函数注意点

    • 函数体内的this对象,也就是定义时所在的对象,而不是使用是所在的对象。this对象的指向在js中是可变的。但在箭头函数中则是固定的。
    • 不可以当作构造函数,即不可使用new命令,否则会抛出一个错误。
    • 不可使用arguments对象,该对象在函数体内不存在。如果要用,可用rest参数替换。也没有super,new.target
    • 不可使用yield命令,因此箭头函数不能用作Generator函数。
  • this指向的固定化,并不是因为内部有绑定this机制,实际原因是箭头函数没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

  • 箭头函数不适用场景

    • 定义对象的方法,且该方法内部包含this。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。

      1
      2
      3
      4
      5
      6
      7
      8
      > const cat = {
      > lives: 9,
      > jumps: () => {
      > this.lives--;
      > }
      > }
      > //上面的this指向的是全局作用域,不是cat
      >
    • 需要动态this时也不应使用箭头函数。下面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

      var button = document.getElementById(‘press’);

      button.addEventListener(‘click’,()=>{

      ​ this.classList.toggle(‘on’);

      });

尾调用(Tail Call)
  • 尾调用优化只在严格模式下开启,正常模式是无效的,因为正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

    • func.arguments:返回调用时函数的那个参数。

    • func.caller:返回调用函数当前函数的那个函数。

      1
      2
      3
      4
      5
      6
      function restricted() {
      'use strict';
      restricted.caller; // 报错
      restricted.arguments; // 报错
      }
      restricted();
  • 尾调用是函数式编程的一个重要概念。指某个函数的最后一步是调用另一个函数。

  • 尾调用之所以与其他调用不同就在于其调用位置。我们知道,函数调用会在内存中形成一个”调用记录“,又称“调用帧”,保存调用的位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方还会形成B的调用帧。等到B运行结束,将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

  • 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

  • 尾调用优化,即只保留内层函数的调用帧。如果所有函数都是尾调用,就可以完全做到每次执行一次时,调用帧只有一项,这将大大节省内存。注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

尾递归
  • 尾部调用自身即为尾递归。

  • 递归是非常耗内存的,因为需要同时保存成千上万个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

    function factorial(n,total){

    ​ if(n === 1) return total;

    ​ return factorial(n-1,n*total);

    }

  • 尾调用优化的Fibonacci数列

    function Fibonacci2(n,ac1 = 1, ac2 = 1){

    ​ if(n<=1) {return ac2};

    ​ return Fibonacci2(n-1,ac2,ac1+ac2);

    }

数组

  • Array.from()将两类对象(类数组和可便利的对象)转为真正的数组。

    • 只要是部署了Iterator接口的数据结构,Array.from就都可以将其转为真正的数组。
    • 扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个对象就无法转换。
    • 类数组对象本质是有length属性,即只要有length属性的对象都可以通过Array.from方法转为数组。

    //es5

    var arr1 = [].slice.call(arrayLike);

    //es6

    let arr2 = Array.from(arrayLike);

    //NodeList对象

    let ps = document.querySelectorAll(‘p’);

    Array.from(ps).filter(p => {

    ​ return p.textContent.length.length >100;

    });

    //arguments对象

    function foo(){

    var args = Array.from(arguments);

    }

    • 对于没有部署该方法的浏览器可使用Array.prototype.slice方法替换。

      const toArray = ( () =>

      ​ Array.from ? Array.from : obj => [].slice.call(obj);

      )( );

    • Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

      Array.from(arrayLike,x => x * x);

      Array.from(arrayLike).map( x=> x*x);

      //两者等价

  • Array.of()用于将一组值转换为数组。

    1
    2
    3
    4
    > Array.of(3, 11, 8) // [3,11,8]
    > Array.of(3) // [3]
    > Array.of(3).length // 1
    >
    • 下面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。

      1
      2
      3
      4
      > Array() // []
      > Array(3) // [, , ,]
      > Array(3, 11, 8) // [3, 11, 8]
      >
  • Array.prototype.copyWithin(target,stsart=0,end=this.length)

    • 数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
  • 查找

    • find():用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找到第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。回调函数可接受三个参数,依次为当前的值,当前的位置和原数组。

      [1,2,3,10,15].find(function(value,index,arr){

      ​ return value > 9;

      }) //10

    • findIndex():返回第一个符合条件的数组成员的位置,如果都不符合则返回-1;

    • 这两个方法都可以接受第二个参数,用于绑定回调函数的this对象。

      function f(v){

      ​ return v > this.age;

      }

      let person = {name:’john’,age:20};

      [12,15,26,42].find(f,person); //26

    • 都可发现NaN,弥补数组中indexOf方法的不足。indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

      [NaN].indexOf(NaN)

      //-1

      [NaN].findIndex(y => Object.is(NaN,y))

      //0

  • fill()使用给定值进行数组填充

    • 如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
    • fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
    • fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    > ['a', 'b', 'c'].fill(7)
    > // [7, 7, 7]
    >
    > new Array(3).fill(7)
    > // [7, 7, 7]
    >
    > ['a', 'b', 'c'].fill(7, 1, 2)
    > // ['a', 7, 'c']
    >
  • 遍历数组

    • entries()keys()values()——用于遍历数组。它们都返回一个遍历器对像,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

    • 如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      > for (let [index, elem] of ['a', 'b'].entries()) {
      > console.log(index, elem);
      > }
      > // 0 "a"
      > // 1 "b"
      > let letter = ['a', 'b', 'c'];
      > let entries = letter.entries();
      > console.log(entries.next().value); // [0, 'a']
      > console.log(entries.next().value); // [1, 'b']
      > console.log(entries.next().value); // [2, 'c']
      >
  • includes()

    • Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似

    • 该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

      1
      2
      3
      4
      > [1, 2, 3].includes(2)     // true
      > [1, 2, 3].includes(4) // false
      > [1, 2, NaN].includes(NaN) // true
      >
      1
      2
      3
      > [1, 2, 3].includes(3, 3);  // false
      > [1, 2, 3].includes(3, -1); // true
      >
    • 没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值.

      1
      2
      3
      4
      5
      6
      7
      8
      > if (arr.indexOf(el) !== -1) {
      > // ...
      > }
      > [NaN].indexOf(NaN)
      > // -1
      > [NaN].includes(NaN)
      > // true
      >
      • indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。
      1
      2
      3
      4
      5
      6
      7
      8
      > const contains = (() =>
      > Array.prototype.includes
      > ? (arr, value) => arr.includes(value)
      > : (arr, value) => arr.some(el => el === value)
      > )();
      > contains(['foo', 'bar'], 'baz'); // => false
      > //简易的替代版本。
      >
    • 另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。

      • Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
      • Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)WeakSet.prototype.has(value)
  • flat()和flatMap()

    • 数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

    • flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为

    • 可以用Infinity关键字作为参数。

    • 如果原数组有空位,flat()方法会跳过空位。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      > [1, 2, [3, 4]].flat()
      > // [1, 2, 3, 4]
      > [1, 2, [3, [4, 5]]].flat(2)
      > // [1, 2, 3, 4, 5]
      > [1, [2, [3]]].flat(Infinity)
      > // [1, 2, 3]
      > [1, 2, , 4, 5].flat()
      > // [1, 2, 4, 5]
      >
    • flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。flatMap()只能展开一层数组。

      1
      2
      3
      4
      5
      6
      7
      > // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
      > [2, 3, 4].flatMap((x) => [x, x * 2])
      > // [2, 4, 3, 6, 4, 8]
      > // 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
      > [1, 2, 3, 4].flatMap(x => [[x * 2]])
      > // [[2], [4], [6], [8]]
      >
    • flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

      1
      2
      3
      4
      > arr.flatMap(function callback(currentValue[, index[, array]]) {
      > // ...
      > }[, thisArg])
      >
  • 数组中的空位

    • 数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

    • 1
      2
      3
      > 0 in [undefined, undefined, undefined] // true
      > 0 in [, , ,] // false
      >
    • ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位.

      • forEach(), filter(), reduce(), every()some()都会跳过空位。
      • map()会跳过空位,但会保留这个值
      • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      > // forEach方法
      > [,'a'].forEach((x,i) => console.log(i)); // 1
      >
      > // filter方法
      > ['a',,'b'].filter(x => true) // ['a','b']
      >
      > // every方法
      > [,'a'].every(x => x==='a') // true
      >
      > // reduce方法
      > [1,,2].reduce((x,y) => x+y) // 3
      >
      > // some方法
      > [,'a'].some(x => x !== 'a') // false
      >
      > // map方法
      > [,'a'].map(x => 1) // [,1]
      >
      > // join方法
      > [,'a',undefined,null].join('#') // "#a##"
      >
      > // toString方法
      > [,'a',undefined,null].toString() // ",a,,"
      >
    • Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

    • 扩展运算符(...)也会将空位转为undefined

    • copyWithin()会连空位一起拷贝。

    • fill()会将空位视为正常的数组位置。

    • for...of循环也会遍历空位.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      > Array.from(['a',,'b'])
      > // [ "a", undefined, "b" ]
      > [...['a',,'b']]
      > // [ "a", undefined, "b" ]
      > [,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
      > new Array(3).fill('a') // ["a","a","a"]
      > let arr = [, ,];
      > for (let i of arr) {
      > console.log(1);
      > }
      > // 1
      > // 1
      >
    • entries()keys()values()find()findIndex()会将空位处理成undefined。由于空位的处理规则非常不统一,所以建议避免出现空位。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      > // entries()
      > [...[,'a'].entries()] // [[0,undefined], [1,"a"]]
      >
      > // keys()
      > [...[,'a'].keys()] // [0,1]
      >
      > // values()
      > [...[,'a'].values()] // [undefined,"a"]
      >
      > // find()
      > [,'a'].find(x => true) // undefined
      >
      > // findIndex()
      > [,'a'].findIndex(x => true) // 0
      >

对象的扩展

属性遍历
  • for…in:遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

  • Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

  • Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

  • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名。

  • Reflect.ownKeys(obj):返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

  • 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

    • 首先遍历所有数值键,按照数值升序排列。
    • 其次遍历所有字符串键,按照加入时间升序排列。
    • 最后遍历所有 Symbol 键,按照加入时间升序排列。
    1
    2
    3
    > Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
    > // ['2', '10', 'b', 'a', Symbol()]
    >
super关键字
  • this关键字总是指向函数所在的当前对象,关键字super则是指向当前对象的原型对象,super只能用在对象的方法中,用在其他地方会报错。

  • JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

    const proto ={foo:’hello’};

    const obj = {

    ​ foo:’world’,

    ​ find(){

    ​ return super.foo;

    ​ }

    }

    Object.setPrototypeOf(obj,proto);

    obj.find(); //“hello”

    //报错

    const obj = {foo:super.foo; //对象的属性}

    //只有对象方法的简写式才可以被js引擎确认定义的是对象的方法。

    //以下两种都报错报错

    const obj = {

    ​ foo:() => super.foo

    }

    1
    2
    3
    4
    5
    6
    > const obj = {
    > foo: function () {
    > return super.foo
    > }
    > }
    >
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};

const obj = {
x: 'world',
foo() {
super.foo();
}
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"

上面代码中,super.foo指向原型对象protofoo方法,但是绑定的this却还是当前对象obj,因此输出的就是world

解构赋值
  • 扩展运算符的解构赋值,不能复制继承自原型对象的属性。
1
2
3
4
5
6
7
> let o1 = { a: 1 };
> let o2 = { b: 2 };
> o2.__proto__ = o1;
> let { ...o3 } = o2;
> o3 // { b: 2 }
> o3.a // undefined
>

对象新增方法

  • Object.is():在所有环境中,只要两个值是一样的,他们就相等

    • 与严格相等(===)功能差不多,但是弥补了NaN不等于自身的缺陷。

    • +0不等于-0

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      > Object.is('foo', 'foo')
      > // true
      > Object.is({}, {})
      > // false
      > +0 === -0 //true
      > NaN === NaN // false
      >
      > Object.is(+0, -0) // false
      > Object.is(NaN, NaN) // true
      >
  • Object.assign()用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。

    • Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。

    • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

    • 如果只有一个参数,Object.assign会直接返回该参数。

    • 如果该参数不是对象,则会先转成对象,然后返回。由于undefinednull无法转成对象,所以如果它们作为参数,就会报错。

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      > const target = { a: 1, b: 1 };
      >
      > const source1 = { b: 2, c: 2 };
      > const source2 = { c: 3 };
      >
      > Object.assign(target, source1, source2);
      > target // {a:1, b:2, c:3}
      >
      > const obj = {a: 1};
      > Object.assign(obj) === obj // true
      > typeof Object.assign(2) // "object"
      > Object.assign(undefined) // 报错
      > Object.assign(null) // 报错
      >
    • 如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefinednull不在首参数,就不会报错。其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。因为只有字符串的包装对象,会产生可枚举属性。

      1
      2
      3
      4
      > let obj = {a: 1};
      > Object.assign(obj, undefined) === obj // true
      > Object.assign(obj, null) === obj // true
      >
      1
      2
      3
      4
      5
      6
      7
      > const v1 = 'abc';
      > const v2 = true;
      > const v3 = 10;
      >
      > const obj = Object.assign({}, v1, v2, v3);
      > console.log(obj); // { "0": "a", "1": "b", "2": "c" }
      >

数组中的遍历

  • reduce求和,求平均值

    1
    2
    3
    4
    5
    arr.reduce((tmp,item,index) => { 
    //tmp是一个前一次的总和
    //item是第几个数的值
    //index是下标值,因第一个tmp是第一个arr[0],所以index是从1开始
    })
  • map 映射

    1
    2
    3
    4
    5
    arr.map((item) => {
    //item是第几个数的值
    return item*2;
    });
    arr.map(item => item*2);
  • filter过滤器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    arr.filter(item => {
    //返回true者留下,返回false消失
    if(item % 3 == 0){
    return true;
    }else{
    return false;
    }
    })
    //相当于
    arr.filter(item => item%3 == 0);
  • forEach 循环迭代

    1
    2
    3
    arr.forEach( (item,index) => {

    })

字符串新增方法

  • startsWith:检测str是否以某些字符的开头
  • endsWith:检测str是否以某个字符的结尾
  • 字符串模板 :${变量}

面向对象class类

1
2
3
4
5
6
7
8
9
class User{
constructor(name,pass){
this.name = name;
this.pass = pass;
}
showName(){
alert(this.name);//不可以使用function,不用加逗号,表示属性,而非方法
}
}
  1. class关键字,构造器和类分开

  2. class里面直接加方法

继承

老版写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function User(name,pass){
this.name = name;
this.pass = pass;
}
User.prototype.showName = function(){
alert(this.name);
}
User.prototype.showPass = function(){
alert(this.pass);
}
function vipUser(name,pass,level){
User.call(this,name,pass);
this.level = level;
}
vipUser.prototype = new User();
vipUser.prototype.constructor = vipUser;
vipUser.prototype.showlevel = function(){
alert(this.level);
};
var v1 = new vipUser('blue','12345',10);
v1.showName();
v1.showlevel();

super——超类,就相当于父类

extends——继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class User{
constructor(name,pass){
this.name = name;
this.pass = pass;
}
showName(){
alert(this.name);
}
showPass(){
alert(this.pass);
}
}
class VipUser extends User{
constructor(name,pass,level){
super(name,pass);
this.level = level;
}
showLevel(){
alert(this.level);
}
}
var v1 = new VipUser('nikita','15243',10);
v1.showName();
v1.showLevel();

json

标准写法:
  • 只能用双引号。
  • 所有名字都必须用引号包起来。
方法:
  • 字符串化:

    1
    2
    JSON.stringify({"a":12,"name":"nikita"});
    //"{"a":12,"name":"nikita"}"
  • 解析为一个json对象:

    1
    2
    JSON.parse({"a":12,"name":"nikita"});
    //{name: "nikita", age: 12}

Promise(消除异步操作)

  • Promise对象只有三种状态:peding(进行中),fulfilled(已成功),rejected(已失败)。

  • Promise实例具有then方法,then返回一个新的Promise实例。Promise对象,如果该对象状态变为resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。

    1
    2
    3
    4
    5
    6
    p.then((val) => console.log('fulfilled:', val))
    .catch((err) => console.log('rejected', err));

    // 等同于
    p.then((val) => console.log('fulfilled:', val))
    .then(null, (err) => console.log("rejected:", err));
  • 用同步一样的方式来书写异步代码。接收一个含resolve和reject的函数参数。

  • resolve——解决,即成功 reject——拒绝,即失败

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    let p = new Promise(function(resolve,reject){
    //异步代码
    $.ajax({
    url:'arr.txt',
    dataType:'json',
    success(arr){
    resolve(arr);
    },
    error(err){
    reject(err);
    }
    })
    });
    //then函数有两个参数,一个是成功的回调函数,一个是失败的回调函数
    p.then(function(){
    alert('成功了');
    },function(){
    alert('失败了');
    })
    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
    //若有两个Promise对象,可使用Promise.all进行全部处理
    //其中的arr是p1和p2中的数据
    function createPromise(url){
    return new Promise(function(resolve,reject){
    $.ajax({
    url,
    dataType:'json',
    success(arr){
    resolve(arr);
    },
    error(err){
    reject(err);
    }
    })
    });
    }
    Promise.all([
    createPromise('data/arr.txt'),
    createPromise('data/json.txt')
    ]).then(function(arr){
    //解构赋值
    let [res1,res2] = arr;
    alert('全部成功')
    },function(){
    alert('至少有一个失败');
    })

    高版本jquery也封装了promise对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Promise.all([
    $.ajax({url:'data/arr.txt',dataType:'json'}),
    $.ajax({url:'data/json.txt',dataType:'json'})
    ]).then(function(results){
    let [arr,json] = results;
    alert('成功了');
    },function(err){
    alert(err);
    })

    Promise.race 竞速,即先到先执行

    1
    2
    3
    4
    5
    6
    Promise.race([
    $.ajax({url:'http://a2.taobai.com/data/users'})
    $.ajax({ur4:'http://a2.taobai.com/data/users'})
    $.ajax({ur7:'http://a2.taobai.com/data/users'})
    $.ajax({ur2:'http://a2.taobai.com/data/users'})
    ])
Promise.prototype.finally()
  • 该方法为不管Promise对象最后如何,都会执行的代码。

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

1
2
3
4
5
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
Promise.all()
  • 用于将多个Promise实力包装为一个新的Promise实例。

    1
    const p = Promise.all([p1,p2,p3]);

    p的状态由p1p2p3决定,分成两种情况。

    (1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

    (2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

1
2
3
4
5
6
7
8
9
10
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
Fetch API是js接口
  • 用于访问和操作HTTP管道的部分,例如请求和响应。

Generator函数

  • 是ES6提供的一种异步编程解决方案,是一个状态机,封装了很多内部状态。执行Generator函数会返回一个遍历器对象,说明Generator函数同时还是一个遍历器对象生成函数,返回的遍历器对象可以一次遍历每个状态。

  • 特征 * yield

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function* test(value1){
    let value2 = yield value1;
    let value3 = yield value2;
    return value3;
    }
    //第一个yield后的值是传入的参数,第二个是第二个next方法传入的...
    var hw = test(1);
    hw.next(5);
    //第一个next传入的参数无效,不管test有无传参
    //{value:1,done:false}
    hw.next(7)
    //{value:7,done:false}
    hw.next(9)
    //{value:9,done:true}

    调用test函数时,该函数并未执行,而是返回一个指向内部状态的指针对象。而调用该函数的next方法则是将指针移向下一个状态,next有位置记忆功能,故每次调用next方法时,都从上一次停下来的地方执行,直到下一个yield或return为止。return与yield功能相似,但return无位置记忆功能,且每个函数只能有一个return语句。且将return语句后面的表达式作为对象的value属性值,若无return,则返回undefined

    当Generator函数运行完毕,但还使用next方法时,返回{value:undefined,done:true},便是遍历已结束。

  • next的参数表示上一个yield表达式的返回值,所以第一个next传递参数是无效的。

  • yield表达式若用在另一表达式中,必须放在圆括号内。

  • yield若用作函数参数或放在赋值表达式的右边,可不加括号。

    1
    2
    3
    4
    5
    console.log('hello' + (yield 123));
    function* demo(){
    foo(yield 'a',yield 'b');
    let input = yield;
    }
与Symbol.iterator的关系
  • 任意一个对象的Symbol.iterator方法等同于该对象的遍历器生成函数。把Generator赋值给对象的Symbol.iterator属性,可使该对象具有Iterator接口。
1
2
3
4
5
6
var myIterable = {};
myIterable[Symbol.iterator] = function* (){
yield 1;
yield 2;
}
[...myIterable] //1,2
  • Generator函数执行后返回一个遍历器对象,该对象本身具有iterator接口,执行后返回自身。
1
2
3
4
function* gen(){}
var g = gen();
g[Symbol.iterator] === g
//true
原生js对象添加Iterator接口的方法
  • 除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。

  • 原生js对象没有遍历接口,无法使用for…of循环,可通过generator函数为其加上接口便可使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
function* objectEntries(obj){
let propKeys = Reflect.ownKeys(obj);//将obj对象转为数组
for(let propKey of propKeys){
yield [propkey,obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
  • 将Generator函数加到对象的symbol.irerator属性上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* objectEntries() {
let propKeys = Object.keys(this);

for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
Generator.prototype.throw
  • Generator函数返回的遍历器对象都有一个throw方法,可在函数体外抛出错误,然后在Generator函数体内捕获。
Generator.prototype.return
  • Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
  • 如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
yield*表达式
  • yield*表达式返回一个表遍历器对象。

  • 如果Generator函数内部调用另一个Generator函数需要在前者函数体内,利用for..of手动完成遍历。若不想手动,则可利用yield*

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
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}

// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}

// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}

for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
  • yield和yield*都只能放在Generator函数中,不能放在普通函数里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//遍历嵌套数组
var arr = [1,[[2,3],4],[5,6]];
var flag = function* (a){
var length = a.length;
for(let i = 0;i < length;i++){
var item = a[i];
if(typeof item != 'number'){
yield* flag(item);
}else{
yield item;
}
}
}
for(var f of flag(arr)){
console.log(f);
}
// 1, 2, 3, 4, 5, 6
协程
  • 传统的编程语言早就有异步编程的解决方案,即多任务的解决方案。其中一种就是协程,意思是多个线程相互协作,完成异步操作。
    • 第一步,协程A开始执行。
    • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B
    • 第三步,(一段时间后)协程B交还执行权。
    • 第四步,协程A恢复执行。
  • 异步任务的封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fetch = require('node-fetch');
function* gen(){
var url = 'http://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
//How people build software.
Thunk函数
  • Thunk函数时自动执行Generator函数的一种方法。
  • 参数求值策略有“传值调用”和”传名调用”两种,Thunk函。编译器的“传名调用”实现,往往是将参数放到一个临时函数中,再将这个临时函数传入函数体中,这个函数体即为Thunk函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f(m){return m*2;}
f(x+5);
//等同于
vat thunk = function(){ return x+5; }
function f(thunk){ return thunk()*2; }

//Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);
Thunkify模块
1
2
3
4
const thunkify = require('thunkify');
const fs = require('fs');
let read = thunkify(fs.readFile);
read('package.json')(function(err,data){});
co模块
  • co模块让你不用编写Generator函数的执行器,Generator函数只要传入co函数即可执行。

    1
    2
    3
    var co = require('co');
    co(generator);//返回一个promise对象,故可用then方法添加回调函数
    co.then(function(){})

co模块的原理

为什么 co 可以自动执行 Generator 函数?

前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co。

基于Promise对象的自动执行

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
const fs = require('fs')
let readFile = function (fileName){
return new Promise(function (resolve,reject){
fs.readFile(fileName,function(err,data){
if(err) return reject(err);
return resolve(data);
});
});
}
var gen = function* (){
let f1 = yield readFile('1.txt');
let f2 = yield readFile('2.txt');
console.log(f1.toString());
console.log(f2.toString());
}
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if(result.done) return result.value;
result.value.then(function(data){
next(data);
})
}
next();
}
run(gen);

async函数

  • 引入async函数,使异步操作更方便。跟Generator函数类似,只需将*改为ansyc,yield改为await。
  • async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
ansyc较Generator函数的优点
  • 内置执行器。Generator函数的执行必须靠执行器,才co模块,而async函数自带执行器。即async函数的执行与普通函数一样,使用函数名调用即可。
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
const fs = require('fs')
let readFile = function (fileName){
return new Promise(function (resolve,reject){
fs.readFile(fileName,function(err,data){
if(err) return reject(err);
return resolve(data);
});
});
}
//generator函数需要再封装一个run函数自执行
// var gen = function* (){
// let f1 = yield readFile('1.txt');
// let f2 = yield readFile('2.txt');
// console.log(f1.toString());
// console.log(f2.toString());
// }
// function run(gen){
// var g = gen();
// function next(data){
// var result = g.next(data);
// if(result.done) return result.value;
// result.value.then(function(data){
// next(data);
// })
// }
// next();
// }
// run(gen)

//async函数有内置执行器
const asyncReadFile = async function(){
const f1 = await readFile('1.txt');
const f2 = await readFile('2.txt');
console.log(f1.toString());
console.log(f2.toString());
}
asyncReadFile()
  • 更好的语义

    • async:异步
    • await表示紧跟在后面的表达式需要等待结果
  • 更广的适应性。

    • co模块约定,yield命令后面只能是Thunk函数或Promise’对象,而async函数的await命令后面可以是Promise对象和原始类型的值(number,string,boolean,但这时会转为立即resolved的promise对象)。
  • 返回值是 Promise。

    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

    进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

async使用形式
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
// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}

async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

async函数内部return语句返回的值会成为then方法回调函数的参数。

async函数抛出的错误会导致返回的Promise对象变为reject状态。抛出的错误会被catch方法回调函数接收到。

1
2
3
4
5
6
7
8
9
10
async function f(flag){
if(flag) throw new Error('出错了');
return 'hello';
}
f(false).then(
v => console.log(v),
e => console.log(e)
)
//hello
//当flag为true时,返回’出错了‘
  • async函数返回的Promise对象必须等到内部所有await命令后面的Promise对象执行完毕才会发生状态改变,除非遇到return语句或抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
await 命令
  • 正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

  • 另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。

  • 任何一个await语句后面的Promise对象变为reject状态,那么整个async函数就都会中断执行。

    • 要想不中断,可将其放入try…catch结构中。
    • 或者在await后面的Promise对象再跟一个catch方法,处理前面可能出现的错误。
    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
    async function f(){
    try{
    await Promise.reject('出错了');
    }catch(e){
    }
    return await Promise.resolve('hello');
    }
    f().then(v => { console.log(v)});//hello

    async function f(){
    await Promise.reject('出错了')
    .catch(e => {console.log(e);});
    return await Promise.resolve('hello')
    }
    f().then(v => { console.log(v)});
    //出错了
    //hello

    async function f() {
    await new Promise(function (resolve, reject) {
    throw new Error('出错了');
    });
    }

    f()
    .then(v => console.log(v))
    .catch(e => console.log(e))
    // Error:出错了

class类

  • 生成实例对象的传统方法是通过构造函数,引入class类概念,利用constructor构造方法创建实例,类中方法不需要加function,毗邻方法间不许用逗号相隔,加了会报错。在类实例上调用方法其实就是调用原型上的方法。
  • prototype对象的constructor属性,直接指向类本身,与ES5相似。constructor是类的默认方法,无显式指定时会模认自动生成。
1
2
3
4
5
6
class B{
constructor(){}
}
let b = new B();
b.constructor === B.prototype.constructor //true
B.prototype.constructor === B //true
  • 由于类的方法都定义在prototype对象上,故类的新方法可添加在prototype对象上。Object.assign可一次添加多个方法。
1
2
3
4
Object.assign(Point.prototype,{
toString(){},
toValue(){}
});
  • 类内部定义的方法都不可枚举。
1
2
3
4
5
6
Object.keys(Point.prototype)
//Object.keys()返回可枚举的键名
//[]
Object.getOwnPropertyNames(Point.prototype)
//["constructor","toString"]
//返回自身全部属性方法名(包括不可枚举属性)
  • constructor方法默认返回实例对象(即this),可指定返回另外一个对象。
1
2
3
4
class Foo{
constructor(){ return Object.create(null); }
}
new Foo() instanceof Foo
类的几种表示法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class B{}
let b = new B();

const MyClass = class Me{}; //Me可省略,只能在内部使用
let inst = new MyClass();//外部使用类只能用MyClass

//立即执行的类
let person = new class{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}('张三')
person.sayName(); //“张三”
noticePoint
  • 类和模块默认就是严格模式。

  • 类不存在变量提升,类使用在前,定义在后会报错。

  • name属性。ES6的类只是ES5的构造函数的一层包装,故函数的许多特性都被Class继承,包括name属性,name属性总是返回紧跟在class关键字后面的类名。

  • 如果类的Symbol.iterator方法前有一星号,就是Generator方法。该方法会返回类的默认遍历器,for…of循环自动调用这个遍历器。

  • 类内部如果有this默认指向类的实例。printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}

print(text) {
console.log(text);
}
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

解决方法

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
//1.给构造函数绑定this
class Logger{
constructor(){
this.printName = this.printName.bind(this);
}
}

//2.使用箭头函数
class Obj{
constructor(){
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myobj //true

//3.使用Proxy,获取方法时自动绑定this
function selfish(target){
const cache = new WeakMap();
const handler = {
get(target,key){
const value = Reflect.get(target,key);
if(typeof value !== 'function'){
return value;
}
if(!cache.has(value)){
cache.set(value,value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target,handle);
return proxy;
}
const logger = selfish(new Logger());
  • 类中的方法前加上static关键字,该不会被实例继承,调用只能通过累来调用,称为静态方法。如果静态方法上包含this关键字,这个this指的是类,而不是实例。故静态方法可与非静态方法重名。
  • 父类的静态方法可被子类继承,并可通过super对象屌用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo{
static bar(){this.baz();}
static baz(){console.log('helloBar');}
static classMethod(){return 'hello';}
}
Foo.classMethod() //‘hello'
Foo.bar() //'helloBar'
var foo = new Foo();
foo.classMethod()
//TypeError:foo.classMethod is not a function

class Bar extends Foo{
static sonMethod(){
return super.classMethod() + ',too';
}
}
Bar.classMethod() //'hello'
Bar.sonMethods() //'hello,too'
  • 实例属性this._count定义在constructor()方法里面,可将属性也可以定义在类的最顶层,其他都不变。实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class IncreasingCounter {
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}

class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
  • 静态属性即Class本身属性,必须在前面加上static关键字。
1
2
3
calss Foo{
static prop = 1;
}
  • 子类继承父类,除了使用extends外,必须在子类的constructor上调用super对象,否则new实例时会报错。这是因为子类自己的this对象必须经过父类的构造函数完成塑造,得到与父类同样的实例属性和方法再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。如果在调用super之前使用this会报错。

  • Object.getPrototypeOf()用于在子类中获取父类,可用该方法判断一个类是否继承了另一个类。

1
2
Object.getPrototypeOf(ColorPoint) === Point
//true
super
  • 既可当函数调用,也可当对象使用。
  • super作为函数调用时,代表父类的构造函数。ES6规定,子类的constructor函数必须执行一次super(),且super()函数只能用在子类的构造函数内,用在其他地方会报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
class A{
constructor(){
console.log(new.target.name);//new.target指向当前正在执行的函数
}
}
class B extends A{
constructor(){
super(); //等价于A.prototype.constructor.call(this)
//super内部的this指向子类的实例
}
}
new A() //A
new B() //B
  • super作为对象时,在普通方法中,指向父类的原型对象;静态方法中,指向父类。这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
constructor() {
this.p = 2;
}
}
A.prototype.x = 3;

class B extends A {
get m() {
return super.p;
}
get x(){
return super.x;
}
}

let b = new B();
b.m // undefined
b.x //3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
constructor() {
this.x = 1;
}
}

class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

  • 如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
  • 在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
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
class Parent {
constructor(){
this.x = '实例的x';
}
static print(){
console.log(this.x);
}
static myMethod(msg) {
console.log('static', msg);
}

myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static m(){
super.print();
}
static myMethod(msg) {
super.myMethod(msg);
}

myMethod(msg) {
super.myMethod(msg);
}
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

Child.x = '这是类的x';
Child.m() //'这是类的x'
类的prototype属性和__proto__属性
  • 每个对象都有__proto__属性,指向对应的构造函数的prototype属性。
  • 子类的__proto__属性表示构造函数的继承,总是指向父类。
  • 子类的prototype属性的__proto__属性表示方法的继承,总是指向父类的prototype属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A{}
class B extends A{}
B.__proto__ === A //true
B.prototype.__proto__ === A.prototype //true

//B的实例继承A的实例
Object.setPrototypeOf(B.prototype,A.prototype);
//B继承A的静态属性
Object.setPrototypeOf(B,A);

//Object.setPrototypeOf方法实现
Object.setPrototypeOf = function(obj,proto){
obj.__proto__ = proto;
return obj;
}

这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。

以下两种特殊情况

  • 子类继承Object类
1
2
3
4
class A extends Object{}
A.__proto__ === Object //true
A.prototype.__proto__ === Object.prototype //true
//A其实就是构造函数Object的复制,A的实例就是Object的实例
  • 不存在任何继承
1
2
3
class A{}
A.__proto__ === Function.prototype //true
A.prototype.__proto__ === Object.prototype //true

A作为一个基类,即不存在任何继承,就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象即object实例,所以A.prototype.__proto__指向构造函数Object的prototype属性。

module模块

  • ES6之前,社区制定了一些模块加载方案。主要是CommonJS——服务器,AMD——浏览器。
1
2
3
4
5
6
7
8
// CommonJS模块,运行时加载,无法做到“运行时加载”
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
  • ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
1
2
// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

除了静态加载带来的各种好处,ES6 模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。
  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

exprot与import
  • export写法:必须提供对外的接口。实质是在接口名与模块内部变量之间建立一一对应的关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 报错
export 1;
// 报错
var m = 1;
export m;

// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};

// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

  • import命令具有提升效果,会提升到整个模块的头部,首先执行。import命令是编译阶段执行的,在代码运行之前。
  • 使用export default 时,对应的import语句不需要使用大括号;使用export时,对应的import语句需要使用大括号。
1
2
3
4
5
export default function crc32(){}
import crc32 from 'crc32';

export function crc32(){}
import {crc32} from 'crc32';

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。故import后面才不用加大括号,因为只可能唯一对应export default命令。

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

1
2
3
4
5
6
7
8
9
10
11
12
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
异步加载
  • 浏览器异步加载js脚本的方法。<script>标签遇到defer或async属性,脚本就会异步加载,渲染引擎遇到这一命令就会开始下载外部脚本,但不会等他下载和执行,而是直接执行后面的命令。
1
2
<script src=""  defer></script>
<script src="" async></script>

defer与async的区别

defer要等到整个页面在内存中正常渲染结束(DOM结构完全生成,以及脚本执行),才会执行。渲染完再执行,若有多个defer脚本,则按顺序加载。

async是下载后执行,即一旦下载完成,渲染引擎就会中断渲染执行这个脚本后再继续渲染。故不能保证加载顺序。

  • 加载ES6模块,对于带有type="module"的脚本都是异步加载,相当于默认是defer属性,渲染完再加载,可设为async属性。

<script type="module" src=""></script>

  • 外部脚本注意事项:
    • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
    • 模块脚本自动采用严格模式,不管有没有声明use strict
    • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
    • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
    • 同一个模块如果加载多次,将只执行一次。
ES6模块与CommonJS模块的差异
  • commonJS模块输出的是一个值的拷贝,且运行时加载。
    • 值拷贝表明一旦输出后,内部值的改变不影响这个值。
  • ES6模块输出的是值的引用,编译时输出接口。
    • JS引擎对脚本静态分析时,遇到模块加载命令import就会生成一个只读引用,等到脚本真正执行时再根据这个只读引用到被加载模块取值。
CommonJS加载原理
  • CommonJS(加载时执行)的一个模块就是一个脚本文件,require命令第一次加载该脚本就会执行整个脚本,然后在内存生成一个对象。
1
2
3
4
5
6
7
{
//node内部加载模块完成后生成的对象
id:'...', //模块名
exports:{...}, //模块输出的各个接口
loaded:true, //该模块脚本是否执行完毕
...
}

以后用到这个模块时就回到exports属性上取值,即使再执行require也不会再次执行该模块而是到缓存中取值,即CommonJS无论加载多少次都只在第一次加载时运行一次,以后再加载只会返回第一次加载的结果,除非手动清除缓存。

ES6模块转码
  • Babel
  • 转码器:ES6 module transpiler(将es6转为CommonJS或AMD模块)
1
2
3
4
5
npm i -g es6-module-transpiler
//转码
compile-modules convert file1.js file2.js
//-o指定转码后的文件名
compile-modules convert -o out.js file1.js
  • 在网页中导入system.js文件
1
2
3
4
5
<script src="system.js"></script>
<script>
//使用System.import加载模块文件,返回Promise对象
System.import('./模块文件').then(function(){})
</script>

编程风格

  • 注意区分Object与Map,只有模拟现实世界的实体对象时才使用Object。如果只是需要key:value的数据结构,使用Map结构,因其有内建的遍历机制。
  • 总是用class代替prototype操作。使用extends实现继承,不会破环instanceof运算的危险。
  • 模块默认输出一个函数,函数名首字母应该小写,默认输出对象时,对象名首字母大写。

异步遍历器

  • Iterator接口是一种数据遍历的协议,只要调用遍历器对象的next方法就会得到一个对象,表示当前遍历指针所在的那个位置的信息。next方法必须是同步的,返回value和done两个属性值。
  • 若next方法是一个异步操作,可将异步操作包装成Thunk函数或Promise对象,即next方法的返回值的value属性是一个Thunk函数或Promise对象,等待以后返回真正的值,而done则还是同步操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function idMaker(){
let index = 0;
return {
next:function(){
return{
value:new Promise(resolve => setTimeOut(() => resolve(index++),1000)),
done:false
}
}
}
}
const it = idMaker();
it.next().value.then(o => console.log(o)) //1
it.next().value.then(o => console.log(o)) //2
  • 一个对象的同步遍历器的接口部署在Symbol.iterator属性上。
  • for…of遍历同步的iterator接口,for await...of循环遍历异步的Iterator接口。createAsyncIterable()返回一个拥有一部遍历器接口的对象
1
2
3
4
5
6
async function f(){
for await (const x of createAsyncIterable(['a','b'])){
console.log(x);
}
}
//a b
  • 异步Generator函数
1
async function* map(){ yield 'hello'; }

vue学习中的问题

登陆切换input复用问题
1
2
3
4
5
6
7
8
9
10
11
<div id="app">
<span v-if="isUser">
<label for="userName">用户账号</label>
<input type="text" id="userName" placeholder="用户账号" key="name">
</span>
<span v-else>
<label for="email">用户账号</label>
<input type="text" id="email" placeholder="用户邮箱" key="email">
</span>
<button @click="isUser = !isUser">切换</button>
</div>

如果我们在输入内容的情况下切换了类型,我们会发现文字依然显示之前的输入内容。按道理来讲,我们应该切换到另外一个input元素中,在另外这个元素中,我们并没有输入内容,所以应该是空的才对?

这是因为Vue在进行DOM渲染时,出于性能考虑,会尽可能的复用已经存在的元素,而不是重新创建新的元素。

解决:

给对应的input添加不同的key,当进行DOM渲染时,就会识别为不同的元素。

vue管道机制部署
1
2
// 即前一个函数的输出是后一个函数的输入
const pipeline = (...func) => val => func.reduce((a,b) => b(a),val);
尾调用解决递归

函数调用自身称为递归。若尾调用自己就称为尾递归。

递归非常耗内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误,但对于尾调用则不会。

1
2
3
4
5
6
7
8
9
10
11
// 递归
function factorial(n){
if(n === 1) return 1;
return n*factorial(n - 1);
}

// 尾调用
function factorial(n,total){
if(n === 1) return total;
return factorial(n - 1, n * total);
}

递归复杂度为O(n),尾递归的复杂度为O(1)。

计算机网络

计算机网络基础

https://iknow-pic.cdn.bcebos.com/c75c10385343fbf2012331c4be7eca8064388fce?x-bce-process=image/resize,m_lfit,w_600,h_800,limit_1

ICMP协议

一个搭建好的网络往往需要先进行一个简单的测试来验证网络是否畅通;但是IP协议并不能提供可靠传输,如果丢包了,IP协议并不能通知传输层是否丢包以及丢包的原因,所以就需要一种协议来完成这样的功能——ICMP。

ICMP功能
  1. 确认IP包是否成功到达目标地址。
  2. 通知在发送过程中的IP包被丢弃的原因。

这里写图片描述

我们需要注意几点:

  1. ICMP是基于IP协议工作的,但是它并不是传输层的功能,因此仍然把它归结为网络层协议

  2. ICMP只能搭配IPv4使用,如果是IPv6的情况下, 需要是用ICMPv6。

ICMP的报文格式

ICMP报文包含在IP数据报中,IP报头在ICMP报文的最前面。一个ICMP报文包括IP报头(至少20字节)、ICMP报头(至少八字节)和ICMP报文(属于ICMP报文的数据部分)。当IP报头中的协议字段值为1时,就说明这是一个ICMP报文。ICMP报头如下图所示。

这里写图片描述

类型 说明
类型 占一字节,标识ICMP报文的类型,从类型值来看ICMP报文可以分为两大类。第一类是取值为1~127的差错报文,第2类是取值128以上的信息报文
代码 占一字节,标识对应ICMP报文的代码。它与类型字段一起共同标识了ICMP报文的详细类型
校验和 这是对包括ICMP报文数据部分在内的整个ICMP数据报的校验和,以检验报文在传输过程中是否出现了差错(其计算方法与在我们介绍IP报头中的校验和计算方法是一样的)

ICMP大概分为两类报文:
一类是通知出错原因 ;一类是用于诊断查询
类型及含义如下:

类型(十进制) 内容
0 回送应答
3 目标不可达
4 原点抑制
5 重定向或改变路由
8 回送请求
9 路由器公告
10 路由器请求
11 超时
17 地址子网请求
18 地址子网应答
常见的ICMP报文

响应请求

我们用的ping操作中就包括了响应请求(类型字段值为8)和应答(类型字段值为0)ICMP报文。

一台主机向一个节点发送一个类型字段值为8的ICMP报文,如果途中没有异常(如果没有被路由丢弃,目标不回应ICMP或者传输失败),则目标返回类型字段值为0的ICMP报文,说明这台主机存在。

目标不可达,源抑制和超时报文

这三种报文的格式是一样的。
(1)目标不可到达报文(类型值为3)在路由器或者主机不能传递数据时使用。
例如:我们要连接对方一个不存在的系统端口(端口号小于1024)时,将返回类型字段值3、代码字段值为3的ICMP报文。
常见的不可到达类型还有网络不可到达(代码字段值为0)、主机不可达到(代码字段值为1)、协议不可到达(代码字段值为2)等等。
(2)源抑制报文(类型字段值为4,代码字段值为0)则充当一个控制流量的角色,通知主机减少数据报流量。由于ICMP没有回复传输的报文,所以只要停止该报文,主机就会逐渐恢复传输速率。
(3)无连接方式网络的问题就是数据报会丢失,或者长时间在网络游荡而找不到目标,或者拥塞导致主机在规定的时间内无法重组数据报分段,这时就要触发ICMP超时报文的产生。
超时报文(类型字段值为11)的代码域有两种取值:代码字段值为0表示传输超时,代码字段值为1表示分段重组超时。

时间戳请求

时间戳请求报文(类型值字段13)和时间戳应答报文(类型值字段14)用于测试两台主机之间数据报来回一次的传输时间。
传输时,主机填充原始时间戳,接受方收到请求后填充接受时间戳后以类型值字段14的报文格式返回,发送方计算这个时间差。
(有些系统不响应这种报文)


计算机网络

OSI七层模型

应用层

应用层:所有能产生网络流量的程序,如qq,需要联网才能正常操作,是网络服务与用户的一个接口。

协议有:HTTP FTP TFTP SMTP SNMP DNS TELNET HTTPS POP3 DHCP。

应用层安全

  1. 开发的应用程序没有漏洞。
表示层

表示层:在传输之前是否进行加密或压缩处理,以及数据在传输的时候是以ASCII码形式传输还是二进制形式传输。一般如果编码格式不对,页面就不能正常加载,会出现乱码现象。(如我传输过来的是utf-8,却以unicode格式来解析网页,势必会出现乱码现象)。

格式有: JPEG ASCII EBCDIC 加密格式等。

会话层

cmd命令行中运行netstat -n可查看会话连接情况,状态为ESTABISHED表示已经建立的会话,TIME_WAIT表示这些会话快释放了。

会话层可用于查木马(盗号木马,如qq被盗,别人就可以给你邮箱发一些东西。远程登录木马,就可以远程登录你的电脑),因为木马总是要和外面建立会话,故可以通过netstat -nb查看建立会话的程序是谁,如果是木马就可以发现了。

建立,管理,终止会话,在五层模型中已经合并到应用层。

对应主机进程,指本地主机与远程主机正在进行会话。

传输层

传输层可进行可靠或不可靠的传输,以及流量控制。定义传输数据的协议端口号,以及流控和差错校验。

协议有:TCP,UDP,数据包一旦离开网卡即进入网络传输层。

网络层

网络层负责选择最佳路径以及规划IP地址(IPv4变为IPv6,只改变网络层)。划分局域段,这是网络层的升级。进行逻辑地址寻址,实现不同网络之间的路径选择。

协议有:ICMP IGMP IP(IPv4 IPv4)。

网络层故障:

  1. 配置错误的IP地址,子网掩码。
  2. 配置错误的网关(网关是网络的出口)。
  3. 路由器没有配置到达目标网络的路由。

网络层安全

  1. 在路由器上使用ACL控制数据包流量。
数据链路层

数据链路层可以标识帧的开始和结尾(特殊符号标识开始和结尾),透明传输(如果中间内容有与标识结尾的标识符相同,会在该内容前加上某些字符告知交换机此处不是结尾),差错校验(会在后面加上一些字符作为校验位)。交换机中将光纤由百兆升级为千兆,这是数据链路层的知识。

建立逻辑连接,进行硬件地址寻址,差错校验等功能,由底层网络定义协议。将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。

数据链路层故障:

  1. MAC地址冲突,我们知道一般MAC地址都是唯一的,电脑生产时就已经定义好每台电脑对应的MAC地址,但是我们设置电脑的MAC地址为某台电脑的MAC地址,这样就可能造成MAC地址冲突。
  2. 当ADSL欠费,电信就会阻断通信。
  3. 网速没办法协商也会出现,服务器规定网速为千兆,而电脑网速为百兆。
  4. 计算机连接到错误的VLAN。

网络安全

  1. ADSL需要输入账号密码。
  2. VLAN交换机端口绑定MAC地址。
物理层

规范了一些接口标准(比如各个公司生产的网卡固定了几根线),电器标准(例如0V代表低电平0,5V代表高电平1)以及规定如何在物理链路上传输更快的速度。更换通信介质,更换通信设备是物理层。

建立,维护,断开物理连接,由底层网络定义协议。

物理层故障,在控制面板——> 所有控制面板项——> 网络和共享中心——> 找到已连接网络,点击查看WLAN状态,如果数据包发送不为0,但接收为0,表示是物理层故障,需要检查线是否完好,或重新接好线。

TCP/IP协议

模型

传输

FCS:校验位。

TCP

TCP提供了

  1. 无差错的数据传输。
  2. 按序传输(数据总是会按照发送的顺序到达)。
  3. 未分段数据流(可以在任意时刻以任意尺寸将数据发送出去)。

网络的性能

速率

连接在计算机网络上的主机在数字信道上传送数据位数的速率,也称为data rate或bit rate。单位是b/s, kb/s, Mb/s, Gb/s。

带宽

数据通信领域中,数字信道所能传送的最高数据率。

吞吐量

在单位时间内通过某个网络的数据量。

时延
  1. 发送时延

    发送时延 = 数据块长度(比特)/ 信道带宽(比特/秒)

  2. 传播时延

  3. 处理时延

  4. 排队时延

时延带宽积
往返时间
利用率
  1. 信道利用率:有数据通过时间 / (有+无)数据通过时间
  2. 网络利用率:信道利用率加权平均值

D = D0 / 1-U

D0 :表示网络空闲时的时延

D:表示信道利用率

数据通信

物理层

数据通信的术语

通信的目的是传送消息。

数据(data)—— 运送消息的实体。

信号(signal)——数据的消息或电磁的表现。

“模拟信号” —— 代表消息的参数的取值是连续的。

“数字信号” —— 代表消息的参数的取值是离散的。

码元(code)—— 在使用时间域的波形表示数字信号时,则代表不同离散数值的基本波形就成为码元。

在数字通信中常常使用时间间隔相同的符号来表示一个二进制数字,这样的时间间隔内的信号称为二进制码元。而这个间隔被称为码元长度。1码元可以携带nbit的信息量。

信道

信道一般表示向一个方向传送信息的媒体,所以我们说平常的通信线路往往包含一条发送信息的信道和一条接收信息的通道。

单向通信(单工通信)——只能有一个方向的通信而没有反方向的交互。

双向交替通信(半双工通信)——通信双方都可以发送信息,但不能双方同时发送/接收。

双向同时通信(全双工通信)——通信双方可以同时发送或接收信息。

信号

基带信号(即基本频带信号)——来自信源的信号。像计算机输出的表示各种文字或图像文件的数据信号都属于基带信号,比如我们说话的声带就是基带信号。

带通信号——把基带信号经过载波调制后,把信号的频率范围搬移到较高的频段以便在信道中传输(即仅在一段频率范围内能够通过信道)。

因此在传输距离较近时,计算机网络都采用基带传输方式,由于近距离范围内基带信号的衰减不大,从而信号内容不会发生变化,因此在传输距离较近时,计算机网络采用基带传输方式。如从计算机到监视器,打印机等外设的信号就是基带传输的。

调幅(AM):载波的振幅随基带数字信号而变化。

调频(FM):载波的频率随基带数字信号而变化。

调相(PM):载波的初始相位随基带数字信号而变化。

调频

集线器

物理层设备集线器的工作特点是在网络中只起到信号放大和重发作用,其目的是扩大网络的传输范围,而不具备信号的定向传送能力。它是一个大的冲突域,因为当一台电脑发送给集线器后,集线器会把该内容转发给连接在集线器上的其他电脑,当其他电脑接收到内容后先对比该内容的MAC地址与自己电脑的MAC地址,相同则进行处理,不同者不做处理,但是我们可以把数据爬出来。

信道复用技术

信道复用**

频分复用技术FDM(Frequency Division Multiplexing):用户在分配到一定的频带后在通信过程中自始自终都占用这个频带。频分复用的所有用户在相同时间内占用不同的带宽资源,请注意,这里的“带宽”是频率带宽而不是数据的发送速率。

时分复用技术TDM(Time Division Multiplexing)是将时间划分为一段段等长的时分复用帧,每一个时分复用的用户在每一个TDM帧中占用固定序号的时隙,在不同时间内占用相同的频带宽度。时分复用

使用时分复用系统传送计算机数据时,由于计算机数据的突发性质,用户对分配到的子信道的利用率一般是不高的。

统计时分复用:通过标记是哪个VLAN。

宽带接入技术

xDSL(用数字技术对现有的模拟电话用户线进行改造)

标准模拟电话信号的频带被限制在3003400Hz的范围内,但用户线本身实际可通过的信号频率仍然超过1MHz。xDSL技术把04kHz低端频谱留给传统电话使用,而把原来没有被利用的高端频谱留给用户上网使用。

数据链路层

数据链路层使用的信道主要有以下两种类型:

  1. 点对点信道。这种信道使用一对一的点对点通信方式。
  2. 广播通信。这种信道使用一对多的广播通信方式,因此过程比较复杂。广播信道上连接的主机很多,因此必须使用专用的共享信道协议来协调这些主机的数据发送。
链路

链路(link)是一条点到点的物理路线,中间没有任何其他的交换结点,一条链路只是一条通路的组成部分。

数据链路(data link)除了物理线路外,还必须有通信协议来控制这些数据的传输,若把实现这些协议的硬件和软件加到链路上,就构成了数据链路。

现最常用的方法是使用适配器(即网卡)来实现这些协议的硬件和软件。

一般的适配器都包括了数据链路层和物理层这两层的功能。

透明传输

透明传输

差错检测

传输过程中可能会产生比特差错,1可能变成0而0也可能变成1.在一段时间内,传输错误的比特占所传输比特总数的比率称为误码率BER(Bit Error Rate)。

误码率与信噪比有很大的关系,为了保证数据传输的可靠性,在计算机网络传输数据时,必须采取各种差错检测措施。

PPP协议

现在全世界使用最多的数据链路层协议是点对点协议PPP(Point-to-Point Protocol)。

用户使用拨号电话线接入因特网时一般都是使用PPP协议。

PPP协议应该满足的要求:

  1. 简单——这是首要的要求
  2. 封装成帧。
  3. 透明性。
  4. 多种网络层协议。
  5. 差错检测。
  6. 检测连接状态。
  7. 最大传送单元。
  8. 网络层地址协商。
  9. 数据压缩协商。

PPP协议不满足的要求:

  1. 纠错。
  2. 流量控制。
  3. 序号。
  4. 多点线路。
  5. 半双工或单工链路。

image-20200429135924281

7E是结束和开头标志,FCS用于差错校验。

PPP协议工作状态:当用户拨号接入ISP时,路由器的调制解调器对拨号做出确认,并建立一条物理连接。PC机向路由器发送一系列的LCP分组(封装成多个PPP帧),这些分组及其响应选择一些PPP参数,和进行网络层配置,NCP给新接入的PC机分配一个临时的IP地址,使PC机成为因特网上的一个主机。通信完毕时,NCP释放网络层连接,收回原来分配出去的IP地址。接着,LCP释放数据链路层连接,最后释放的是物理层的连接。

数据结构与算法

数据结构与算法

数组

数组方法大全

数组解构赋值应用
1
2
3
4
5
6
// 交换变量
[a, b] = [b, a]
[o.a, o.b] = [o.b, o.a]
// 生成剩余数组
const [a, ...rest] = [...'asdf'] // a:'a',rest: ["s", "d", "f"]
复制代码
数组浅拷贝
1
2
3
4
5
6
const arr = [1, 2, 3]
const arrClone = [...arr]
// 对象也可以这样浅拷贝
const obj = { a: 1 }
const objClone = { ...obj }
复制代码

浅拷贝方法有很多如arr.slice(0, arr.length)/Arror.from(arr)等,但是用了...操作符之后就不会再想用其他的了~

数组合并
1
2
3
4
5
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const arr3 = [7, 8, 9]
const arr = [...arr1, ...arr2, ...arr3]
复制代码

arr1.concat(arr2, arr3)同样可以实现合并,但是用了...操作符之后就不会再想用其他的了~

数组去重
1
2
3
const arr = [1, 1, 2, 2, 3, 4, 5, 5]
const newArr = [...new Set(arr)]
复制代码

new Set(arr)接受一个数组参数并生成一个set结构的数据类型。set数据类型的元素不会重复且是Array Iterator,所以可以利用这个特性来去重。

数组取交集
1
2
3
4
5
const a = [0, 1, 2, 3, 4, 5]
const b = [3, 4, 5, 6, 7, 8]
const duplicatedValues = [...new Set(a)].filter(item => b.includes(item))
duplicatedValues // [3, 4, 5]
复制代码
数组取差集
1
2
3
4
const a = [0, 1, 2, 3, 4, 5]
const b = [3, 4, 5, 6, 7, 8]
const diffValues = [...new Set([...a, ...b])].filter(item => !b.includes(item) || !a.includes(item)) // [0, 1, 2, 6, 7, 8]
复制代码
数组转对象
1
2
3
4
5
6
7
8
const arr = [1, 2, 3, 4]
const newObj = {...arr} // {0: 1, 1: 2, 2: 3, 3: 4}
const obj = {0: 0, 1: 1, 2: 2, length: 3}
// 对象转数组不能用展开操作符,因为展开操作符必须用在可迭代对象上
let newArr = [...obj] // Uncaught TypeError: object is not iterable...
// 可以使用Array.form()将类数组对象转为数组
let newArr = Array.from(obj) // [0, 1, 2]
复制代码
数组摊平
1
2
3
4
5
6
7
const obj = {a: '群主', b: '男群友', c: '女裙友', d: '未知性别'}
const getName = function (item) { return item.includes('群')}
// 方法1
const flatArr = Object.values(obj).flat().filter(item => getName(item))
// 经大佬指点,更加简化(发现自己的抽象能力真的差~)
const flatArr = Object.values(obj).flat().filter(getName)
复制代码

二维数组用array.flat(),三维及以上用array.flatMap()

数组常用遍历

数组常用遍历有 forEach、every、some、filter、map、reduce、reduceRight、find、findIndex 等方法,很多方法都可以达到同样的效果。数组方法不仅要会用,而且要用好。要用好就要知道什么时候用什么方法。

遍历的混合使用

filtermap方法返回值仍旧是一个数组,所以可以搭配其他数组遍历方法混合使用。注意遍历越多效率越低~

1
2
3
4
5
6
const arr = [1, 2, 3, 4, 5]
const value = arr
.map(item => item * 3)
.filter(item => item % 2 === 0)
.map(item => item + 1)
.reduce((prev, curr) => prev + curr, 0)
检测数组所有元素是否都符合判断条件
1
2
const arr = [1, 2, 3, 4, 5]
const isAllNum = arr.every(item => typeof item === 'number')
检测数组是否有元素符合判断条件
1
2
const arr = [1, 2, 3, 4, 5]
const hasNum = arr.some(item => typeof item === 'number')
找到第一个符合条件的元素/下标
1
2
3
4
5
6
7
8
9
10
const arr = [1, 2, 3, 4, 5]
const findItem = arr.find(item => item === 3) // 返回子项
const findIndex = arr.findIndex(item => item === 3) // 返回子项的下标

let findIndex
arr.find((item, index) => {
if (item === 3) {
findIndex = index
}
})
数组使用误区

数组的方法很多,很多方法都可以达到同样的效果,所以在使用时要根据需求使用合适的方法。

垃圾代码产生的很大原因就是数组常用方法使用不当,这里有以下需要注意的点:

array.includes() 和 array.indexOf()

array.includes() 返回布尔值,array.indexOf() 返回数组子项的索引。indexOf 一定要在需要索引值的情况下使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = [1, 2, 3, 4, 5]

// 使用indexOf,需要用到索引值
const index = arr.indexOf(1) // 0
if (~index) { // 若index === -1,~index得到0,判断不成立;若index不为-1,则~index得到非0,判断成立。
arr.spilce(index, 1)
}

// 使用includes,不需要用到索引值
// 此时若用indexOf会造成上下文上的阅读负担:到底其他地方有没有用到这个index?
const isExist = arr.includes(6) // true
if (!isExist) {
arr.push(6)
}

另外评论区大佬指出,array.indexOf()NaN 会找不到,返回-1array.includes()能找到,返回true~

1
2
[NaN].includes(NaN) // true
[NaN].indexOf(NaN) // -1

array.find() 、 array.findIndex() 和 array.some()

array.find()返回值是第一个符合条件的数组子项,array.findIndex() 返回第一个符合条件的数组子项的下标,array.some() 返回有无复合条件的子项,如有返回true,若无返回false。注意这三个都是短路操作,即找到符合条件的之后就不在继续遍历。

在需要数组的子项的时候使用array.find() ;需要子项的索引值的时候使用 array.findIndex() ;而若只需要知道有无符合条件的子项,则用 array.some()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const arr = [{label: '男', value: 0}, {label: '女', value: 1}, {label: '不男不女', value: 2}]

// 使用some
const isExist = arr.some(item => item.value === 2)
if (isExist) {
console.log('哈哈哈找到了')
}

// 使用find
const item = arr.find(item => item.value === 2)
if (item) {
console.log(item.label)
}

// 使用findIndex
const index = arr.findIndex(item => item.value === 2)
if (~index) {
const delItem = arr[index]
arr.splice(index, 1)
console.log(`你删除了${delItem.label}`)
}

建议在只需要布尔值的时候和数组子项是字符串或数字的时候使用 array.some()

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
// 当子包含数字0的时候可能出错
const arr = [0, 1, 2, 3, 4]

// 正确
const isExist = arr.some(item => item === 0)
if (isExist) {
console.log('存在要找的子项,很舒服~')
}

// 错误
const isExist = arr.find(item => item === 0)
if (isExist) { // isExist此时是0,隐式转换为布尔值后是false
console.log('执行不到这里~')
}


// 当子项包含空字符串的时候也可能出错
const arr = ['', 'asdf', 'qwer', '...']

// 正确
const isExist = arr.some(item => item === '')
if (isExist) {
console.log('存在要找的子项,很舒服~')
}

// 错误
const isExist = arr.find(item => item === '')
if (isExist) { // isExist此时是'',隐式转换为布尔值后是false
console.log('执行不到这里~')
}

array.find() 和 array.filter()

只需要知道 array.filter() 返回的是所有符合条件的子项组成的数组,会遍历所有数组;而 array.find() 只返回第一个符合条件的子项,是短路操作。

合理使用 Set 数据结构

由于 es6 原生提供了 Set 数据结构,而 Set 可以保证子项不重复,且和数组转换十分方便,所以在一些可能会涉及重复添加的场景下可以直接使用 Set 代替 Array,避免了多个地方重复判断是否已经存在该子项。

1
2
3
4
5
6
const set = new Set()
set.add(1)
set.add(1)
set.add(1)
set.size // 1
const arr = [...set] // arr: [1]
强大的reduce

array.reduce 遍历并将当前次回调函数的返回值作为下一次回调函数执行的第一个参数。

利用 array.reduce 替代一些需要多次遍历的场景,可以极大提高代码运行效率。

  1. 利用reduce 输出一个数字/字符串

假如有如下每个元素都由字母’s’加数字组成的数组arr,现在找出其中最大的数字:(arr不为空)

1
2
3
4
5
6
7
8
9
10
11
const arr = ['s0', 's4', 's1', 's2', 's8', 's3']

// 方法1 进行了多次遍历,低效
const newArr = arr.map(item => item.substring(1)).map(item => Number(item))
const maxS = Math.max(...newArr)

// 方法2 一次遍历
const maxS = arr.reduce((prev, cur) => {
const curIndex = Number(cur.replace('s', ''))
return curIndex > prev ? curIndex : prev
}, 0)
  1. 利用reduce 输出一个数组/对象
1
2
3
4
5
6
7
8
9
const arr = [1, 2, 3, 4, 5]

// 方法1 遍历了两次,效率低
const value = arr.filter(item => item % 2 === 0).map(item => ({ value: item }))

// 方法1 一次遍历,效率高
const value = arr.reduce((prev, curr) => {
return curr % 2 === 0 ? [...prev, curr] : prev
}, [])

掌握了上面两种用法,结合实际需要,就可以用 reduce/reduceRight 实现各种奇巧淫技了。

实例:利用 reduce 做下面这样的处理来生成想要的 html 字符串:

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
// 后端返回数据
const data = {
'if _ then s9': [
'作用属于各种,结构属于住宅,结构能承受作用,作用属于在正常建造和正常使用过程中可能发生',
'作用属于各种,结构属于住宅,结构能承受作用,作用属于在正常建造和正常使用过程中可能发生',
'作用属于各种,结构属于住宅,结构能承受作用,作用属于在正常建造和正常使用过程中可能发生'
],
'if C then s4': [
'当有条件时时,结构构件满足要求,要求属于安全性、适用性和耐久性',
'当有条件时时,住宅结构满足要求,要求属于安全性、适用性和耐久性'
]
}

const ifthens = Object.entries(data).reduce((prev, cur) => {
const values = cur[1].reduce((prev, cur) => `${prev}<p>${cur}</p>`, '')
return `
${prev}
<li>
<p>${cur[0]}</p>
${values}
</li>
`
}, '')

const html = `
<ul class="nlp-notify-body">
${ifthens}
</ul>
`

生成的 html 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<ul class="nlp-notify-body">            
<li>
<p>if _ then s9</p>
<p>作用属于各种,结构属于住宅,结构能承受作用,作用属于在正常建造和正常使用过程中可能发生</p>
<p>作用属于各种,结构属于住宅,结构能承受作用,作用属于在正常建造和正常使用过程中可能发生</p>
<p>作用属于各种,结构属于住宅,结构能承受作用,作用属于在正常建造和正常使用过程中可能发生</p>
</li>
<li>
<p>if C then s4</p>
<p>当有条件时时,结构构件满足要求,要求属于安全性、适用性和耐久性</p>
<p>当有条件时时,住宅结构满足要求,要求属于安全性、适用性和耐久性</p>
</li>
</ul>

这里还有一个替代 reverse 函数的技巧

由于 array.reverse() 函数会改变原数组自身,这样就限制了一些使用场景。如果我想要一个不会改变数组自身的 reverse 函数呢?拿走!

1
2
3
const myReverse = (arr = []) => {
return arr.reduceRight((prev, cur) => [...prev, cur], []) // 也可以返回逗号表达式 (prev.push(cur), prev)
}

用JavaScript封装栈
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
// 封装栈
function Stack(){
// 栈中的属性
this.items = [];
// 栈中的相关操作
// 1.将元素压入栈中
Stack.prototype.push = function(element){
this.items.push(element);
};
// 2.将元素弹出栈中
Stack.prototype.pop = function(){
return this.items.pop();
};
// 3.查看一下栈顶元素
Stack.prototype.check = function(){
return this.items[this.items.length - 1];
};
// 4.判断栈中是否有元素
Stack.prototype.isEmpty = function(){
return this.items.length == 0;
};
// 5.获取栈中的元素的个数
Stack.prototype.size = function(){
return this.items.length;
};
// 6.toString方法
Stack.prototype.toString = function(){
// 方法一:
// var resultString = '';
// for(var i = 0; i < this.items.length; i ++){
// resultString += this.items[i] + ' ';
// }
// return resultString;
// 方法二:
return this.items.join(' ');
};
}

之所以不使用this.push=function(){},而是采用原型的方法,是因为通过prototype原型的方法相当于给整个类添加了方法,而this.push方式则仅是给某个实例添加方法。

用栈将十进制转为二进制
  • 因为我们习惯使用十进制,而计算机里面的所有内容都是用二进制数字表示的(0和1)
  • 可采用对十进制的数字进行除二取余法,将十进制转为二进制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//函数:将十进制转为二进制
function dec2bin(decNumber){
// 1.定义栈对象
var stack = new Stack();
while(decNumber > 0){
stack.push(decNumber % 2);
decNumber = Math.floor(decNumber / 2);
console.log(decNumber)
}
var binaryString = '';
while(!stack.isEmpty()){
binaryString += stack.pop();
}
return binaryString;
}

队列

队列结构
  • 队列是一种受限的数据结构,可解决某些特定的问题。它的受限之处在于他只允许在表的前端(font)进行删除操作,而在表的后端(rear)进行插入操作。

  • 队列的实现和栈一样有两种方案:

    • 基于数组实现
    • 基于链表实现
  • 队列常见的操作

    • enqueue(element):向队列尾部添加一个或多个新的项。
    • dequeue():移除队列的第一(即排在队列最前面的)项,并返回移除的元素;
    • front():返回队列中第一个元素。
    • isEmpty():判断队列中是否含有元素。
    • size():返回队列中的元素个数。
    • toString():将队列中的内容转为字符串形式
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
// 用数组封装队列
function Queue(){
this.item = [];

// 1.添加元素
Queue.prototype.enqueue = function(element){
this.item.push(element);

};
// 2.移除元素
Queue.prototype.dequeue = function(){
return this.item.shift();
};
// 3.查看队列中第一个元素
Queue.prototype.front = function(){
return this.item[0];
};
// 4.判断元素是否为空
Queue.prototype.isEmpty = function(){
return this.item.length == 0;
};
// 5.队列中的元素个数
Queue.prototype.size = function(){
return this.item.length;
};
// 6.将队列中的内容转为字符串
Queue.prototype.toString = function(){
return this.item.join(' ');
}
队列击鼓传花
  • 参数:所有参与人的性名,基于此的数字
  • 结果:最终剩下的一人的姓名
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
 // 击鼓传花
function passGame(nameList, num) {
//创建一个队列
var queue = new Queue();
// 将所有人加入队列中
for(var i = 0; i < nameList.length; i++){
queue.enqueue(nameList[i]);
}
// 不是num,重新加入队列末尾
// 是num时,从队列中删除并把不是num的值重新放入队列中
// 当队列中只剩下一人退出循环
while (queue.size() > 1) {
for (var i = 0; i < num - 1; i++) {
queue.enqueue(queue.dequeue());
}
queue.dequeue();
}
var index = nameList.indexOf(queue.front());
return queue.front();
}// 击鼓传花
function passGame(nameList, num) {
//创建一个队列
var queue = new Queue();
// 将所有人加入队列中
for(var i = 0; i < nameList.length; i++){
queue.enqueue(nameList[i]);
}
// 不是num,重新加入队列末尾
// 是num时,从队列中删除并把不是num的值重新放入队列中
// 当队列中只剩下一人退出循环
while (queue.size() > 1) {
for (var i = 0; i < num - 1; i++) {
queue.enqueue(queue.dequeue());
}
queue.dequeue();
}
var index = nameList.indexOf(queue.front());
return queue.front();
}
优先级队列
  • 普通的队列插入一个元素,数据就会被放入后端,并且需要前面的所有元素处理完后才会处理前面的数据。但是优先级队列,在插入一个元素的的时候会考虑该数据的优先级。
  • 和其他数据优先级进行比较,比较完成后可得出这个元素在队列中正确的位置。
  • 优先级队列主要考虑的问题:
    • 每个元素不再只是一个数据,且包含数据的优先级。
    • 在添加方式中,根据优先级放入正确的位置。
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
// 封装优先级队列
function PriorityQueue() {
// 内部类
function QueueElement(element, priority) {
this.element = element;
this.priority = priority;
}
// 封装属性
this.item = [];
// 实现插入方法
PriorityQueue.prototype.enqueue = function (element, priority) {
// 创建QueueElement对象
var queueElement = new QueueElement(element, priority);
if (this.item.length == 0) {
this.item.push(queueElement);
} else {
let flag = false;
for (let i = 0; i < this.item.length; i++) {
if (queueElement.priority < this.item[i].priority) {
this.item.splice(i, 0, queueElement);
flag = true;
break;
}
}
if (!flag) {
this.item.push(queueElement);
}
}
}
// 2.移除元素
PriorityQueue.prototype.dequeue = function () {
return this.item.shift();
};
// 3.查看队列中第一个元素
PriorityQueue.prototype.front = function () {
return this.item[0];
};
// 4.判断元素是否为空
PriorityQueue.prototype.isEmpty = function () {
return this.item.length == 0;
};
// 5.队列中的元素个数
PriorityQueue.prototype.size = function () {
return this.item.length;
};
// 6.将队列中的内容转为字符串
PriorityQueue.prototype.toString = function () {
let result = '';
for(let i = 0; i < this.item.length; i++){
result += this.item[i].priority + ' ' + this.item[i].element + ' ';
}
return result;
}
}
var priorityQueue = new PriorityQueue();
priorityQueue.enqueue('nikita',10);
priorityQueue.enqueue('nikita1',110);
priorityQueue.enqueue('nikit2a',101);
priorityQueue.enqueue('nikitak',210);
console.log(priorityQueue.toString());

链表

单向链表

要存储多个元素,有两个选择:数组和链表。

但不同于数组,链表中的元素在内存中不必是连续的空间,链表的每一个元素由一个存储元素本身的节点和指向下一个元素的引用(即指针)组成。

相对于数组,链表优势:

  1. 内存不必连续,可充分利用计算机的内存,实现灵活的内存动态管理。
  2. 链表不必在创建时就确定大小,并且大小可以无限的延伸下去。
  3. 链表在插入和删除操作时,时间复杂度可达O(1),相对数组效率高很多。

劣势:

  1. 链表访问任何一个位置的元素都必须从头开始。(无法跳过第一个元素访问任何一个元素)。
  2. 无法通过下标直接访问元素,必须从头开始,直到找到元素。

![1 (3)](C:\Users\CCY\Desktop\1 (3).png)

常见操作:

  1. append(element):向链表尾部添加一个新的项。
  2. insert(position,element):向特定位置添加一个项
  3. get(position)
  4. indexOf(element)
  5. update(position):修改某个位置的元素
  6. removeAt(position)
  7. remove(element)
  8. isEmpty()
  9. size()
  10. toString()
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
![4](C:\Users\CCY\Desktop\4.png)// 封装单向链表
function LinkedList(){
// 内部类,节点类
function Node(data){
this.data = data;
this.next = null;
}
// 定义属性
this.head = null;
this.length = 0;

// 追加append方法
LinkedList.prototype.append = function(data){
let newNode = new Node(data);
if(this.length == 0){
this.head = newNode;
}else{
let current = this.head;
while(current.next){
current = current.next;
}
current.next = newNode;
}

this.length++;
}

// toString方法
LinkedList.prototype.toString = function(){
let resultString = '';
let current = this.head;
while(current){
resultString += current.data + ' ';
current = current.next;
}
return resultString;
}

// 在任意位置加元素
LinkedList.prototype.insert = function(position,data){
// 对position进行边界判断
if(position < 0 || position > this.length){
return false;
}else{
let newNode = new Node(data);
let index = 0;
let previous = null;
let current = this.head;
while(index++ < position){
previous = current;
current = current.next;
}
previous.next = newNode;
newNode.next = current;
}
this.length++;
return true;
}

// 获取对应位置的值
LinkedList.prototype.get = function(position){
// 对position边界判断
if(position < 0 || position >= this.length){
return null;
}else{
let current = this.head;
let index = 0;
while(index++ < position){
current = current.next;
}
return current.data;
}
}

// 返回元素索引值indexOf,没有返回-1
LinkedList.prototype.indexOf = function(value){
let current = this.head;
let index = 0;
while(current){
if(current.data == value){
return index;
}
index++;
current = current.next;
}
return -1;
}

// update方法
LinkedList.prototype.update = function(position,value){
// 对position边界判断
if(position < 0 || position >= this.length){
return false;
}else{
let current = this.head;
let index = 0;
while(index++ < position){
current = current.next;
}
current.data = value;
return true;
}
}

// 在特定位置删除数据
LinkedList.prototype.removeAt = function(position){
// 方便后续返回里面的值
let current = this.head;
// 对position边界判断
if(position < 0 || position >= this.length){
return false;
}else{
let index = 0;
let previous = null;
while(index++ < position){
previous = current;
current = current.next;
}
current.next = previous.next;
}
this.length -= 1;
return current.data;
}

// 在列表中删除某个值
LinkedList.prototype.remove = function(value){
// 获取data在列表中的位置
let position = this.indexOf(value);

return this.removeAt(position);
}

// 是否为空
LinkedList.prototype.isEmpty = function(){
return !this.length;
}

// 元素个数
LinkedList.prototype.size = function(){
return this.length;
}
}
双向链表

既可以从头遍历到尾,又可以从尾遍历到头。

实现原理是:一个节点既有向前连接的引用,也有一个向后连接的引用。

但有以下缺点:

每次插入删除某个节点时,需要处理四个引用,而不是两个,也就是实现起来更困难一些。并且相当于单向链表,必然占用内存空间更大些。4

特点:

  1. 可以使用一个head和一个tail分别指向头部和尾部的节点。
  2. 每个节点由三部分组成:前一个节点的指针(prev)/保存的元素(item)/后一个节点的指针(next)。
  3. 双向链表的第一个节点的prev是null,最后一个节点的next为null。
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// 封装双向链表
function DoubleLinkedList(){
// 内部节点类
function Node(data){
this.data = data;
this.prev = null;
this.next = null;
}

// 内部属性
this.head = null;
this.tail = null;
this.length = 0;

// append追加方法
DoubleLinkedList.prototype.append = function(data){
let newNode = new Node(data);
if(this.length == 0){
this.head = newNode;
this.tail = newNode;
}else{
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
}

// 任意位置插入值
DoubleLinkedList.prototype.insert = function(position,value){
if(position < 0 || position >= this.length) return false;
let newNode = new Node(value);
// 原链表为空
if(this.length == 0){
this.prev = newNode;
this.tail = newNode;
}else if(position == 0){
this.head.prev = newNode;
newNode.next = this.head;
this.head = newNode;
}else if(position == this.length){
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}else{
let index = 0;
let current = this.head;
while(index++ < position){
current = current.next;
}
newNode.next = current;
newNode.prev = current.prev;
current.prev.next = newNode;
current.prev = newNode;
}

this.length++;
}

// get方法
DoubleLinkedList.prototype.get = function(position){
if(position < 0 || position >= this.length) return null;
let index = 0;
let current = this.head;
while(index++ < position){
current = current.next;
}
return current.data;
}

// indexOf方法
DoubleLinkedList.prototype.indexOf = function(value){
let index = 0;
let current = this.head;
while(current){
if(current.data == value){
return index;
}
current = current.next;
index++;
}
return -1;
}

// update方法
DoubleLinkedList.prototype.update = function(position,newValue){
if(position < 0 || position >= this.length) return false;
let current = this.head;
let index = 0;
while(index++ < position){
current = current.next;
}
current.data = newValue;
return true;
}

// 移除数据
DoubleLinkedList.prototype.removeAt = function(position){
if(position < 0 || position >= this.length) return null;
let current = this.head;
if(position == 0){
this.head.next.prev = null;
this.head = this.head.next;
}else if(position == this.length - 1){
current = this.tail;
this.tail.prev.next = null;
this.tail = this.tail.prev;
}else{
let index = 0;
while(index++ < position){
current = current.next;
}
current.prev.next = current.next;
current.next.prev = current.prev;
}
this.length -= 1;
return current.data;
}

// remove方法
DoubleLinkedList.prototype.remove = function(value){
let position = this.indexOf(value);
return this.removeAt(position);
}

// isEmpty方法
DoubleLinkedList.prototype.isEmpty = function(){
return !this.length;
}

// size方法
DoubleLinkedList.prototype.size = function(){
return this.length;
}

DoubleLinkedList.prototype.getHead = function(){
return this.head.data;
}

DoubleLinkedList.prototype.getTail = function(){
return this.tail.data;
}
// 将链表转成字符串
DoubleLinkedList.prototype.toString = function(){
return this.backwardString();
}

DoubleLinkedList.prototype.forwardString = function(){
let current = this.tail;
let resultString = '';
while(current){
resultString += current.data + ' ';
current = current.prev;
}
return resultString;
}

// 从前向后遍历
DoubleLinkedList.prototype.backwardString = function(){
let current = this.head;
let resultString = '';
while(current){
resultString += current.data + ' ';
current = current.next;
}
return resultString;
}
}

集合

几乎每种编程语言中,都有集合结构,比较常见的实现方式是哈希表。

集合通常是由一组无序的,不能重复的元素组成与数学中的集合不同,计算机中的集合表示的结构中元素是不允许重复的。ES6中的Set类就是一种集合类。

集合常见操作:
  1. add(value)向集合添加一个新的项。
  2. remove(value):从集合中一个数
  3. has(value):值是否在集合中,有返回true,没有返回false
  4. clear():移除集合所有项。
  5. size():length属性
  6. values():返回所有值的数组。
集合间的操作

并集

交集

差集

子集

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
// 封装集合
function Set(){
// 属性
this.item = {};

// add方法
Set.prototype.add = function(value){
// 先判断是否有该值
if(this.has(value)){
return false;
}

this.item[value] = value;
return true;
}

Set.prototype.has = function(value){
return this.item.hasOwnProperty(value);
}

Set.prototype.remove = function(value){
// 判断有无该值
if(!this.has(value)){
return false;
}
delete this.item[value];
return true;
}

Set.prototype.clear = function(){
this.item = {};
}

Set.prototype.size = function(){
return Object.keys(this.item).length;
}

Set.prototype.values = function(){
return Object.keys(this.item);
}

// 并集
Set.prototype.union = function(otherSet){
// this: 集合对象A
// otherSet:集合对象B
let unionSet = new Set();
let value = this.values();
for(let i = 0; i < value.length;i++){
unionSet.add(value[i])
}
// 去除B集合中的元素,判断是否需要添加到新的集合
value = otherSet.values();
for(let i = 0; i < value.length;i++){
unionSet.add(value[i]);
}
return unionSet;
}

// 交集
Set.prototype.intersection = function(otherSet){
let intersection = new Set();
let value = this.values();
for(let i = 0; i < value.length;i++){
if(otherSet.has(value[i])){
intersection.add(value[i]);
}
}
return intersection;
}

// 差集
Set.prototype.differce = function(otherSet){
let differce = new Set();
let value = this.values();
for(let i = 0; i < value.length;i++){
if(!otherSet.has(value[i])){
differce.add(value[i]);
}
}
return differce;
}

// 子集:集合A是否为集合B的子集
Set.prototype.subSet = function(otherSet){
let value = this.values();
for(let i = 0; i < value.length;i++){
if(!otherSet.has(value[i])){
return false;
}
}
return true;
}

}

哈希表

哈希表无论多少条数据,插入删除值需要接近常量的时间:即O(1)的时间级。实际上,只需要几个机器指令即可完成。

哈希表的速度比树快,基本可在瞬间找到想要的元素,且哈希表相对于树来说编码要容易得多。哈希表基于数组,但效率比数组高。

哈希表相对于数组:

  1. 哈希表中的数据没有顺序,所以不能以一种固定的方式,比如从小到大来遍历其中的元素。
  2. 哈希表中的key不允许重复,不能放置相同的key用于保存不同的元素。
字母转数字

方案一:把数字相加求和产生的数组下标太少。

方案二:与27的幂相乘求和产生的数组下标又太多。

哈希化

哈希化:将大数字转化为数组范围内下标的过程,我们称之为哈希化。

哈希函数:通常我们将单词转为大数字,大数字在进行哈希化的代码实现放在一个函数中,这就是哈希函数。

哈希表:最终将数据插入到这个数组,对整个结构的封装,我们就称之为哈希表。

哈希化后的下标值仍可能会重复,解决方案有二:

1.链地址法

链地址解决冲突的办法是每个数组单元中存放的不再是单个数据,而是一个链条(数组和链表),当发现重复时,便把重复的元素插入到链表的首端或末端即可。当查询时,先根据哈希化后的下标值找到对应的位置,再取出链表,依次查询寻找的数据。

一般哈希化的index找出这个链表或数组时,通常就会使用线性查找,这时数组和链表的效率是差不多的。

但若是需要将新插入的数据放在数组或链表的最前面,因为觉得新插入的数据用于取出的可能性更大,这种情况最好采用链表,因为数组在首位插入数据时需要其他项后移,链表则没有此问题。5

2.开放地址法

开放地址法的主要工作方式是寻找空白的单元格来添加重复的数据。

6

线性探测

线性的查找空白的单元。

插值:当我们通过哈希化找到下标值时,发现该位置已经有值了,下标值就自加1开始一点一点查找合适的位置(空的位置)

查询:首先经过哈希化得到下标值,比较该下标值的结果与查询的数值是否相同,相同则返回,不同则线性查找(下标值加1),查到空位置即停止。因为插值时不可能跳过空位置去其他位置。

删除:删除一个数据项时,不可以将这个位置下标的内容设置为null,因为将它设置为null可能影响我们之后查询其他操作,所以通常删除一个位置的数据项时,需将其进行特殊处理(比如设置为-1)。

当我们之后看到-1位置的数据项时就知道查询时要继续查询,但是插入该位置可放置数据。

线性探测有一个较严重的问题就是聚集(连续填充单位),它会影响哈希表的性能,无论删除/插入/删除都会影响。

  1. 节点的度(degree):节点的子树个数。
  2. 树的度:树的所有节点中最大的度数。
  3. 叶节点(leaf):度为0的节点(也称为叶子节点)。
  4. 父节点(parent):有子树的节点是其树的根节点的父节点。
  5. 子节点(child)
  6. 兄弟节点路径和路径长度:一个节点序列的父节点,路径所包含的边的个数为路径的长度。
  7. 节点的层次(level):规定根节点在1层,其他节点的层数是其父节点的层数加1.
  8. 树的深度(depth):树中所有节点中的最大层次是这棵树的深度。
二叉树
  1. 一个二叉树第i层的最大节点数为2^(i-1),i>=1;
  2. 深度为k的二叉树有最大的节点总数为:2^k-1,k>=1;
  3. 对任何非空二叉树T,若n0表示叶结点的个数,n2是度为2的非叶节点个数,则两者关系n0 = n2 + 1。
二叉搜索树

二叉搜索树(BST ,Binary Search Tree),也称二叉排序树或二叉查找树。二叉搜索树是一颗二叉树,可以为空;二分查找法其实就是利用的二叉搜索树。

如果不为空,满足以下性质:

  1. 非空左子树的所有键值小于其根节点的键值。
  2. 非空右节点的所有键值大于其根节点的键值。
  3. 左,右子树本身也都是二叉树。

常见操作:

  1. insert(key):向树中插入一个新的键

  2. search(key):树中查找键

  3. inOrderTraverse:通过中序遍历所有节点。

  4. preOrderTraverse:先序

  5. postOrderTraverse:后序

  6. min

  7. max

  8. remove(key)

    如果我们要删除的节点current有两个子节点,甚至子节点还有子节点,这种情况下就需要从current下面所有节点中找到一个最接近current的节点来替换current。

    最接近curren的节点必定是比current小一点点的节点(它必是current左子树中的最大值,叫做前驱)和比current大一点点的节点(current右子树的最小值,叫做后继

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
function BinarySearchTree() {
function Node(key) {
this.key = key;
this.left = null;
this.right = null;
}
this.root = null;
// insert方法:对外给用户调用的方法
BinarySearchTree.prototype.insert = function (key) {
// 根据key值创建节点
let newNode = new Node(key);
// 判断根节点是否为空,空节点直接将新节点赋给根节点
// 非空节点则调用内部insertNode方法
if (this.root == null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}

BinarySearchTree.prototype.insertNode = function (node, newNode) {
if (newNode.key < node.key) { //向左查找
if (node.left == null) {
node.left = newNode;
} else { //向右查找
this.insertNode(node.left, newNode);
}
} else {
if (node.right == null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}

// 树的遍历
// 1.先序遍历
BinarySearchTree.prototype.preOrderTraversal = function (handler) {
this.preOrderTraversalNode(this.root, handler);
}
BinarySearchTree.prototype.preOrderTraversalNode = function (node, handler) {
if (node != null) {
// 先序就是先处理遍历来的数
handler(node.key);
// 处理经过节点的左子节点
this.preOrderTraversalNode(node.left, handler);
// 处理经过节点的右子节点
this.preOrderTraversalNode(node.right, handler);
}
}

// 中序遍历
BinarySearchTree.prototype.midOrderTraversal = function (handler) {
this.midOrderTraversalNode(this.root, handler);
}
BinarySearchTree.prototype.midOrderTraversalNode = function (node, handler) {
if (node != null) {
this.midOrderTraversalNode(node.left, handler);
handler(node.key);
this.midOrderTraversalNode(node.right, handler);
}
}

// 右序遍历
BinarySearchTree.prototype.postOrderTraversal = function (handler) {
this.postOrderTraversalNode(this.root, handler);
}
BinarySearchTree.prototype.postOrderTraversalNode = function (node, handler) {
if (node != null) {
this.postOrderTraversalNode(node.left);
this.postOrderTraversalNode(node.right);
handler(node.key);
}
}

//层序遍历
BinarySearchTree.prototype.levelOrderTraversal = function(){
if(!this.root) return false; //头节点为空返回false
let result = []; //创建一个数组存放结果
let tree = []; //创建一个数组存放二叉树
tree.push(this.root); //先传入头节点
while(tree.length){ //当tree数组长度不为空
let node = tree.shift(); //将数组中的第一个节点放到node中
result.push(node.key); // 将node节点的值放入result中
if(node.left){ //如果node的左节点不为空,就将左节点压入tree数组中
tree.push(node.left);
}
if(node.right){ //如果node的右节点不为空,就将左节点压入tree数组中
tree.push(node.right);
}
}
return result;
}


// 找最小值,在最左边位置
BinarySearchTree.prototype.min = function () {
let node = this.root;
while (node.left) {
node = node.left;
}
return node.key;
}

// max
BinarySearchTree.prototype.max = function () {
let node = this.root;
while (node.right) {
node = node.right;
}
return node.key;
}

// search搜索特定的值
// 递归搜索
// BinarySearchTree.prototype.search = function (key) {
// return this.searchNode(this.root, key);
// }
// BinarySearchTree.prototype.searchNode = function (node, key) {
// if (node == null) {
// return false;
// }
// if (key < node.key) {
// return this.searchNode(node.left, key);
// } else if (key > node.key) {
// return this.searchNode(node.right, key);
// } else {
// return true;
// }
// }
// 循环搜索
BinarySearchTree.prototype.search = function(key){
let node = this.root;
while(node){
if(key < node.key){
node = node.left;
}else if(key > node.key){
node = node.right;
}else{
return true;
}
}
return false;
}

// 删除节点
BinarySearchTree.prototype.remove = function(key){
// 寻找要删除的节点,定义变量,保存一些信息
let parent = null;
let current = this.root;
let isLeftChild = true;

// 开始寻找删除的节点
while(current.key != key){
parent = current;
if(key < current.key){
isLeftChild = true;
current = current.left;
}else{
isLeftChild = false;
current = current.right;
}
// 若已经找到最后的节点,依然没找到等于key的节点
if(current == null) return false;
}

// 找到了要删除的节点
// 1.是一个叶节点
if(current.left == null && current.right == null){
if(isLeftChild){
parent.left = null;
}else{
parent.right = null;
}
}
// 2.删除节点有一个子节点
else if(current.right == null){
if(current == this.root){
this.root = current.left;
}else if(isLeftChild){
parent.left = current.left;
}else{
parent.right = current.left;
}
}else if(current.left == null){
if(current == this.root){
this.root = current.right;
}else if(isLeftChild){
parent.left = current.right;
}else{
parent.right = current.right;
}
}
// 3.删除的节点上有两个节点
else{
// 找到后继
let successor = this.getSuccessor(current);
if(current == this.root){
this.root = successor;
}else if(isLeftChild){
parent.left = successor;
}else{
parent.right = successor;
}

// 将删除节点的左子树
successor.left = current.left;
}

}

// 找后继的方法
BinarySearchTree.prototype.getSuccessor = function(delNode){
// 定义变量,保存找到的后继
let successor = delNode;
let current = delNode.right;
let successorParent = delNode.parent;

// 循环查找
while(current != null){
successorParent = successor;
successor = current;
current = current.left;
}
// 判断寻找的后继节点是否直接就是delNode的right节点
if(successor != delNode.right){
successorParent.left = successor.right;
successor.right = delNode.right;
}
return successor;

}
}
let bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(42);
bst.insert(5);
bst.insert(8);
let resultStr = '';
bst.preOrderTraversal(function (key) {
resultStr += key + ' ';
})
console.log(resultStr);
bst.remove(11);
resultStr = '';
bst.midOrderTraversal(function (key) {
resultStr += key + ' ';
})
console.log(resultStr);

我们发现删除操作很棘手。实际上,因为它复杂,所以我们尽量避免删除操作。做法是在Node类中添加一个boolean的字段,比如isDeleted。要删除一个节点时就将该字段设为true。进行其他操作时,比如find(),在查找前会先判断这个节点是不是被标记为删除。这样比较简单,不会删除原来的树结构,但在二叉树存储中仍保留着本该被删除的节点,造成了很大的空间浪费。

二叉搜索树可快速找到给定关键字的数据项,并且可快速插入和删除数据项。但是当插入的数据是有序的数据时,树的深度就会变得很大,变成非平衡二叉树。

非平衡树:

  1. 比较好的二叉搜索树数据项应该是左右分布均匀的,但是插入连续数据后,分布的不均匀即为非平衡树。
  2. 对于一颗平衡二叉树来说,查找和插入等操作的效率是O(logN)。
  3. 非平衡二叉树相当于编写了一个链表,查找效率变成了O(N)。

AVL树

AVL树是最早的一种平衡树,他有办法保持树的平衡(每个节点多存储了一个额外的数据),时间复杂度为O(logN),但每次删除插入操作不及红黑树,所以整体效率不如红黑树。

红黑树

红黑树除了满足二叉搜索树的基本规则外,还有以下特性:

  1. 节点都是黑色或红色。
  2. 根节点是黑色。
  3. 每个叶子节点都是黑色的空节点(Null节点)。
  4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根所有路径上不能有两个连续的红色节点)。
  5. 从任意节点到其每个叶子的所有路径都包含相同数目的黑色节点。

以上约束确保了红黑树的关键特性:

  1. 从根到叶子的最长可能路径不会超过最短可能路径的两倍长。最短者的可能路径是都是黑色节点,最长的可能路径是红色和黑色交替。
  2. 红黑树基本是平衡的,虽然没有做到绝对的平衡,但是可以保证在最坏的情况下依然是高效的。
|