JS ---变量提升、函数提升

479 阅读8分钟

背景

JS代码的执行包括两个过程:预解析处理过程和逐行解读过程。在代码逐行解读前,JS引擎需要进行代码的预解析处理。在预解析过程中,当前作用域中的var变量声明和函数定义将被提升到作用域的最高处。

JS 预解析

  • 对于var声明的变量,初始值为undefined;
  • 对函数定义,变量名为函数名,函数变量的初始值为函数定义本身;
  • 对命名参数,如果函数调用时没有指定参数值,则命名参数的初始值为undefined
  • 对命名函数,如果函数调用时指定了参数值,则命名参数的初始值为指定的参数值

预解析四步骤

*** 函数中的形参***
function test(x,y){}
// x,y就是形参,形参是在函数定义时,声明的变量

***函数的实参****
function test(x,y){
 // ....
}
test(1,3);// 1,3就是函数的实参
  • 函数体系里的预编译过程: 函数执行时
    • 1:创建AO对象
    • 2:找形参和变量声明,将变量和形参作为AO属性名,值为undefined
    • 3:将实参值和形参统一
    • 4:在函数体里面找函数声明,值赋予函数体(⚠️函数表达式不会提升 var b = function(){....})
function test(a, b) {
   console.log(a); // function a () {}
   console.log(b);  // undefined
   var b = 234; 
   console.log(b); // 234
   a = 123; 
   console.log(a); // 123
   function a () {}  // 预编译时函数声明已经提升
   var a; // 预编译时变量声明已经提升
   b = 234;
   var b = function () {} //函数表达式不能提升
   console.log(a);  // 123
   console.log(b);  // function () {}
}
test(1);


// 解析:
1:创建AO对象,执行期上下文
AO {}
2:找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
AO {
 a:undefined,
 b:undefined,
}
3:将实参值和形参统一
AO {
 a: 1, // a为形参,1为实参
 b:undefined
}
4: 在函数体里面找函数声明,值赋予函数体
AO{
a:function a (){}
b:undefined
}
  • 全局的预编译过程页面加载完成时执行
    • 1:生成GO对象
    • 2:找变量声明,将变量作为AO属性名,值为undefined
    • 3:再找函数声明,值赋予函数体
console.log(test); // 1.打印下边整个test函数(在GO中找)
function test(test) { // 2.函数声明,整个函数包括函数体 先不看
   console.log(test);  // 4.打印function test () {}(在AO中找)
   var test = 234; // 5.(替换AO中test的值为234)
   console.log(test); // 打印234
   function test() {} 
} 
test(1); // 3.调用函数(执行之前预编译先创建AO)
var test = 123; // 7.替换GO 中test的值为234
console.log(test);// 打印123

// 解析:
1:创建GO对象
GO{}
2:找变量声明,将变量作为AO属性名,值为undefined
GO{
 test:undefined
}
3:再找函数声明,值赋予函数体
GO{
test:function test(test){
  // 整个函数体
 }
}
GO对象创建完成,开始读代码,直到读到全局调用test函数时,函数预编译,开始创建AO对象
1:创建AO对象
AO{}
2:找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
AO{
  test:undefined
}
3:将实参值和形参统一
AO{
test:1
}
4:再函数体里面找函数声明,值赋予函数体
AO{
test:function test(){}
}

作用域

  • 作用域的作用在于隔断变量,给变量增加命名空间。在作用域里定义的变量,作用域外无法使用。
  • 作用域另一个作用在于避免了无用变量的定义。

变量声明提升:

定义:变量提升收集当栈内存作用域形成时,JS代码执行前,浏览器会将带有var,function关键字的变量提前进行声明declare(值默认是undefined),定义defined。这种预先处理的机制就叫做变量提升机制也叫预定义。

白话文:

  • 如果在函数体外定义函数或使用var声明变量,则变量和函数的作用域会提升到整个代码的最前面。
  • 如果在函数体内定义函数或使用var声明变量,变量和函数的作用域则会被提升到整个函数的最高处。

image.png

  • var不带var的区别

全局作用域中不带var声明变量的相当于给window对象设置一个属性

私有作用域(函数作用域),带var的是私有变量不带var的是会向上级作用域查找,如果上级作用域也没有就一直找到window为止。这个查找的过程叫作用域链

全局作用域中使用var申明的变量会映射到window下称为属性

