0%

AJAX

简介与实例

AJAX:即异步的JavaScript和XML,不是一种新的编程语言,而是一种使用现有标准的新方法。

AJAX 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。最大的优点在于不重新加载页面的情况下,可以与服务器交换数据并更新部分网页内容。

XMLHttpRequest 是 AJAX 的基础;有现代浏览器均支持 XMLHttpRequest 对象(IE5 和 IE6 使用 ActiveXObject)。XMLHttpRequest 用于在后台与服务器交换数据。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

variable=new XMLHttpRequest();

请求与响应

XMLHttpRequest 对象用于和服务器交换数据。如需将请求发送到服务器,我们使用 XMLHttpRequest 对象的 open() 和 send() 方法:

1
2
xmlhttp.open("GET","ajax_info.txt",true);
xmlhttp.send();

open(method,url,async)规定请求的类型、URL 以及是否异步处理请求。

  • method:请求的类型;GET 或 POST
  • url:文件在服务器上的位置
  • async:true(异步)或 false(同步)

send(string)将请求发送到服务器。

  • string:仅用于 POST 请求

如需获得来自服务器的响应,请使用 XMLHttpRequest 对象的 responseText 或 responseXML 属性。

responseText:获得字符串形式的响应数据。

responseXML:获得 XML 形式的响应数据。

如果来自服务器的响应并非 XML,请使用 responseText 属性。responseText 属性返回字符串形式的响应,因此您可以这样使用:

1
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;

如果来自服务器的响应是 XML,而且需要作为 XML 对象进行解析,请使用 responseXML 属性:

1
2
3
4
5
6
7
8
xmlDoc=xmlhttp.responseXML;
txt="";
x=xmlDoc.getElementsByTagName("ARTIST");
for (i=0;i<x.length;i++)
{
txt=txt + x[i].childNodes[0].nodeValue + "<br>";
}
document.getElementById("myDiv").innerHTML=txt;

AJAX事件

当请求被发送到服务器时,我们需要执行一些基于响应的任务。每当 readyState 改变时,就会触发 onreadystatechange 事件。readyState 属性存有 XMLHttpRequest 的状态信息。下面是 XMLHttpRequest 对象的三个重要的属性:

onreadystatechange 存储函数(或函数名),每当 readyState 属性改变时,就会调用该函数。
readyState 存有 XMLHttpRequest 的状态。从 0 到 4 发生变化。0: 请求未初始化1: 服务器连接已建立2: 请求已接收3: 请求处理中4: 请求已完成,且响应已就绪
status 200: “OK” 404: 未找到页面

在 onreadystatechange 事件中,我们规定当服务器响应已做好被处理的准备时所执行的任务。当 readyState 等于 4 且状态为 200 时,表示响应已就绪:

回调函数

回调函数是一种以参数形式传递给另一个函数的函数。如果您的网站上存在多个 AJAX 任务,那么您应该为创建 XMLHttpRequest 对象编写一个标准的函数,并为每个 AJAX 任务调用该函数。该函数调用应该包含 URL 以及发生 onreadystatechange 事件时执行的任务(每次调用可能不尽相同):

1
2
3
4
5
6
7
8
9
10
function myFunction()
{
loadXMLDoc("/try/ajax/ajax_info.txt",function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
}
});
}

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
function showHint(str)
{
var xmlhttp;
if (str.length==0)
{
document.getElementById("txtHint").innerHTML="";
return;
}
if (window.XMLHttpRequest)
{
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp=new XMLHttpRequest();
}
else
{
// IE6, IE5 浏览器执行代码
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.getElementById("txtHint").innerHTML=xmlhttp.responseText;
}
}
xmlhttp.open("GET","/try/ajax/gethint.php?q="+str,true);
xmlhttp.send();
}

源代码解析:

如果输入框为空 str.length==0,则该函数清空 txtHint 占位符的内容,并退出函数。

如果输入框不为空,showHint() 函数执行以下任务:

  • 创建 XMLHttpRequest 对象
  • 当服务器响应就绪时执行函数
  • 把请求发送到服务器上的文件
  • 请注意我们向 URL 添加了一个参数 q (带有输入框的内容)

由上面的 JavaScript 调用的服务器页面是 ASP 文件,名为 “gethint.asp”。

下面,我们创建了两个版本的服务器文件,一个用 ASP 编写,另一个用 PHP 编写。

JSON

语法与简介

JSON: JavaScript Object Notation(JavaScript 对象表示法);JSON 是存储和交换文本信息的语法。类似 XML。JSON 比 XML 更小、更快,更易解析

  • JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation)
  • JSON 是轻量级的文本数据交换格式
  • JSON 独立于语言:JSON 使用 Javascript语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。 目前非常多的动态(PHP,JSP,.NET)编程语言都支持JSON。
  • JSON 具有自我描述性,更易理解

JSON 文本格式在语法上与创建 JavaScript 对象的代码相同。

由于这种相似性,无需解析器,JavaScript 程序能够使用内建的 eval() 函数,用 JSON 数据来生成原生的 JavaScript 对象。

与XML对比

与 XML 相同之处:

  • JSON 是纯文本
  • JSON 具有”自我描述性”(人类可读)
  • JSON 具有层级结构(值中存在值)
  • JSON 可通过 JavaScript 进行解析
  • JSON 数据可使用 AJAX 进行传输

与 XML 不同之处:

  • 没有结束标签
  • 更短
  • 读写的速度更快
  • 能够使用内建的 JavaScript eval() 方法进行解析
  • 使用数组
  • 不使用保留字

为什么使用 JSON?

对于 AJAX 应用程序来说,JSON 比 XML 更快更易使用:

使用 XML:

  • 读取 XML 文档
  • 使用 XML DOM 来循环遍历文档
  • 读取值并存储在变量中

使用 JSON:

  • 读取 JSON 字符串
  • 用 eval() 处理 JSON 字符串

JSON语法

JSON 语法是 JavaScript 对象表示语法的子集。

  • 数据在名称/值对中
  • 数据由逗号分隔
  • 大括号 {} 保存对象
  • 中括号 [] 保存数组,数组可以包含多个对象

JSON 数据的书写格式是:key : value。名称/值对包括字段名称(在双引号中),后面写一个冒号,然后是值。

“name” : “菜鸟教程”。等价于这条 JavaScript 语句:name = “菜鸟教程”。

JSON 值可以是:

  • 数字(整数或浮点数)
  • 字符串(在双引号中)
  • 逻辑值(true 或 false)
  • 数组(在中括号中)
  • 对象(在大括号中)
  • null

JSON 数组在中括号 [] 中书写:数组可包含多个对象:对象 sites 是包含三个对象的数组。每个对象代表一条关于某个网站(name、url)的记录。

1
2
3
4
5
6
7
{
"sites": [
{ "name":"菜鸟教程" , "url":"www.runoob.com" },
{ "name":"google" , "url":"www.google.com" },
{ "name":"微博" , "url":"www.weibo.com" }
]
}

因为 JSON 使用 JavaScript 语法,所以无需额外的软件就能处理 JavaScript 中的 JSON。通过 JavaScript,您可以创建一个对象数组,并像这样进行赋值:

1
2
3
4
5
var sites = [
{ "name":"runoob" , "url":"www.runoob.com" },
{ "name":"google" , "url":"www.google.com" },
{ "name":"微博" , "url":"www.weibo.com" }
];

sites[0].name可以访问 JavaScript 对象数组中的第一项

  • JSON 文件的文件类型是 .json
  • JSON 文本的 MIME 类型是 application/json

与XML相比

JSON 和 XML 都用于接收 web 服务端的数据。JSON 和 XML在写法上有所不同,如下所示:

1
2
3
4
5
6
7
{
"sites": [
{ "name":"菜鸟教程" , "url":"www.runoob.com" },
{ "name":"google" , "url":"www.google.com" },
{ "name":"微博" , "url":"www.weibo.com" }
]
}
1
2
3
4
5
6
7
8
9
10
11
<sites>
<site>
<name>菜鸟教程</name> <url>www.runoob.com</url>
</site>
<site>
<name>google</name> <url>www.google.com</url>
</site>
<site>
<name>微博</name> <url>www.weibo.com</url>
</site>
</sites>

JSON 与 XML 的相同之处:

  • JSON 和 XML 数据都是 “自我描述” ,都易于理解。
  • JSON 和 XML 数据都是有层次的结构
  • JSON 和 XML 数据可以被大多数编程语言使用

JSON 与 XML 的不同之处:

  • JSON 不需要结束标签
  • JSON 更加简短
  • JSON 读写速度更快
  • JSON 可以使用数组

最大的不同是:XML 需要使用 XML 解析器来解析,JSON 可以使用标准的 JavaScript 函数来解析。

为什么 JSON 比 XML 更好?

XML 比 JSON 更难解析。

JSON 可以直接使用现有的 JavaScript 对象解析。

针对 AJAX 应用,JSON 比 XML 数据加载更快,而且更简单:

使用 XML

  • 获取 XML 文档
  • 使用 XML DOM 迭代循环文档
  • 接数据解析出来复制给变量

使用 JSON

  • 获取 JSON 字符串
  • JSON.Parse 解析 JSON 字符串

JSON对象与数组

JSON 对象使用在大括号({})中书写。

对象可以包含多个 key/value(键/值)对。key 必须是字符串,value 可以是合法的 JSON 数据类型(字符串, 数字, 对象, 数组, 布尔值或 null)。key 和 value 中使用冒号(:)分割。每个 key/value 对使用逗号(,)分割。

你可以使用点号(.)来访问对象的值。你也可以使用中括号([])来访问对象的值:

循环对象

你可以使用 for-in 来循环对象的属性:在 for-in 循环对象的属性时,使用中括号([])来访问属性的值:

1
2
3
4
var myObj = { "name":"runoob", "alexa":10000, "site":null };
for (x in myObj) {
document.getElementById("demo").innerHTML += myObj[x] + "<br>";
}

嵌套 JSON 对象

JSON 对象中可以包含另外一个 JSON 对象:你可以使用点号(.)或者中括号([])来访问嵌套的 JSON 对象。x = myObj.sites.site1; // 或者 x = myObj.sites[“site1”];

1
2
3
4
5
6
7
8
9
myObj = {
"name":"runoob",
"alexa":10000,
"sites": {
"site1":"www.runoob.com",
"site2":"m.runoob.com",
"site3":"c.runoob.com"
}
}

你可以使用点号(.)来修改 JSON 对象的值:你可以使用中括号([])来修改 JSON 对象的值:

我们可以使用 delete 关键字来删除 JSON 对象的属性:你可以使用中括号([])或者点号.来删除 JSON 对象的属性:

数组作为 JSON 对象

JSON 数组在中括号中书写。JSON 中数组值必须是合法的 JSON 数据类型(字符串, 数字, 对象, 数组, 布尔值或 null)。

JavaScript 中,数组值可以是以上的 JSON 数据类型,也可以是 JavaScript 的表达式,包括函数,日期,及 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
{
"name":"网站",
"num":3,
"sites":[ "Google", "Runoob", "Taobao" ]
}
x = myObj.sites[0];

//可以使用for-in来访问数组
for (i in myObj.sites) {
x += myObj.sites[i] + "<br>";
}
//也可以for循环
for (i = 0; i < myObj.sites.length; i++) {
x += myObj.sites[i] + "<br>";
}
//嵌套JSON对象中的数组,JSON对象中的数组可以包含另外一个数组,或者另外一个JSON对象;
myObj = {
"name":"网站",
"num":3,
"sites": [
{ "name":"Google", "info":[ "Android", "Google 搜索", "Google 翻译" ] },
{ "name":"Runoob", "info":[ "菜鸟教程", "菜鸟工具", "菜鸟微信" ] },
{ "name":"Taobao", "info":[ "淘宝", "网购" ] }
]
}
//可以使用for-in来循环访问每个数组:
for (i in myObj.sites) {
x += "<h1>" + myObj.sites[i].name + "</h1>";
for (j in myObj.sites[i].info) {
x += myObj.sites[i].info[j] + "<br>";
}
}

JSON的方法与使用

JSON.parse()

JSON 通常用于与服务端交换数据。在接收服务器数据时一般是字符串。我们可以使用 JSON.parse() 方法将数据转换为 JavaScript 对象。

JSON.parse(text[ , reviver])

  • **text:**必需, 一个有效的 JSON 字符串。
  • reviver: 可选,一个转换结果的函数, 将为对象的每个成员调用此函数。
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
//如果从服务器接收了以下的数据:
{ "name":"runoob", "alexa":10000, "site":"www.runoob.com" }
//我们使用 JSON.parse() 方法处理以上数据,将其转换为 JavaScript 对象
var obj = JSON.parse('{ "name":"runoob", "alexa":10000, "site":"www.runoob.com" }');
//解析前要确保你的数据是标准的 JSON 格式,否则会解析出错解析完成后,我们就可以在网页上使用 JSON 数据了
<p id="demo"></p>

<script>
var obj = JSON.parse('{ "name":"runoob", "alexa":10000, "site":"www.runoob.com" }');
document.getElementById("demo").innerHTML = obj.name + ":" + obj.site;
</script>

//我们可以使用AJAX从服务器请求JSON数据,并解析为JS对象
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
myObj = JSON.parse(this.responseText);
document.getElementById("demo").innerHTML = myObj.name;
}
};
xmlhttp.open("GET", "/try/ajax/json_demo.txt", true);
xmlhttp.send();
//如果服务端接受的是数组的JSON数据,则JSON.parse会将其转换为JS数组
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
myArr = JSON.parse(this.responseText);
document.getElementById("demo").innerHTML = myArr[1];
}
};
xmlhttp.open("GET", "/try/ajax/json_demo_array.txt", true);
xmlhttp.send();

异常:解析数据,JSON不能存储Date对象,若需要存储Date对象,则需要先将其转换成字符串,之后再将字符串转换成Date对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var text = '{ "name":"Runoob", "initDate":"2013-12-14", "site":"www.runoob.com"}';
var obj = JSON.parse(text);
obj.initDate = new Date(obj.initDate);

document.getElementById("demo").innerHTML = obj.name + "创建日期: " + obj.initDate;
//我们可以启用JSON.parse的第二个参数reviver,一个转换结果的函数,对象的每个成员调用此函数
var text = '{ "name":"Runoob", "initDate":"2013-12-14", "site":"www.runoob.com"}';
var obj = JSON.parse(text, function (key, value) {
if (key == "initDate") {
return new Date(value);
} else {
return value;
}});

document.getElementById("demo").innerHTML = obj.name + "创建日期:" + obj.initDate;

//解析函数:JSON不允许包含函数,但可以将函数作为字符串存储,之后再将字符串转换为函数
var text = '{ "name":"Runoob", "alexa":"function () {return 10000;}", "site":"www.runoob.com"}';
var obj = JSON.parse(text);
obj.alexa = eval("(" + obj.alexa + ")");

document.getElementById("demo").innerHTML = obj.name + " Alexa 排名:" + obj.alexa();

JSON.stringify()

JSON 通常用于与服务端交换数据。在向服务器发送数据时一般是字符串。我们可以使用 JSON.stringify() 方法将 JavaScript 对象转换为字符串。

JSON.stringify(value[, replacer[, space]])

  • value:

    必需, 要转换的 JavaScript 值(通常为对象或数组)。

  • replacer:

    可选。用于转换结果的函数或数组。

    如果 replacer 为函数,则 JSON.stringify 将调用该函数,并传入每个成员的键和值。使用返回值而不是原始值。如果此函数返回 undefined,则排除成员。根对象的键是一个空字符串:””。

    如果 replacer 是一个数组,则仅转换该数组中具有键值的成员。成员的转换顺序与键在数组中的顺序一样。当 value 参数也为数组时,将忽略 replacer 数组。

  • space:

    可选,文本添加缩进、空格和换行符,如果 space 是一个数字,则返回值文本在每个级别缩进指定数目的空格,如果 space 大于 10,则文本缩进 10 个空格。space 也可以使用非数字,如:\t。

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
var obj = { "name":"runoob", "alexa":10000, "site":"www.runoob.com"};
//使用JSON.stringify()方法处理以上数据,并将其转换为字符串

var myJSON = JSON.stringify(obj);
document.getElementById("demo").innerHTML = myJSON;

//同样也可以把JS数组转换成JSON字符串
var arr = [ "Google", "Runoob", "Taobao", "Facebook" ];
var myJSON = JSON.stringify(arr);
document.getElementById("demo").innerHTML = myJSON;

//JSON.stringify() 会将所有日期转换为字符串。之后你可以再将字符串转换为 Date 对象
var obj = { "name":"Runoob", "initDate":new Date(), "site":"www.runoob.com"};
var myJSON = JSON.stringify(obj);
document.getElementById("demo").innerHTML = myJSON;
//JSON 不允许包含函数,JSON.stringify() 会删除 JavaScript 对象的函数,包括 key 和 value
var obj = { "name":"Runoob", "alexa":function () {return 10000;}, "site":"www.runoob.com"};
var myJSON = JSON.stringify(obj);

document.getElementById("demo").innerHTML = myJSON;

