# 执行上下文、作用域、闭包、立即执行函数

# 执行上下文

JS 代码在遇到一个函数执行时,会进入预编译状态,生成执行上下文,然后再执行。做题目谨记解题的几大过程,此类题目就迎刃而解,变得很简单

# 预编译之前需要了解的

a(); // a
function a() {
  console.log("a");
}
console.log(a); //undefined
var a = 2; // 变量声明 + 变量赋值

口诀:

  • 函数声明整体提升(函数声明永远提升到 js 文件最前面)
  • 变量 声明提升
  • 任何变量,如果变量未经声明就赋值,归全局所有
function a() {
  var b = (c = 20);
  console.log(window.c);
  console.log(window.b);
}
a(); //c未声明就赋值归全局所有 20 undefined
  • 一切声明的全局变量,就归 window 所有(可以理解为 window 是全局的域对象,定义变量 b,相当于往仓库里添加 b)
// 全局范围内

var b = 10;
console.log(b); //10
console.log(window.b); //10

# 预编译过程

# 函数预编译

预编译发生在函数执行的前一刻

  1. 创建 AO 对象 --- 执行期上下文对象(Activation Object)
  2. 形参变量声明,将变量和形参名作为 AO 属性名,值为 undefined (变量提升)
  3. 将实参值和形参相统一
  4. 在函数里面找函数声明,将函数名作为属性值挂上,值为函数体
  5. 在这过程中还有两个步骤:arguments 作为属性,值为实参数组,this 作为属性,this 为 window
function fn(a) {
  console.log(a)
  var a = 123
  console.log(a)
  function a() {}
  console.log(a)
  var b = function()
  console.log(b)
  function d(){}
}
fn(1)

预编译环节

1. 创建 AO 对象(执行期上下文)
2. AO{
  a: undefined,
  b: undefined

}
3. AO{
  a: 1,
  b: undefined
}4. AO{
  a: func a(),
  b: undefined,
  d: func d()
}

预编译完成之后 AO对象相当于电脑里的东西,然后去执行代码更新AO

1. 打印 func a()
2. a 被赋值 123,打印 123
3. function a() 已经被提升,跳过
4. b 被赋值 fun() 打印 fun()

# 全局预编译

全局预编译过程相比函数预编译过程少了第三步,没有参数的概念。如果又存在全局又存在函数则首先考虑全局在考虑函数预编译执行代码过程中如果 AO 有该变量,就先取 AO 的,没有再去 GO 中取。

  1. 创建 GO 全局对象(GO === window),Global Object
  2. 寻找声明变量,赋值 undefined
  3. 寻找函数声明,函数名作为属性名,函数体作为值
  4. this 作为属性,值为 window
console.log(test)
function test(test) {
  console.log(test)
  var test = 234
  console.log(test)
  function test() {}
}
test(1)
var test = 123

全局预编译

1. GO {
  test: function test()
}

执行过程:
1. 打印function test()
2. 跳过声明函数

执行函数test(1)前一刻,进入函数预编译

1. AO{
  test:undefined
}
2. AO{
  test:1
}
3. AO{
  test:func
}

执行过程:
1. 打印func
2. AO{
  test:234
}
3. 打印 234

遇到此问题口诀: 1. 首先处理全局对象 2. 处理函数执行前预编译,也就是分析出执行上下文对象 3. 执行期间赋值

# 例题

function test(a, b) {
  console.log(a);
  c = 0;
  var c;
  a = 3;
  b = 2;
  console.log(b);
  function b() {}
  function d() {}
  console.log(b);
}
test(1);
查看答案 1、2、2
按照上述步骤分析:

1. AO{
    a:undefined,
    b:undefined,
    c:undefined
  }
2. AO{
    a:1,
    b:undefined,
    c:undefined
  }
3. AO{
    a:1,
    b:func(),
    c:undefined,
    d:func()
  }

代码执行:
1. 打印1
2. AO{
      a:3,
      b:2,
      c:9,
      d:func
    }
3. 打印2
4. 打印2
5. 结果 1,2,2


function test(a, b) {
  console.log(a);
  console.log(b);
  var b = 234;
  console.log(b);
  a = 123;
  console.log(a);
  function a() {}
  var a;
  b = 234;
  var b = function() {};
  console.log(a);
  console.log(b);
}
test(1);
查看答案 func,undefined,234,123,123,func
1. AO{
  a:undefined,
  b:undefined,
}
2. AO{
  a: 1,
  b: undefined
}
3. AO{
  a: func()
  b:undefined
}

