JavaScript递归、预编译篇

此篇理论性较强,建议慢慢看,慢慢理解

1. 递归 :

其实递归就是一种找规律和找出口的方法,他特别符合人的思维过程。

(1):用函数体写出 n 的阶乘1
function mun(n){
	if(n == 0 || n == 1){
		return 1;
	}
	return n * mun(n - 1);
}
console.log(mun(5)); //输出120

解析:我们先来找规律,比如说实参传一个 5 进去,我们就来求 5 的阶乘,首先,我们写的这个函数他的功能就是求 n 的阶乘的,我们发现 5 的阶乘就是 5 乘以 4 的阶乘, 4 的阶乘就等于 4 乘以 3 的阶乘,那么,n 的阶乘就是 n 乘以 n-1 的阶乘,我们这个函数就是求阶乘的,所以有 return n * mul(n - 1);好,我们来验证一下,mul(5)=5mul(4),由于前边是 return,所以这么算肯定还没完,return 最后必须返回一个具体的值才可以,所以系统会继续算 mul(4)=4mul(3),然后 mul(3)=3mul(2), mul(2)=2mul(1)……他会一直这么循环的算下去,所以递归必须找到一个出口,不然就是死循环,由于我们知道 1 的阶乘等于 1,所以我们用 if 语句表示当 i 等于 1 或 者 0 的时候返回 1,因为 0 的阶乘是 1,那么,这就可以了,反着推,mul(1)=1,mul(2)=21=2,mul(3)=32=6,mul(4)=46=24,那 mul(5)=524=120.

注意:递归必须最后用 return,return 必须返回一个具体的值,所以他会一直循环的计算。

(2):写一个函数,实现斐波那契数列2
function fb(n){
	if(n == 1 || n == 2){
		return 1;
	}
	return fb(n - 1) + fb(n - 2);
}
fb(5);  //输出5

解析:斐波那契数列就是第三位等于头两位的和,那第 n 位 fb(n)就等于 fb(n - 1) + fb(n - 2); 好,假如说我们求第 5 位,实参传 5,fb(5)= fb(4)+ fb(3) ,fb(4)= fb(3)+ fb(2), fb(3)= fb(2)+ fb(1)……好,现在我们找出口,因为我们知道前两位是 1,所以当 n = 1 或者 n = 2 时返回 1,即 fb(1)= 1,fb(2)= 1,那么 fb(3)= 1+1 = 2,fb(4)= 2+1 = 3,fb(5)= 3+2 = 5.

2. 预编译前奏 :

JavaScript 执行三部曲:我们知道,JS 是解释性语言,解释一行执行一行,但是在解释执行之前,他会进行两个过程,语法分析和预编译,语法分析就是他先会把整篇 js 代码扫描一遍,看看有没有语法错误,例如少写一个括号,标点错误等等,如果有语法错误则一行都不执行,接下来进行的就是预编译。预编译结束之后才是解释一行执行一行。在学习预编译之前我们要了解一些知识:

(1):比如说
test();   //输出 a
function test(){
	console.log("a");
} 

我们知道,如果把 test( ) 写在下边的话他会执行,但是我们现在把 test( ) 写在上边, 他其实也会执行,输出 a,我们 js 不是解释一行执行一行吗?为什么写在上边还可以执行呢?其实就是预编译的作用,再比如:

console.log(a);  //输出 undefined
var a = 123;
console.log(a);  //输出 123

我们如果把输出放在下边,会输出 123,但是反过来放在上边他会输出 undefined,我们假如没有下边的 var a = 123 就是变量未经声明就使用肯定会报错,但是有了之后为啥不报错?而且输出的是 undefined?这也是预编译的作用。鉴于以上两种情况,我们总结出两句话:
第一句:函数声明整体提升
解释:就是不管你函数声明写在哪里,他都会在逻辑上把他提到最前边,所以上边的 test()写在上边或者下边效果是一样的。
第二句:变量 只有声明提升
解释:比如说上边的 var a = 123 他其实是两个过程,变量声明加赋值,var a;a = 123; 声明提升就是他会把变量声明 var a 提到逻辑的最前边,所以输出 undefined。但是这两句话太肤浅了,比如说:

console.log(a);
function a(a){
	var a = 234;
	var a = function(){}
	a();
}
var a = 123;

这个时候 a 得啥?函数是 a,变量是 a,形参也是 a,而且不会报错,那么输出得啥? 这就是那两句话解决不了的,那两句话只是把预编译过程中的两个现象抽象出来当做方法来用,其实那不是知识点,等我们学完预编译,这个问题轻松解决。在学习预编译之前,我们还要知道以下两个知识点:

(2):imply global 暗示全局变量