//我们可以在执行 JSON.stringify() 函数前将函数转换为字符串来避免以上问题的发生:
var obj = { "name":"Runoob", "alexa":function () {return 10000;}, "site":"www.runoob.com"};
obj.alexa = obj.alexa.toString();
var myJSON = JSON.stringify(obj);

document.getElementById("demo").innerHTML = myJSON;
//因此不建议在JSON中使用函数

JSON使用与教程

JSON 最常见的用法之一,是从 web 服务器上读取 JSON 数据(作为文件或作为 HttpRequest),将 JSON 数据转换为 JavaScript 对象,然后在网页中使用该数据。为了更简单地为您讲解,我们使用字符串作为输入进行演示(而不是文件)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//创建包含 JSON 语法的 JavaScript 字符串:
var txt = '{ "sites" : [' +
'{ "name":"菜鸟教程" , "url":"www.runoob.com" },' +
'{ "name":"google" , "url":"www.google.com" },' +
'{ "name":"微博" , "url":"www.weibo.com" } ]}';
//由于 JSON 语法是 JavaScript 语法的子集,JavaScript 函数 eval() 可用于将 JSON 文本转换为 JavaScript 对象。eval() 函数使用的是 JavaScript 编译器,可解析 JSON 文本,然后生成 JavaScript 对象。必须把文本包围在括号中,这样才能避免语法错误。
var obj = eval ("(" + txt + ")");
//在网页中使用 JavaScript 对象
var txt = '{ "sites" : [' +
'{ "name":"菜鸟教程" , "url":"www.runoob.com" },' +
'{ "name":"google" , "url":"www.google.com" },' +
'{ "name":"微博" , "url":"www.weibo.com" } ]}';

var obj = eval ("(" + txt + ")");

document.getElementById("name").innerHTML=obj.sites[0].name
document.getElementById("url").innerHTML=obj.sites[0].url

//JSON解析器
eval() 函数可编译并执行任何 JavaScript 代码。这隐藏了一个潜在的安全问题。
使用 JSON 解析器将 JSON 转换为 JavaScript 对象是更安全的做法。JSON 解析器只能识别 JSON 文本,而不会编译脚本。
在浏览器中,这提供了原生的 JSON 支持,而且 JSON 解析器的速度更快。
较新的浏览器和最新的 ECMAScript (JavaScript) 标准中均包含了原生的对 JSON 的支持。

JSONP

Jsonp(JSON with Padding) 是 json 的一种”使用模式”,可以让网页从别的域名(网站)那获取资料,即跨域读取数据。为什么我们从不同的域(网站)访问数据需要一个特殊的技术( JSONP )呢?这是因为同源策略。同源策略,它是由 Netscape 提出的一个著名的安全策略,现在所有支持 JavaScript 的浏览器都会使用这个策略。

动态规划框架

动态规划问题的一般形式是求最值;求解动态规划的核心问题是穷举,肯定要把所有可行的答案穷举出来然后在其中找最值;且一般具有重叠子问题,需要备忘录或者DP table来优化。

关键:状态转移方程,明确[状态]->定义DP数组、函数的含义->明确[选择]->明确basecase

带备忘录的递归,有时候耗时的原因是重复计算,那么可以造备忘录,每次算出某子问题后不急着返回,先记到备忘录中;每次遇到一个子问题时,先去备忘录查一查,如果发现已经解决过,就直接拿出来用。

一般使用一个数组充当备忘录,当然也可以使用hash表,思想是一样的。

斐波那契数列问题:

1
2
3
4
5
6
7
8
const fit = (N) => {
if (N < 1) return 0;
var memo = new Array(N + 1);
memo[1] = memo[2] = 1;
for (let i = 3; i <= N; i++)
memo[i] = memo[i-1] + memo[i - 2];
return memo[N];
}

带备忘录的递归解法和迭代的动态规划解法其实一样,递归自顶向下,迭代递归自底向上;

动态规划的另一个重要特性:最优子结构;凑零钱问题:给你k种面值的硬币,面值分别为C1、C2、…Ck;再给一个总金额amount,问最少需要多少硬币凑出,如果不能凑出则返回-1。

要符合最优子结构,子问题之间必须相互独立,状态转移方程的步骤

1、确定状态,也就是原问题和子问题变化的变量,唯一状态为目标金额amount;

2、确定dp函数的定义:当前目标金额为n,则至少需要dp(n)个硬币;

3、确定选择并择优:对于每个状态,可以做出选择改变当前状态;具体至当前,无论目标金额多少,选择就是从面额列表coins种选择一个硬币,然后目标金额就会减少。

4、最后明确base case,显然目标金额为0时,所需硬币数量为0;当目标金额小于0时,无解,返回-1。

一般这种题需要使用备忘录来消除子问题,减少其冗余从而节省时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const coinChange = (coins, amount){
let memo = [];
const dp = (n) => {
if (memo[n] != null) return memo[n];
//先查备忘录,避免重复计算
if (n == 0) return 0;
if (n < 0) return -1;
res = MAX_VALUE;
for (let i = 0; i < coins.length; i++){
let subproblem == dp(n - coins[i]);
if (subproblem == -1) continue;
res = min(res, 1 + subproblem);
}
if (res != MAX_VALUE){
memo[n] = res;
}
else memo[n] = -1;
return memo[n];
}
return dp(amount);
}

除了这种自顶向下的带备忘录递归,自底向上的dp数组迭代也一样可以消除子问题:dp[i] = x,表示当目标金额为i时,至少需要x枚硬币。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const coinChange = (coins, amount) =>{
let dp = new Array(amount + 1);
for (let i = 0; i < amount + 1; i++){
dp[i] = amount + 1;
}
//先初始化dp数组,长度与初始值
for (i = 0; i < dp.length; i++){
//内层for在求所有子问题+1的最小值
for (let j = 0; j < coins.length; j++){
//子问题无解则跳过
if (i - coins[j] < 0) continue;
dp[i] = min(dp[i], 1 + dp[i - coin]);
}
}
if (dp[amount] == amount + 1) return -1;
else return dp[amount];
}

动态规划之背包问题

给你一个则装载重量为W的背包和N个物品,每个物品有重量和价值两个属性,其中第i个物品重量为wt[i],价值为val[i],求使用该背包装物品的最大价值。

典型动态规划:物品不可分割,要么装进包里,要么不装,不可能切分。解决这种问题没有排序的巧妙方法,只能穷举所有可能,根据动态规划套路走流程:

1、明确状态与选择;状态:背包容量与可选择的物品;选择:装进背包或不装进背包。

1
2
3
4
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ....
dp[状态1][状态2][...] = 择优(选择1, 选择2, ...)

2、明确DP数组:描述当前问题局面,需要使用DP数组把状态表示出来,而状态有2个,因此为二维数组dp[i] [w] :对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值为dp[i] [w]。且base case 就是dp[0] [..] = dp[..] [0] = 0。

3、根据选择,思考状态转移的逻辑:即把物品i装不装进背包的代码逻辑怎么体现。如果不装:dp[i] [w] = dp[i-1] [w],即继承以前的结果;如果装:dp[i] [w] = dp[i-1] [w - wt[i-1]] + val[i-1],在装第i个的前提下该如何最大价值,应寻求剩余重量w - wt[i-1]限制下的最大重量,再加上确定的第i个物品价值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const knapsack = (W, N, wt, val) =>{
let dp = new Array();
for (let i = 0; i < N + 1; i++){
dp[i] = new Array();
for (let j = 0; j < W + 1; j++){
dp[i][j] = 0;
}
}//先定义二维数组,并均初始化为0
for (i = 1; i <= N; i++){
for (w = 1; w <= W; W++){
if (w - wt[i - 1] < 0){
//当前背包容量放不下时,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
//装入或不装入背包的择优
dp[i][w] = max(dp[i - 1][w - wt[i - 1]] + val[i - 1], dp[i - 1][w]);
}
}
}
return dp[N][W];
}
//使用动态规划遍历时,同时择优并用DP表来存储已经计算完的数据。

动态规划之子集背包分割

怎么将⼆维动态规划压缩成⼀维动态规划吗?这就是状态压缩,很容易的,

题目:给定一个只包含正整数的非空数组,是否可以将该数组分割成两个子集,使两个子集的元素和相等:

对于这个问题,看起来和背包没有任何关系,为什么说它是背包问题呢? ⾸先回忆⼀下背包问题⼤致的描述是什么: 给你⼀个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两 个属性。其中第 i 个物品的重量为 wt[i] ,价值为 val[i] ,现在让你⽤ 这个背包装物品,最多能装的价值是多少?

那么对于这个问题,我们可以先对集合求和,得出 sum ,把问题转化为背 包问题:给⼀个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i] 。现在让你装物品,是否存在⼀种装法,能够恰好将背包装满

你看,这就是背包问题的模型,甚⾄⽐我们之前的经典背包问题还要简单⼀ 些,下⾯我们就直接转换成背包问题,开始套前⽂讲过的背包问题框架即就可。

1、明确状态与选择;状态:背包容量、可选择的物品;选择:是否装进背包

2、dp数组定义:dp[ i ] [ j ]= x 表⽰,对于前 i 个物品,当前背包的容量为 j 时,若 x 为 true ,则说明可以恰好将背包装满,若 x 为 false ,则说明不能恰 好将背包装满。

3、根据选择来思考状态转移的逻辑,恰好把背包装满的条件,其实刚好又取决于其上一个状态;换句话说,如果 j - nums[i-1] 的重量可以被恰好装满,那么只要把第 i 个物品装进去,也可恰好装满 j 的重量;否则的话,重量 j 肯定是装不满的。

4、basecase:想求的最终答案是dp[N] [sum/2],因此初始条件为dp[…] [0] = true和dp[0] […] = false;

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
const canPartition = (nums) => {
let sum = 0;
for (let i = 0; i < nums.length; i++){
sum += nums[i];
}
if (sum % 2 != 0) return false;
let len = nums.length;
sum = sum / 2;
let dp = new Array();

//根据初始条件来初始化
for (i = 0; i <= len + 1; i++) {
dp[i] = new Array();
for (let j = 0; j <= sum + 1; j++){
dp[i][j] = false;
}
}
for (i = 0; i <= n; i++){
dp[i] [0] = true;
}

for (i = 0; i <= n; i++){
for (j = 0; j <= sum; j++){
if (j - nums[i - 1] < 0){
//背包不足,不能装入第i个物品
dp[i] [j] = dp[i - 1] [j];
} else {
//装入或不装入的选择
dp[i][j] = dp[i - 1] [j] | dp[i - 1] [j - nums[i - 1]];
}
}
}
return dp[n] [sum];
}

动态规划之零钱兑换(完全背包问题)

题目:给定不同面额的硬币和一个总金额,假设每种硬币有无数个,写出函数来计算能凑成总金额的硬币总和数。

有⼀个背包,最⼤容量为 amount ,有⼀系列物品 coins ,每个物品的重量为 coins[i] ,每个物品的数量⽆限。请问有多少种⽅法,能够把背包恰 好装满?

这个问题和我们前⾯讲过的两个背包问题,有⼀个最⼤的区别就是,每个物 品的数量是⽆限的,这也就是传说中的「完全背包问题」,没啥⾼⼤上的, ⽆⾮就是状态转移⽅程有⼀点变化⽽已。

1、状态与选择;状态:背包的容量,可选择的物品;选择:是否装进背包;

2、dp数组:若只使用前i个物品,当背包容量为j时,有dp[i] [j]种方法可以装满背包;

3、base case为dp[0] [..] = 0;dp[..] [0] = 1;不使用任何面值则无法完成,背包容量为0则仅有一种。

