JavaScript学习笔记5

111 阅读19分钟

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(){}
 ---------------------------------------------------------------------------------------------------------------------------
Question1:为什么函数内 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
 ---------------------------------------------------------------------------------------------------------------------------
Question1GOAO 是同步创建的吗?

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
 --------------------------------------------------------------------------------------------------------------------------
Question1:第 51 行, c = 3 ,这条语句为暗示全局变量,在预编译中,会发生什么?                
Explanation1:按照先 GOAO 的步骤,在 AO 时,遇到暗示全局变量,则同步更新到 AO 对象中去,而 AO 也继续走下去,不会中断暂停等待!