# 执行上下文
在了解什么叫执行上下文之前我们需要区分一个小概念那就是程序执行和代码执行。我们的代码在执行之前是需要有一个环境的,就像我们做饭一样,做饭之前需要有做饭的环境就是厨房。代码执行就相当于做饭,做饭需要各种烹饪工具,而程序执行就是给代码执行创建一个可以让代码执行环境,相当于给一个厨房。程序执行的时候会在内存里面开辟空间存放各种代码执行需要的东西。而程序在解析和运行的时候也需要依赖一些环境,这些程序执行所依赖和使用的环境就叫做执行上下文。
执行上下文根据范围的不同分为全局执行上下文和函数执行上下文。它是根据 js 的作用域来区分的,因为一般来说 js 就分为两种作用域,一种是全局作用域也就是最外层的作用域,另一种就是函数作用域。而上面两种执行上下文就是和这些作用域一一对应的。
程序执行时主要做三件事:
- 收集变量,生成变量对象(预解析是其中的一个步骤)
- 确定 this 指向
- 确定作用域链
有了这三步以后,我们的代码才能执行。其中第一步的生成变量对象实际上就是把我们声明的那些 var 变量 和函数都打包装进一个对象里面。在全局执行上下文运行的时候那些在全局作用域下的 var 变量和声明的函数都会收集进 global 对象里,global 就是一个全局变量对象。
var a = 10; | |
console.log(window); |
# 执行上下文调用栈
程序为了管理执行上下文(确保程序的执行顺序)所创建的一个栈数据结构,被称作执行上下文栈。本质上为了确保程序按预定的调用顺序执行,就使用栈这种结构来保存管理执行上下文。
function foo(){ | |
var a = 10; | |
function fn(){ | |
console.log(a); | |
} | |
fn(); | |
} | |
foo(); |
程序开始执行:(全局环境和函数环境)
全局执行上下文(分为创建阶段和执行阶段)代码开始执行之前和之后
1、全局执行上下文压入执行上下文栈)
创建上下文阶段:
1、收集变量形成变量对象 (函数 var 的变量会收集)
预解析(其实在创建变量对象的时候已经做了预解析)
2、确定 this 指向(可以认为确定执行者)
3、创建自身执行上下文的作用域链
注意:同时确定函数在调用时候的上级作用域链。(根据 ECMA 词法去确定,看内部是否引用外部变量确定)
2、执行全局执行上下文
执行全局上下文阶段
为变量真正赋值
顺着作用域链查找要使用的变量或者函数执行
函数执行上下文
1、函数执行上下文压栈
1、收集变量 (var 形参 arguments 函数)
2、确定 this 指向(可以认为确定执行者)
3、创建自身执行上下文的作用域链
注意:同时确定函数在调用时候的上级作用域链。(根据 ECMA 词法去确定,看内部是否引用外部变量确定)
函数的作用域链: 自己定义的时候已经确定了函数在调用时候的上级作用域链,因此,在函数调用的时候,只需要将自己的变量对象添加到上级作用域链的顶端;就形成自己的作用域链
2、执行函数执行上下文
为变量真正赋值
顺着作用域链查找要使用的变量或者函数执行
# 预解析
预解析就是我们平常所说的变量提升。它会先解析函数,在解析变量。如果有函数重名,则函数覆盖。如果有变量重名则忽略。
# 两个函数重名
函数重名,函数覆盖
foo(); // sphinx | |
function foo(){ | |
console.log('asuhe'); | |
} | |
function foo(){ | |
console.log('sphinx'); | |
} |
# 变量与变量重名
重名变量的值被覆盖
var a = 10; | |
var a = 20; | |
console.log(a); // 20 |
# 函数与变量重名
函数和变量重名,则忽略变量
function a(){ | |
console.log('asuhe'); | |
} | |
var a; | |
a(); // asuhe |
但是如果同名的变量在函数调用之前被赋值了,则会覆盖原来的函数
function a(){ | |
console.log('asuhe'); | |
} | |
a(); // asuhe | |
var a = 10; | |
a(); // TypeError 此时 a 已经不是函数了,被覆盖成了 10 |
var c = 1; | |
function c(c){ | |
console.log(c); | |
var c = 3; | |
} | |
c(2); //TypeError | |
-------- | |
// 浏览器执行时,上述代码被翻译为 | |
function c(c){ | |
var c; | |
console.log(c); | |
c = 3; | |
} | |
var c; | |
c = 1; | |
c(2); | |
-------- | |
// 变形 1 | |
function c(c){ | |
console.log(c); | |
var c = 3; | |
} | |
c(2); // 2 | |
------- | |
// 变形 2 | |
function c(c){ | |
var c = 3; | |
console.log(c); | |
} | |
c(2); // 3 | |
------ | |
// 变形 3 | |
function c(c){ | |
console.log(c); | |
let c = 3; | |
} | |
c(2); // SyntaxError |
# 小测验
var a; | |
function a() {} | |
console.log(typeof a) // function |
var fn = function () { | |
console.log(fn) | |
} | |
fn() //fn 函数体 | |
-------- | |
function fn(){console.log(fn)}; | |
fn(); //fn 函数体 |
# 作用域
作用域就是用来确定确定变量起作用的范围的。** 作用域在代码定义的时候确定死了,而不是在执行的时候确定。** 作用域的主要作用就是隔离变量,防止变量命名污染。
var a = 10; | |
function fn(){ | |
console.log(a) | |
}; | |
function foo(){ | |
var a = 20; | |
fn(); | |
} | |
foo(); // 10 |
而且在 js 中只有全局作用域和函数作用域这两种作用域。像对象和循环等等带有 { } 的都不存在作用域,不能限定变量范围
var obj = { | |
fn:function(){ | |
console.log(fn); | |
} | |
} | |
obj.fn(); // ReferenceError | |
------- | |
if(true){ | |
function foo(){ | |
console.log('asuhe'); | |
} | |
}else{ | |
function fn(){ | |
console.log('sphinx'); | |
} | |
}; | |
foo(); // asuhe | |
console.log(fn); //undefined fn 会被当做变量提升,但是不会获得定义 |
# 作用域链
作用域链是使用执行上下文当中变量对象所组成的链条结构(数组结构)是真实存在的,查找的时候其实真正是先去自身的变量对象当中查找,如果没有,去上级执行上下文的变量对象当中去查找,直到找到全局执行上下文的变量对象; 函数调用的时候上一级的变量对象其实是在函数定义的时候都已经确定好的。
当一个内部作用域引用外部作用域的变量时,外部作用域会被加入内部作用域变量对象的作用域链数组中,否则不加入。
var b = 20; | |
var c = 30; | |
function fn(){ | |
var a = 10; | |
function fn1(){ | |
console.log(a,b,c); | |
} | |
fn1(); | |
} | |
fn(); // 10 20 30 | |
// 此时因为 fn1 内引用了 fn 内的变量,fn 的变量对象会被加入 fn1 的作用域链以便查找变量 |
var b = 20; | |
var c = 30; | |
function fn(){ | |
var a = 10; | |
function fn1(){ | |
console.log(b,c); | |
} | |
fn1(); | |
} | |
fn(); //20 30 |