4、状态转移方程的逻辑:如果不把第i个物品装进背包,则凑出面额j的方法数dp[i] [j]等于dp[i - 1] [j],继承之前的结果;如果你把第i个物品装进了背包,则说明使用了coins[i - 1] 这个面额,那么dp[i] [j] = dp [i] [j-coins[i - 1]]

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
const change = (amount, coins) => {
let len = coins.length;
let dp = new Array();

//根据初始条件来初始化
for (let i = 0; i <= len + 1; i++) {
dp[i] = new Array();
for (let j = 0; j <= sum + 1; j++){
dp[i][j] = 0;
}
}
for (i = 0; i <= n; i++){
dp[i] [0] = 1;
}
for (i = 1; i <=n; i++){
for (j = 1; j <= amount; j++){
if (j - coins[i - 1] >= 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][amount];
}

动态规划最长递增子序列

动态规划的通用技巧:数学归纳思想;最长递增子序列LIS;子串与子序列名词的区别:字串一定是连续的,而子序列不一定是连续的。

1、明确状态与选择:状态,序列nums的所有字母;选择:更新子序列还是

2、用dp数组来描述状态,dp[i]表示以nums[i]这个数结尾的最长递增子序列的长度;后续的动态规划子序列解题模板总结了常见的套路:

3、根据选择思考动态转移方程的逻辑:已知dp[0..n-1],根据nums[n]的值,便能够判断如何更新,找到结尾比nums[n]小的子序列,然后把nums[n]接到最后,形成新子序列,其长度为原子序列+1;且由于未知之前子序列的大小关系,需要使用遍历将前面的dp值全部遍历一遍;

4、base case: dp[i]初始值为1,因为以nums[i]结尾的LIS最少要包含它自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const lengthOfLIS = (nums) => {
let dp = new Array();
for (let i = 0; i < nums.length; i++){
dp[i] = 1;
}
for (i = 0; i < nums.length; i++){
for (let j = 0; j < i; j++){
if (nums[i] > nums[j]){
dp[i] = max.(dp[i], dp[j] + 1);
}
}
}
let res = 0;
//将每个以nums[i]结尾的LIS长度存到dp之后,遍历一遍找出最大的那个
for (i = 0; i < dp.length; i++){
res = max.(res, dp[i]);
}
return res;
}

动态规划:最长公共子序列LCS

LCS是典型的二维动态规划,目的是求两个字符串的LCS长度,子序列问题基本都用动态规划来实现,穷举加剪枝。

1、明确状态与选择:状态,两个字符串;选择,是否添加该元素作为最长公共子序列。

2、确定dp数组的含义: 两字符串数组的通用套路,dp[i] [j]代表s1[1…i]和s2[1…j]的LCS长度。

3、找状态转移方程:其实字符串问题的状态转移套路都差不多,都是判断字符在还是不在的问题,只有2种选择;如果某字符在LCS中,则其肯定同时存在于s1和s2中。

思路为:用2个指针从后往前遍历s1、s2,若相等则该字符一定在lcs中,否则这两个字符至少有一个不在lcs中,需要丢弃一个;

4、base case:均初始化为0,最短可以没有公共元素。

暴力的递归写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const longestCommonSubsequence = (str1, str2) => {
const dp = (let i , let j) => {
if (i === -1 || j === -1) return 0;
//先初始化空串的base case
if (str1[i] === str[j]){
//找到一个lcs的元素则继续往前找
return dp(i - 1, j - 1) + 1;
} else{
//否则,则找出让lcs最长的
return max(dp(i - 1, j), dp(i, j - 1));
}
}
return dp(str1.length - 1, str2.length - 1);
}

可以通过备忘录或者DPtable来优化时间复杂度,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const longestCommonSubsequencr = (str1, str2) => {
let m = str1.length, n = str2.length;
let dp = new Array();
for (let i = 0; i < m; i++){
dp[i] = new Array();
for (let j = 0; j < n; j++){
dp[i][j] = 0;
}
}
//构建DPtable并初始化
for (i = 0; i < m; i++){
for (j = 0; j < n; j++){
if (str[i] === str[j]){
dp[i][j] = 1 + dp[i - 1][j - 1];
} else{
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m - 1][n - 1];
}

对于str1[i]与str2[j]不等的情况,是否可能两个字符都不在,但其实因为后面的选择max始终选择最大的,因此不用考虑,必然被排除在外。先像文本一样写出伪代码来定义DPtable理清逻辑,思考每个状态有哪些选择,用正确逻辑作出正确判断就能做出正确选择。

经典动态规划:编辑距离

给定两个字符串s1和s2,计算出将s1转换成s2所使用的最少操作数,可用操作只有插入、删除、替换一个字符。

思路:解决两个字符串的动态规划问题,一般都是用2个指针i、j分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*base case:当i走完s1或者j走完s2时,可以直接返回另一个字符串剩下的长度;
if s1[i] == s2[j]:
啥也不做(skip)
i、j同时移动
else:
三选一:
插入、删除、替换
三选一选择的标准是:均尝试一遍,哪个操作最后得到的编辑距离最小。*/
const minDistance = (s1, s2) => {
const dp = (i, j) =>{
if (i == -1) return j + 1;
if (j == -1) return i + 1;
//定义base case
if (s1[i] == s2[j]) return dp(i - 1, j - 1);
else return min(dp(i, j - 1) + 1,dp(i - 1, j) + 1, dp(i - 1, j - 1) + 1);
}
return dp(s1.length - 1, s2.length - 1);
}

dp(i, j)返回s1[0..i]和s2[0..j]的最小编辑距离;同样,上述的解法属于暴力破解,需要使用动态规划技巧来优化重叠子问题

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
const minDistance = (s1, s2) => {
let m = str1.length, n = str2.length;
let dp = new Array();
for (let i = 0; i < m; i++){
dp[i] = new Array();
for (let j = 0; j < n; j++){
dp[i][j] = 0;
}
}
//构建DPtable并初始化
for (int i = 1; i <= m; i++){
dp[i][0] = i;
}
for (int j = 1; j <= n; j++){
dp[0][j] = j;
}
//进行合适的DPtable初始化,DPtable自底向上求解,递归从上至下
for (i = 1; i <= m; i++){
for (j = 1; j <= n; j++){
if (s1[i] === s2[j]){
dp[i][j] = dp[i - 1][j - 1];
} else{
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
return dp[m][n];
}

一般来说,处理两个字符串的动态规划问题,均按照本文思路处理,建立DPtable;但这里仅给出了最小编辑距离,具体如何修改则需要给dp数组增加额外的信息。则使用JS中的对象数组,dp[i] [j]中存储的是一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let arrayObj = [{}];//定义对象数组
let m = str1.length, n = str2.length;
let dp = new Array();
for (let i = 0; i < m; i++){
dp[i] = new Array();
for (let j = 0; j < n; j++){
dp[i][j] = {
'val' : 0,
'choice' : 0,
//val为其原本dp数组值,choice为操作,0代表什么也不做,1代表插入,2代表删除,3代表替换
};
}
}
//最终结果为dp[m][n],后续则一步步反推,插入=>左移、替换=>左移并上移、删除=>上移,形成一条路径;这样一步步走回来了。

动态规划的子序列模板(最长回文子序列)

子序列通常涉及两个字符串,例如:两个字符串的最长公共子序列;既然要用动态规划,关键在于dp数组的定义与寻找状态转移关系,不同问题需要使用不同的dp数组定义。

一、一维的dp数组

最长递增子序列LIS:在子数组array[0…i]中,以array[i]为结尾的目标子序列的长度是dp[i]。

目的:符合归纳法,能够从前n-1个的结果中推断出来第n个的结果

二、二维的dp数组

涉及两个字符串、数组时:

在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i] [j];

只涉及一个字符串、数组:

在子数组array[i…j]中,我们要求的子序列(最长回文子序列)的长度为dp[i] [j]。

最长回文子序列

DPtable的定义:在子串s[i…j]中,最长回文子序列的长度为dp[i] [j];找状态转移方程的关键在于归纳思维,即如何从已知的结果推断出未知的部分。

具体来说,如果我们想求dp[i] [j],假设已经知道子问题dp[i + 1] [j - 1]的结果,是否能想办法算出dp[i] [j]的值。可以,取决于s[i]和s[j]的字符。1、如果他两相等则它们加上便就是当前的最大回文子序列;2、如果她两不相等,说明两者不可能同时出现在最长回文子序列中,于是把它们分别加入s[i+1…j-1]中,看看哪个子串产生的回文子序列更长即可。

base case:dp[i] [j] = 1;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const longestPalindromeSubseq = (str1) => {
let m = str1.length;
let dp = new Array();
for (let i = 0; i < m; i++){
dp[i] = new Array();
for (let j = 0; j < n; j++){
dp[i][j] = 0;
}
}
for (i = 0; i < n; i++){
dp[i][i] = 1;
}
//根据上面的状态转移方程,为保证每次计算dp[i][j],左、下、左下三个方向的位置均被计算出来,只能斜着遍历或反着遍历。
for (i = n - 1; i >= 0; i--){
for (let j = i + 1; j < n; j++){
if (s[i] == s[j]){
dp[i][j] = dp[i + 1][j - 1] + 2;
} else{
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}

动态规划的正则表达式

算法的设计是⼀个螺旋上升、逐步求精 的过程,绝不是⼀步到位就能写出正确算法。

写递归的技巧是管好当下,之后的事抛给递归

动态规划答疑篇

最优子结构

最优子结构:从子问题的最优结果能够推断出更大规模问题的最优结果;想要满足最优子结构,子问题之间必须相互独立。遇到这种最优子结构失效的情况,策略是改造问题。等价转化:最大分数差<=>最高分数和最低分数的差<=>求最高与最低分数。

最优子结构并不是动态规划独有的性质,但反过来,动态规划一定是要求的求最值的。动态规划就是通过base case一步步反推往上,只有符合最优子结构的问题才有发生这种链式反应的性质。

找寻最优子结构的过程其实就是证明状态转移方程正确性的过程,方程符合最优子结构就可以写暴力解了,写出暴力解就能看出有没有重叠子问题,有则优化。

dp数组的遍历方向

对dp数组的遍历方向有所困惑,可以正向、反向遍历,乃至斜向遍历;

1
2
3
4
5
6
//斜着遍历
for (let l = 2; l <= n; l++){
for (let i = 0; i <= n - 1; i++){
let j = l + i - 1;
}
}

dp遍历设置的两大原则:1、遍历的过程中,所需的状态必须是已经计算出来的;2、遍历的终点必须是存储结果的那个位置;

即根据base case处于的位置来判断dp数组计算的起步,根据递推关系来判断dp数组递推的方向。

动态规划二维变一维

动态规划之股票买卖

题目:给定一个数组,它的第i个元素是一支给定的股票在第i天的价格,设计一个算法来计算你所能获取的最大利润,且你最多可以完成k笔交易。(你不能同时参与多笔交易,必须在再次购买之前出售)

穷举框架

这⾥,我们不⽤递归思想进⾏穷举,⽽是利⽤「状态」进⾏穷举。我们具 体到每⼀天,看看总共有⼏种可能的「状态」,再找出每个「状态」对应的 「选择」。我们要穷举所有「状态」,穷举的⽬的是根据对应的「选择」更 新状态。

⽐如说这个问题,每天都有三种「选择」:买⼊、卖出、⽆操作,我们⽤ buy, sell, rest 表⽰这三种选择。但问题是,并不是每天都可以任意选择这三 种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后。那么 rest 操作 还应该分两种状态,⼀种是 buy 之后的 rest(持有了股票),⼀种是 sell 之 后的 rest(没有持有股票)。⽽且别忘了,我们还有交易次数 k 的限制,就 是说你 buy 还只能在 k > 0 的前提下操作。

这个问题的「状态」有三个,第⼀个是天 数,第⼆个是允许交易的最⼤次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨⽤ 1 表⽰持有,0 表⽰没有持有)。然后我们⽤⼀个 三维数组就可以装下这⼏种状态的全部组合:

⽽且我们可以⽤⾃然语⾔描述出每⼀个状态的含义,⽐如说 dp[3][2][1] 的含义就是:今天是第三天,我现在⼿上持有着股票,⾄今最多进⾏ 2 次交 易。再⽐如 dp [2] [3] [0] 的含义:今天是第⼆天,我现在⼿上没有持有股 票,⾄今最多进⾏ 3 次交易。很容易理解,对吧?

我们想求的最终答案是 dp [n - 1] [K] [0],即最后⼀天,最多允许 K 次交易, 最多获得多少利润。读者可能问为什么不是 dp[n - 1] [K] [1]?因为 [1] 代表⼿ 上还持有股票,[0] 表⽰⼿上的股票已经卖出去了,很显然后者得到的利润 ⼀定⼤于前者。

记住如何解释「状态」,⼀旦你觉得哪⾥不好理解,把它翻译成⾃然语⾔就 容易理解了。

状态转移框架

1
2
3
4
5
6
7
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) 
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能: 要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有; 要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能: 要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票; 要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了

现在,我们已经完成了动态规划中最困难的⼀步:状态转移⽅程。如果之前 的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就 ⾏了。不过还差最后⼀点点,就是定义 base case,即最简单的情况

1
2
3
4
5
6
7
8
9
dp[-1][k][0] = 0 
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,⽤负⽆穷表⽰这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,⽤负⽆穷表⽰这种不可能

总结一下:

1
2
3
4
5
6
7
base case:
dp[-1][k][0] = dp[i][0][0]
dp[-1][k][0] = dp[i][0][1] = -infinity

状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

k=1时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for (int i = 0; i < n; i++) { 
if (i - 1 == -1){
dp[i][0] = 0;
// 解释:
// dp[i][0]
// = max(dp[-1][0], dp[-1][1] + prices[i])
// = max(0, -infinity + prices[i]) = 0
dp[i][1] = -prices[i];
//解释:
// dp[i][1]
// = max(dp[-1][1], dp[-1][0] - prices[i])
// = max(-infinity, 0 - prices[i])
// = -prices[i]
continue;
}
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];

第⼀题就解决了,但是这样处理 base case 很⿇烦,⽽且注意⼀下状态转移 ⽅程,新状态只和相邻的⼀个状态有关,其实不⽤整个 dp 数组,只需要⼀ 个变量储存相邻的那个状态就⾜够了,这样可以把空间复杂度降到 O(1):

k= +infinity

k为正无穷,则可以把k、k-1看作一样,同样可以改写框架,数组中k已经不会改变,则不需要记录k。

1
2
3
4
5
6
7
8
9
10
int maxProfit_k_inf(int[] prices) {
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
}
return dp_i_0;
}

k = infinity + coldDown

每次 sell 之后要等⼀天才能继续交易。只要把这个特点融⼊上⼀题的状态转 移⽅程即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) ;
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]);
//解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,⽽不是 i-1 。
int maxProfit_with_cool(int[] prices) {
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
int dp_pre_0 = 0;
// 代表 dp[i-2][0]
for (int i = 0; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
dp_pre_0 = temp;
}
return dp_i_0;
}

k = +infinity with fee

每次交易要⽀付⼿续费,只要把⼿续费从利润中减去即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) ;
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee);
//解释:相当于买⼊股票的价格升⾼了。 在第⼀个式⼦⾥减也是⼀样的,相当于卖出股票的价格减⼩了。
int maxProfit_with_fee(int[] prices, int fee) {
int n = prices.length;
int dp_i_0 = 0,
dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee);
}
return dp_i_0; }

k = 2

k = 2 和前⾯题⽬的情况稍微不同,因为上⾯的情况都和 k 的关系不太⼤。 要么 k 是正⽆穷,状态转移和 k 没关系了;要么 k = 1,跟 k = 0 这个 base case 挨得近,最后也没有存在感。 这道题 k = 2 和后⾯要讲的 k 是任意正整数的情况中,对 k 的处理就凸显出 来了。

这道题由于没有消掉 k 的影响,所以必须要对 k 进⾏穷举:

1
2
3
4
5
6
7
8
9
10
11
12
int max_k = 2; 
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++) {
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) {
/*处理 base case */
}
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
}// 穷举了 n × max_k × 2 个状态,正确。
return dp[n - 1][max_k][0];

k = any integer

有了上⼀题 k = 2 的铺垫,这题应该和上⼀题的第⼀个解法没啥区别。但是 出现了⼀个超内存的错误,原来是传⼊的 k 值会⾮常⼤,dp 数组太⼤了。 现在想想,交易次数 k 最多有多⼤呢? ⼀次交易由买⼊和卖出构成,⾄少需要两天。所以说有效的限制 k 应该不超 过 n/2,如果超过,就没有约束作⽤了,相当于 k = +infinity。这种情况是之 前解决过的。 直接把之前的代码重⽤:

1
2
3
4
5
6
7
8
9
10
11
12
int maxProfit_k_any(int max_k, int[] prices) { 
int n = prices.length;
if (max_k > n / 2)
return maxProfit_k_inf(prices);
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++)
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) {
/* 处理 base case */
}
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i0]);
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices;

动态规划与回溯对比

题目:给定一个非负整数组,a1,a2,…an和一个目标数,S。现在你有两个符号+和-;对于数组中的任意一个整数,你都可以添加一个符号到前面,返回可以使最终数组和为目标数S的所有添加符号的方法数。

回溯思路

回溯算法就是一个暴力穷举算法;关键在于1、路径(即已经做出的选择),2、选择列表(当前可以做的选择),3、结束条件(到达决策树底层,无法再做选择的条件)。

1、路径:之前已经给前面的数字写出的正负号;2、选择列表:即每个数字可以选择给出正号或者负号;3、结束条件:即已经走到了最后一个数字;

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
let result = 0;
//主函数
const findTargetSumWays = (nums, target) => {
if (nums.length == 0) return 0;
backtrack(nums, 0, target);
return result

const backtrack = (nums, i, rest){
//base case
if (i == nums.length){
if (rest == 0){
//说明恰好凑出target
result++;
}
return;
}
//给nums[i]选择-号
rest += nums[i];
//递归来穷举nums[i+1]的可能性
backtrack(nums, i + 1, rest);
//撤销选择
rest -= nums[i];

//给nums[i]选择+号
rest -= nums[i];
//穷举nums[i+1]
backtrack(nums, i + 1, rest);
//撤销选择
rest += nums[i];
}

}

时间复杂度为O(2^N),N为nums的大小,其实这个回溯算法就是个二叉树的遍历问题,树的高度就是nums的长度,故时间复杂度就是这棵二叉树的节点数,其实比较低效。

消除重叠子问题

动态规划快的原因就是消除了重叠子问题,关键在于看是否可能出现重复的状态;对于递归函数来说,函数参数中会变得参数就是状态,对于backtrack函数即i和rest。那么怎么一眼看出存在重叠子问题?

1、先抽象出算法得递归框架;

1
2
3
4
def dp(i, j):
dp(i - 1, j - 1)#1
dp(i, j - 1)#2
dp(i - 1, j)#4

2、对于子问题dp(i-1, j-1),如何通过原问题dp(i, j)来得到呢?有多种不同路径:比如dp(i,j)=>#1,和dp(i, j)=>#2 =>#3。一旦发现一条重复路径就说明存在巨量重复路径,也就是重叠子问题。

对于上题:

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
void backtrack(int i, int rest){
backtrack(i + 1,rest - nums[i]);
backtrack(i + 1, rest + nums[i]);
}
//当nums[i]=0时,出现两种状态完全相同得递归函数,这就是重叠子问题
//因此状态(i, rest)可以用备忘录来优化


const fingTargetSumWays = (nums, target) =>{
if (nums.length == 0) return 0;
return dp(nums, 0, target);
}

//备忘录
const memo = new Map();
const dp = (nums, i, rest) =>{
//base case
if (i == nums.length) {
if (rest == 0) return 1;
return 0;
}
//避免重复计算,转换成字符串从而作为Map的键来存储
let key = i.toString() + "," + rest.toString;
if (memo.has(key)) {
return memo.get(key);
}
//还是穷举
let result = dp(nums, i + 1, rest - nums[i]) + dp(nums, i + 1, rest + nums[i]);
//这两种情况,"+"或者"-"号
memo.set(key, result);
}

动态规划

上述的消除重叠子问题的方案,在最坏情况下的时间复杂度仍为O(2^N),因为底层逻辑没变,暴力穷举,加上顺手进行了剪枝,提升了算法在某些情况的效率,但不算质的飞跃。其实,这个问题可以转化成一个子集划分的问题,而子集划分问题又是一个典型的背包问题。

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
//我们把nums划分为两个子集A、B,分别代表分配+的数和分配-的数,则它们跟target存在如下关系:
sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)
//因此可以把问题转化为:nums中存在几个子集A,使得A中元素的和为(target + sum(nums)) / 2。
//这样就变成了经典的背包问题
//dp[i][j]代表前i个数构成结果为j的方法个数

const fingTargetSumWays = (nums, target) => {
let sum = 0;
for (let i = 0; i < nums.length; i++){
sum += nums[i];
}
let len = nums.length;
sum = (sum + target) / 2;
let dp = new Array();
//根据初始条件来初始化
for (i = 0; i <= len + 1; i++) {
dp[i] = new Array();
for (let j = 0; j <= sum + 1; j++){
dp[i][j] = 0;
}
}
for (j = 0; j <= n; i++){
dp[0][j] = 0;
}
//没有物品则没办法装背包,则为0
for (i = 0; i <= n; i++){
dp[i][0] = 1;
}
//背包的最大载重为0,则只有一种方法,即什么都不装

for (i = 1; i <= nums.length; i++){
for (j = 1; j <= sum; j++){
if (j - nums[i - 1] < 0){
//背包不足,不能装入第i个物品
dp[i][j] = dp[i - 1] [j];
} else {
//装入或不装入的选择
dp[i] [j] = dp[i - 1] [j] + dp[i - 1] [j - nums[i - 1]];
}
}
}
return dp[nums.length][sum];
}

总结一下:回溯算法虽好,但是复杂度很高,即使消除一些冗杂计算也只是剪枝,没有本质的改变;而动态规划则比较玄学,经过各种改造后,从一个加减法问题变成了子集问题,又变成背包问题,经过各种套路写出解法,还得使用状态压缩,加上反向遍历。

回溯算法框架

解决一个回溯问题,其实即使一个决策树的遍历过程,只需要思考3个问题,1、路径:就是已经做出的选择;2、选择列表:就是你当前可以做的选择;3、结束条件:到达决策树底层,无法再做选择的条件。

