执行上下文
在理解预解析之前,我们先了解下 执行上下文(Execution Context) 的概念
- 每次当控制器转到可执行代码的时候,就会进入一个执行上下文。
- 执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。
- 每一个函数执行时,都会给对应的函数创建这样一个执行环境
- JavaScript中的运行环境大概包括三种情况。
1. 全局环境:JavaScript代码运行起来会首先进入该环境
2. 函数环境:当函数被调用执行时,会进入当前函数中执行代码
3. eval(不建议使用,可忽略)
- 当代码在执行过程中,遇到以上三种情况,都会生成一个执行上下文,因此在一个JavaScript程序中,必定会产生多个执行上下文
- JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)。
栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文:
console.log("全局作用域")
function A() {
console.log("函数A的作用域")
function B() {
console.log("函数B的作用域")
}
B();
}
A();
例如:一个全局作用域下有个函数A,函数A的作用域下又包含了个函数B
1. 一个程序的运行,首先进入的是全局环境(全局作用域),然后全局上下文就被放到栈的最下面
2. 当运行到函数A时,就会创建一个函数A正在执行的上下文,放到栈里面,因为全局已经放进栈里了,所有函数A会放到全局的上面,也就是栈顶
3. 当运行到函数A里面的函数B时,就会把函数B放进去,叠在函数A上,此时因为执行的是函数B,所有此时函数B是正在执行的上下文
4. 当函数B执行完后,就会从栈里面取出去,此时的执行上下文就是函数A,当函数A执行完后,被取出去了,全局就是此时的执行上下文(牵涉到 js 垃圾回收机制)
5. 全局上下文在浏览器窗口关闭后出栈。
总结:
1. 执行上下文是单线程,类似于乒乓球筒,先放进去的,要把上面的都取出来才能拿到下面的
2. 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待(要用乒乓球,肯定是先取上面的)
3. 全局上下文只有唯一的一个,它在浏览器关闭时出栈
4. 函数的执行上下文的个数没有限制
5. 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。
预解析/预编译
当调用一个函数时(激活),一个新的执行上下文就会被创建。一个执行上下文的生命周期可以分为两个阶段。创建阶段和执行阶段,而预解析就发生在 创建阶段的声明变量对象 的时候,js的代码的执行机制就是先预解析,再执行代码
- 创建阶段:执行上下文会分别创建变量对象,建立作用域链,以及确定this指向
- 执行阶段:完成变量赋值,函数引用,以及执行其他代码
局部函数预解析(变量对象创建过程),分为4个阶段
function fn(a,c){
console.log(a)
var a = 123
console.log(a)
console.log(c)
function a(){}
if(false){
var d = 678
}
console.log(d)
console.log(b)
var b = function (){}
console.log(b)
function c(){}
console.log(c)
}
fn(1,2)
/* 预编译分析 */
1. 创建变量对象(又称AO对象)
AO{
}
2. 找形参和变量的声明,作为AO对象的属性名,值为undefined
AO{
a: undefined
c: undefined
d: undefined
b: undefined
}
3. 实参与形参相统一
AO{
a: 1
c: 2
d: undefined
b: undefined
}
4. 找函数声明,会覆盖掉变量的声明
AO{
a: a(){}
c: c(){}
d: undefined
b: undefined
}
全局函数预解析(变量对象创建过程),分为3个阶段
fn(); // 能打印
var fn = 100;
fn(); // 报错:不是一个函数
function fn(){
console.log("我是 fn 函数");
}
/* 预编译分析 */
5. 创建 GO( Grobal Object ) 对象
GO{
}
6. 找变量声明(包括函数表达式形式var fun = function(){})
GO{
fn: undefined
}
7. 找函数声明,函数会覆盖变量声明
GO{
fn: fn(){}
}
函数参数(函数的形参)
function fn(b){
function b(){
}
b();
}
fn(200);
- 真实的执行过程
function fn(b){
/*
预解析先声明参数var b,并赋值,在声明函数b
*/
var b = 200;
b();
function b(){}
b();
}
fn(200);
变量声明(变量和赋值式函数)
let/const
声明的变量,仍然会提前被收集到变量对象中,但和var
不同的是,let/const
定义的变量,不会在这个时候给他赋值undefined
。
console.log(num); // undefind
console.log(a); // 报错
let a = 10;
var num = 100;
- 预解析会把 var 关键字定义的变量在代码执行之前先声明,但此时并赋值为
undefind
console.log(num); // undefind
var num = 100;
console.log(num); // 100
- 真实的执行过程
/*
预解析先声明var num,但此时并未赋值
*/
var num;
console.log(num); // undefind
num = 100;
console.log(num); // 100
注意:
- 赋值式函数:
var fn = function(){}
- 按照 var 的规则进行解析
fn(); // 报错
var fn = function fn(){
console.log("我是fn函数");
}
- 真实的执行过程
/*
在预解析,只是告诉浏览器,fn 这个名字可以使用,但是没有赋值
*/
var fn;
fn() // 报错
fn = function fn(){
console.log("我是fn函数");
}
fn();
函数声明(声明式函数)
- 在内存中先声明有一个变量名是函数名,并且这个名字代表的内容是一个函数
fn(); // 第一句
/*
预解析在所有代码执行之前,告诉浏览器,fn 这个名字可以使用,并且 fn 代表的是一个函数
*/
function fn(){
console.log("我是fn函数");
}
- 真实的执行过程
/*
会把函数定义阶段提到调用阶段的上面
*/
function fn(){
console.log("我是fn函数");
}
fn(); // 第一句
预解析隐患
if 里面的代码也会进行预解析
- 这是你以为的:
console.log(num); // undefined
if(false){
var num = 100;
}
console.log(num); // 100
- 实际上:if 条件不管成不成立,里面的代码都会进行预解析
console.log(num); // undefined
if(false){
var num = 100;
}
console.log(num); // undefined
- 相当于:
console.log(num); // undefined
if(false){
var num;
num = 100;
}
console.log(num); // undefined
return 后面的代码也会进行预解析
- return 后面的代码虽然不执行,但是也会进行预解析
function fn(){
var num = 100;
console.log(num); // 100
console.log(n); // undefined
return;
var n = 200;
}
fn();
- 相当于:
function fn(){
var num;
var n;
num = 100;
console.log(num); // 100
console.log(n); // undefined
return;
n = 200;
}
fn();
- 书写代码建议:
1. 函数名 不要与 变量同名
2. 变量名以名词为主,2个或3个单词组合使用
如:username,userInfoAge
3. 函数名以功能区分,尽量语义化
如:getColor(),setColor()
4. 尽量使用 赋值式函数 来定义
下一篇:函数作用域https://blog.youkuaiyun.com/qq_45677671/article/details/114988263