关于JS预编译和作用域,你都清楚了吗?

386 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

js 预编译是什么

先看一段代码

// 函数
function fn(a, c) {
  console.log(a, "a1");
  var a = 123;
  console.log(a, "a2");
  console.log(c, "c1");
  // 函数声明
  function a() {}
  console.log(a, "a3");
  console.log(b, "b1");
  // 函数表达式
  var b = function () {};
  console.log(b, "b2");
  // 函数声明
  console.log(d, "d");
  function d() {}
  function c() {}
  console.log(c, "c2");
}
//调用函数
fn(1, 2);

你知道上面代码的执行结果,如果你不复制代码到浏览器中看结果,那么我相信你已经理解了预编译,也就不用再看下去了,如果你不知道结果,那么你可以尝试看下去。相信你会有所收获。

什么是预编译

预编译分为全局预编译和局部预编译,上面的代码就是局部预编译,全局预编译发生在页面加载完成时候执行,而局部预编译发生在函数执行前。

全局预编译步骤

  • 创建 GO 对象(全局对象)
  • 找变量声明,将变量名作为 GO 属性名,值为 undefined
  • 查找函数声明,作为 GO 属性,值赋值给函数体

局部预编译步骤

  • 创建 AO 对象(执行上下文)
  • 找形参和变量声明,将变量和形参作为 AO 属性名,值为 undefined
  • 将实参值和形参值统一
  • 在函数体里面找函数声明,值赋值给函数体

逐步分析代码

// 开始会创建 AO 局部编译对象
AO:{

}
// 然后查找形参和变量声明作为 AO的属性并且赋值为undefined
// 因为 function d(){} 是函数体 要到最后一步才查找
AO:{
    a:undefined,
    c:undefined,
    b:undefined,
}
// 将形参和实参的值统一 赋值给AO对应的属性
AO:{
   a:1,
   c:2,
   b:undefined
}
// 在函数体里找函数声明,值赋值给函数体(如果函数声明和变量一样,那么会覆盖变量声明)
// 因为 b 是函数表达式,不是函数声明,所以此时b还是undefined
AO:{
  a:function a(){},
  c:function c(){},
  b:undefined,
  d:function d(){}
}
// 执行到这里接下来就要开始 js解释执行,也就是逐行执行代码
// 函数
function fn(a, c) {
  console.log(a, "a1");
  var a = 123;
  console.log(a, "a2");
  console.log(c, "c1");
  // 函数声明
  function a() {}
  console.log(a, "a3");
  console.log(b, "b1");
  // 函数表达式
  var b = function () {};
  console.log(b, "b2");
  // 函数声明
  console.log(d, "d");
  function d() {}
  function c() {}
  console.log(c, "c2");
}
//调用函数
fn(1, 2);
// 预编译结果
AO:{
  a:function a(){},
  c:function c(){},
  b:undefined,
  d:function d(){}
}
// 首先执行console.log(a,'a1')
function a(){} "a1"
// 接下来 a赋值变成123 AO 的属性 a就变成了123
// 打印a2
123 "a2"
// 打印c1
function c(){} "c1"
// 打印a3 此时AO属性的 a值依然是123
123 "a3"
// 打印 b1 此时b是undefined

undefined "b1"
// 这个时候函数 function (){} 赋值给b 所以AO属性的b 为 function(){}
// 打印 b2
function(){} 'b2'
// 打印 d
function d(){} "d"
// 打印c2
function c(){}  "c2"

最终答案

yu01.jpg

全局预编译

全局预编译根据相对应的步骤逐步分析即可

作用域

作用域

作用域是指程序源代码中定义变量的区域。
作用域分为全局作用域和函数作用域

看一段代码来了解下全局作用域和函数作用域

// 全局作用域
var a = 1;

function foo() {
  // 函数作用域
  console.log(a);
}

function bar() {
  // 函数作用域
  var a = 2;
  foo();
}

bar();

词法作用域(静态作用域)和动态作用域

看上边代码,会输出什么呢?

词法作用域结果:从 foo 内部查找局部变量 a,没有就根据书写位置继续向上查找 也就是 a=1,所以输出 1 动态作用域结果:从 foo 内部查找局部变量 a,没有继续向上查找,就是从调用的函数作用查找,找到 a=2,多以打印 2

而 js 是采用的词法作用域,所以 在 js 环境下会输出 1

作用域的浅层次理解

1.全局作用域

  • 全局作用域在页面打开时创建,页面关闭时销毁

  • 编写在 script 中的变量和函数,作用域为全局,在任何地方都可以访问到

  • 在全局作用域中有全局对象 window,代表一个浏览器窗口,由浏览器创建,可以直接调用

  • 全局作用域中声明的变量和函数会作为 window 对象的属性和方法保存

    2.函数作用域

  • 调用函数时,函数作用域被创建,函数执行完毕,函数作用域被销毁

  • 每调用一次函数就会创建一个新的函数作用域,他们之间相互独立

  • 在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量

  • 在函数作用域访问变量,函数会在自身作用域查找,没有找到则会到函数的上一级作用域查找,直到全局作用域,找不到则返回 null

作用域的深层次理解

  • 执行期的上下文
    • 当函数代码执行的前期,会创建一个执行期上下文的内部对象 AO (作用域)
    • 这个内部对象是预编译的时候创建出来的,因为当函数被调用的时候,会先进行预编译
    • 在全局代码执行的前期会创建一个执行期的上下文对象 GO
  • 预编译
    • 预编译参考第一篇文章,有详细解释

作用域链

看一段代码

var a = 1;
function fn() {
  var b = 2;
  function bar() {
    console.log(a + b);
  }
  return bar();
}
fn();

当我们执行 bar 函数时,我们会在 bar 内部查找 a 和 b 变量,如果没有,那么我们会继续向上查找,到 fn 的作用域查找,这个时候找到 b,这个时候我们还差一个变量 a 继续向上查找,一直会到最后的全局作用域,如果全局作用域没有,那么返回 null,这个过程形成的链就是作用域链