即任何变量,如果变量未经声明就赋值,此变量就为全局变量所有。

解释:假如我们直接 console.log(a)这叫变量未经声明就使用肯定会报错,但是我 们 a = 10,这叫变量未经声明就赋值,系统不会报错,而且你输出 a 得 10.就好像这个变量被声明了一样,但是这个和声明的变量还是不一样。我们再来看,变量未经声明就赋值,此变量为全局变量,全局对象就是 window,这个 window 属于对象,对象上可以加一些属性,比如说 window.a=10,我们 console.log(window.a)就得到 10. 好, 现在我们单纯访问 a,即 console.log(a)也得 10.就是说未经声明就赋值的变量为全局所有也属于 window 所有,其实全局对象就是 window,即 a=10 就是 window.a=10.

(3):一切声明的全局变量,全是 window 属性

解释:全局上的任何变量即使你声明了也归 window 所有,比如说你在全局 var b=234, 那么你输出 window.b 也是 234,即全局 var b=234 就是 window.b=234。其实 window 就是全局的域,比如说我们在全局 var a=123,当我们要输出 a 的时候去哪里拿呢?就去 window 里拿,其实你 var 一个 a 等于 123,就相当于在 window 里边挂了一个 a 等于 123,所以你输出 window.a 也是 123,你下一次访问 a 的时候他就会去 window 里边找看有没有这个 a。其实你在全局 console.log(a)访问的 a 就是 window.a。 比如说:

function test(){
	var a = b = 123;
}
test();

变量赋值是从后往前的,所以这句的过程是先让 b=123 然后 var 一个 a 等于 b 的值, 所以我们这里的 b 属于未经声明就赋值的变量,所以我们输出 window.b 得 123,输出 window.a 得 undefined,因为只有声明的全局变量才属于 window,这里的 a 是局部变量。

注:涉及到作用域,详见下篇。

3. 函数预编译 :
(1):预编译四部曲

第一步:创建 AO 对象。
第二步:找形参和变量声明,将变量和形参名作为 AO 的属性名,值为 undefined。
第三步:将实参值与形参统一。
第四步:在函数体里找函数声明,值赋予函数体。
下面用几个例子来讲解预编译四部曲。
例 1:

function fn(a){
	console.log(a); // 1  输出function a(){} 
	var a = 123;    // 2	
	console.log(a); // 3  输出123
	function a(){}; // 4
	console.log(a); // 5  输出123
	var b = function (){}  // 6
	console.log(b); // 7  输出function (){}
	function d(){}; // 8
}
fn(1);

备注:为了方便起见,解析时提到的第几行就是上面代码后面的数字。
解析:这里边又有函数声明,又有变量,而且都叫 a,都提升的话,到底谁再谁前边? 谁覆盖谁?这里边有一个覆盖的问题,所以这一块就是预编译发生的过程,预编译发生在函数执行的前一刻,所以等函数执行的时候,预编译已经发生完了。预编译就把这些矛盾调和了,那么,现在就这个例子,我们来讲预编译四部曲。
第一步:创建 AO 对象,AO 对象就是 Activation Object,它是活跃对象,我们可以理解成作用域,但是他翻译过来叫执行期上下文。创建完之后就是 AO{ }(通常第一步 都是创建 AO 对象,不重要,重要的是下边的,以后解析时忽略第一步)
第二步:他会去找函数里的形参和变量声明,将变量和形参名作为 AO 的属性名,值为 undefined。在此例中,形参是 a,然后第 2 行有变量声明是 a,第 6 行有个变量声明是 b(虽然后边是函数,但是前面属于变量声明),这里边有两个 a,只记一个,所以:

AO{
a:undefined, 
b:undefined
}

第三步:将实参值与形参统一。这里的形参是 a,实参是 1,上一步的 a 是 undefined, 所以在这一步把 a 的值改为 1:

AO{
a:1, 
b:undefined
}

第四步:在函数体里找函数声明,值赋予函数体。第 4 行有一个函数声明 a,第 8 行有个函数声明是 d(第 6 行是函数表达式,不是函数声明),找到了之后,依然把函数名作为 AO 对象的属性名挂起来,值为函数体,所以这时 a 的值改为函数体,再挂个 d, 值为函数体:

AO{
a:function a(){},
b:undefined,
d:function d(){} 
}

