函数的执行顺序
1.语法分析(扫描一遍,看有没有语法错误之类的)
2.预编译
3.解释执行(解释一行,执行一行)
预编译和执行是分不开的,预编译就发生在函数执行的前一刻。预编译完成,马上逐行解释执行。下面讲预编译,里边含有解释执行的东西。
先铺垫几个知识点
函数声明整体提升
变量,声明提升
暗示全局变量,任何变量如果未经声明就赋值,此变量归全局所有。一切声明的全局变量,全是window的属性,比如b=3;意思就是window.b=3
变量,声明提升
var a=123相当于,var a;a=123;变量,
声明提升,把var a提升了,打印的话就是undefined
预编译四部曲:
1.创建AO对象
2.找形参和变量声明,将形参名和变量名作为AO属性名,增加到AO里面,并且赋值undefined
3.将实参赋值给形参
4.在函数体里找函数声明,值赋予函数体
第一步:
生成一个活动对象(Active Object),简称AO,又叫做执行期上下文
函数在运行前,会执行词法分析
第二步:
分析形参和变量声明,如var age;或var age=25;
AO.age=undefine
第三步:
将实参赋值给形参,如果实参age=30,那么AO里面也跟着变化,这时候
AO.age=30
第四部:
分析函数的声明,如果有function age(){}
把函数赋给AO.age ,覆盖上一步分析的值
下面是一个例子,详细讲述函数执行三部曲,AO执行四部曲的
<script>
function fn(a) {
console.log(a);
var a=123; //相当于var a并且赋值为123
console.log(a);//第三行
function a() { } //这个叫函数声明
console.log(a);
var b=function () { } //这个叫函数表达式,是b的声明赋值
console.log(b);
function d() { } //这个叫函数声明
}
fn(1);
//首先进行语法扫描分析,没发现语法错误,就开始预编译,然后逐行解释执行
//下面重点讲预编译四部曲
/*1.创建AO对象
2.找形参和变量声明,将形参名和变量名作为AO属性名,
增加到AO里面,并且赋值undefined,这时候
AO:{
a:undefine,(形参是a,变量声明也有var a,那就只留一个)
b: undefine,(变量声明var a和var b)
}
3.将实参赋值给形参,这时候
AO:{
a:undefine—>1,(实参a是1,这时候a=1)
b: undefine
}
4.分析函数的声明,如果有同名的就覆盖,因为函数声明优先级最高,这时候
AO:{
a:undefine—>1—>function a() { },(有同名的函数声明,就覆盖)
b: undefine,(b不变)
d:function d() { } (增加了一个d,属于函数声明)
}
下面进入解释一行,执行一行的阶段,已经看过的就不再看了(比如var a)
第一行:console.log(a);从AO里边找,打印出函数体
第二行,var a已经不看了,只剩下赋值123了,这时候AO有变化
AO:{
a:undefine—>1—>function a() { }—>123,
b: undefine,
d:function d() { }
}
第三行:console.log(a)打印出123
第四行:也不看了,之前已经提升过了
第五行:console.log(a)也打印出123
第六行:var b就不看了,只是把b赋值为函数体,这时候又修改AO
AO:{
a:undefine—>1—>function a() { }—>123,
b: undefine—>function () { },
d:function d() { }
}
第七行:console.log(b);打印出函数体function () { }
第八行:不看了,之前提升过了。
视频教程可以参考渡一教育递归,预编译(上)
*/
</script>
预编译不仅发生在函数内,也发生在全局
下面是单信老师课件部分:
- 编译原理
软件开发语言的编译模式分为:
编译型编程语言:代码开发好以后,执行编译命令,将编译的文件下载到电脑上安装即可使用
如:c语言, c++, Java....
解释型编程语言:代码开发好以后,不执行编译。 在执行代码时才进行编译,解释一句执行一句
如:JS php jsp asp .net ...
js执行两个阶段:
预编译阶段:将函数和变量定义进行提升(将函数和变量的定义提到作用域的第一句之前执行)。 扫描上下文环境,如果有语法错误有报错。
解释执行阶段:一句一句执行
作用域:
根据上下文环境(函数会产生独立的运行环境),将作用域划分为:
-
全局作用域
函数外定义的所有内容(函数、变量)都是全局作用域,在任何地方都可以使用 -
局部作用域
函数内定义的内容都是局部作用域,只能在函数内部使用 -
块级作用域
在ES5中没有块级作用域的概念,可以使用IIFE(立即执行函数表达式)实现块级作用域
在ES6中,可以使用 let实现块级作用域
//全局作用域
var a='hello222'; //对于全局变量来说,要不要var关键字都可以,a就是window的属性
var fn=function(){
//局部作用域
var b="world";
};
fn();
看下面一个例子,求1-100的和,说为什么要引入块级作用域
var sum=0;
for(var i=1; i<=100; i++){
sum+=i;
}
console.log(sum); //这是我想要的结果,但也附带的出现了i这个全局变量污染
console.log(i); //但计算过程中的i,由于没块级作用域,变成一个全局变量。 产生全局污染。
//总结,这样的计算方式会产生全局污染
//改进版:将运算代码放在函数中,避免全局污染。必须返回一个值作为最终计算结果供全局使用
//IIFE:立即执行函数表达式
var sum=function(){
var sum=0;
for(var i=1; i<=100; i++){
sum+=i;
}
return sum;
}();
//这样写的话,就增加了一个函数名,仍然有可能造成全局污染,也不是最好的处理办法。
//经典IIFE的写法,再次改进
(function(){
var sum=0;
for(var i=1; i<=100; i++){
sum+=i;
}
window.sum=sum;
})();
//如果直接定义函数,就必须写函数名。想不写函数名就必须把函数定义变成函数表达式: + - ! (),这里就变成了一个立即执行函数表达式,最好是前面再加个分号 ;
//这样处理后就只得到一个sum,并没有产生全局i和函数名污染
为了更加方便的解决这个问题,引入了块级作用域let,不会形成变量提升
//ES6可以直接使用let实现块级作用域
var sum=0;
for(let i=1; i<=100; i++){
sum+=i;
}
console.log(sum);
console.log(i);
//使用ES6的let定义块级作用域,不会造成全局污染
作用域链:
函数操作时,先查找内部的数据,找不到再去找上一级作用域的数据,再找不到再上一级。形成一个查询数据的作用域链条,就叫作用域链。
IIFE 立即执行函数表达式:
1 什么是IIFE?
IIFE: Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数。
定义函数后立即执行一次,没有函数名,无法再次调用,也不会造成全局染污。
2 IIFE有什么用?
可以用于解决运算过程中产生的全局污染问题
也是ES5中实现块级作用域的方式
3 IIFE的特点
将所有运算代码都放在匿名函数中进行运算
函数是一个表达式,没有函数名,必须立即执行一次
执行一次以后就消失,将所有过程产生的数据和变量一起销毁,不会造成全局污染
4 IIFE的写法
(function(){
...
window.a=result
})();
下面还是那个求和的例子,讲如何使用IIFE和产生的效果
//正常运算1到100的累加,想要的是运算结果,中间过程不管
var sum=0;
for(var i=1;i<=100;i++){
sum+=i;
}
console.log(sum); //这个结果是想要的
console.log(i); //运算完以后i会一直遗留在全局上,造成全局污染
//使用IIFE进行改进
var s=(function(){
var sum=0;
for(var i=1;i<=100;i++){
sum+=i;
}
return sum; //函数内计算的结果必须返回出去
})();
//另外一种用法
(function(){
var sum=0;
for(var i=1;i<=100;i++){
sum+=i;
}
window.s=sum; //函数内计算的结果必须返回出去,不写return,而是把结果赋值给window的属性s,这个是闭包的雏形了。
})();
console.log(s);
阶乘
阶乘是函数的一种形式,函数内部再次调用函数自身
它必须有退出循环执行的条件,必须有进入执行的条件,
经典写法如下,求5的阶乘,就是54321
function fn(n) {
if(n===1){
return 1
}
return n*arguments.callee(n-1)
}
console.log(fn(10));
arguments.callee代替fn函数自身
闭包:
- 什么是闭包?
闭包是一种作用域的体现,正常情况下函数内部可以访问函数外部的数据,而函数外部不能访问函数内部的数据;
可以通过特殊写法(闭包):将函数内的子函数暴露到全局上,可以在外部调用并且可以访问函数内的数据。闭包可以让全局访问函数内的数据。 - 闭包的作用
实现外部可以访问函数内部的数据 - 闭包的特征
闭包一定是函数内嵌套函数
闭包是一种作用域的体现,体现内部可以访问外部的数据,而正常情况下外部不能访问内部的数据
闭包可以实现外部访问内部函数,内部函数访问上一级函数的数据。实现外部访问内部数据。
闭包是IIFE的写法,由于内部数据被全局所调用,延缓资源回收。 - 闭包的写法
- 闭包会导致内存无法进行回收
//闭包的写法,第一种
(function(){
var i=1;
window.show=function(){
return i++;
}
})();
//闭包的写法,第二种
var count=(function(){
var i=1;
return function(){
return i++;
}
})();
闭包
当JavaScript引擎解析脚本时,分为“预编译”和“解释执行”两个阶段。
1、“预编译”阶段。首先会创建一个环境的上下文对象,然后把使用var声明的变量,作为上下文对象的属性,以undefined先行初始化;使用function关键字声明的函数,也作为上下文对象的属性,定义出来,而函数则保留了定义的内容。----在这个过程中,函数定义的优先级 高于 变量定义。
2、“解释执行”阶段。遇到变量解析,会先在当前上下文对象中找,如果没找到并且当前上下文对象有prototype属性,则去原型链中找,否则将会去作用域链中找。
在js中,执行环境(execution context)是一个非常重要的概念。
程序代码运行时,会产生一个专门用来管理所需的变量和函数的对象---环境对象,这个对象我们不能通过代码编写操作他,但解析器在处理数据时会在后台使用他。
1、全局环境就是最外围的一个执行环境,可以在代码任意位置访问到。在浏览器中,全局环境就是window对象(因此,所有的全局变量和函数,都是window对象上的属性和方法)。
2、局部环境以函数为主。每个函数都有自己独立的执行环境。
局部作用域一般只在固定的代码片段内可访问到,最常见的就是函数内部,所以在很多地方就会有人把它称为函数作用域。
网友评论