目录
一、函数预编译
引擎运行过程
1.通篇检查语法错误SyntaxError
1.5 预编译
函数的预编译发生在函数执行的前一刻,若函数仅定义不执行,则不会发生预编译
2.执行(解释一行执行一行)
预编译步骤
私有上下文
1.创建 AO对象
AO activation object 活跃对象 / 函数上下文
2.形参和变量声明提升
寻找函数中的形参和变量声明并提升(此时变量不赋值)
3.将实参赋值给形参
4.内部函数声明提升
寻找函数体内的函数声明 并赋值函数体
注:函数和变量提升区别
• 变量声明仅声明提升,赋值不会提升
• 函数声明整体提升,函数体也会提升
test(); //输出1
console.log(b); //输出undefined
function test(){
console.log(1);
}
var b = 10;
//var b = 10; 实际是两步:var b -> b =10; 变量仅声明提升,赋值在全局执行时进行
5.执行
此时变量被赋值,注意:匿名函数表达式也属于变量赋值!
全局上下文
1 创建 GO对象
global object 全局上下文 ===window
2 寻找变量声明
变量仅声明提升,赋值不提升
3 寻找函数声明
函数声明整体提升, 赋值函数体
4 执行
练习题1
console.log(a);//输出函数体a
a();//调用函数a
function a(a){
console.log(a);//输出 undefined(形参未设置默认值)
console.log(b);//输出 内部函数体b
var a = 10;
var a = function(){
//内部函数a执行语句
}
var b = 20;
function b(){
//内部函数b执行语句
}
console.log(a);//输出内部函数体a,匿名函数表达式在执行阶段赋值
console.log(b);//输出20,内部函数体b是函数声明,会被执行阶段的 b = 20 覆盖
}
var a = 1;
console.log(a);//输出1,覆盖函数声明
练习题2
var x = 1,
y = z = 0;
function add(n){
return n = n + 1;
}
y = add(x);
function add(n){
return n = n + 3;
}
z = add(x);
console.log(x, y, z);// 1,4,4
二、函数作用域
1. 对象
• 函数也是一种对象类型、引用类型、引用值
• 函数可以访问的属性 .length .name .prototype
• 对象 -> 有些属性使我们无法访问,属于JS引擎内部的私有属性
2. [[scope]] 域
2.1 函数被定义时,生成一个JS内部的私有属性 [[scope]],只能JS引擎读取
2.2 [[scope]]属性值:保存该函数的作用域链
2.3 作用域链在函数被定义时即生成,此时第0位存储GO;函数被执行时前一刻(预编译的时候)才生成AO,并存储在作用域链的第0位,GO随之后移(栈结构)
(GO:全局的执行期上下文;AO:函数的执行期上下文)
2.4 函数执行完成后,AO被销毁
2.5 如果再次执行该函数,重新生成一个全新的AO,即 AO 是一个即时的存储容器
三、函数作用域链
function a(){
function b(){
var b = 2;
}
var a = 1;
b();
}
var c = 3;
a();
以上代码执行过程如下:
- 1.页面打开的时候
- 全局已经在执行,在全局执行的前一刻(全局预编译)会生成GO。GO里存储全局下的所有对象,其中包括函数 a 和全局变量 c
- 函数a被定义
- 全局预编译时期,函数a 整体被提升,此时系统生成一个[[scope]]属性,[[scope]]中保存函数a的作用域链,第0位保存当前环境下的全局执行上下文GO
- 2.函数 a 执行
- 2.1 函数 a 预编译
- 生成AO,存储到作用域链第0位
- 函数执行期上下文 AO里存储函数a 下的所有对象,包括内部函数b 和内部变量a
- 此时作用域链的第0位保存AO,GO 被保存到作用域链的第1位
- 函数 b 被定义
- 系统生成一个[[scope]]属性,保存函数b的作用域链
- 在函数 a 环境下,函数 b 在定义的时候,和函数 a 执行期的作用域链相同(指向相同的上下文对象)
- 生成AO,存储到作用域链第0位
- 2.2 函数 a 执行
- 查找变量从函数a 的作用域从顶端往下依次查找(因此当局部变量和全局变量重名时,优先取局部变量值)
- 2.3.函数 b 执行
- 2.3.1函数 b 预编译
- 第 0 位保存函数b自身的AO,函数a 的AO和全局的GO依次向下排列
- 2.3.2 函数 b 执行
- 2.3.3函数 b 执行结束
- 函数b 的AO 被销毁,回归函数b 定义的时候的[[scope]]
- 2.3.1函数 b 预编译
- 2.4 函数 a 执行结束
- 函数a 的 AO 被销毁 -> 由于函数a的AO中存储着函数b,因此导致函数b 的[[scope]]同时被销毁
- 函数a 回归 函数a 被定义的时候的[[scope]],第0位存储GO
- 2.1 函数 a 预编译
四、闭包
闭包定义
内部函数被外部函数以外的变量引用时,就形成了一个闭包
原理
闭包会产生原来的作用域链的不释放
缺点
过度的闭包可能会导致内存的泄露,或者加载太慢
function test(){
var a = 10;
function add(){
a++;
console.log(a);
}
function reduce(){
a--;
console.log(a);
}
return [add,reduce];
}
var arr = test();
arr[0]();//11
arr[1]();//10
五、立即执行函数
普通全局函数:
需要手动执行,但执行后函数不会被释放,函数仍然存储在全局上下文GO中;
立即执行函数:
可以自动执行,执行后自己会销毁,因此只能执行一次;
1.函数语法
两种写法
1、 (function(){})();
2、 (function(){}());
(function test(){
var a = 1,
b = 2;
console.log(a,b)
})();
test();// 报错 Uncaught ReferenceError 说明函数执行完毕就销毁了
传参、返回值
• (function(形参){语句}(实参)) ;
• 立即执行函数可以返回值,声明一个变量来保存返回的值
var num = (function(a, b){
return a + b;
}(2, 4));
console.log(num);// 6
2.立即执行函数原理
- 圆括号()会将函数声明变成表达式,函数立即执行,函数名自动被忽略
①括号里有内容:让包裹的内容变成表达式 -> (function(){})让函数声明变成表达式;
②空括号():执行符号,表明函数被执行 -> ()前面必须是表达式,才能用执行符号();
//正确写法1
var test1 = function(){
console.log(1);
}(); // 输出 1 匿名函数赋值给变量是一个表达式
//正确写法2
(function test2(){
console.log(2);
})(); // 输出 2 括号()将函数声明转为表达式
//错误写法1
function test2(){
console.log(2);
}(); // 报错 Uncaught SyntaxError 函数声明不是表达式
//错误写法2,但不会报错
function test3(a){
console.log(a);
}(6); // 不会执行函数,但也不会报错,因为引擎将(6)看成一个独立的表达式,而不是test3的执行
//面试题
var a = 10;
if(function b(){}){
a += typeof(b);
}
console.log(a);//输出"10undefined"
//(function b(){}) 圆括号使函数声明变为表达式 -> 表达式会忽略函数名称
//-> typeof(b)中的b变为undefined
- 【+ - ! || &&】 也会将函数声明变成表达式
//面试题
+ function test(){
console.log(2);
}(); // 输出2
! function test(){
console.log(2);
}(); // 输出2
~ function test(){
console.log(2);
}(); // 输出2
0 || function test(){
console.log(2);
}(); // 输出2
六、立即执行函数与闭包结合
- 单独用闭包会导致遍历可能出问题
//函数test()执行完毕,作用域链不再指向函数test的AO对象
//arr被return到外部,arr[i]中的匿名函数仍然指向函数test的AO对象;
//当外部调用arr[i]时,函数作用域链会向上寻找变量i;
//但是函数test已经执行完毕,test函数AO对象里的变量i保存为最后一次函数运行时候的值,只能输出10
function test(){
var arr = [];
var i = 0;
for(;i < 10;i++){
arr[i] = function(){
console.log(i + ' '); //输出10个10
};
}
return arr;
}
var a = test();
for(var j = 0;j < 10;j++){
a[j]();
} //输出 10个10
- 两种方式解决以上问题
- 外部传参
- 立即执行函数
//外部传参
function test(){
var arr = [];
var i = 0;
for(;i < 10;i++){
arr[i] = function(num){
console.log(num + ' ');
};
}
return arr;
}
var a = test();
for(var j = 0;j < 10;j++){
a[j](j);
} //输出0 1 2 3 4 5 6 7 8 9
//立即执行函数
function test(){
var arr = [];
var i = 0;
for(;i < 10;i++){
(function(j){
arr[j] = function(){
console.log(j + ' ');
};
})(i);// function(j)是一个立即执行函数
}
return arr;
}
var a = test();
for(var j = 0;j < 10;j++){
a[j]();
} //输出 0 1 2 3 4 5 6 7 8 9
//test()函数执行,执行到for循环时,每次循环生成一个立即执行函数,立即执行函数内部声明局部变量j
// ->外部实参i赋值给j -> arr[j]中的匿名函数访问到j值,并赋值给arr[j]
// ->立即执行函数执行完自动销毁,但arr[j]仍指向IIFE函数中的j(for循环产生10个IIFE -> 10个独立的j)
立即执行函数面试题
<body>
<ul id="list">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
<script>
var list = document.getElementById("list"),
li = list.children;
for(var i = 0 ;i<li.length;i++){
li[i].onclick=function(){
console.log(i);
// 结果总是3.而不是0,1,2
//因为i是贯穿整个作用域的,而不是给每一个li分配一个i,
//点击事件是异步,用户一定是在for运行完了以后,才点击,此时i已经变成3了。
}
}
</script>
</body>
用立即执行函数解决以上问题
立即执行函数,给每个li创建一个独立的作用域,
在立即执行函数执行的时候,i的值从0到2,对应三个立即执行函数,这3个立即执行函数里边的j分别是0,1,2
var list = document.getElementById("list"),
li = list.children;
for(var i = 0 ;i<li.length;i++){
(function(j){
li[j].onclick = function(){
console.log(j);
})(i); //把实参i赋值给形参j
}
}