这样,AO 对象就创建完了,预编译就完了,现在来解释执行:
第 1 行:输出 a,去哪里找 a,就去 AO 对象里找 a(不是在下边的语句找 a,你写的语句是给电脑看的,你想拿东西就必须在电脑定义的冰箱里拿,AO 就是那个冰箱。真正的存储机构是 AO,因为预编译就是把 AO 创建好方便你去用),所以第 1 行输出得 function a(){}
第 2 行:var a = 123,其实这一句不完全执行,因为在预编译的第二步将变量和形参名作为 AO 的属性名,这里就是变量声明提升的过程,变量声明已经被提升上去了,你 再声明就不用看了,因为已经看过了,所以第二句只剩下 a=123 没执行,执行后 a 的 值改为 123:

AO{
a:123,
b:undefined,
d:function d(){} 
}

第 3 行:输出 a,去 AO 对象里找,输出得 123.
第 4 行:在预编译时看过了,不用看了
第 5 行:输出 a 得 123
第 6 行:var b 看过了,执行 b = function(){}

AO{
a:123,
b:function (){},
d:function d(){} 
}

第 7 行:输出 b 得 function(){}
第 8 行:看过了
最后输出的结果:function a(){} 123 123 function(){}

例 2:
让我们理清楚思路再来一个例子:

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

第一步:创建AO对象

AO{
}

第二步: 找形参和变量声名 值为 undefined

AO{
	a:undefined,
	b:undefined
}

第三步:将实参值和形参统一:

AO{
	a:1,
	b:undefined
}

第四步:找函数声明 值赋予函数体:

AO{
	a:function a(){},
	b:undefined
}

好现在预编译完毕,解释执行 :
第一行: AO 中找 a,输出 function a(){}
第二行: AO 中找 b,输出 undefined
第三行: var b 看过直接跳过,将后面的变量赋值 = 234 赋予 AO 中的 b

AO{
	a:function a(){},
	b:234
}

第四行: AO 中找 b,输出 234
第五行: 将123 赋予 AO 中的 a

AO{
	a:123,
	b:234
}

第六行: AO 中找 a,输出123
第七行第八行: 看过了跳过
第九行: 此时 AO 中 b = 234,读到这行将 234 再赋一次,故 b 的值不变还是 234
第十行: 将函数体赋予 b

AO{
	a:123,
	b:function (){}
}

第十一行: AO 中找 a,输出123
第十二行: AO 中找 b,输出function (){}
所以输出的值以此为: function a(){} undefined 234 123 123 function (){}

4. 全局预编译 :

全局的预编译和函数里的预编译一样,只不过少了几个步骤:
第一步:创建 GO 对象。(GO 对象就是 Global Object,是全局的执行期上下文,换了个名,其实和 AO 是一样的)
第二步:找变量声明,将变量名作为 GO 的属性名,值为 undefined。
第三步:找函数声明,值赋予函数体。
示例:

var a = 123;       // 1
function a(){};    // 2
console.log(a);    // 3  输出123

解析:第一步跳过,执行第二步

GO{
	a: undefined
}

第三步:将函数体 function a(){} 赋予 GO 中的 a

GO{
	a: function a(){}
}

此时预编译完成解析执行:
第一行: 将123赋予 GO 中 a

GO{
	a: 123
}

第二行: 预编译时看过 此时跳过
第三步: AO 中找 a,输出123
最后输出 a 得 123.

其实我们上边讲的 window 就是 GO,GO 就是一个筐,为了我们拿东西方便,这个筐还有一个名就是 window。我们定义完的东西要放到 GO 里边储存,也就是放到 window 里边储存,所以 window 自来就有了这些变量的引用。你在全局访问 a 就拿的是 GO 里的 a 也是 window 里的 a。我们上边讲的未经声明就赋值的变量归 window 所有也就是归 GO 所有。

能看到这里你已经成功一半了,看到这里你还能理解下去那么恭喜你,比我强,反正作者本人在学习渡一公开课第一遍时学到这里时蒙圈的。。。

最近几篇采用了大量渡一教育的内容,如感兴趣请自行到官网学习,讲的真的很好,最近也长篇大论的讲了各种原理,导致可能看不明白,不过别担心,下一篇我会精心准备一些例题来巩固这些知识。

声明:作者通过观看渡一教育免费公开课及相关文档总结的笔记,不做任何商业用途,仅供学习交流,感谢指正,如有侵权烦请立马联系,欢迎转载,请注明出处。


  1. 阶乘的概念
    阶乘是基斯顿·卡曼(Christian Kramp,1760~1826)于 1808 年发明的运算符号,是数学术语。一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。1808年,基斯顿·卡曼引进这个表示法。
    阶乘的计算方法
    任何大于等于1 的自然数n 阶乘表示方法:n! = 1 * 2 * 3 * … * (n - 1)nn! = n (n - 1)! ↩︎

  2. 斐波那契数列
    斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(1)=1F(2)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从 1963 年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。 ↩︎

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山大王杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值