1
2
3
4
5
6
7
8
9
10
result = []
def backtrack(路径,选择列表)
if 满足结束条件:
result.add(路径)
return

for 选择 in 选择列表:
做选择
backtrack(路径,选择列表)
撤销选择

核心其实就是for循环里面的递归,在递归调用前做选择,递归调用之后撤销选择。

回溯之全排列问题

穷举n个不重复数的全排列,求有多少个。

则可以画出该算法的决策树如下,每个节点都在做决策。[2]就是路径,记录你已经做过的选择;[1,3]是选择列表,表示当前可以做出的选择;[结束条件]就是遍历到树的底层,即选择列表为空的时候。

我们定义的backtrack函数其实就像一个指针,在这棵树上游走同时正确维护每个节点的属性,每当走到树的底层,其路径就是一个全排列。

全排列

树的遍历问题:前序遍历、后序遍历。前序遍历代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行;函数在树上游走,正确维护其属性就是要在这两个特殊时间点搞动作。我们只要在递归之前做出选择,递归之后撤销选择,就能得到每个节点的选择列表、路径。

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
List<List<Integer>> res = new LinkedList<>(); 
/* 主函数,输⼊⼀组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
// 触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));
return;
}
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (track.contains(nums[i]))
continue;
// 做选择
track.add(nums[i]);
// 进⼊下⼀层决策树
backtrack(nums, track);
// 取消选择
track.removeLast();
}
}

我们这⾥稍微做了些变通,没有显式记录「选择列表」,⽽是通过 nums 和 track 推导出当前的选择列表:因为穷举整棵决策树是⽆法避免的。这也是回溯算法的⼀ 个特点,不像动态规划存在重叠⼦问题可以优化,回溯算法就是纯暴⼒穷*举,复杂度⼀般都很⾼。

回溯之N皇后问题

给你⼀个 N×N 的棋盘,让你放置 N 个 皇后,使得它们不能互相攻击。 这个问题本质上跟全排列问题差不多,决策树的每⼀层表⽰棋盘上的每⼀ ⾏;每个节点可以做出的选择是,在该⾏的任意⼀列放置⼀个皇后。

则可以直接套用框架:

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
vector<vector<string>> res; 
/* 输⼊棋盘边⻓ n,返回所有合法的放置 */
vector<vector<string>> solveNQueens(int n) {
// '.' 表⽰空,'Q' 表⽰皇后,初始化空棋盘。
vector<string> board(n, string(n, '.'));
backtrack(board, 0);
return res;
}

// 路径:board 中⼩于 row 的那些⾏都已经成功放置了皇后
// 选择列表:第 row ⾏的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后⼀⾏
void backtrack(vector<string>& board, int row) {
// 触发结束条件
if (row == board.size()) {
res.push_back(board);
return;
}
int n = board[row].size();
for (int col = 0; col < n; col++) {
// 排除不合法选择
if (!isValid(board, row, col))
continue;
// 做选择
board[row][col] = 'Q';
// 进⼊下⼀⾏决策
backtrack(board, row + 1);
// 撤销选择
board[row][col] = '.';
}
}
//这部分的主要代码其实跟全排列差不多,都是遍历的思想,先检查是否合理,再进行决策并递归进下一层,下一层递归遍历完之后再撤销选择。
/* 是否可以在 board[row][col] 放置皇后? */
bool isValid(vector<string>& board, int row, int col) {
int n = board.size();
// 检查列是否有皇后互相冲突
for (int i = 0; i < n; i++) {
if (board[i][col] == 'Q')
return false;
}
// 检查右上⽅是否有皇后互相冲突
for (int i = row - 1, j = col + 1;
i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q')
return false;
}
// 检查左上⽅是否有皇后互相冲突
for (int i = row - 1, j = col - 1;
i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q')
return false;
}
return true;
}

函数 backtrack 依然像个在决策树上游⾛的指针,通过 row 和 col 就可 以表⽰函数遍历到的位置,通过 isValid 函数可以将不符合条件的情况剪枝:

这个问题的复杂度确实⾮常⾼,看看我们的决策树,虽 然有 isValid 函数剪枝,但是最坏时间复杂度仍然是 O(N^(N+1)),⽽且⽆ 法优化。如果 N = 10 的时候,计算就已经很耗时了。 有的时候,我们并不想得到所有合法的答案,只想要⼀个答案,怎么办呢其实特别简单,只要稍微修改⼀下回溯算法的代码即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 函数找到⼀个答案后就返回 
true bool backtrack(vector<string>& board, int row) {
// 触发结束条件
if (row == board.size()) {
res.push_back(board);
return true;
}
...
for (int col = 0; col < n; col++) {
...
board[row][col] = 'Q';
if (backtrack(board, row + 1))
return true;
board[row][col] = '.';
}
return false;
}

回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置 做⼀些操作,算法框架如下: backtrack 函数时,需要维护⾛过的「路径」和当前可以做的「选择列 表」,当触发「结束条件」时,将「路径」记⼊结果集

动态规划的三个需要明确的点就是「状态」「选择」和 「base case」,是不是就对应着⾛过的「路径」,当前的「选择列表」和 「结束条件」某种程度上说,动态规划的暴⼒求解阶段就是回溯算法。只是有的问题具有 重叠⼦问题性质,可以⽤ dp table 或者备忘录优化,将递归树⼤幅剪枝,这 就变成了动态规划。⽽今天的两个问题,都没有重叠⼦问题,也就是回溯算 法问题了,复杂度⾮常⾼是不可避免的。

DFS与BFS详解

DFS的简要说明

(1):深度优先搜索(Depth-First-Search)是搜索算法的一种。是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。

(2):DFS是图论里面的一种搜索算法,他可以由一个根节点出发,遍历所有的子节点,进而把图中所有的可以构成树的集合都搜索一遍,达到全局搜索的目的。所以很多问题都可以用dfs来遍历每一种情况,从而得出最优解,但由于时间复杂度太高,我们也叫做暴力搜索

(3):DFS如同数据结构中的栈结构,是属于一种后进先出的结构,比如说一个羽毛球筒,把它装满之后,我们开始使用时,拿的总是最后放进去的那一个。所以这就导致了所有的点进入栈时有一个顺序,我们称之为 :DFS序

(4):根据dfs的英文全写,我们可以知道这是一种深度优先搜索的搜索算法,什么叫做深度优先?意思就是它搜索一颗子树时,它要遍历到底才会 “回头” ,比如说:上有向图中的 搜索模式 为(以DFS序来描述):a->b->e->b->f->c->f->b->c->b->a->c->a->d->g->d->a,有人就会问为什么搜索到c的时候不搜索a呢?因为我们假设的这是一个有向图。而且我们可以看到如果 你面对的图是一个 无向图,这个时候这个树将会持续搜索因为可能会形成环路,使得栈的结构一直进进出出,导致程序崩溃,所以我们也因该注意,在写DFS时,如果面对的是一个无向图的话我们需要进行标记。一般的标记方法有:①这个点的父节点被标记,使得子节点不能回到父节点造成循环②访问过的节点被标记。这两种方法视情况而定。

(5):对于暴搜来说,因其复杂度太高,我们就会想去优化它的复杂度,这一过程称为剪枝,剪纸分为可行性剪枝和最优化剪枝,关于剪枝引申出一种名为记忆化搜索的方法,该方法与动态规划类似。

BFS简要说明

(1):与DFS相对的那就是BFS了,BFS称为宽度优先搜索也叫做广度优先搜索,他是按层遍历每一种状态的下一种状态。

搞不懂?没关系我们来看一下一个例题:给你一个整数X,问是否存在一个这样一个数字A:①这个数字是X的倍数②这个数字仅由0与1这两种数字组成 例如 X=2,A=10。这个题当然可以用 DFS来做,我们以1为起点 这样就成了一个 0 1 二叉树,意思就是说每一种 情况 我们都可以 有两种选择 要么 添0,要么在后面 添1。所以我们可以写出代码:

1
2
3
4
5
6
7
8
9
10
void dfs(int start)
{
if(A%X==0)//这是我们搜索结束的条件
{
ans=A;//让ans记录答案开始return
return ans;
}
dfs(start*10);
dfs(start*10+1);
}

所以我们开始试例子:输入 2按照dfs序的话,首先不符合条件,然后进入下一步搜索,X*10=10,发现10%2==0,然后开始返回ans记录A,这就是最后答案,但是你会发现程序崩溃了。
我们分析一下原因:DFS为深度优先搜索,所以我们的左子树可以看作是在末尾加0,然后右子树可以看作在末尾加1,所以这样就形成了一棵树。按照DFS我们的意图是让他搜索左子树,所以在左子树中找到了 满足条件的 数 :10。但是为什么程序会崩溃呢?

(2)因为搜索到树之后,按照我们刚刚的DFS序,这个树遍开始回溯(你可以想象成这棵树开始回缩),每回溯到一个节点,因为这个节点的 右子树还没有搜索,所以会再去搜索这个节点的右子树,但是这个节点的右子树是在末尾进行加1,而末尾是1绝对不可能是 2 的倍数 所以这个树会一直按照 往右子树 搜索的趋势 进行下去,导致程序崩溃。

1
2
3
4
5
6
7
8
9
10
11
//那是不是这个问题只能枚举了呢?其实DFS是可以的,在网络大神的果断判断之下这个问题出了一个新解:当搜索的深度为19时,此时的答案一定会出现,这时候回溯就好了。所以说我们加一个step记录深度就可以了:
void dfs(int start,int step)
{
if(A%X==0&&step==19)//这是我们搜索结束的条件
{
ans=A;//让ans记录答案开始return
return ans;
}
dfs(start*10,step+1);
dfs(start*10+1,step+1);
}

(3):还有没有更好的方法呢?在考虑这个问题的时候我们回顾一下刚刚为什么程序崩溃,原因就是因为DFS是一个深度优先搜索,他每次必须要 深搜到底 ,所以对于末尾是1来说永远没有结束条件所以它一直会搜索下去!这个时候我们可不可以想有没有这样一种搜索模式,出现A=10这个情况之后我们立刻停止就可以了呢?当然是有的,那就是BFS。

(4):弄完上面这个例题,也就真正的引出了BFS也发现了DFS的一些小缺点。下面说一下上面留下的定义:按层遍历每一种的状态的下一种状态。首先BFS是与数据结构中的 队列 相似,队列是一种 先进先出的结构,这又比如说 你把一些水果按时间一天一个放进一个箱子里,但这些水果时间长了会变质,我们假设变质日期是一样的,那么你每次拿都是拿的最早放进去的那个。

所以说对于刚刚那个题目而言,我们如果按层遍历就不会出现程序崩溃的现象,比如说 第二层有10 11,那我们就不必搜索下一层了

DFS应用

(1).DFS由于有回溯的步骤,所以我们可以对其进行更新,比如并查集合并时的状态压缩都是使用DFS,还有图论中的 tarjan算法,lca等等。

(2):搜索 有状态约束的 问题,比如说:N皇后问题;

BFS应用

(1):求最短路,这个是图论里面的一种应用被称为SPFA算法,在我之前的博客中有总结过,所以在这里不再提。

(2):求迷宫最短路,这种题目用DFS也是可以解的,用 DFS的思路:有许多条路可以到达终点我们求一下 到达终点的最小值。这样是非常耗时间的 而BFS不一样因为BFS是按层遍历,所以 每一层对于根节点来说距离一定是最小的,不需要再次更新,我们到达终点时这个距离一定一定是最小的,比如说:

(3):同时移动性题目问题,火山在喷发,你在跑问是否可以跑出去。详情之前解释过这个问题:

(4):倒水问题,每次倒水状态之间的转移,之前也有过例题:&&POJ341pots

(5):路径还原,问从一个点到另一个点 的最短路径是多少,而题目不要求你输入最短距离,而是要求你还原路径这种时候通常需要加一个结构体数组来记录,其实上面的例题pots也是一个路径还原的问题,用一个简单的例题总结一下路径还原:

leetcode题目

167.两数之和

给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。

函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
vector<int> v;
for (int i = 0; i < numbers.size(); i++){
int t = target - numbers[i], l = i + 1, r = numbers.size() - 1;
while (l <= r){
int mid = (l + r) / 2;
if (numbers[mid] == t){
v.push_back(i + 1);
v.push_back(mid + 1);
return v;
}
if (numbers[mid] < t){
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return v;
}
};

7、整数反转

给出一个32位的整数,需要将这个整数中每位上的数字进行反转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int reverse(int x) {
long long ans = 0;
while (x) {
ans = ans * 10 + x % 10;
x /= 10;
}
if (ans < INT_MIN || ans > INT_MAX){
return 0;
}
return (int)ans;
}
};
//小的类型会越界,则添加一个大的类型将其存储,并在后面添加判断语句。

9、判断回文数

判断一个整数是否是回文数;即从右往左和从左往右一样;其实就是将该数反转调过来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
bool isPalindrome(int x) {
long long y = 0, z = x;
if (z < 0){
return false;
}
while (z){
y = y * 10 + z % 10;
z /= 10;
}
return y == x;
}
};

13、罗马数字转整数

14、最长公共前缀

编写一个函数来查找字符串数组中最长公共前缀,不存在则返回空字符串。关键在于用两个for循环,一个用于遍历字符串数组,另一个用于遍历每个字符串中字符。

相当于就是两个for循环来分别遍历字符串和字符串内字符,然后break用于提前退出来节省时间。

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 Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if (strs.size() == 0){
return "";
}
string ans = strs[0];
for (int i = 1; i < strs.size(); i++){
string t = ans;
ans = "";
for (int j = 0; j < t.size() && j < strs[i].size(); j++){
if (t[j] == strs[i][j]){
ans += t[j];
}else{
break;
}
}
if (ans == ""){
break;
}
}
return ans;
}
};

20、有效的括号

完全包含问题则适用于栈来进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
bool isValid(string s) {
stack <char> sta;
for (int i = 0; i < s.size(); i++){
if (s[i] == '(' || s[i] == '[' || s[i] == '{'){
sta.push(s[i]);
}
else{
if (sta.empty() || (s[i] == ')' && sta.top() != '(') ||
(s[i] == ']' && sta.top() != '[') ||
(s[i] == '}' && sta.top() != '{') ){
return false;
}
sta.pop();
}
}
return sta.empty();
}
};

21、合并两个有序链表

将两个升序链表合并成一个新的升序链表并返回,新链表由拼接给定的两个链表的所有节点组成;关键在于链表的操作以及判断边界条件。

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
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var mergeTwoLists = function(l1, l2) {
var ans = new ListNode();
var root = ans;
while (l1 != null || l2 != null){
if (l1=== null){
root.next = l2;
break;
}
if (l2 === null){
root.next = l1;
break
}
if (l1.val < l2.val){
root.next = l1;
l1 = l1.next;
}
else{
root.next = l2;
l2 = l2.next;
}
root = root.next;
}
return ans.next;
};

26、删除排序数组重复项

双指针法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @param {number[]} nums
* @return {number}
*/
var removeDuplicates = function(nums) {
let left = 0, right = 1;
while(right < nums.length){
if(nums[left] !== nums[right]){
nums[++left] = nums[right];
}
right++;
}
return left + 1;
};

27、移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素

1
2
3
4
5
6
7
8
9
var removeElement = function(nums, val) {
let ind = 0;
for (let i = 0; i < nums.length; i++){
if (nums[i] != val){
nums[ind++] = nums[i];
}
}
return ind;
};

CSS样式设计

flex布局语法

传统的解决方案,基于盒装模型,依赖display 属性 + position属性 + float属性。对于特殊布局非常不方便(比如垂直居中)。flex布局可以简便,完整,响应式地实现各种页面布局,目前已得到所有浏览器的支持。

flex布局概念

flex 是 Flexible Box 的缩写,意为”弹性布局”,用来为盒装模型提供最大的灵活性。设为 flex 布局以后,子元素的float,clear和vertical-align属性将失效。采用flex布局的元素,称为flex容器,它的所有子元素自动成为容器成员,称为flex项目

我们知道,轴包括主轴和交叉轴默认情况下,主轴的方向是从左向右的,交叉轴垂直于主轴,逆时针方向90度;交叉轴是由主轴决定的,主轴又是由flex-direction决定的;flex-direction属性设置在父容器上,这样子才可以生效。

1
2
3
4
5
<div class="wrapper">
<div class="flex1">子盒子#flex1: 1 </div>
<div class="flex2">子盒子#flex2: 1 </div>
</div>
flex-direction: row | row-reverse | column | column-reverse

当你给父盒子(wrapper)设置属性 flex-direction: row时,flex容器的主轴被定义为与文本方向相同,主轴起点和主轴终点与内容方向相同,简单来说就是主轴沿水平方向向右;

当你给父盒子(wrapper)设置属性 flex-direction: row-reverse时,主轴方向与文本方向相反,因此沿着水平方向向左。

当你给父盒子(wrapper)设置属性 flex-direction: column时,可以看到子盒的布局发生了变化,形成了在Y轴上的布局方式,且书写方式与布局一样。flex容器的主轴和块轴相同,主轴起点与主轴终点和书写模式的前后点相同;主轴变为Y轴方向。

