# 执行上下文、作用域、闭包、立即执行函数
# 执行上下文
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
# 预编译过程
# 函数预编译
预编译发生在函数执行的前一刻
- 创建 AO 对象 --- 执行期上下文对象(Activation Object)
- 找形参和变量声明,将变量和形参名作为 AO 属性名,值为 undefined (变量提升)
- 将实参值和形参相统一
- 在函数里面找函数声明,将函数名作为属性值挂上,值为函数体
- 在这过程中还有两个步骤:
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 中取。
- 创建 GO 全局对象(GO === window),Global Object
- 寻找声明变量,赋值 undefined
- 寻找函数声明,函数名作为属性名,函数体作为值
- 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,func1. 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,3001. 首先是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、2341. 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}]
图解流程:
# 例题
- 尝试用作用域分析以下函数
function a() {
function b() {
function c() {}
c();
}
b();
}
a();
查看答案
分析区分定义和执行期的各个函数的 [[scope]]
即可
- 看题输入答案,并解释为什么
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
# 闭包的作用
- 实现公有变量
例如: 实现累加器
function a() {
var num = 0;
function b() {
num++;
console.log(num);
}
return b;
}
var demo = a();
demo();
demo(); // 调用多少次就累加多少 b函数内部可以任意使用 num变量
- 可以做缓存(存储结构)
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相当于一个存储结构在使用
- 可以实现封装,属性私有化
使用闭包可以封装数据,外部无法访问,只能够调用函数来获取里面的值
function createCache() {
const data = {}; //外界无法访问这个数据,只能调用get函数获取值
return {
get: function(key) {
return data[key];
},
set: function(key, value) {
data[key] = value;
}
};
}
- 模块化开发,防止污染全局变量
# 闭包的几种表现形式
- 内部函数被保存到了外部
- 内部函数作为函数参数传递
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);