JavaScript 预解析/预解释
JavaScript 是弱类型、解释型的脚本语言。
JavaScript 是解释型计算机语言,在程序执行之前,会先将定义的程序,预先解释,也就是预先模拟执行一遍,会找程序中的关键词 var function 两个关键词。
实例
// 先输出
// 如果是没有使用 var 关键词声明的变量,执行结果是会报错
// 如果使用 var 关键词声明的变量,执行结果是 undefined
// 如果是报错,之后的所有程序,都不会执行
// 如果是 undefined 之后程序还可以继续执行
// 实际项目中,会有这样提前使用变量的操作
// 如果不是使用 var 声明的变量,会报错,整个程序就无法进行了
console.log(int);
// 后定义变量
var int = 100;
console.log('我是之后的其他程序');
fun();
function fun(){
console.log(123);
}
这个效果,也就是使用 var 定义的变量,可以先调用,结果是undefined,不会报错;使用 function 声明的函数,可以先调用,后执行,能正常调用函数,都是JavaScript程序预解释/预解析的结果。
预解析原理
预先读取JavaScript中的所有程序内容,找到所有 var 和 function 关键词,来进行预解释和预解析,告诉计算机程序,有哪些变量是使用 var 关键词声明。
如果提前使用 var 声明的变量,告诉计算机程序,这个变量已经存在,只是当前没有赋值,执行结果是undefined,不会报错;如果是使用 function 声明的函数,告诉计算机程序,这个函数已经存在,并且告诉计算机这个函数的内存地址,可以正常调用使用这个函数。
实例
// 提前调用函数和变量
console.log(a,b);
console.log(c,d);
fun1();
fun2();
fun3();
// 定义函数和变量
var a = 100;
var b = 200;
c = 300;
d = 400;
function fun1(){}
function fun2(){}
var fun3 = function(){};
// 正常调用
console.log(a,b);
console.log(c,d);
fun1();
fun2();
fun3();
原理分析
1. JavaScript会预读所有的程序代码
2. 获取所有通过 var 和 function 定义的变量和函数:
var 定义的变量有 a,b,fun3;
function 定义的函数有 fun1,fun2。
提前给变量 a,b,fun3 中存储信息 undefined;
提前给变量 fun1,fun2 中存储的是 内存地址。
以上步骤是预读和存储信息的步骤。
3. 执行代码时:
提前调用变量:a,b 此时记录的信息中有a,b 执行结果是 undefined
c,d 没有在记录信息中 执行结果是 报错
提前调用函数:fun1,fun2时,都会读取其中提前存储的内存地址,正确调用函数。
fun3 时,执行的是 undefined,不是内存地址,无法调用函数。
以上是提前调用函数变量,执行的预解析的过程。
执行赋值之后,var 声明的变量,会重新赋值,覆盖之前的 undefined
function 声明的函数,会重新赋值,仍然是内存地址
特别强调
对预解析所谓的变量提升理解,是不准确的。
错误说法:
将 var,function声明的变量和函数,在程序的最开始重新定义,提升变量、函数和定位的位置。
在JavaScript的源码中是没有这样的操作的。
预解析/预解释的无下限操作
只要是写在程序中的 var 和 function关键词,都会被预解析,不管程序会不会被执行,都会执行预解析。
如果是不执行的程序, var 会进行预解析,如果提前使用,结果是 undefined 。
如果是函数, function 会预解析,但是不执行的函数是不能调用的。
函数不能调用的原因:预解析会执行,也会存储内存地址,但是程序不执行,这个内存地址中就没有内容,执行结果会报错。
实例
console.log(int);
fun(); // 结果会报错
if(false){
// 当if判断,条件执行结果是true时,才会执行程序
// 如果条件执行结果是false,var int = 100 是不会执行的
var int = 100;
function fun(){
console.log(123);
}
}
console.log(int);
fun();
JavaScript 作用域
变量的作用域
所谓的变量的作用域,就是变量的使用范围,每一个变量都有自己的使用范围。
变量的作用域分为两种
1. 定义在函数之内的变量
包括函数的参数,定义在函数内部的变量,都称为局部作用域变量 / 局部变量。
局部变量只能在函数内容部使用,如果外部想要直接调用,是不行的。
实例
// 此时函数中定义的变量,参数a和变量b,都是局部作用域变量
// 只能在函数内部被调用使用
// 函数外部不能直接使用调用
// 只要是在函数外部,调用函数内部的变量,都是报错
function fun1(a){
var b = 100;
console.log(a,b); // 函数内部调用
}
fun1('北京');
console.log(a,b); // 函数外部调用,报错
2. 定义在函数之外的变量
定义在函数外部的变量,称为全局作用域变量/全局变量。
在函数的内部是可以使用全局作用域变量的,但是是有条件的,具体情况看函数变量的调用赋值原则。
实例
var int = 100;
function fun2(){
console.log(int);
}
fun2();
函数变量的调用赋值原则
调用变量的原则
会先在当前作用域中,找是否有这个变量。
如果有,就直接使用这个变量;如果没有,去父级作用域寻找这个变量。
如果父级作用域有这个变量,就使用这个变量;如果父级作用域没有这个变量,再向上一层寻找,如果找到这个变量,就直接使用。
如果所有的作用域都没有这个变量,执行结果是报错。
只会向父级作用域找变量,不会向子级作用域找变量。
函数之外的调用,只能调用全局作用域的变量。如果全局作用域没有该变量,则会调用函数内部赋值语句升级定义的全局变量。
赋值变量的原则
会先在当前作用域中,找是否有这个变量。
如果有,就直接对这个变量赋值;如果没有,去父级作用域寻找这个变量。
如果父级作用域有这个变量,就对这个变量赋值;如果父级作用域没有这个变量,再向上一层寻找,如果找到这个变量,就直接进行赋值。
如果所有的作用域都没有这个变量,赋值语句会升级为定义变量语句,并且定义的是全局作用域变量。
只会向父级作用域找变量,不会向子级作用域找变量
实例
赋值和调用情况1
// 只有局部变量int,没有全局变量int
function fun(){
var int = 100;
int = 200; // 赋值的是局部int
console.log(int); // 调用的是局部int 200
}
fun();
console.log(int); // 调用全局int,不存在,报错
赋值和调用情况2
// 只有全局变量int,没有局部变量int
var int = '北京';
function fun(){
int = 200; // 赋值的是全局int
console.log(int); // 调用的是全局int 200
}
fun();
console.log(int); // 调用全局int 200
赋值和调用情况3
// 全局、局部作用域都有变量int
var int = '北京';
function fun(){
var int = 100;
int = 200; // 赋值的是局部int
console.log(int); // 调用的是局部int 200
}
fun();
console.log(int); // 调用全局int '北京'
赋值和调用情况4
// 全局、局部作用域都没有变量int
function fun(){
int = 200; // 赋值语句升级为定义变量语句,定义int并且赋值200,int还是一个全局变量,函数外部能调用
console.log(int); // 调用的是函数内的int 200,但是是全局变量
}
fun();
console.log(int); // 调用的是函数内部,升级为全局变量的int 200
递归函数
定义
递归函数是一种特殊的函数,就是在函数内部,调用函数自己本身。
实际当中,我们基本上用不到递归函数,只是作为概念,了解掌握几个相应的demo就可以了。
语法
function fun1(){
// 在函数fun1中调用函数fun1自己本身
fun1()
}
实例
// 倒序输出
for(var i = 5 ; i >= 1 ; i--){
console.log(i);
} // 输出结果是 5,4,3,2,1
// 递归方式输出
function fun(num){
num--;
if(num >=1){
fun(num);
}
// 将执行的输出,写在递归调用之下
console.log(num); // 输出结果是 0,1,2,3,4
}
fun(5);
递归原理分析:
第一次调用函数,实参赋值是5
执行程序num-- 执行结果是4,判断true,调用函数本身,此时先调用函数,先不输出
第二次调用函数,实参赋值是4
执行程序num-- 执行结果是3,判断true,调用函数本身,此时先调用函数,先不输出
第三次调用函数,实参赋值是3
执行程序num-- 执行结果是2,判断true,调用函数本身,此时先调用函数,先不输出
第四次调用函数,实参赋值是2
执行程序num-- 执行结果是1,判断true,调用函数本身,此时先调用函数,先不输出
第五次调用函数,实参赋值是1
执行程序num-- 执行结果是0,判断false,不再执行调用函数,执行 console.log,输出程序,此时,输出的数值是0
执行完输出,当前函数的执行完毕,继续执行之前的输出。
执行 console.log,输出程序,此时,输出的数值是1
执行完输出,当前函数的执行完毕,继续执行之前的输出。
执行 console.log,输出程序,此时,输出的数值是2
执行完输出,当前函数的执行完毕,继续执行之前的输出。
执行 console.log,输出程序,此时,输出的数值是3
执行完输出,当前函数的执行完毕,继续执行之前的输出。
执行 console.log,输出程序,此时,输出的数值是4
执行完输出,当前函数的执行完毕,所有的函数执行都完毕,整个递归结束。
递归总结
1. 调用递归函数,一定要有条件,无条件的调用递归函数,会一直执行调用,不会停止。
2. 调用递归函数,进入递归函数,会由外至内执行,结束递归函数,会从内至外执行。