当你给父盒子(wrapper)设置属性 flex-direction: column-reverse;主轴与Y轴方向的书写方向相反。

容器

父容器

父容器包括justify-content:设置子元素在主轴方向上的对齐方式。

justify-content:flex-start;子元素沿着主轴方向开始对齐;

justify-content:flex-end;子元素沿着主轴方向终点对齐

justify-content:center;子元素在主轴方向水平居中;

justify-content:space-between;子元素在主轴方向上两端对齐,且项目之间间隔相等;

justify-content:space-around;子元素在主轴方向上均匀排列每个元素,每个元素周围分配相同的空间。

align-items:设置子元素在交叉轴方向上的对齐方式。

align-items: flex-start;子元素在交叉轴方向上起点对齐;

align-items: flex-end;子元素在交叉轴方向上终点对齐;

align-items: center;子元素在交叉轴方向上居中对齐;

align-items: baseline;子元素在交叉轴方向上以文字基线对齐;

align-items: stretch;默认属性,将占满整个容器的高度。

  • flex-wrap 设置换行方式
    • 绝对子容器是否可以选择换行,一般而言有三种状态,支持换行的话,也支持逆序换行。
  • flex-flow 设置轴向与换行组合
    • 是 flex-direction 和 flex-wrap 的简写。
    • 所以只要掌握,flex-directionflex-wrap即可。
  • align-content 多行沿交叉轴对齐方式
    • 当子容器多行排列时,设置行与行之间的对齐方式。
子容器

flex属性定义在主轴是如何伸缩的;1、子容器是有弹性的,它们会自动填充剩余空间,子容器的伸缩比由flex属性决定;2、flex是多个属性的缩写,允许1-3个值的连写。

  • flex-grow 设置扩展比例
  • flex-shrink 设置收缩比例
  • flex-basis 设置基准大小
  • order 设置排列顺序

align-self属性 单独设置子容器如何沿交叉轴排列;1、每个子容器都可以单独定义沿交叉轴排列方式。2、该属性的取值跟父容器中的align-items属性一致,如果两者相同的话,则以子容器align-self属性为主。

align-self : flex-start;起始端对齐;align-self : flex-end;末尾段对齐;align-self : baseline;基线对齐(第一行文字的基线对齐);align-self : stretch:拉伸对齐。

SCSS预处理器

出现原因:1、CSS无法嵌套书写导致代码繁重、冗杂、逻辑混乱;2、没有变量和样式复用机制,属性值只能以字面量的形式重复输出。

sass支持标准的CSS多行注释以及单行注释,前者会被完整输出到编译后的CSS文件中,而后者不会。

变量与数据类型

变量以美元符号开头,赋值方法与CSS属性的写法一样;

在CSS的样式中,直接使用变量的名称即可调用变量。

作用域:变量支持块级作用域,嵌套规则内定义的变量只能在嵌套规则内使用,不在嵌套规则内定义的变量则可在任何地方使用(全局变量);将局部变量转换为全局变量可以添加!global声明。

字符串:有引号字符串和无引号字符串。数字:带单位数字与不带单位数字;单位会和数字当做一个整体,进行算数运算。空值:只有一个值null。布尔值。数组。映射。元素。

嵌套

在创建的样式中经常会有重复的标签样式出现,scss提供了更简单的方法来实现选择器。

使用嵌套时调用父选择器:进行伪类样式的处理,一般会看作被嵌套选择器是父选择器的后代,之后再用hover实现伪类。但有时,我们不想使用该关系时,想直接使用父选择器再加上&,这样后续使用& &-text 等价于.nav .nav-text会自动使用父选择器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.nav {
height: 100px;
ul {
margin: 0;
li {
float: left;
list-style: none;
padding: 5px
}
a {
display: block;
color: #000;
padding: 5px;
&:hover {
background
}
}
}
}

同样,嵌套除了可以用在样式的规则中,从而减少样式书写中重复的部分,除此之外,样式也可以用于属性中;使用属性的嵌套可以更加简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
body {
font: {
family: Helvetica, Arial, sans-serif;
size: 15px;
weight: normal;
}
}

.nav {
border: 1px solid #000{
left: 0;
right: 0;
}
}

mxin混合

mixin有点像是JS中的函数mixin;在SCSS中使用mixin用的是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@mixin 名字(参数1, 参数2...){
...
}
@mixin alert($text-color, $background) {
color: $text-color;
back-ground-color: $background;
a {
color: darken($text-color, 10%);
}
}

.alert-warning {
@include alert(#8a6d3b, #fcf8e3);
//@include来使用alert定义的样式
}

@extend继承扩展

用一个选择器去继承另一个选择器中定义的所有样式,避免重复的样式编写。这种继承同时会继承alert里面与其相关的内部嵌套选择器的样式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@import "base";//输入时则不用加上前面的下划线

.alert {
padding: 15px;
}

.alert a {
font-weight: bold;
}

.alert-info {
@extend .alert;
background-color:
}

@import

便于将一个项目需要的样式分割成不同的小的部分Partials,项目名以下划线开头来提醒其为小部分,浏览器不会去单独编译其为CSS。

数值与字符串

SCSS中数值均会包含单位;且提供了一些数字函数来更方便地处理数字。

abs():返回输入参数的绝对值;round(3.5):四舍五入函数;ceil(3.2):进位函数;floor(3.6):退位函数;percentage(650px / 1000px):变为百分数;min、max函数

关键字、带引号字符串、不带引号字符串;

+可以将两个字符串连接到一块,返回的结果会自带引号;字符串+数字=》字符串;

字符串函数:to-upper-case:变大写;to-lower=-case:变小写;str-length:返回长度

颜色

rgb函数:rgb(255, 100, 0)橙色;rgba函数:可以使颜色拥有透明度的设置,0~1,(255, 255, 0, 0.8);

HSL函数(色相、饱和度、明度):hsla(60, 100%, 50%, 0.5);

adjust-hue函数,调整色相的值,adjust-hue($base-color-hsl, 137deg);

lighten和darken函数:改变函数的明暗程度,lighten($base-color, 30%);

saturate、desaturate:增加或减少颜色的纯度,即饱和度,saturate($base-color, 50%);

transparentize、opacify:增加与

TypeScript与JS关系

JS是一个动态类型语言,即类型不固定,写起来灵活、自由但后期不太好维护,且出错之后不好去管理与排查。TS便是用来解决JS这方面的问题,在JS基础上加了一些类型特性,类型约束+自动补全+智能提示。

类型系统带来什么:1、接口;2、重载;3、泛型。

TS是为了解决JS在编码过程中,在类型的使用错误问题;解决的方法有很多。关注点不在于增加什么新功能,而是仅仅扩大JS的类型检测、类型设定。

index.d.ts:类型声明文件,

三大组成部分:ECMAScript;DOM(文档对象模型):对文档的操作;BOM(浏览器对象模型):主要包括对浏览器的相关操作;

let和const

let、const和var的区别

var的特性

1、var可以重复声明;

2、var作用域:全局作用域和函数作用域;

3、会进行预解析,但可能造成代码混乱;

let的特性

1、let在同一作用域下不能进行重复声明;

2、let作用域:全局作用域和块级作用域;if、while、for等有{}的语句,let会考虑在花括号之间的为块级作用域,仅在代码块内部起作用;(与C++类似)

3、不进行预解析;JS有预解析机制,在下面声明而在上面去调用并不会报错,然而使用let声明时不会进行预解析;

const的特性

1、const为常量,声明时必须赋值,而之后并不能对其进行修改;

2、其他情况与let一样,不能重复声明、不能预解析、为块级作用域。

块级作用域

之前在写代码时,为了避免与全局冲突,一般会先声明一个匿名函数,并马上对其调用;

1
2
3
(function(){

})()

有了块级作用域之后,可以直接用{}来生成代码块,便能直接在代码块里添加代码;

如果在循环之中添加了一个块级作用域,将要如何去处理,

1
2
3
4
5
6
7
8
9
{
let lis = document.querySelectorAll("li");
for (let i = 0; i < lis.length; i++){
lis[i].onclick = function(){
console.log(i);
}
}
}
//这么写相当于生成了lis.length个代码块,遍历i时每个i的值均声成代码块,循环几次便开辟几个块级作用域;

解构赋值

对象的解构赋值

对象可以有若干属性,且每个属性方法衷都可以存储数据;当你希望用其他的变量将原变量的属性存储起来,原本的JS写法如下:

1
2
3
4
5
6
let obj = {
a: 1,
b: 2
};
let a = obj.a;
let b = obj.b;

在ES6的标准下,可以把声明改成这样:

1
2
3
4
5
6
let obj = {
a: 1,
b: 2
};
let {a, b, c} = obj;
//语法中对象的名字必须与obj里面的属性名称一样,这样才能进行对象解构赋值,故c为undefined

数组的解构赋值

对象的解构赋值要求变量名与属性名一致,但数组的话,仅要求顺序必须一致;

1
2
3
4
5
6
7
8
let arr = ["a", "b", "c"];
let [e, f] = arr;

//面试题:如何快速交换a, b值
let a = 0;
let b = 1;
[a, b] = [b, a];
//使用数组解构赋值便能快速交换;

字符串的解构赋值

1
2
3
let str = "abc";
let [e, f] = str
//其实跟数组一样,根据顺序的索引来解构赋值

展开运算符

数组展开

1
2
3
4
5
let arr = [1, 2, 3, 4];
let arr2 = ["a", "b",...arr,"c", "d"];
//如何简单处理,将两个数组加一起
//剩余参数
let [a, b,...c] = arr;

对象展开

1
2
3
4
5
6
7
8
9
10
11
let obj = {
a:1,
b:2
};
let obj2 = {
...obj,
c:3,
d:4
};
//不建议对象直接赋给另一个对象,不然一个改变另一个也会改变,因为其本身传递的是一个地址,那么如何来处理呢,用obj解构来代替obj,其本质是所有内容而不是对象的地址
let obj2 = {...obj};

set对象

set本身是一个函数,用于构建对象,还有Data、Array,调用后返回构造对象,统称构造函数,用于构造某一类型的对象,即对象的实例化。

1
2
3
4
5
6
7
8
9
let arr = [1, 2, 3, 4, 5]
let s = new Set(arr);
arr = [...s]
//arr返回之后便是去重之后的结果
s.size//size属性,储存保留之后值得个数
s.clear();//清空
s.delete("a");//删除某项值
s.add(6);//添加某项值
s.has();//查看是否包含某个值

Map对象

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = [
["a",1],
["b",2],
["c",3]
];
let s = new Map(arr);
//map结构会存成键值对得形式key-value
clear();//清空所有值
delete();//删除某一项
//参数:key,数据的key值;返回值,true、false是否删除成功
get();//获取某一项值
has();//是否包含某一项
set(key, val);//设置一个值

函数新增内容

箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//function(){return ;}
let fn = ()=>{
console.log(1);
};
fn();
//箭头函数,=>
//形参=>返回值
let fn = nub => nub *2;
//多个形参时,则加括号(形参,形参)=>返回值
//()=>返回值,没有参数时也要加上括号
// ()=>{
// 执行语句
// return 返回值
// }
//箭头函数没有arguments,即不能使用不定参,但可以使用扩展运算符加剩余参数来进行实现不定参数的功能,
let fn = (a,b...arg)=>
//rest参数存在数组中,即拿到该数组便能拿到剩余参数,其实跟前面的展开运算符一致

1
2
3
4
5
6
document,onclick = function(){
let fn = ()=>{
console.log(this);
};
fn();
}

使用正常函数时,this指针一般指向windows,但换成箭头函数时,this指针指向document;因为箭头函数本身没有this,调用箭头函数的this时,指向的是其声明时所在的作用域的this;

参数默认值问题:ES6中有简便方法设置参数默认值

1
2
3
let fn = (a = 2,b = 10)=>{

}

数组新增方法

日期对象

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
new Date().getTime();//方法属于Date返回的对象的方法,需要对对象进行调用
Date.now();//直接使用Date的方法;
new Array()
Array.from();
arr.forEach();//需要区分一下哪些方法是构造函数的本身方法,而哪些方法是需要利用对象来调用的方法

//新增方法如下
Array.from()//把一个类数组转换成真正的数组,类数组:有下标有length;返回值:转换之后的新数组;是Array构造函数的方法
{
let lis = document.querySelectorAll("#list li");
//map是数组方法,因此需要先将lis从类数组变成数组
let arr=[];
lis = Array.from(lis);
lis.map(item=>{
return item;
});
lis = Array.from(lis,(item,index)=>{
console.log(item, index);
return index;
});
}
//利用from组成新数组
//of方法将放入的元素组成新数组后返回
{
Array.of(1,2,3,4,"5");
}
//isArray方法:判断接收的数据是否是数组,类数组不是数组

数组的find、findIndex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
//find查找数组中满足要求的第一个元素的值
let arr = ["a", "b", "c", "d"];
arr.indexOf("a");
let arr = [1,2,3,4];
let val = arr.find((item,index)=>{
if(item > 3){
return true;
}
});
val = arr.find(item=> item >= 3);
console.log(val);
//fingIndex返回数组中满足要求的第一个元素的索引
}

数组扁平化处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let arr = {
["小明""18"],
["小刚""18"],
}
arr.flat(n);
//flat(n)将多维数组转换成低维的数组,向下提取n层;后续传递的参数决定了提取几层
//当嵌套多层时,使用
flat(Infinity);//提取无限层,确保变为一维数组
//利用flatMap来扁平化处理后得到每一项
let newArr = arr.flatMap((item,index)=>{
cosole.log(item,index);
item = item.filter((item,index)=>{
return index == 0;
});
return item;
});
console.log(newArr);

flatMap参数:callback回调函数,可以生成一个新数组中元素的参数;可选参数:thisArg,执行callback函数时,使用的this值;返回值:一个包含将数组和子数组中所有元素的数组。

flatMap:只能处理一层的数组,要处理多层的话需要进行if判断是否里面还存在数组,并进行递归操作进行多次扁平化。

1
2
3
4
5
//fill语句,将数组用...数据填充满,从第几位开始填充,且本身是不修改原数组长度的
//includes判断数组中是否包含一个指定的值
let arr = ["a","b","c","d","e"];
arr.includes("c",2);
//后面参数代表从第几位开始检索

字符串新增方法

1
2
3
4
5
6
7
8
9
10
11
12
13
let str = "开课吧和面为课堂"
str.startsWith("开课吧");//返回一个bool值
str.endsWith("面为课堂");
str.repeat(30);
//模板字符串,如何快速地构造出想要的字符串
//${}插值表达式,可以用值,也可以用函数,适合该值有复杂的逻辑处理
let p = document.querySelector("p");
let name = "小明";
let age = 18;
let school = "大学";
p.innerHTML = `今年<strong>${name}</strong>就要<strong>${age}</strong>岁了,终于升入<strong>${school}</strong>了`
//先用``反引号将整个值包起来,在用插值表达式来一个个插入值
//模板字符串可以换行

对象新增方法

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
let a = 0;
let b = 1;
let name = "小明";
let obj = {
a,
b,
c(){
console.log("a");
},
[name]: 111;
};
//可以直接在obj里面起属性名表达式,可通过变量来给属性名赋值。
//对象合并,其实可以使用之前的对象展开来进行合并
let obj = {
a: 1,
b :2
};
let obj2 = {
c : 3,
d :4
};
Object.assign(obj2 ,obj);//第一个参数为合并传入的对象

//is方法 obj函数下的方法,接受两个值并判断是否一样

Babel

babel是一个JavaScript编译器,用于语法编译,把JS本身不识别、不兼容的语法糖编译成兼容的,babel最简单的使用:将babel引入到页面当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let a = 0;
let b = 1;
let obj = {
a,
b,
c(){
console.log(1);
}
};
let obj2 = {
d: 4,
...obj,
e: 5
};

Promise

Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点:

1、对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变;

2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。缺点:无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

回调函数

一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

其实回调就是一种利用函数指针进行函数调用的过程.

为什么要用回调呢?比如我要写一个子模块给你用,来接收远程socket发来的命令.当我接收到命令后,需要调用你的主模块的函数, 来进行相应的处理.但是我不知道你要用哪个函数来处理这个命令,我也不知道你的主模块是什么.cpp或者.h,或者说,我根本不用关心你在主模块里怎么处理它,也不应该关心用什么函数处理它……怎么办呢?使用回调!

使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己写的一个函数(这个函数就是回调函数)的地址作为参数传递给那个函数。回调其实就是提供使用某模块的一种方法。回调函数就好比是一个中断处理函数。

在解释这种思想前我想先说明一下,回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。回调函数本身是一种破坏系统结构的设计思路,回调函数会绝对的变化系统的运行轨迹,执行顺序,调用顺序。回调函数的出现会让读到你的代码的人非常的懵头转向。

既然我们需要模块间的协作,同时我们又厌恶的摒弃模块间你中有我我中有你的暧昧关系那如何生成系统呢,答案是函数指针(不一定一定是函数指针)也就是使用回调的方式。如果一个对象关心另一个对象的状态变化那么给状态的变化注册回调函数让它通知你这类状态的改变,这样在封装了模块变化的同时实现了模块间的协作关系另辟独径的给对象解耦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var async=function(callback){
setTimeout(function(){ //1秒后回调
callback('data');
},1000);
};
async(function(data){
alert(data);
});


