Five
一: 递归函数(Recursion Function)
1.1 什么是递归函数?
一个函数在其内部调用自身的操作,这个函数就是递归函数。
- 递归是一种应用非常广泛的算法(编程技巧)。
- 递归被用于处理包含有更小的子问题的一类问题。
1.2 递归的例子 --- > 引用源(数据结构与算法之美)
周末你带着女朋友去电影院看电影,女朋友问你,咱们现在坐在第几排啊?电影院里面太黑了,看不清,没法数,现在你怎么办?
别忘了你是程序员,这个可难不倒你,递归就开始排上用场了。于是你就问前面一排的人他是第几排,你想只要在他的数字上加一,就知道自己在哪一排了。但是,前面的人也看不清啊,所以他也问他前面的人。就这样一排一排往前问,直到问到第一排的人,说我在第一排,然后再这样一排一排再把数字传回来。直到你前面的人告诉你他在哪一排,于是你就知道答案了。
这就是一个非常标准的递归求解问题的分解过程,去的过程叫“递”,回来的过程叫“归”。 递推公式
f(n)=f(n-1)+1 其中,f(1)=1
1.3 递归需要满足的三个条件
上例是非常典型的递归,那究竟什么样的问题可以用递归来解决呢?只要同时满足以下三个条件,就可以用递归来解决。
- 一个问题的解可以分解为几个子问题的解
- 何为子问题?子问题就是数据规模更小的问题。比如,前面讲的电影院的例子,要知道,“自己在哪一排”的问题,可以分解为“前一排的人在哪一排”这样一个子问题。
- 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
- 电影院那个例子,求解“自己在哪一排”的思路,和前面一排人求解“自己在哪一排”的思路,是一模一样的。
- 存在递归终止条件
- 把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件。
- 还是电影院的例子,第一排的人不需要再继续询问任何人,就知道自己在哪一排,也就是 f(1)=1,这就是递归的终止条件。
1.4 如何编写递归代码?
分析问题并找出其中的规律,在写出递推公式。
找出终止条件。
1.4.1代码演示
//函数:计算一个正整数的阶乘
//迭代写法 for循环
let result = 1;
function factorial(n){
for(let i = 1; i <= n; i++){
result *=i;
}
console.log(result);
}
console.log(factorial(5));
//120
//函数:计算一个正整数的阶乘
//递归写法
//阶乘正向: 1*1=1 1*2=2 2*3=6 6*4=24 24*5=120
//阶乘反向: 120=5*24 24=4*6 6=3*2 2=2*1 1=1*1
//找出规律: f(n)=5*24 f(n)=4*f(4-1) f(n)=3*f(3-1)
//递推公式: f(n)=n*f(n-1)
//终止条件: n <= 1
//f(5) = 5 * f( 5 - 1 ) ---> n = 5 ---> 条件判断 5 <= 1 不成立(false) ---> f(5) = 5 * f(4) ---> f(5) = 5 * 24 ---> 返回 120
//f(4) = 4 * f( 4 - 1 ) ---> n = 4 ---> 条件判断 4 <= 1 不成立(false) ---> f(4) = 4 * f(3) ---> f(4) = 4 * 6 ---> 返回 24
//f(3) = 3 * f( 3 - 1 ) ---> n = 3 ---> 条件判断 3 <= 1 不成立(false) ---> f(3) = 3 * f(2) ---> f(3) = 3 * 2 ---> 返回 6
//f(2) = 2 * f( 2 - 1 ) ---> n = 2 ---> 条件判断 2 <= 1 不成立(false) ---> f(2) = 2 * f(1) ---> f(2) = 2 * 1 ---> 返回 2
//f(1) = 1 * f( 1 - 1 ) ---> n = 1 ---> 条件判断 1 <= 1 成立(true) --->返回 1
//f(0) = 0 * f( 0 - 1 ) ---> n = 0 ---> 条件判断 0 <= 1 成立(true) --->返回 1
function factorial(n){
if(n <= 1){
return 1;
}
return n * factorial(n - 1);
}
factorial(5);//120
1.5在JavaScript中递归的注意事项
使用递归时,应优先考虑使用迭代方法(for循环),因为迭代方法的效率通常比递归方法高。
如果必须使用递归,应确保递归层数、处理数据量、递归次数的量不能太大,否则会出现栈溢出错误。
出现栈溢出错误时,使用console.log()语句添加一个计数,来判断递归深度是否超出,并进行优化或使用其它方法解决问题。
1.5.1代码演示
//记录递归调用的次数,当大于这个数的时候,手动设置报错信息
let count=0;
function sum(n) {
count++;
if(count>10000){ //当然这个数字事先无法估算,只能凭经验适当调整
console.error('超出最大调用次数');
return;
}
if (n == 1){
return 1;
}
return sum(n - 1) + n
}
sum(99999);
二:预编译前置知识
2.1 JavaScript引擎执行机制
- 语法分析 ---> JavaScript引擎通篇检查JS代码是否有低级的语法错误。
- 预编译 --> 可以简单理解为在内存中开辟一些空间,存放一些变量与函数。
- 解释执行 ---> JavaScript引擎解释一行JS代码并执行一行JS代码。
2.2代码演示 :语法分析
//实例一
console.log(a); //此时的结束分号为中文输入法下的分号,语法分析,并报错。
console.log(a); //正确语句也将不会被执行
//打印结果:Uncaught SyntaxError: Invalid or unexpected token
//语法错误: 无效或意外标记
//实例二
console.log(a); //将正确语句移到错误语句前
console.log(a); //错误语句
console.log(a); //正确语句
//打印结果:Uncaught SyntaxError: Invalid or unexpected token
//语法错误: 无效或意外标记
//代码分析:
//通过上述两例,可以验证并发现,JavaScript引擎,并不是读取代码后,就立即解释一行执行一行。
//而是,先通篇检查代码是否有低级的语法错误,这就是JavaScript引擎执行机制的第一步,语法分析。
2.2 变量声明提升
变量只有声明提升,赋值不提升。
无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined。
2.2.1 代码演示:变量声明提升现象
//实例一 现象1,打印a变量。
console.log(a);
var a =10;
//打印结果:undefined
//打印结果为undefined,为什么呢?在看下一个实例。
//实例二 现象2,打印a变量。
console.log(a);
var a;
//打印结果:undefined
//去除10赋值给a变量后,但声明了变量a,打印结果还是undefined,为什么呢?在看下一个实例。
//实例三 现象3,打印a
console.log(a);
//var a; 注释这段代码
//打印结果:Uncaught ReferenceError: a is not defined
//把变量声明给注释后,就会报引用错误,这个正常,无需解释。在看下一个实例。
//实例四 现象4,打印a变量。
var a;
console.log(a);
//打印结果:undefined
//把声明变量a,移动到console.log()语句前,打印结果还是undefined,由此可见,实例四与实例六(现象2和现象4)表现是一样的。
//分析:
//实例一、二、四的打印结果都为undefined,而undefined是一个原始数据类型,表示未定义或未赋值的变量。由此可见实例一中,在console.log()语句内的变量a只声明而已,并没有赋值。所以才会出现undefined。
//同时在实例二和四中,声明变量的书写位置发生前后变化,但表现还是一样。
var a = 2;
//这段代码正确的理解是:
//第一步. 先声明一个变量a;
//第二步. 在将2赋值给了变量a。
2.3:函数声明整体提升
无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面。
2.3.1代码演示:函数声明整体提升现象
//实例一 现象1,函数声明后并执行。
function test(){
console.log('函数声明,再执行函数!')
}
test();
//打印结果:函数声明,在执行函数!
//正常现象
//实例二 现象2,函数执行在前,函数声明在后,正常执行,不报错。
test();
function test(){
console.log('函数执行在前,函数声明在后!')
}
//打印结果:函数执行在前,函数声明在后!
//函数执行在前与在后,表现都一样。
//分析:
//通过现象1与现象2两例,可以验证并发现,函数执行(函数调用),可以不按照先后顺序来执行。
2.4 变量声明与函数声明 提升总结
函数声明提升与变量声明提升的优先级。 函数在前,变量在后!
2.5 暗示全局变量(imply global)
2.5.1 什么是全局?
在 JavaScript 中,全局是指在所有作用域中都可见的变量、函数或对象。全局变量和函数存储在全局作用域中,即
window对象中。全局变量、函数和对象可以从任何作用域中访问。在 JavaScript 中,全局作用域是指整个 JavaScript 文件。
例如,在函数中可以访问全局变量,而在全局作用域中也可以访问函数。
全局变量、函数和对象可以通过
window对象访问。window对象是 JavaScript 中的一个特殊对象,它表示浏览器窗口。例如,可以使用
window.variable访问全局变量variable,也可以使用window.function()调用全局函数function()。
// 全局变量
var a = 10;
// 全局函数
function foo() {
console.log("Hello, world!");
}
// 全局对象
const obj = {
name: "John Doe",
age: 30,
};
// 使用 `window` 对象访问全局变量、函数和对象
console.log(window.a); // 10
window.foo(); // Hello, world!
console.log(window.obj); // { name: "John Doe", age: 30 }
2.5.2 什么是暗示全局变量?
暗示全局变量(imply global)是指未使用
var关键字声明的变量。暗示全局变量将存储在全局作用域中,即window对象中。暗示全局变量有以下几个特点:
- 变量未使用
var关键字声明。- 变量存储在全局作用域中。
- 变量对所有函数可见。
//暗示全局变量,未使用var关键字声明。
a =1;
console.log(a);
console.log(window.a);
//打印结果:
//1
//1
//在上述示例中,变量 a 未使用 var 关键字声明,因此它是一个暗示全局变量。该变量将存储在全局作用域中,即 window 对象中。
//验证全局变量存储在全局作用域中,同时暗示全局变量也在全局作用域中,也就是window对象中。
var a = 1;
b = 2;
console.log('正常访问' + 'a' + '的值为' + a );
console.log('正常访问' + 'b' + '的值为' + b );
console.log('window对象' + 'a' + '的值为' + window.a );
console.log('window对象' + 'b' + '的值为' + window.b );
//打印结果:
//正常访问a的值为1
//正常访问b的值为2
//window对象a的值为1
//window对象b的值为2
//------------分割线------------
//暗示全局变量对所有的函数可见
//全局变量也是一样
var a = 1;
function test1(){
var b = 2; //函数局部变量
c = 3; //暗示全局变量
console.log(a); //全局变量可以在函数内部中被访问
}
test1();
//打印结果:1
function test2(){
console.log('This is the value of the global variable a' + a ); //全局变量打印
console.log('This is the value of the global variable c' + c ); //暗示全局变量打印
}
test2();
//打印结果:
//This is the value of the global variable a 1
//This is the value of the global variable c 3
function test3(){
console.log('This is the value of a imply global variable b.' + b ); //test1()函数的局部变量,打印报错
}
test3();
//打印结果:Uncaught ReferenceError: b is not defined (b 未被定义)
//原因:因为是b是test1()函数的局部变量。归属与test1()函数,其他函数以及其它作用域中不可访问。
var a = 1;
function test1(){
var b = 2; //函数局部变量
c = 3; //暗示全局变量
console.log(a); //全局变量可以在函数内部中被访问
}
test1();
//打印结果:1
console.log(window.b); //undefined ,在全局访问,此时window对象中没有对应的b,所以才会出现undefined
console.log(b); //报错 Uncaught ReferenceError: b is not defined (b 未被定义),全局作用域中 b 没有定义,所以直接报错
三:预编译
预编译发生在代码执行的前一刻,预编译简单理解,就是在内存中开辟一些空间,用来存储变量和函数。
JavaScript 预编译分为:
- 全局对象(GO---Global Object),也叫全局执行期上下文,应用在整个JavaScript文件执行前一刻;
- 全局对象,解析全局代码时被创建
- 活跃对象(AO---Activation Object),也叫函数执行期上下文,应用于函数执行前的一刻。
- 活跃对象,解析函数代码时被创建
知识回顾:
var a = 1这个语句中,var a是变量声明,a = 1是赋值。 个人理解:预编译,就是在执行 变量声明提升 函数声明整体提升。
3.1 预编译之活跃对象(AO)
AO预编译过程:
(1)创建AO对象;
(2)寻找形参和变量声明,将形参名和变量名作为 AO 的属性名,值为undefined;
形参名,只在函数名后的小括号()内寻找形参名。
变量声明,如遇与形参同名的变量声明,则忽略。
- 函数表达式,同样也是变量声明,需添加到AO对象中。
- 暗示全局变量,不参与,预编译过程。
(3)将实参值与形参统一,也就是实参值赋给形参;
(4)寻找函数声明,将函数名作为 AO 对象的属性名,值为整个函数体。
3.1.1 代码演示:AO预编译解析范例
function testAo(a){ |//第一步:创建AO对象
console.log(a);//function a(){} |//AO = {
var a = 1; |//第二步:寻找形参与变量声明,将形参名和变量名作为AO的属性名,值为undefined
console.log(a);//1 |//a:undefined(形参名)
function a(){} | //b:undefined(函数表达式,同样也是变量声明)
console.log(a);//1 | //第三步:将实参值赋值给形参
var b =function(){} | //a:undefined ---> 2(值覆盖)
console.log(b)//function (){} | //b:undefined(保持不变)
function d(){} | //第四步:寻找函数声明,函数名作为AO对象名,值为函数体
} | //a:undefined ---> 2 ---> function a(){}(值又被覆盖)
| //b:undefined
testAo(2); | //d:function d(){}
| //}
--------------------------------------------------------------------------------------------------------------------------
Explanation:根据本例来解析函数预编译中的细节
第一步:根据当前函数,创建临时AO对象;
第二步:寻找形参与变量声明,将形参名与变量名作为AO的属性名,值为undefined;
2.1、形参:只在函数名后的小括号()内寻找形参名;
2.2、变量声明:遇到与形参同名的变量声明,则忽略;
2.3、函数表达式,同样是变量声明,需要参与到第二步中,并添加到AO对象。
第三步:将实参值赋值给形参;
3.1、赋值:也就是将形参默认的undefined值,覆盖,换成实参值。
第四步:寻找函数声明,函数名作为AO对象名,值为函数体;
4.1、遇到与形参同名的函数声明,则将实参赋给形参的值覆盖,换成当前的函数声明的函数体;
4.2、遇到与变量同名的函数声明,则将变量声明的undefined值覆盖,换成当前的函数声明的函数体。
---------------------------------------------------------------------------------------------------------------------------
//Print result and explanation:
//第一次console.log(a):function a(){} ---> Why?
//两种理解方式,也可以说是一种:1:预编译过程中,已经进行了a值的覆盖;2:变量声明与函数声明整体提升的提升现象。
//所以,结果为function a(){}。
//第二次console.log(a):1 ---> Why?
//在打印语句上一行的语句(var a = 1;)已经对局部变量a重新赋值。
//而在打印语句下一行的语句(function a(){}),在预编译过程中已操作过,所以函数调用执行时,会对其进行忽略处理。
//所以,结果为1。
//第三次console.log(a):1 ---> Why?
//解释同上。所以,结果还是为1。
//第四次console.log(a):function (){} ---> Why?
//在打印语句的上一行的语句为(var b =function(){})函数表达式(匿名函数),又根据函数表达式的特性,是将右边赋值给左边。
//所以,结果为函数体。
3.1.2 代码演示:函数预编译解析之变量与函数提升现象
预编译需要与实际执行的代码流相结合
//现象1 预编译过程 //现象2
//区别:与现象2 代码,9 行与 10 行调换位置 区别:无任何区别 //区别:与现象1代码,9 行与 10 行调换位置
---------------------------------------------------------------------------------------------------------------------------
function test(a,b){ | //第一步:创建AO对象 |function test(a,b){
console.log(a);//1 | //AO = { |console.log(a);//1
c = 0; | //第二步:寻找形参与变量声明 |c = 0;
var c; | //a:undefined |var c;
a = 5; | //b:undefined |a = 5;
b = 6; | //c:undefined |console.log(b);//function b(){}
console.log(b);//6 | //第三步:将实参值赋值给形参 |b = 6;
function b(){} | //a:undefined ---> 1 |function b(){}
function d(){} | //b:undefined |function d(){}
console.log(b);//6 | //c:undefined |console.log(b);//6
} | //第四步:寻找函数声明 |}
test(1); | //a:undefined ---> 1 |test(1);
| //b:undefined ---> function b(){} |
| //c:undefined |
| //d:function d(){} |
---------------------------------------------------------------------------------------------------------------------------
Question:
现象1:为什么第二次的 console.log(b) ,不是打印 function b(){} 而是打印 6 ?
现象2:为什么 9 行与 10 行调换位置后,第二次的 console.log(b); ,打印的是 function b(){} ?
Explanation:
现象1:因为在预编译的过程中,b 的值已经发生了变化 b:undefined ---> function b(){} ,同时在函数调用执行时,按照代码书写顺序,执行到第 9 行时,b 的值又重新被赋值为 6 ,所以第二次的 console.log(b) 打印结果为 6 。
现象2:因为在预编译的过程中,b 的值已经发生了变化 b:undefined ---> function b(){} ,又因 b 重新赋值语句书写位置在第二次 console.log(b) 语句后,所以在函数调用执行时,打印出来 b 的值为 function b(){} ,第三次打印才是 6 。
3.2 预编译之全局预编译(GO)
预编译步骤:
(1)创建GO对象。GO即 Global Object 活跃对象。
(2)找变量声明,将变量作为GO 的属性名,值为undefined。
(3)查找函数声明,函数名作为 GO 对象的属性名,值为整个函数体。
3.2.1 代码演示:GO预编译解析范例
var a = 1; | //第一步:创建GO对象
function a(){ | //GO = {
console.log(a); | //第二步:寻找变量声明,将变量名作为GO的属性名,值为undefined
} | //a:undefined
console.log(a);//1 | //第三步:寻找函数声明,函数名作为GO对象名,值为函数体
| //a:undefined ---> function a(){}
---------------------------------------------------------------------------------------------------------------------------
Question:
1:为什么函数内 console.log 语句没有打印出来?
2:为什么第五行 console.log 语句打印 1 而不是 function a(){} ?
3:如果 a 函数调用执行,此时的 a 会时什么?又为什么呢?
Explanation:
1:因为 a 函数并没有调用执行。
2:是因为在预编译过程中,a 的值是 a 函数体,当以上代码在执行时, a 又被重新赋值为 1 ,所以就把预编译过程中得到的值覆盖掉了。
3:如果 a 函数调用执行,则 a 的值为1,因为在当前的 a 函数体内,没有 a变量,此时会向上找,也就是去全局中找,如果有,就会拿过来,并显示,如果没有,则会报错!
---------------------------------------------------------------------------------------------------------------------------
console.log(a, b);//function a(){} undefined | //第一步:创建GO对象
function a(){} | //GO = {
var b = function(){} | //第二步:寻找变量声明,将变量名作为GO的属性名,值为undefined
| //a:undefined
| //b:undefined
| //第三步:寻找函数声明,函数名作为GO对象名,值为函数体
| //a:undefined ---> function a(){}
| //b:undefined
---------------------------------------------------------------------------------------------------------------------------
question:
1:为什么打印 b 时,显示的 undefined ?而不是 function (){} ?
Explanation:
1:因为预编译,匿名函数不参与预编译;同时也是因为代码书写位置不同,打印的结果也会不同。如果是放在 console.log() 语句前,则打印的就是 b 的匿名函数体。且 JavaScript 是解释一行,执行一行。
3.3 全面的预编译(GO 与 AO)
GO 对象何时被创建?在 JavaScript 代码加载时就已经创建好了,并且在整个程序运行期间一直存在。
AO 对象何时被创建?AO 对象是在函数调用执行前一刻被临时创建,函数执行完毕后,AO 对象就会被销毁。
按照先 GO 后 AO 的步骤,在 AO 时,遇到暗示全局变量,则同步更新到 AO 对象中去,而 AO 也继续走下去,不会中断暂停等待!
3.3.1 代码演示
var b = 3; | //第一步:创建GO对象
console.log(a); //function a(){...} | //GO = {
function a(a){ | //第二步:寻找变量声明,将变量名作为GO的属性名,值为undefined
console.log(a);//function a(){} | //b:undefined
var a = 2; | //第三步:寻找函数声明,函数名作为GO对象名,值为函数体
console.log(a);//2 | //a:function a(){...}
function a(){} | //}
var b = 5; | //第四步:创建AO对象
console.log(b);//5 | //AO = {
} | //第五步:寻找形参与变量声明,形参与变量名作为AO对象名,值为undefined
a(1); | //a:undefined
| //b:undefined
| //第六步:将实参值赋值给形参
| //a:undefined ---> 1
| //b:undefined
| //第七步:寻找函数声明,函数名作为AO对象名,值为函数体
| //a:undefined ---> 1 ---> function a(){...}
| //b:undefined
---------------------------------------------------------------------------------------------------------------------------
Question:
1:GO 和 AO 是同步创建的吗?
Explanation:
1:不是同步创建的:
1.1 GO 是在 JavaScript 代码加载时就已经创建好了,并且在整个程序运行期间一直存在;
1.2 AO 对象是在函数调用执行前一刻被临时创建,函数执行完毕后,AO 对象就会被销毁。
---------------------------------------------------------------------------------------------------------------------------
a = 1; | //第一步:创建GO对象
function test(){ | //GO = {
console.log(a);//undefined | //第二步:寻找变量声明,将变量名作为GO的属性名,值为undefined
a = 2; | //a:undefined
console.log(a);//2 | //第三步:寻找函数声明,函数名作为GO对象名,值为函数体
var a = 3; | //test:function test(){...}
console.log(a);//3 | //第四步:创建AO对象
} | //AO = {
test(); | //第五步:寻找形参与变量声明,形参与变量名作为AO对象名,值为undefined
var a; | //形参没有,找变量声明
| //a:undefined
| //第六步:将实参值赋值给形参
| //没有实参值
| //a:undefined
| //第七步:寻找函数声明,函数名作为AO对象名,值为函数体
| //a:undefined
---------------------------------------------------------------------------------------------------------------------------
function test(){ | //第一步:创建GO对象
console.log(b);//undefined | //GO = {
if(a){ | //第二步:寻找变量声明,将变量名作为GO的属性名,值为undefined
var b = 2; | //a:undefined
} | //第五步:同步更新 暗示全局变量
| //c:undefined
c = 3; | //第三步:寻找函数声明,函数名作为GO对象名,值为函数体
console.log(c);//3 | //a:undefined
} | //test:function test(){...}
var a; | //第四步:创建AO对象
test(); | //AO = {
a = 1; | //第五步:寻找形参与变量声明,将形参和变量名作为AO的属性名,值为undefined
console.log(a);//1 | //无形参,继续找变量
| //b:undefined
| //第六步:将实参值赋值给形参
| //无实参
| //b:undefined
| //第七步:寻找函数声明,函数名作为AO对象名,值为函数体
| //b:undefined
--------------------------------------------------------------------------------------------------------------------------
Question:
1:第 51 行, c = 3 ,这条语句为暗示全局变量,在预编译中,会发生什么?
Explanation:
1:按照先 GO 后 AO 的步骤,在 AO 时,遇到暗示全局变量,则同步更新到 AO 对象中去,而 AO 也继续走下去,不会中断暂停等待!