执行阶段:
1. 打印func
2. 打印undefined
3. AO{
  a: 1,
  b:234
}
4. 打印234
5. AO{
  a:123,
  b:234
}
6. 打印123
7. AO{
  a:123,
  b:func
}
8. 打印123,
9. 打印func

global = 100;
function fn() {
  console.log(global);
  global = 200;
  console.log(global);
  var global = 300;
  console.log(global);
}
fn();
var global;
查看答案 undefined,200,300
1. 首先是GO{
  global:undefined,
}
2. GO{
  global:undefined,
  fn:function
}
执行过程:
1. GO{
  global:100,
  fn:function
}

执行fn函数前一刻,进入函数预编译

1. AO{
  global:undefined
}
执行过程:
1. 打印undefined,因为AO有值,不会去取GO的值
2. AO{
  global:200
}
3. 打印200
4. AO{
  global:300
}
5. 打印300

function test() {
  console.log(b);
  if (a) {
    var b = 100;
  }
  console.log(b);
  c = 234;
  console.log(c);
}
var a;
test();
a = 10;
console.log(c);
查看答案 undefined、undefined、234、234
1. GO{
  a : undefined
}

执行: 执行过程中执行test()之前进入函数预编译

1. 此处不需要管任何其他干扰因素(if for),上面的四个步骤就是真理
AO{
  b: undefined
}
下面都是全局执行的第一步,只不过是函数的内部执行
·························
  函数执行过程:
  1. 打印undefined
  2. if条件不成立
  3. 打印undefined
  4. 注意c是未经声明就赋值的变量。给GO

  GO{
    a:undefined,
    c: 234
  }
  5. 打印234
···························

2. GO{
  a: 10,
  c: 234
}
3. 打印 234

function bar() {
  return foo;
  foo = 10;
  function foo() {}
  var foo = 11;
}
console.log(bar);
console.log(bar());
查看答案

过程也很简单,就是打印前一刻进入函数预编译,但是预编译直接 return 了,AO 里面的 foo 就是个 function,然后继续执行全局代码打印出 function,第一个 bar 是全局 GO 中的属性,第二个是执行结果


a = 100;
function demo(e) {
  function e() {}
  arguments[0] = 2;
  console.log(e);
  if (a) {
    var b = 123;
    function c() {} // if里面不能声明function c:undefined
  }
  var c;
  a = 10;
  var a;
  console.log(b);
  f = 123;
  console.log(c);
  console.log(a);
}
var a;
demo(1);
console.log(a);
console.log(f);
查看答案

# 作用域和作用域链

# 什么是作用域和作用域链

[[scope]]: js 的函数的可以视为一个对象,对象中有一些属性我们可以访问,但有些不可以,这些属性仅供 js 引擎存取,[[scope]]就是其中之一,[[scope]]指的就是我们所说的作用域,其中存储了执行期上下文的集合

作用域链: [[scope]]中所存储的执行期上下文对象的集合层链式链接,我们把这种链式链接叫做作用域链

# 作用域链的角度分析问题

原则: 自由变量的查找永远都是作用域链顶部向下查找

function a() {
  function b() {
    var b = 234;
    console.log(b);
  }
  var a = 123;
  b();
}
var glob = 333;
a();
1. 首先是 a函数 的定义,此时a的[[scope]]存放的是GO
2. 接着 a函数 的执行,此时a的[[scope]]顶部添加了 a函数的执行期上下文对象AO
3. 接着 a函数 的执行导致了 b函数的定义,此时 b函数借用 a函数的[[scope]],这里就是一模一样的作用域
4. 接着就是 b函数的执行,b的[[scope]]添加自己的执行期上下文对象AO
5. b函数 执行完成,销毁自己的执行期上下文AO,此时b的[[scope]]回到定义阶段
6. 接着就是 a函数执行完成,销毁自己的执行期上下文AO,此时a的[[scope]]只有GO

..............

1. a定义  a.[[scope]] = [GO:{glob:333,a:func}]
2. a执行  a.[[scope]] = [AO:{a:123,b:func},GO:{glob:333,a:func}]
3. b定义  b.[[scope]] = [AO:{a:123,b:func},GO:{glob:333,a:func}]
4. b执行  b.[[scope]] = [bAO:{b:234},AO:{a:123,b:func},GO:{glob:333,a:func}]
5. b销毁  b.[[scope]] = [AO:{a:123,b:func},GO:{glob:333,a:func}]
6. a销毁  a.[[scope]] = [GO:{glob:333,a:func}]

图解流程:

# 例题

  1. 尝试用作用域分析以下函数
function a() {
  function b() {
    function c() {}
    c();
  }
  b();
}
a();
查看答案