var friends = ["Mike", "Stacy", "Andy", "Rick"];
friends.forEach(function (eachName, index){
console.log(index + 1 + ". " + eachName); // 1. Mike, 2. Stacy, 3. Andy, 4. Rick
});

需要注意的很重要的一点是回调函数并不会马上被执行。它会在包含它的函数内的某个特定时间点被“回调”(就像它的名字一样)。因此,即使第一个jQuery的例子如下所示:

1
2
3
4
5
//匿名函数不会再参数中被执行
//这是一个回调函数
$("#btn_1").click(function(){
alert("Btn 1 Clicked");
});

这个回调函数在包含它的函数内的某一点执行,就好像这个回调函数是在包含它的函数中定义的一样。这意味着回调函数本质上是一个闭包。正如我们所知,闭包能够进入包含它的函数的作用域,因此回调函数能获取包含它的函数中的变量,以及全局作用域中的变量。

回调函数基本原理

命名或匿名函数作为回调

在前面的jQuery例子以及forEach的例子中,我们使用了再参数位置定义的匿名函数作为回调函数。这是在回调函数使用中的一种普遍的魔术。另一种常见的模式是定义一个命名函数并将函数名作为变量传递给函数。

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
//全局变量
var allUserData = [];

//普通的logStuff函数,将内容打印到控制台
function logStuff (userData){
if ( typeof userData === "string")
{
console.log(userData);
}
else if ( typeof userData === "object"){
for(var item in userData){
console.log(item + ": " + userData[item]);
}
}
}

//一个接收两个参数的函数,后面一个是回调函数
function getInput (options, callback){
allUserData.push(options);
callback(options);
}

//当我们调用getInput函数时,我们将logStuff作为一个参数传递给它
//因此logStuff将会在getInput函数内被回调(或者执行)
getInput({name:"Rich",speciality:"Javascript"}, logStuff);
//name:Rich
//speciality:Javascript

传递参数给回调函数

既然回调函数在执行时仅仅是一个普通函数,我们就能给它传递参数。我们能够传递任何包含它的函数的属性(或者全局书讯给)作为回调函数的参数。在前面的例子中,我们将options作为一个参数传递给了毁掉函数。现在我们传递一个全局变量和一个本地变量:

在执行之前确保回调函数是一个函数

在调用之前检查作为参数被传递的回调函数确实是一个函数,这样的做法是明智的。同时,这也是一个实现条件回调函数的最佳时间。我们来重构上面例子中的getInput函数来确保检查是恰当的。

1
2
3
4
5
6
7
8
9
function getInput(options, callback){
allUserData.push(options);

//确保callback是一个函数
if(typeof callback === "function"){
//调用它,既然我们已经确定了它是可调用的
callback(options);
}
}

使用this对象的方法作为回调函数时的问题

当回调函数是一个this对象的方法时,我们必须改变执行回调函数的方法来保证this对象的上下文。否则如果回调函数被传递给一个全局函数,this对象要么指向全局window对象(在浏览器中)。要么指向包含方法的对象。

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
var clientData = {
id: 094545,
fullName : "Not Set",
//setUsrName是一个在clientData对象中的方法
setUserName: fucntion (firstName, lastName){
//这指向了对象中的fullName属性
this.fullName = firstName + " " + lastName;
}
}

function getUserInput(firstName, lastName, callback){
//在这做些什么来确认firstName/lastName

//现在存储names
callback(firstName, lastName);
}
//在下面你的代码例子中,当clientData.setUsername被执行时,this.fullName并没有设置clientData对象中的fullName属性。相反,它将设置window对象中的fullName属性,因为getUserInput是一个全局函数。这是因为全局函数中的this对象指向window对象

//全局函数中的thos都是指向window对象的
getUserInput("Barack","Obama",clientData.setUserName);

console.log(clientData,fullName); //Not Set

//fullName属性将在window对象中被初始化
console.log(window.fullName); //Barack Obama

使用Call和Apply来保存this

使用回调函数时一定要注意由于函数位置的改变,导致的this指针指向位置不同

我们知道了每个Javascript中的函数都有两个方法:Call 和 Apply。这些方法被用来设置函数内部的this对象以及给此函数传递变量。

call接收的第一个参数为被用来在函数内部当做this的对象,传递给函数的参数被挨个传递(当然使用逗号分开)。Apply函数的第一个参数也是在函数内部作为this的对象,然而最后一个参数确是传递给函数的值的数组。

1
2
3
4
5
6
7
8
9
10
11
12
//注意到我们增加了新的参数作为回调对象,叫做“callbackObj”
function getUserInput(firstName, lastName, callback. callbackObj){
//在这里做些什么来确认名字

callback.apply(callbackObj, [firstName, lastName]);
}
//我们将clientData.setUserName方法和clientData对象作为参数,clientData对象会被Apply方法使用来设置this对象
getUserName("Barack", "Obama", clientData.setUserName, clientData);

//clientData中的fullName属性被正确的设置
console.log(clientUser.fullName); //Barack Obama

多重回调函数

我们可以将不止一个的回调函数作为参数传递给一个函数,就像我们能够传递不止一个变量一样。

在执行异步代码时,无论以什么顺序简单的执行代码,经常情况会变成许多层级的回调函数堆积

  1. 给你的函数命名并传递它们的名字作为回调函数,而不是主函数的参数中定义匿名函数。
  2. 模块化L将你的代码分隔到模块中,这样你就可以到处一块代码来完成特定的工作。然后你可以在你的巨型应用中导入模块

Promise基本用法

Promise对象是一个构造函数,用来生成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
28
29
30
31
32
33
34
35
36
37
38
const promise = new Promise(function(resolve, reject) {
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
//Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

//resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
//Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

//Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});

//then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});

promise.then(function() {
console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved
//Promise 新建后就会立即执行,首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

下面我们写异步加载图片和实现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
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();

image.onload = function() {
resolve(image);
};

image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};

image.src = url;
});
}
//上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。

const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();

});

return promise;
};

getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});

//上面代码中,getJSON是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象。需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。
//如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,

const p1 = new Promise(function (resolve, reject) {
// ...
});

const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
//一个异步操作p2的结果是返回另一个异步操作p1,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。

const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})

p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
//上面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

//调用resolve或reject并不会终结 Promise 的参数函数的执行。
//一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})

Promise的各类方法使用

then方法

Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

1
2
3
4
5
6
7
8
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function (comments) {
console.log("resolved: ", comments);
}, function (err){
console.log("rejected: ", err);
});
//上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数

catch方法

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

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
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
//上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

// 写法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
//这两种写法是等价的,reject()方法的作用等同于抛出错误;
//如果promise状态已经变成resolved,再抛出错误是无效的Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了
//Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获

//一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});

// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
//因为第二种写法可以捕获前面then方法中执行的错误,也·更接近同步的写法(try、catch),因此建议总是使用catch()方法

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

不过,Node.js 有一个unhandledRejection事件,专门监听未捕获的reject错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。

一般总是建议,Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。

finally方法

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

finally本质上是then方法的特例。如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

finally方法的实现:

1
2
3
4
5
6
7
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};

all方法

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

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

上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。p的状态由p1p2p3决定,分成两种情况。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
const databasePromise = connectDatabase();

const booksPromise = databasePromise
.then(findAllBooks);

const userPromise = databasePromise
.then(getCurrentUser);

Promise.all([
booksPromise,
userPromise
])
.then(([books, user]) => pickTopRecommendations(books, user));

上面代码中,booksPromiseuserPromise是两个异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommendations这个回调函数。

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

race方法

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

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

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

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
28
29
30
31
function start() {  
return new Promise((resolve, reject) => {
resolve('start');
});
}

start()
.then(data => {
// promise start
console.log('result of start: ', data);
return Promise.resolve(1); // p1
})
.then(data => {
// promise p1
console.log('result of p1: ', data);
return Promise.reject(2); // p2
})
.then(data => {
// promise p2
console.log('result of p2: ', data);
return Promise.resolve(3); // p3
})
.catch(ex => {
// promise p3
console.log('ex: ', ex);
return Promise.resolve(4); // p4
})
.then(data => {
// promise p4
console.log('result of p4: ', data);
});

promise俗称链式调用,它是es6中最重要的特性之一
简单的说可以不停的then调用嵌套在调用(异步之后,链式调用方式执行回调),这种操作方式称为promise

⑴. resolved(全部置为完成状态)
①.初始化:比如说以国家,省份,县市(china ,jiangshu ,xian)三个方法来演示下链式调用关系(采用setTimeout模拟异步操作)

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
 function  china(){
console.log('china中国')
var p =new Promise(
function( resolve,reject ) {
setTimeout(function(){
console.log('中国 国家')
resolve('教育大省份')
},1000)
}
)
return p;
}

function jiangshu(data){
console.log('江苏'+data);
var p=new Promise(function(resolve,reject){
setTimeout(function (){
console.log('江苏 省份')
resolve('地级市');
},2000)
})
return p;
}

function xian(data){
console.log('盱眙县'+data)
var p=new Promise(function(resolve,reject){
setTimeout(function(){
console.log('盱眙县');
resolve ('淮河镇')
},2000)
})
return p;
}
china ().then(jiangshu).then(xian).then(function(data){
console.log(data)
})
/*输出:china 中国
中国 国家
江苏教育大省份
江苏 省份
盱眙县地级市
盱眙县
淮河镇

\rejected(部分置为无效状态)**
①.初始化:同样的以上述的函数为例

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
function  china(){
console.log('china中国')
var p =new Promise(
function( resolve,reject ) {
setTimeout(function(){
console.log('中国 国家')
reject('教育大省份')
},1000)
}
)
return p;
}

function jiangshu(data){
console.log('江苏是'+data);
varp=new Promise(function(resolve,reject){
setTimeout(function(){
console.log('江苏 省份')
resolve('地级市');
},2000)
})
returnp;
}
//函数写完之后,就开始结合then来链式调用了
china()
.then(jiangshu,function(data){ console.log(data)})

// 等同于(null不执行)
china()
.then(null,function(data){ console.log(data)})

//等同于(直接执行catch回调,抛出异常,页面也不会卡死,直接走catch)
china()
.then(jiangshu).catch(function(data){console.log(data)})
//(备注:为reject的时候,执行then的第二个参数回调,不会执行jiangshu)
//控制台输出:china 中国
//中国 国家
//教育大省份

JS方法/函数重载的姿势

JavaScript不支持重载的语法,它没有重载所需要的函数签名。

ECMAScript函数不能像传统意义上那样实现重载。而在其他语言(如 Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。如前所述,ECMAScirpt函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。 — JavaScript高级程序设计(第3版)3.7.2小节

在JavaScript中,函数名本身就是变量,函数声明类似于变量赋值。当同个函数名被多次声明时,后声明的内容将覆盖前面的内容。尽管JavaScript无法做到真正的重载,但是可以通过检查传入函数中参数的类型和数量并作相应的处理,从而实现重载的效果,曲线救国。

借助流程控制语句

通过判断传入参数的数量(arguments.length),执行相应的代码块。

巧用闭包特性

1
2
3
4
var ninja = {};
addMethod(ninja, 'whatever', function(){/* code */});
addMethod(ninja, 'whatever', function(a){/* code */});
addMethod(ninja, 'whatever', function(a,b){/* code */});

addMethod函数接收3个参数:目标对象、目标方法名、函数体,当函数被调用时:

先将目标object[name]的值存入变量old中,因此起初old中的值可能不是一个函数;接着向object[name]赋值一个代理函数,并且由于变量old、fn在代理函数中被引用,所以old、fn将常驻内存不被回收。

1
2
3
4
5
6
7
8
9
10
11
12
function addMethod(object, name, fn) {
var old = object[name]; // 保存前一个值,以便后续调用
object[name] = function(){ // 向object[name]赋值一个代理函数
// 判断fn期望接收的参数与传入参数个数是否一致
if (fn.length == arguments.length)
// 若是,则调用fn
return fn.apply(this, arguments)
else if (typeof old == 'function') // 若否,则判断old的值是否为函数
// 若是,则调用old
return old.apply(this, arguments);
};
}

代理函数被调用时:

先判断传入参数与其父级作用域中fn期望接收参数的个数是否一致,若是则调用该fn;

若否,则判断其父级作用域中old值类型是否为函数,若是则调用该old;

当old中存有上一次生成的代理函数时,则会重复前面两个步骤,直至old值不为代理函数


上述两种方法都是通过检查参数个数来实现重载,不区分参数类型。此外,方法1在继承时重载的那些函数无法被重写,而方法2通过逐个执行代理函数,比对参数个数,直至找到目标函数,效率不高。

巧用引用类型特性

核心思想:由于ECMAScript函数是一种引用类型对象,可扩展属性与方法。借此通过创建一个容器用于存储要重载的函数,并将容器挂载到代理函数上以便后续访问,而代理函数利用闭包特性访问容器。

重载顺序:首先查找参数类型匹配的函数,其次查找参数个数匹配的函数。

存储格式:键值对,键名由逗号与参数个数或参数类型组成,键值为要重载的函数,如下:

1
2
3
4
5
{
',0': function(){/* code */},
',1': function(a){/* code */},
',string,number': function(a,b){/* code */}
}

工具函数被调用时

  1. 先判断是否已重载过,若有,直接将要重载的函数按格式存入容器;
  2. 若未重载过,则创建一个容器变量;
  3. 判断未重载前的值是否为一个函数,若是,则以逗号+参数个数的格式存入容器;
  4. 将要重载的函数存入容器;
  5. 代理原函数,并将容器挂载到代理函数上;
  6. 当代理函数被调用时,将依次查找容器中匹配的函数并调用。
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
/**
* 重载工具函数
* @param {Object} ctx - 上下文
* @param {String} name - 函数名
* @param {Function} fn - 函数体
* @param {String} type - 参数类型
* @author 范围兄 <ambit_tsai@qq.com>
* @example 不指定参数类型
* overload(obj, 'do', function(){...});
* overload(obj, 'do', function(a){...});
* @example 指定参数类型
* overload(obj, 'do', function(a,b){...}, 'string,number');
*/
function overload(ctx, name, fn, type){
type = type? type.trim().toLowerCase(): fn.length;
// 已重载过
if(typeof ctx[name]==='function' && typeof ctx[name]._$fnMap==='object'){
ctx[name]._$fnMap[','+type] = fn; // 将fn存入_$fnMap
return;
}
// 未重载过
var fnMap = {}; // 容器
if(typeof ctx[name] === 'function'){
// 若ctx[name]是一个函数,则存入容器
fnMap[','+ctx[name].length] = ctx[name];
}
fnMap[','+type] = fn;
ctx[name] = function overloading(){ // 代理
var args = arguments,
len = args.length,
type, i;
for(i=0, type=''; i<len; ++i){ // 计算参数类型
type += ',' + typeof args[i];
}
// 依次匹配:参数类型->参数个数
if(fnMap[type]) return fnMap[type].apply(this, args);
if(fnMap[','+len]) return fnMap[','+len].apply(this, args);
throw 'Overload: no matched function';
};
ctx[name]._$fnMap = fnMap; // 将fnMap挂载到代理上
}

JS异步操作的方法

回调函数

回调函数是异步编程中最基本的方法。假设有三个函数f1、f2、f3f2需要等待f1的执行结果,而f3是独立的,不需要f1和f2的结果,如果我们写成同步,就是这样的:

1
2
  f1();
  f2();  f3()

如果f1执行的很快,可以; 但是如果f1执行的很慢,那么f2和f3就会被阻塞,无法执行。这样的效率是非常低的。但是我们可以改写,将f2写成是f1的回调函数,如下:

1
2
3
4
5
6
  function f1(callback){
    setTimeout(function () {
      // f1的任务代码
      callback();
    }, 1000);
  }

 那么这时候执行代码就是这样:

1
2
f1(f2);
f3()

这样,就是一个异步的执行了,即使f1很费时间,但是由于是异步的,那么f3()就会很快的得到执行,而不会受到f1和f2的影响。

注意: 如果我们把f1写成这样呢?

1
2
3
4
function f1(callback){
  // f1的任务代码
  callback();
}

然后,我们同样可以这么调用:

1
2
f1(f2);
f3()

这时候还是异步的吗? 答案:不是异步。 这里的回调函数并非真正的回调函数,如果没有利用setTimeout含函数,那么f3()的执行同样需要等到f1(f2)完全执行完毕,这里要注意。而我们就是利用setTImeout才能做出真正的回调函数。

事件监听

另一种异步的思路是采用事件驱动模式。任务的执行不取决于代码的顺序, 而取决于某个事件是否发生。 还是以f1、f2、f3为例子。 首先,为f1绑定一个事件(这里采用jquery的写法):

1
2
f1.on('done', f2);
f3()

这里的意思是: 当f1发生了done事件,就执行f2, 然后,我们对f1进行改写:

1
2
3
4
5
6
  function f1(){
    setTimeout(function () {
      // f1的任务代码
      f1.trigger('done');
    }, 1000);
  }

f1.trigger(‘done’)表示, 执行完成后,立即触发done事件,从而开始执行f2。

这种方法的优点就是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以去耦合,有利于实现模块化,缺点就是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

发布订阅

第二种方法的事件,实际上我们完全可以理解为“信号”,即f1完成之后,触发了一个 ‘done’,信号,然后再开始执行f2。

我们假定,存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”这个信号, 从而知道什么时候自己可以开始执行。 这个就叫做“发布/订阅模式”, 又称为“观察者”模式 。

这个模式有多种实现, 下面采用Ben Alman的Tiny PUb/Sub,这是jQuery的一个插件。

首先,f2向”信号中心”jquery订阅”done”信号,

1
jQuery.subscribe("done", f2);

然后,f1进行如下改写:

1
2
3
4
5
6
  function f1(){
    setTimeout(function () {
      // f1的任务代码
      jQuery.publish("done");
    }, 1000);
  }

jquery.pushlish(“done”)的意思是: f1执行完成后,向“信号中心”jQuery发布“done”信号,从而引发f2的执行。

此外,f2完成执行后,也可以取消订阅(unsubscribe)。

  

1
 jQuery.unsubscribe("done", f2);

这种方法的性质和“事件监听”非常类似,但是明显是优于前者的,因为我们可以通过查看“消息中心”,了解到存在多少信号、每个信号有多少个订阅者,从而监控程序的运行。

promise对象

promise是commonjs工作组提出来的一种规范,目的是为异步编程提供统一接口。

简答的说,它的思想是每一个异步任务返回一个promise对象,该对象有一个then方法,允许指定回调函数。 比如,f1的回调函数f2,可以写成:

1
f1().then(f2);

f1要进行下面的改写(这里使用jQuery的实现):

1
2
3
4
5
6
7
8
 function f1(){
    var dfd = $.Deferred();
    setTimeout(function () {
      // f1的任务代码
      dfd.resolve();
    }, 500);
    return dfd.promise;
  }

这样的优点在于,回调函数编程了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现很多强大的功能 。

如:指定多个回调函数:

1
 f1().then(f2).then(f3);

再比如,指定发生错误时的回调函数:

1
f1().then(f2).fail(f3);

而且,他还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。 所以,你不用担心是否错过了某个事件或者信号,这种方法的确定就是编写和理解,都比较困难。

generator函数的异步应用

generator函数将JavaScript异步编程带入了一个全新的阶段!

比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。

相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着

  

协程

 传统的编程语言中,早就有了异步编程的解决方案,其中一种叫做协程,意思是多个线程互相协作,完成异步任务

  协程优点像函数,又有点像线程,运行流程如下:

  • 第一步,协程A开始执行。
  • 第二步,协程A执行到一半,进入暂停执行权转移到协程B
  • 第三步,(一段时间后)协程B交还执行权
  • 第四步,协程A恢复执行

  上面的协程A,就是异步任务,因为它分为两段(或者多段)执行。

  举例来说,读取文件的协程写法如下:

1
2
3
4
5
function *asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}

  上面代码的函数asyncJob是一个协程,奥妙就在于yield命令, 它表示执行到此处,执行权交给其他协程,也就是说yield命令是异步两个阶段的分界线。

  协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续向后执行,它的最大优点就是代码的写法非常像同步操作,如果去除yield命令,简直是一模一样。

协程的Generator函数实现

  Generator函数是协程在ES6中的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

  整个Generator函数就是一个封装的异步任务,或者说异步任务的容器。 异步任务需要暂停的地方,都用yield语句注明。 如下:

1
2
3
4
5
6
7
8
function* gen(x) {
var y = yield x + 2;
return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

  在调用gen函数时 gen(1), 会返回一个内部指针(即遍历器)g。 这是Generator函数不同于普通函数的另一个地方,即执行它(调用函数)不会返回结果, 返回的一个指针对象 。调用指针g的next方法,会移动内部指针(即执行异步任务的第一阶段),指向第一个遇到的yield语句,这里我们是x + 2,但是实际上这里只是举例,实际上 x + 2 这句应该是一个异步操作,比如ajax请求。 换言之,next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。 value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示Generator函数是否执行完毕,即是否还有下一个阶段。

Generator函数的数据交换和错误处理

  Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

  next返回值的value属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。

1
2
3
4
5
6
7
8
function* gen(x){
var y = yield x + 2;
return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

  上面代码中,第一next方法的value属性,返回表达式x + 2的值3。第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收。因此,这一步的value属性,返回的就是2(变量y的值)。

Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}

var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

上面代码的最后一行,Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try...catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

异步任务的封装**下面看看如何使用 Generator 函数,执行一个真实的异步任务。**

1
2
3
4
5
6
7
var fetch = require('node-fetch');

function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}

  上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。

  执行这段代码的方法如下。

1
2
3
4
5
6
7
8
var g = gen();
var result = g.next();

result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});

  上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

  可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

1
2
3
4
5
6
7
8
9
10
11
function* gen(x) {
yield 1;
yield 2;
yield 3;
return 4;
}
var a = gen();
console.log(a.next());
console.log(a.next());
console.log(a.next());
console.log(a.next());

  最终,打印台输出

img

即开始调用gen(),并没有真正的调用,而是返回了一个生成器对象,a.next()的时候,执行第一个yield,并立刻暂停执行,交出了控制权; 接着,我们就可以去a.next() 开始恢复执行。。。 如此循环往复。  

每当调用生成器对象的next的方法时,就会运行到下一个yield表达式。 之所以称这里的gen()为生成器函数,是因为区别如下:

  • 普通函数使用function来声明,而生成器函数使用 function * 来声明
  • 普通函数使用return来返回值,而生成器函数使用yield来返回值。
  • 普通函数式run to completion模式 ,即一直运行到末尾; 而生成器函数式 run-pause-run 模式, 函数可以在执行过程中暂停一次或者多次。并且暂停期间允许其他代码执行。

async/await

async函数基于Generator又做了几点改进:

  • 内置执行器,将Generator函数和自动执行器进一步包装。
  • 语义更清楚,async表示函数中有异步操作,await表示等待着紧跟在后边的表达式的结果。
  • 适用性更广泛,await后面可以跟promise对象和原始类型的值(Generator中不支持)

  很多人都认为这是异步编程的终极解决方案,由此评价就可知道该方法有多优秀了。它基于Promise使用async/await来优化then链的调用,其实也是Generator函数的语法糖。 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

  await得到的就是返回值,其内部已经执行promise中resolve方法,然后将结果返回。使用async/await的方式写回调任务:

1
2
3
4
5
6
7
8
9
10
11
async function dolt(){
console.time('dolt');
const time1=300;
const time2=await step1(time1);
const time3=await step2(time2);
const result=await step3(time3);
console.log(`result is ${result}`);
console.timeEnd('dolt');
}

dolt();

  可以看到,在使用await关键字所在的函数一定要是async关键字修饰的。

  功能还很新,属于ES7的语法,但使用Babel插件可以很好的转义。另外await只能用在async函数中,否则会报错

JS的事件执行机制

一、js的内存模型

img

img

二、js代码执行机制:

  • 所有同步任务都在主线程上的栈中执行。

  • 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。

  • 一旦”栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,选择出需要首先执行的任务(由浏览器决定,并不按序)。

三、宏任务与微任务:

  1. MacroTask(宏观Task) setTimeout, setInterval, , requestAnimationFrame(请求动画), I/O

  2. MicroTask(微观任务) process.nextTick, Promise, Object.observe, MutationObserver

  3. 先同步 再取出第一个宏任务执行 所有的相关微任务总会在下一个宏任务之前全部执行完毕 如果遇见 就 先微后宏

案例一:(在主线程上添加宏任务)

1
2
3
4
5
6
7
8
9
console.log(1)

setTimeout(function () {

console.log(2);

},0)

console.log(3) //1 3 2

先看代码:一个打印,一个定时器,一个打印

因为定时器是异步操作,又是宏任务,所以先执行第一个打印,接着将setTimeout放入宏任务队列,接着执行第二个打印,再执行宏任务队列中的setTimeout

案例二:(在主线程上添加微任务)

1
2
3
4
5
6
7
8
console.log(1)
new Promise(function(resolve,reject){
console.log('2')
resolve()
}).then(function(){
console.log(3)
})
console.log(4) //1 2 4 3

先看代码:一个打印,一个new promise,一个promise.then,一个打印

因为new promise会立即执行,promise.then是异步操作且是微任务

所以,先执行第一个打印,执行new Promise,将promise.then放入微任务队列,接着执行第二个打印,再执行微任务队列中的promise.then

案例三:(宏任务中创建微任务)

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
console.log('1');

setTimeout(function () {
console.log('2');
new Promise(function (resolve) {
console.log('3');
resolve();
}).then(function () {
console.log('4')
})
},0)

new Promise(function (resolve) {
console.log('5');
resolve();
}).then(function () {
console.log('6')
})

setTimeout(function () {
console.log('7');
new Promise(function (resolve) {
console.log('8');
resolve();
}).then(function () {
console.log('9')
})
console.log('10')
},0)

console.log('11')  

// 1 5 11 6 2 3 4 7 8 10 9

先看代码:一个打印,第一个定时器,一个new promise,一个promise.then,第二个定时器,一个打印

定时器是异步操作,又是宏任务,\promise.then是异步操作且是微任务****

所以,先执行第一个打印(1),将第一个定时器放入宏任务队列,执行new Promise(5),将promise.then放入微任务队列,将第二个定时器放入宏任务队列,执行打印(11);

主线程上的代码执行完毕后,看是否有微任务?此时:微任务队列中有一个promise.then,执行它(6);微任务执行完毕看宏任务队列;

此时宏任务队列中两个定时器,延时都是0秒,所以按顺序执行就ok,先执行第一个定时器

第一个定时器中:一个打印,一个mew promise,一个promise.then(微任务);**(宏任务中包含微任务,一定要将宏任务中的微任务执行完,再去执行下一个宏任务)**

先执行打印(2),再执行new promise(3),**再执行promise.then(**4);第一个宏任务执行完,执行第二个宏任务(第二个定时器)

第二个定时器中:一个打印,一个new promise,一个promise.then(微任务),一个打印

先执行第一个打印(7),再执行new promise(8),再执行第二个打印(10),在执行promise.then(9) 

案例四:(微任务中创建宏任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
new Promise((resolve) => {
console.log("1")
resolve()
}).then(() => {
console.log("2")
setTimeout(() => {
console.log("3")
},0)
})
setTimeout(() => {
console.log("4")
},1000)
console.log("5") //1 5 2 3 4

先看代码:一个new promise,(一个then,一个定时器(0秒)),一个定时器(1秒),一个打印 微任务中有宏任务,则将宏任务放入宏任务队列任务中

先执行new promise(1),再将promise.then放入微任务队列,将定时器放入宏任务队列(0秒),将定时器放入宏任务队列(1秒),执行打印(5)

接着看微任务队列,执行promise.then(2);微任务队列中都执行完再看宏任务队列

宏任务队列中两个定时器,一个延时0秒,一个延时1秒,所以先执行延时0秒的那个

第一个定时器:执行(3);

第二个定时器:执行(4)

JS事件执行

本文是承接Promise来说的,大家都知道,JavaScript脚本是单线程的语言,虽然有H5的Web-Worker加持,但是创建出来的子线程完全受主线程控制,且不得操作DOM,所以还是无法改变JavaScript单线程的本质

JavaScript是单线程执行的,无法同时执行多段代码。当某一段代码正在执行的时候,所有后续的任务都必须等待,形成一个队列。一旦当前任务执行完毕,再从队列中取出下一个任务,这也常被称为 “阻塞式执行”。所以一次鼠标点击,或是计时器到达时间点,或是Ajax请求完成触发了回调函数,这些事件处理程序或回调函数都不会立即运行,而是立即排队,一旦线程有空闲就执行。假如当前JavaScript线程正在执行一段很耗时的代码,此时发生了一次鼠标点击,那么事件处理程序就被阻塞,用户也无法立即看到反馈,事件处理程序会被放入任务队列,直到前面的代码结束以后才会开始执行。如果代码中设定了一个setTimeout,那么浏览器便会在合适的时间,将代码插入任务队列,如果这个时间设为0,就代表立即插入队列,但不是立即执行,仍然要等待前面代码执行完毕。所以 setTimeout 并不能保证执行的时间,是否及时执行取决于JavaScript 线程是拥挤还是空闲。

这里就涉及到了执行栈(Stack)和队列任务(Queue Task)的概念,将同步任务都放入主线程的Stack当中,将异步和延时的任务都放入Event Queue里面等待执行,Event Queue即为事件队列,所包含的全是事件,等执行栈为空之后就代表主线程执行完毕,再去Event Queue中读取第一个事件放入主线程,执行完毕再读取第二个…因此形成一个JavaScript的Event Loop(事件循环),Event Loop就是JavaScript的实现异步的一种方式,也是JavaScript的执行机制。

至于定时器(timer)嘛,因为里面的参数有一个是回调函数,另一个是延时执行的毫秒数,所以他也要放进队列中,而上面的引用部分有个延时0毫秒,它的含义就是立即放入队列,而不是立即放进执行栈执行;JavaScript还有一种函数叫做回调函数,阮一峰大神是这么说的:

所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

下面这幅图是从别人那里偷来的(ps:主要这幅图太有说服力度了,不信你看):

![img](https:////upload-images.jianshu.io/upload_images/8560482-92ec4b6e10c45e30.png?imageMogr2/auto-orient/strip|imageView2/2/w/601/format/webp

nodeJs里面提出了和任务队列有关联的方法process.nextTick(callback),它的含义是本次循环完毕等到下一次循环开始再执行,也就是在当前执行栈的尾部。

在网上经常看到这样的关键字,从广义上来讲,我们弄明白了同步异步,但是狭义上来说还有两个新的概念,其实这个概念我还真不确定官方是否同意,我是看到闹闹不爱闹在掘金中阐明的:

  1. 宏任务macro task [ˈmækrəʊ]:当前调用栈中执行的代码成为宏任务。(主代码快,定时器等等)。exp:script(全局任务),setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
  2. 微任务micro task [ˈmaɪkrəʊ]: 当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件。(promise.then,proness.nextTick等等)。exp:process.nextTick,promise,Object.observer,MutationObserver
  3. 宏任务中的事件放在callback queue中,由事件触发线程维护;微任务的事件放在微任务队列中,由js引擎线程维护。

不管这个东西存不存在,既然国人都这么叫了,那我是这么理解的:
他们口中的宏观任务就是我们的回调函数,宏观任务和微观任务就是我们的Event Queue,执行栈执行完毕会执行微观任务再执行宏观任务,我不建议大家继续这么称呼,其实macro task和micro task都属于是浏览器执行js的执行机制,这个我不是为了较真,估计我也是被我们公司的老总教训的太多了,不去不去怕了怕了o((⊙﹏⊙))o….

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(function() {
console.log('setTimeout');
});
Promise.resolve(function () {
console.log('resolve');
});
new Promise(function(resolve) {
console.log('promise');
resolve();
}).then(function() {
console.log('then');
});
console.log('console');

其实这个栗字很简单,进入script主线程,遇到setTimeout push到macro task,遇到resolve push到macro task,new Promise立即执行,率先打印,then push到micro task,接着第二个打印console,然后执行micro task打印出then,接着执行macro task打印出setTimeout,因为Promise.resolve这个回到函数未调用,有的浏览器报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
console.log('1');
setTimeout(() => {
console.log('9');
this.$nextTick(() => {
console.log('11');
});
new Promise(function(resolve) {
console.log('10');
resolve();
}).then(function() {
console.log('12')
});
},5000);
this.$nextTick(() => {
console.log('3');
});
new Promise(function(resolve) {
console.log('2');
resolve();
}).then(function() {
console.log('4');
});
setTimeout(() => {
console.log('5');
this.$nextTick(() => {
console.log('7');
});
new Promise(function(resolve) {
console.log('6');
resolve();
}).then(function() {
console.log('8');
});
});

看到诸多异步延时任务先不要慌,一步一步来解读,代码中的this.$nextTick(callback)千万不要解读成上面的process.nextTick(callback),否则你会被坑惨的,process是nodeJs里面的,nodeJs执行机制和JavaScript的执行机制是不同的,nodeJs不会看你代码的层级关系哦,只关心你的事件的类型,按照这个顺序来执行代码,而我们的js是按照父级的事件,有着层级关系的执行。
vueJs的主线程先执行,首先打印出1,第一个setTimeout push到macro task,nextTick放入micro task,Promise立即执行,then push进micro task,第二个setTimeout push到macro task,接着执行micro task,打印3 4,最后执行macro task,注意这里有个坑,macro task里面有两个timer,第一个5000ms之后执行,所以先执行第二个,所以最后的答案小学生都知道,打印顺序从1到12。

防抖函数

一、函数为什么要防抖

有如下代码

1
2
3
window.onresize = () => {
console.log('触发窗口监听回调函数')
}

当我们在PC上缩放浏览器窗口时,一秒可以轻松触发30次事件。手机端触发其他Dom时间监听回调时同理。

这里的回调函数只是打印字符串,如果回调函数更加复杂,可想而知浏览器的压力会非常大,用户体验会很糟糕。

resizescroll等Dom事件的监听回调会被频繁触发,因此我们要对其进行限制。

二、实现思路

函数去抖简单来说就是对于一定时间段的连续的函数调用,只让其执行一次,初步的实现思路如下:

第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。

三、Debounce 应用场景

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 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
function debounce(method, wait, immediate) {
let timeout
// debounced函数为返回值
// 使用Async/Await处理异步,如果函数异步执行,等待setTimeout执行完,拿到原函数返回值后将其返回
// args为返回函数调用时传入的参数,传给method
let debounced = function(...args) {
return new Promise (resolve => {
// 用于记录原函数执行结果
let result
// 将method执行时this的指向设为debounce返回的函数被调用时的this指向
let context = this
// 如果存在定时器则将其清除
if (timeout) {
clearTimeout(timeout)
}
// 立即执行需要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
if (immediate) {
// 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
// 这样确保立即执行后wait毫秒内不会被再次触发
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
// 如果满足上述两个条件,则立即执行并记录其执行结果
if (callNow) {
result = method.apply(context, args)
resolve(result)
}
} else {
// 如果immediate为false,则等待函数执行并记录其执行结果
// 并将Promise状态置为fullfilled,以使函数继续执行
timeout = setTimeout(() => {
// args是一个数组,所以使用fn.apply
// 也可写作method.call(context, ...args)
result = method.apply(context, args)
resolve(result)
}, wait)
}
})
}

// 在返回的debounced函数上添加取消方法
debounced.cancel = function() {
clearTimeout(timeout)
timeout = null
}

return debounced
}

需要注意的是,如果需要原函数返回值,调用防抖后的函数的外层函数需要使用Async/Await语法等待执行结果返回

使用方法见代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function square(num) {
return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', async () => {
let val
try {
val = await debouncedFn(4)
} catch (err) {
console.error(err)
}
// 停止缩放1S后输出:
// 原函数的返回值为:16
console.log(`原函数返回值为${val}`)
}, false)

具体的实现步骤请往下看

五、Debounce 的实现

1. 《JavaScript高级程序设计》(第三版)中的实现

1
2
3
4
5
6
7
8
9
10
11
12
function debounce(method, context) {
clearTimeout(method.tId)
method.tId = setTimeout(() => {
method.call(context)
}, 1000)
}

function print() {
console.log('Hello World')
}

window.onresize = debounce(print)

我们不停缩放窗口,当停止1S后,打印出Hello World。

有个可以优化的地方: 此实现方法有副作用(Side Effect),改变了输入值(method),给method新增了属性

2. 优化第一版:消除副作用,将定时器隔离

1
2
3
4
5
6
7
8
9
10
11
function debounce(method, wait, context) {
let timeout
return function() {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
method.call(context)
}, wait)
}
}

