ECMAScript - 函数预编译、作用域与作用域链、闭包、立即执行函数

本文深入解析JavaScript中的函数预编译过程,包括创建AO对象、形参和变量提升、内部函数声明提升等步骤。同时,介绍了函数作用域、作用域链、闭包和立即执行函数的概念及应用,探讨了它们在实际编程中的重要性。通过实例分析,帮助读者理解函数执行的细节和闭包可能导致的问题,并提供了解决方案。
摘要由CSDN通过智能技术生成

一、函数预编译

引擎运行过程

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 执行期的作用域链相同(指向相同的上下文对象)
    • 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.4 函数 a 执行结束
      • 函数a 的 AO 被销毁 -> 由于函数a的AO中存储着函数b,因此导致函数b 的[[scope]]同时被销毁
      • 函数a 回归 函数a 被定义的时候的[[scope]],第0位存储GO

四、闭包

闭包定义
内部函数被外部函数以外的变量引用时,就形成了一个闭包
原理
闭包会产生原来的作用域链的不释放
缺点
过度的闭包可能会导致内存的泄露,或者加载太慢

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
        }
      }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值