分析区分定义和执行期的各个函数的 [[scope]] 即可

  1. 看题输入答案,并解释为什么
function a() {
  function b() {
    var bbb = 234;
    console.log(aaa);
  }
  var aaa = 123;
  return b;
}
var global = 100;
var demo = a();
demo();
查看答案

# 闭包

# 闭包的概念

当内部函数被保存到了外部,就叫做闭包。闭包的产生会照成原有作用域链未释放,导致内存泄漏。

内存泄漏: 占用的多,剩下的少了

但凡是内部函数被保存到了外部,都属于闭包

function a() {
  var num = 100;
  function b() {
    num++;
    console.log(num);
  }
  return b;
}
var demo = a();
demo();
demo();
原理分析:
首先是a定义,此时a的作用域只包含了全局执行上下文GO,
然后就是a的执行,这个时候作用域链顶端插入a函数的执行期上下文aAO,
里面有num属性:100,然后a执行导致了b的定义,b拿了a的劳动成果直接将作用域链搬过来,
接着b并没有执行,而是返回到了外部。返回后a执行完成,销毁了自己的执行期上下文,
但是没有影响到b搬过来的执行期上下文,接着b执行,产生自己的执行期上下文,
自己的上下文没有就去a里面拿,然后加1。然后执行完成,销毁。又执行一次,
又产生了一个新的执行期上下文,num在原有基础上+1,最后执行完成销毁。
所以最后打印的是101,102

function test() {
	var arr = [];
	for(var i = 0;i < 10;i++){
		arr[i] = function() {
			console.log(i) // 打印10个10
		}
	return arr;
}
var myArr = test(); // 这句话就代表test执行完,10个函数已经保存出来了,i=10
for(var j = 0;j < 10;j++){
  myArr[j](); // 分别调用10个函数

}
原理分析:
首先保存出去10个函数和test都形成了闭包,
而且都是使用公用的同一个i变量,
在这些函数保存出去之前 i变量已经变成了10,
然后再去争先恐后的执行,所以打印10个10

# 闭包的作用

  1. 实现公有变量

例如: 实现累加器

function a() {
  var num = 0;
  function b() {
    num++;
    console.log(num);
  }
  return b;
}
var demo = a();
demo();
demo(); // 调用多少次就累加多少 b函数内部可以任意使用 num变量
  1. 可以做缓存(存储结构)
function eater() {
  var food = "";
  var obj = {
    eat: function() {
      console.log("i am eating" + food);
      food = "";
    },
    push: function(myFood) {
      food = myFood;
    }
  };
  return obj;
}

var eater1 = eater();
eater1.push("banana");
eater1.eat(); // 打印 i am eating banana 这里的eater()和obj里面的两个函数形成了闭包,每次都会使用一个food,这个food相当于一个存储结构在使用
  1. 可以实现封装,属性私有化

使用闭包可以封装数据,外部无法访问,只能够调用函数来获取里面的值

function createCache() {
  const data = {}; //外界无法访问这个数据,只能调用get函数获取值
  return {
    get: function(key) {
      return data[key];
    },
    set: function(key, value) {
      data[key] = value;
    }
  };
}
  1. 模块化开发,防止污染全局变量

# 闭包的几种表现形式

  • 内部函数被保存到了外部
  • 内部函数作为函数参数传递
var a = 1;
function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  bar(baz);
}

function bar(fn) {
  // 这就是闭包
  fn();
}
// 输出2,而不是1
foo();
  • 在定时器、事件监听、Ajax 请求、跨窗口通信、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

  • 立即执行函数

var count = (function() {
  var num = 0;
  return function() {
    num++;
    console.log(num);
  };
})();

count(); //1
count(); //2

# 立即执行函数

此类函数只会执行一次,并且直接销毁内存,不会占用内存空间,常常用来初始化数据

(function() {
  console.log(1);
})(); //1  这里的括号可以放在外面

// 初始化变量

var a = (function(b, c) {})(1, 2); // a=3

# 规则

  • 只有表达式才会执行
function test(){}() // 直接报错,因为根本不是表达式

(function test(){}()) // 这样就不会报错,因为最外面的括号是数学括号,是下面的一种变型
  • 被执行符号执行的表达式会自动放弃表达式的名字,所以这种情况下,就相当于立即执行函数:
var demo = (function() {
  console.log(1);
})();
//立即执行函数 放弃demo名字

# 经典题

var x = 1;
if (function f() {}) {
  // 表达式外面有括号,这个f()就自动销毁了
  x += typeof f; // 1 + 'undefined' = '1undefined'
}
console.log(x);
Last update: 7/18/2021, 4:49:27 PM