3. 优化第二版:自动调整this正确指向

之前的函数我们需要手动传入函数执行上下文context,现在优化将 this 指向正确的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(method, wait) {
let timeout
return function() {
// 将method执行时this的指向设为debounce返回的函数被调用时的this指向
let context = this
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
method.call(context)
}, wait)
}
}

4. 优化第三版:函数可传入参数

即便我们的函数不需要传参,但是别忘了JavaScript 在事件处理函数中会提供事件对象 event,所以我们要实现传参功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function debounce(method, wait) {
let timeout
// args为返回函数调用时传入的参数,传给method
return function(...args) {
let context = this
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
// args是一个数组,所以使用fn.apply
// 也可写作method.call(context, ...args)
method.apply(context, args)
}, wait)
}
}

5. 优化第四版:提供立即执行选项

有些时候我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发n毫秒后,才可以重新触发执行。

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
function debounce(method, wait, immediate) {
let timeout
return function(...args) {
let context = this
if (timeout) {
clearTimeout(timeout)
}
// 立即执行需要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
if (immediate) {
// 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
// 这样确保立即执行后wait毫秒内不会被再次触发
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
method.apply(context, args)
}
} else {
// 如果immediate为false,则函数wait毫秒后执行
timeout = setTimeout(() => {
// args是一个类数组对象,所以使用fn.apply
// 也可写作method.call(context, ...args)
method.apply(context, args)
}, wait)
}
}
}

6. 优化第五版:提供取消功能

有些时候我们需要在不可触发的这段时间内能够手动取消防抖,代码实现如下:

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
function debounce(method, wait, immediate) {
let timeout
// 将返回的匿名函数赋值给debounced,以便在其上添加取消方法
let debounced = function(...args) {
let context = this
if (timeout) {
clearTimeout(timeout)
}
if (immediate) {
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
method.apply(context, args)
}
} else {
timeout = setTimeout(() => {
method.apply(context, args)
}, wait)
}
}

// 加入取消功能,使用方法如下
// let myFn = debounce(otherFn)
// myFn.cancel()
debounced.cancel = function() {
clearTimeout(timeout)
timeout = null
}
}

至此,我们已经比较完整地实现了一个underscore中的debounce函数。

六、遗留问题

需要防抖的函数可能是存在返回值的,我们要对这种情况进行处理,underscore的处理方法是将函数返回值在返回的debounced函数内再次返回,但是这样其实是有问题的。如果参数immediate传入值不为true的话,当防抖后的函数第一次被触发时,如果原始函数有返回值,其实是拿不到返回值的,因为原函数是在setTimeout内,是异步延迟执行的,而return是同步执行的,所以返回值是undefined

第二次触发时拿到的返回值其实是第一次执行的返回值,第三次触发时拿到的返回值其实是第二次执行的返回值,以此类推。

1. 使用回调函数处理函数返回值

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
function debounce(method, wait, immediate, callback) {
let timeout, result
let debounced = function(...args) {
let context = this
if (timeout) {
clearTimeout(timeout)
}
if (immediate) {
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
result = method.apply(context, args)
// 使用回调函数处理函数返回值
callback && callback(result)
}
} else {
timeout = setTimeout(() => {
result = method.apply(context, args)
// 使用回调函数处理函数返回值
callback && callback(result)
}, wait)
}
}

debounced.cancel = function() {
clearTimeout(timeout)
timeout = null
}

return debounced
}

这样我们就可以在函数防抖时传入一个回调函数来处理函数的返回值,使用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function square(num) {
return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false, val => {
console.log(`原函数的返回值为:${val}`)
})

window.addEventListener('resize', () => {
debouncedFn(4)
}, false)

// 停止缩放1S后输出:
// 原函数的返回值为:16

2. 使用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
28
29
30
31
32
33
34
35
36
function debounce(method, wait, immediate) {
let timeout, result
let debounced = function(...args) {
// 返回一个Promise,以便可以使用then或者Async/Await语法拿到原函数返回值
return new Promise(resolve => {
let context = this
if (timeout) {
clearTimeout(timeout)
}
if (immediate) {
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
result = method.apply(context, args)
// 将原函数的返回值传给resolve
resolve(result)
}
} else {
timeout = setTimeout(() => {
result = method.apply(context, args)
// 将原函数的返回值传给resolve
resolve(result)
}, wait)
}
})
}

debounced.cancel = function() {
clearTimeout(timeout)
timeout = null
}

return debounced
}

使用方法一:在调用防抖后的函数时,使用then拿到原函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function square(num) {
return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', () => {
debouncedFn(4).then(val => {
console.log(`原函数的返回值为:${val}`)
})
}, false)

// 停止缩放1S后输出:
// 原函数的返回值为:16

使用方法二:调用防抖后的函数的外层函数使用Async/Await语法等待执行结果返回

使用方法见代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function square(num) {
return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', async () => {
let val
try {
val = await debouncedFn(4)
} catch (err) {
console.error(err)
}
console.log(`原函数返回值为${val}`)
}, false)

// 停止缩放1S后输出:
// 原函数的返回值为:16

C++的继承与多态

接口继承与实现继承

派生类将基类中除去构造函数和析构函数的其他方法继承了过来。public继承概念由两部分组成,函数接口(function interfaces)继承和函数实现(function implementations)继承。作为类的开发人员,我们主要研究类的三种继承情况:
1、派生类只继承成员函数的接口(也就是声明),需要自己来重新定义该函数的实现;
2、派生类同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现;
3、派生类同时继承函数的接口和实现,并且不允许覆盖任何东西,只能利用父函数的实现;

1
2
3
4
5
6
7
8
9
10
11
class Shape{//形状
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectID() const;
...
};


class Rectangle:public Shape{...};//矩形
class Ellipse:public Shape{...};//椭圆

Shape是个抽象类,它的纯虚函数draw使它成为一个抽象类,所以客户不能够创建Shape class的实体,只能创建它的派生类的实体

Shape类声明了三个函数,第一个是draw,在视屏中划出当前对象,第二个是error,准备让那些“需要报导某个错误”的成员函数调用,第三个是objectID,返回当前对象的独一无二的整数识别码,每个函数的声明方式都不相同,draw是个纯虚函数(pure virtual),error是个虚函数( 简朴的(非纯)impure virtual函数),objectID是个非虚函数(non-virtual)函数。

纯虚函数通常有两个特点:它们必须被任何“继承了他们”的具象类重新声明;并且它们在抽象类中通常没有定义。
所以结论是:声明一个纯虚函数的目的是为了让派生类只继承函数的接口。

虚函数(简朴的impure virtual函数)背后的故事和纯虚函数(pure virtual函数)有点不同,一如往常,派生类继承其函数接口,但虚函数(简朴的impure virtual函数)会提供一份实现代码,派生类可能覆写(override)它,所以结论是:

声明虚函数(简朴的impure virtual函数)的目的是让派生类继承该函数的接口和缺省实现,考虑error函数,其接口表示,每个类都必须支持一个“当遇上错误是可调用”的函数,但每个类可自由处理错误,若某个类不想针对错误做出任何特殊行为,它可以退回到Shape类提供的缺省错误处理行为。但是允许虚函数(简朴的impure virtual函数)同时指定函数声明和函数缺省行为,却有可能造成危险,考虑下面的例子:

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
//XYZ航空公司的飞机继承体系,该公司只有A型和B型两种飞机,两者都以相同方式飞行,因此XYZ设计的继承体系为:
class Airport{...};


class Airplane{
public :
virtual void fly(const Airport& destination);
...
}


void Airplane::fly(const Airport& destination)
{
//缺省代码,将飞机飞至指定的目的地
}


class ModelA:public Airplane{...};
class ModelB:public Airplane{...};


//现在,新增加一个C型飞机,C型和A型、B型的飞行方式不同,XYZ公司的程序员在继承体系中针对C型飞机加了一个类,但由于急于让飞机上线,竟然忘了定义其fly函数:


class ModelC:public Airplane{
... //为声明fly函数
}

若代码中出现如下操作:
Airport PDX(…);//PDX是机场名字
Airplane* pa = new ModelC;

pa->fly(PDX);//调用Airplane::fly

这将酿成大祸,这个程序试图以ModelA或ModelB的飞行方式来飞ModelC。解决该问题的要点在于切断“虚函数接口”和其“缺省实现”之间的连接,下面是一种做法:

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
class Airplane{
public:
virtual void fly(const Airplane& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
// 缺省行为,将飞机飞至指定的目的地
}


//现在ModelA和ModelB调用的飞行的缺省实现为:
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
...
}
};


class ModelB:public Airplane{
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
...
}
};




//现在ModelC class 不可能意外继承不正确的fly实现代码,因为Airplane中的纯虚函数迫使ModelC必须提供自己的fly版本:




class ModelC:public Airplane{
public:
virtual void fly(const Airport& destination);
{
...
}
};


void ModelC::fly(const Airport& destination)
{
//将C型飞机飞至指定目的地
}

最后考虑Shape的非虚函数objectID()。若成员函数是个非虚函数,意味着它并不打算在派生类中有不同的行为,实际上非虚成员函数所表现的不变性远重要于特异性,因为它表示不论派生类变得多么特异化,就其自身而言,它的行为都不可以改变。

1、接口继承和实现继承不同。在public继承之下,派生类总是继承基类的接口;

2、纯虚函数只是具体指定接口继承;

3、虚函数( 简朴的(非纯)impure virtual函数)具体指定接口继承及缺省实现继承;

4、非虚函数(non-virtual函数)具体指定接口继承以及强制性实现继承。

总结

接口继承:派生类只继承函数的接口

实现继承:派生类同时继承函数的接口和实现

虚函数是重载的一种表现方式,是一种动态的重载方式。

非虚函数:继承该函数的接口和一份强制性实现,继承类必须含有某个接口,必须使用基类的实现

虚函数:会继承该函数的接口和缺省实现。继承类必须含有某个接口,可以自己实现,也可以不实现,而采用基类定义的缺省实现。

纯虚函数:纯虚函数在基类中没有定义,接口继承。含有纯虚函数的类无法实例化。要求继承类必须含有某个接口,并对接口函数实现。

多态与继承

继承访问修饰符

继承方式有三种——public、protected和private,不同的继承方式对继承到派生类中的基类成员有什么影响? 总的来说,父类成员的访问限定符通过继承派生到子类中之后,访问限定符的权限小于、等于原权限。其中,父类中的private成员只有父类本身及其友元可以访问,通过其他方式都不能进行访问,当然就包括继承。protected多用于继承当中,如果对父类成员的要求是——子类可访问而外部不可访问,则可以选择protected继承方式。

父子类中同名元素

overload重载

函数重载有三个条件,一函数名相同,二形参类型、个数、顺序不同,三相同作用域。根据第三个条件,可知函数重载只可能发生在一个类中

overhide隐藏

在派生类中将基类中的同名成员方法隐藏,要想在派生类对象中访问基类同名成员得加上基类作用域。(注意,如果该同名方法在基类中实现了重载,在派生类对象中同样需要指定作用域,而不能通过简单的传参,调用带参重载方法)

override函数覆盖

基类、派生类中的同名方法 函数头相同(参数、返回值),且基类中该方法为虚函数,则派生类中的同名方法将基类中方法覆盖。函数隐藏和函数覆盖都是发生在基类和派生类之间的,可以这么理解:基类和派生类中的同名函数,除去是覆盖的情况,其他都是隐藏的情况。

引用与指针

. 基类对象和派生类对象

派生类对象可以赋值给基类对象,基类对象不可以赋值给基类对象;对于基类对象和派生类对象,编译器默认支持从下到上的转换,上是基类,下是派生类。

基类指针(引用)和派生类指针(引用)

基类指针(引用)可以指向派生类对象,但只能访问派生类中基类部分的方法,不能访问派生类部分方法。派生类指针(引用)不可以指向基类对象,解引用可能出错,因为派生类的一些方法可能基类没有。

虚函数

分析:当Base类中有虚函数时,不论是Base类还是Derive类,它们的大小都增加了4个字节。并且当Base指向Derive对象时,Base的类型却变为Derive,不再和指针本身的类型相关,这是怎么回事呢?

虚函数指针

实际上,Base和Derive类增加的4个字节就是虚函数指针的大小,每一个类只要有虚函数(包括继承而来的),它就有且只有一个虚函数指针,类的大小就是总的成员变量的大小加上一个虚函数指针的大小。虚函数指针指向的是一张虚表,里面是这个类所有虚函数的地址,一个类对应一张虚函数表,而虚函数指针存在于每一个对象中,并且永远占据对象内存的前四个字节。

虚函数表又称为“虚表”,它在编译期间就已经确定,在程序运行时就会被装载到只读数据段,在整个程序运行期间都会一直存在。一个类实例化的多个对象,它们 的虚函数指针指向的是同一张虚表。

虚函数要求

成员函数能实现为虚函数需要满足两个前提条件: 1.成员方法能取地址 2.成员方法依赖于对象。第一点毋庸置疑,虚函数表中需要存储虚函数的地址。第二点,我们怎么调用虚函数的?通过虚函数指针来找到虚表从而调用其中的方法,而虚函数指针又存在于对象中,所以这就意味着虚函数的调用需要依赖对象。

那么,我们可以确定一些不能实现为虚函数的方法: 1.构造函数——构造函数就是用来创建对象的,如何将其实现为虚函数,使其依赖一个对象调用? 2.inline函数——内联函数直接在调用点展开,不能取地址 3.static方法——静态方法是属于整个类的,不依赖与单个对象。

成员函数能实现为虚函数需要满足两个前提条件: 1.成员方法能取地址 2.成员方法依赖于对象。第一点毋庸置疑,虚函数表中需要存储虚函数的地址。第二点,我们怎么调用虚函数的?通过虚函数指针来找到虚表从而调用其中的方法,而虚函数指针又存在于对象中,所以这就意味着虚函数的调用需要依赖对象。

前面我们探讨了那些不能实现虚函数的情况,析构函数是可以的。那么什么时候应该将析构函数实现为虚函数呢?答案是:当基类指针指向堆上开辟的派生类对象时。

静态绑定发生在编译阶段、动态绑定发生在运行阶段。