a = 12  //  ===> window.a
var a = b = 12
/*相当于*/
var a = 12
b = 12
  • 函数左边的变量提升

普通函数下变量提升示例

f()   // 输出:你好
function f(){
   console.log('你好')
}
f()  // 输出:你好

因为带function的已经进行了变量提升

匿名函数下的带=的变量提升

f()  // 报错
var f = function(){
   console.log('你好')
}
f()
/*相当于*/
var f = undefined
...
f() = undefined() // f is not function
  • 重名问题下的变量提升(var a 和 function a (){})

带var和带function重名条件下的变量提升优先级,函数先执行

console.log(a)
var a = 1
function a (){
  console.log(1)
}

/*输出*/
f a(){console.log(1)}

js 并不是在我们定义一个变量的时候,声明完成之后立即赋值,而是把所有用到的变量全部声明之后,再到变量的定义的地方进行赋值,变量的声明的过程就是变量的提升.

变量的提升,提升的其实是变量的声明,而不是变量的赋值。(即所有声明变量或声明函数都会被提升到当前函数的顶部。)

总结:
  • 声明操作在预编译阶段进行,将变量声明提升到当前作用域顶部,默认赋值为 undefined

  • 赋值操作则会被留在原地等待代码执行

  • 变量提升是在 JavaScript 预编译时进行,在代码开始运行之前

函数声明提升

JavaScript 有两种创建函数的方式

1:函数声明

function test(m,n){
   return m + n
}

函数也存在声明提升,和变量的异同在于:

  • 相同点:都会提升到当前作用域(全局、函数)顶部
  • 不同点:函数声明提升会在预编译阶段把函数和函数体整体都提前到当前作用域顶部
test(10,10)
function test(m,n){
   return m + n
}

等同于以下代码

// 预编译阶段
var test
test = function(m,n){
    return m + n
}
// 执行阶段
test(10,10) // 20

** 2:函数表达式** (通过将匿名函数赋值给变量的操作)

var test = function (m,n){
   return m + n
}

通过函数表达式创建的函数和函数声明不同,函数本身不会被提升,但test变量是通过var声明的,因此会存在变量声明。

上述代码等同于

// 预编译阶段
var test
// 执行阶段
test = function (m,n){
   return m + n
}
总结

函数表达式看作两部分

1:声明变量test 2: 给变量test赋值匿名函数

变量提升和函数提升的顺序

在作用域中,不管是变量还是函数,都会提升到作用域最开始的位置,不同的是,函数的提升后的位置是在变量提升后的位置之后的。(先对变量提升,后面排函数的提升)

函数声明提升的优先级要高于变量声明提升。

例子:

function foo() {
  console.log(a);
  var a = 1; // 变量
  console.log(a);
  function a() {} // 函数
  console.log(a);
}
foo();

上述代码解析为:

function foo() {
  var a;
  function a() {}
  console.log(a); // a()
  a = 1;
  console.log(a); // 1
  console.log(a); // 1
}
foo();

⚠️:只有声明的变量和函数才会进行提升,隐式全局变量不会提升。 例子:

function foo() {
  console.log(a);
  console.log(b); // 报错
  b = 'aaa';
  var a = 'bbb';
  console.log(a);
  console.log(b);
}
foo();

解析:b不会进行变量提升

同时存在函数声明和变量声明时,函数声明在前,变量声明在后

例子:

getName();  //1
var getName = function () {
    console.log(2);
}

function getName() {
    console.log(1);
}

getName();  //2

代码解析

// 预编译阶段
// 优先函数声明提升
var getName;
getName = function () {
    console.log(1);
};
// 变量声明提升
// 但因此前已声明过 getName 变量,通过 var 多次声明变量,后续声明会被忽略
var getName;

//执行阶段
getName();  //1
getName = function () {
    console.log(2);
};
getName();  //2

函数表达式

var getName 与 function getName 都是声明语句,区别在于 var getName = function(){} 是函数表达式,而 function getName(){} 是函数声明。

函数表达式最大的问题,在于js会将此代码拆分为两行代码分别执行。

console.log(x);//输出:function x(){}
var x=1;
function x(){}

实际执行的代码为,先将 var x=1 拆分为 var x; 和 x = 1; 两行,再将 var x; 和 function x(){} 两行提升至最上方变成:

var x;
function x(){}
console.log(x);
x=1;

所以最终函数声明的x覆盖了变量声明的x,log输出为x函数.