JavasSript基础之变量提升

143 阅读14分钟

个人笔记

变量提升

变量提升:在当前上下文中(全局/私有/块级),JS代码自上而下执行之前,浏览器会提前把当前上下文中所有带var/function关键字的变量进行提前的声明或者定义(可以理解为词法解析的一个环节,词法解析一定发生在代码执行之前)

例如:

var a=10;

  • 声明declare:var a(此时a的值为undefined
  • 定义defined:a=10(此时a的值为10var只会提前的声明fcuntion会提前的声明+定义
console.log(a)
//Uncaught ReferenceError: a is not defined
console.log(a)//undefined
var a = 1
/* 
 * 全局上下文中的变量提升
 *   func=函数   函数在这个阶段声明赋值都做了
 */
func();//OK
function func() {
	var a = 12;
	console.log('OK');
}
// 代码执行之前:全局上下文中的变量提升
var a;   //默认值是undefined
console.log(a); //=>undefined
var a = 12; //=>创建值12  不需要在声明a了(变量提升阶段完成了,完成的事情不会重新处理) a=12赋值
a = 13; //全局变量a=13
console.log(a); //=>13

用函数表达式创建函数,变量提升阶段只会声明func,不会赋值,例如:

func(); //=>Uncaught TypeError: func is not a function
//因为此时func变量提升,并且值为undefined
var func = function () {
	console.log('OK');
};
func();
var func = function () {
	console.log('OK');
};
func();//OK

额外知识 把原本作为值的函数表达式匿名函数“具名化”,但是这个名字不能在外面访问,不会在当前当下文中创建这个名字,只有内部可以访问到

var func = function AAA() {

// 这样声明函数的作用:当函数执行,在形成的私有上下文中,会把这个具名化的名字做为私有上下文中的变量(值就是这个函数)来进行处理
console.log('OK');
console.log(AAA); //=>当前函数
AAA(); //递归调用   具名化之后可以不用使用arguments.callee了(arguments.callee严格模式下不支持)
};
// AAA();  //=>Uncaught ReferenceError: AAA is not defined
func();

具名函数的作用之一:递归调用

setTimeout(function fn(){
    fn()
},1000)

只有带varfunction会变量提升(ES6中的letconst不会)

console.log('OK'); //=>'OK'
console.log(a); //=>Uncaught ReferenceError: Cannot access 'a' before initialization  不能在LET声明之前使用变量
let a = 12;
a = 13;
console.log(a);

基于var或者function全局上下文中声明的变量(全局变量)会映射到GO(全局对象window)上一份,作为他的属性;而且接下来其中一个修改,另外一个也会跟着修改

/*
 * EC(G)变量提升
 */
 console.log(a); //=>Uncaught ReferenceError: a is not defined
 a = 13;
 console.log(a);
var a = 12;
console.log(a); //=>12  全局变量
console.log(window.a); //=>12 映射到GO上的属性a

window.a = 13;
console.log(a); //=>13 映射机制是一个修改另外一个也会修改

题目

fn(); //=>5
function fn(){ console.log(1); }  //=>不再处理,变量提升阶段搞过了
fn(); //=>5
function fn(){ console.log(2); }
fn(); //=>5
var fn = function(){ console.log(3); }  //=>var fn不用在处理了,但是赋值在变量提升阶段没处理过,此处需要处理  fn=window.fn=>3
fn(); //=>3
function fn(){ console.log(4); }
fn(); //=>3
function fn(){ console.log(5); }
fn(); //=>3

解析: 首先在执行之前会变量提升,过程是这样

  • function fn(){ console.log(1); }=>function开头,声明+赋值声明fn,赋值为打印1的函数
  • function fn(){ console.log(2); }=>fn声明过了,不再声明,只赋值打印2
  • var fn = function(){ console.log(3); }=>var开头,只声明,不赋值,又因为声明过了,所以不作处理
  • function fn(){ console.log(4); } fn赋值为打印4
  • function fn(){ console.log(5); } fn赋值为打印5

接着开始执行

  • 前三个fn都打印5
  • 接着到var fn = function(){ console.log(3); },已声明,所以赋值为打印3
  • 接着后面三个fn都打印3
var foo = 1;
function bar() {
	if (!foo) {
		var foo = 10;
	}
	console.log(foo);//10
}
bar();

变量提升后如下:

var foo = undefined;
foo = 1
function bar() {
	var foo = undefined;
	if (!foo) {
		foo = 10;
	}
	console.log(foo);//10
}
bar();

var foo = 1;
function bar() {
	if (false) {
		var foo = 10;
	}
	console.log(foo);//undefied
}
bar();

总结:

  • 先词法分析,将代码解析为浏览器可以执行的样子
  • EC(G)变量提升:把当前上下文中所有带 var(提前声明) / function(提前声明+定义) 进行提前的声明或者定义
  • 全局上下文中,基于var/function声明的变量,也相当于给window设置了对应的属性
  • 变量提升阶段,我们的函数就已经声明+定义了,所以可以在创建函数的代码之前执行函数
fn();
function fn() {} 
  • 函数表达式:变量提升阶段,只会声明fn,没有赋值,所以fn必须在创建的代码之后执行(推荐)
var fn = function () {};
fn(); 
/*代码执行前:变量提升
		var a;
*     var b;
*     var c;
*     fn = 0x000; [[scope]]:EC(G)

*/
console.log(a, b, c); //=>undefined * 3
var a = 12,
   b = 13,
   c = 14;
function fn(a) { //代码执行遇到创建函数的代码会直接的跳过:因为在变量提升阶段已经处理过了
   /*
    * EC(FN)私有上下文
    *   作用域链:<EC(FN),EC(G)> 
    *   形参赋值:a=10
    *   变量提升:--
    *   代码执行:
    */
   console.log(a, b, c); //=>10 13 14
   a = 100; //私有a=100
   c = 200; //全局c=200
   console.log(a, b, c); //=>100 13 200
   // 函数执行完成后:没有返回值(RETURN)、出栈释放
}
b = fn(10); //先把函数执行,执行的返回结果赋值给全局变量b  b=undefined
console.log(a, b, c); //=>12 undefined 200
/*
 * EC(G) 
 *   变量提升:
 *     var i;
 *     A = 0x000; [[scope]]:EC(G)
 *     var y;
 *     B = 0x001; [[scope]]:EC(G)
 */
var i = 0;

function A() {
    /*
     * EC(A1) 「闭包」
     *   作用域链:<EC(A1),EC(G)>
     *   形参赋值:--
     *   变量提升:;80*
     *     var i; 8
     *     x = 0x100; [[scope]]:EC(A1)
     */
    var i = 10;

    function x() {
        /*
         * EC(X1) 
         *   作用域链:<EC(X1),EC(A1)>
         *   形参赋值:--
         *   变量提升:--
         */
        /*
         * EC(X2) 
         *   作用域链:<EC(X2),EC(A1)>
         *   形参赋值:--
         *   变量提升:--
         */
        console.log(i); //10 10
    }
    return x; //return 0x100;
}
var y = A(); //y=0x100;
y();

function B() {
    /* 
     * EC(B)
     *   作用域链:<EC(B),EC(G)>
     *   形参赋值:--
     *   变量提升:
     *     var i;
     */
    var i = 20;
    y();
}
B();
// 函数执行,它的上级作用域(上下文)是谁,和函数在哪执行是没有关系的,“只和在哪创建有关系”:在哪创建的,它的[[scope]]就是谁,也就是它的上级上下文就是谁!!

函数执行,它的作用域(上级上下文) 是什么,和函数在哪执行是没有关系的,只和在哪创建有关系。在哪创建的,它的[[scope]](作用域)就是什么,也就是它的上级上下文就是谁!

/*
 * EC(G)
 *   变量提升
 *     var a;
 *     var obj;
 *     fn = 0x000; [[scope]]:EC(G) 
 */
var a = 1;
var obj = { //obj = 0x001;
    name: "tom"
};

function fn() {
    /* 
     * EC(FN)
     *   作用域链:<EC(FN),EC(G)>
     *   形参赋值:--
     *   变量提升:
     *     var a2;
     */
    var a2 = a; //私有a2=1
    obj2 = obj; //window.obj2=0x001;
    a2 = a; //私有a2=1
    obj2.name = "jack"; //把全局0x001堆内存中的name修改为'jack'
}
fn();
console.log(a); //=>1
console.log(obj); //=>{name:'jack'}
/*
 * EC(G)
 *   变量提升:
 *     var a;
 *     fn = 0x000; [[scope]]:EC(G)
 */
var a = 1;

function fn(a) {
    /*
     * EC(FN)
     *   作用域链:<EC(FN),EC(G)>
     *   形参赋值:a=1
     *   变量提升:
     *      var a; 「这一步浏览器会忽略,因为a私有变量已经存在于AO(FN)中了」
     *      a = 0x001; [[scope]]:EC(FN)  「不会重复声明,但是需要重新赋值」
     */
    console.log(a); //=>函数0x001
    var a = 2;
    console.log(a); //=>2
    function a() {
        /* 直接跳过,变量提升已经搞过了 */
    }
    console.log(a); //=>2
}
fn(a); //fn(1)
console.log(a); //=>1
/*
 * EC(G)
 *   变量提升:
 *     var a;
 *     fn = 0x000; [[scope]]:EC(G)
 */
console.log(a); //=>undefined
var a = 12;
function fn() {
    /* 
     * EC(FN)
     *   作用域链:<EC(FN),EC(G)>
     *   形参赋值:--
     *   变量提升:
     *     var a;
     */
    console.log(a); //=>undefined
    var a = 13;
}
fn();
console.log(a); //=>12

/*
 * EC(G)
 *   变量提升:
 *     var a;
 *     fn = 0x000; [[scope]]:EC(G)
 */
console.log(a); //=>undefined
var a = 12;
function fn() {
    /* 
     * EC(FN)
     *   作用域链:<EC(FN),EC(G)>
     *   形参赋值:--
     *   变量提升:--
     */
    console.log(a); //=>12
    a = 13; //全局a=13
}
fn();
console.log(a); //=>13

获取一个变量的值,首先看是否为自己私有变量,不是自己私有的,则按照作用域链向上查找,看是否为上级上下文的变量,一直到全局向下文为止(作作用域链机制)如果全局下也没有这个变量,则继续看window下是否有这个属性,如果也没有这个属性,则直接报错:Uncaught ReferenceError: a is not defined

/*
 * EC(G)
 *   变量提升:
 *     fn = 0x000; [[scope]]:EC(G)
 */
console.log(a); //ncaught ReferenceError: a is not defined
//获取一个变量的值,首先看是否为自己私有变量,不是自己私有的,则按照作用域链查找,看是否为上级上下文的...一直到全局向下文为止!如果全局下也没有这个变量,则继续看window下是否有这个属性,如果也没有这个属性,则直接报错:a is not defined
a = 12;
function fn() {
    console.log(a);
    a = 13;
}
fn();
console.log(a);
/*
 * EC(G)
 *   变量提升:
 *     fn = 0x000; [[scope]]:EC(G)
 */
a = 12;
function fn() {
    console.log(a);//12
    a = 13;
}
fn();
console.log(a);//13
/*
 * EC(G)
 *   变量提升:
 *     var foo; 
 */
var foo = 'hello';
(function (foo) {
    /*
     * EC(ANY)
     *   作用域链:<EC(ANY),EC(G)>
     *   形参赋值:foo='hello'
     *   变量提升:
     *     var foo; 「无需重复声明」
     */
    console.log(foo); //=>'hello'
    // A||B:A为真返回A的值,A为假返回B的值
    // A&&B:A为真返回B的值,A为假返回A的值
    // ||和&&同时出现的时候,&&的优先级是高于||
    var foo = foo || 'world'; //foo='hello'
    console.log(foo); //=>'hello'
})(foo); //自执行函数(立即执行函数)执行:传递实参 'hello'
console.log(foo); //=>'hello'

特殊题目1(新旧浏览器机制不一样导致)

判断体中含function

  • 全局上下文中的变量如果在判断体之中,不论条件是否成立,都要进行变量提升
  • 新版的不同:判断体中带function的在新版本浏览器(ie10以上)中会提前声明,不会在提前的赋值了)
console.log(a, func); //=>新版本 undefined undefined
  if (!("a" in window)) {
  //=>"a" in window 检测a是否为window的一个属性   !TRUE => FALSE
  
  var a = 1;
  function func() {}
}
console.log(a,func); //=>新版本 undefined undefined
  • 老版本:提升声明+赋值
    • var a
    • var func=函数
  • 新版本:只提升声明,不赋值
    • var a(全局上下文中声明一个a也相当于 window.a)
    • var func

判断体之外的情况下,function声明+赋值都会一起提升(不管新旧浏览器)

花括号(除函数)中含function

涉及到块级上下文,块级作用域的新老版浏览器的不同特性(新:ie11以及以上,chrome新版浏览器,老:ie10 以及以下,老版的chrome浏览器)

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

以上输出新老版本浏览器不一样

ie10以及以下(包括老版本谷歌浏览器)结果:

chrome浏览器最新结果:

分析:

在老版本的浏览器中,只要不是函数的大括号(不管是循环体的大括号还是for循环的大括号)都是一样的机制,都要进行变量提升,没什么特殊的机制。新版浏览器有许多不一样的机制

注意截图里的第11步,在第11步只声明了,还没赋值,在第22步映射的时候,才赋值的。

  1. EC(G)下变量提升:当前上下文中,出现在"非函数和对象"的大括号中的function,只声明不定义
  2. EC(G)下代码执行:如果大括号中出现了function/let/const等关键字,则会形成一个全新的块级私有上下文
  3. 在块级上下文中没有thisauguments,只会初始化作用域链,变量提升,代码执行
  4. 因为function(){}即在全局上下文生命了,也在私有上下文中声明+定义了,接下来,执行到function(){}这一步的时候,在私有块级上下文中,因为这一步已经处理过了(声明+定义),所以不会重复执行,但是因为这一步操作he全局上下文也有关系,所以把这一行代码"之前"对foo的操作(声明+定义),都"映射/同步"给全局一份,但"之后"对foo的操作都认为是自己私有的,和全局没有关系了

这种机制出现的原因:

  • 向后兼容ES3/ES5的语法规范
  • 向前兼容ES6的心语法规范(块级上下文)

接下来的机制都按新版本来

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

分析:

  • 第2步:此时全局foo只声明,未定义,为undefined
  • 第8,9步,变量提升第一次定义+声明,第二次覆盖声明
  • 第11步,第一次映射,把之前对foo的操作(8,9步)映射给全局foo一份,此时全局foo就是指向的堆内存0x001
  • 第13步只把块级作用域下的foo赋值为1(这里少标了一步赋值为1)
  • 第14步,第二次映射,把之前对foo的操作(8,9步+foo=1)映射给全局foo一份,此时全局foo=1
  • 最后打印1
debugger//可以debugger看一下全局作用域和块级作用域下的每一步的变量的变化
console.log(foo)//undefined(函数foo在遇到块的时候,变量提升只声明,不定义)
{	
	console.log(foo)//块级作用域里经过第一次提升(clg(1)定义+声明),第二次提升((clg(2))只声明)过的第二个foo函数
    function foo() {//第一次映射
    	console.log(1)
    }
    console.log(foo)//此时还是块级作用域中的第二个foo
    foo = 1;
    console.log(foo)//块级作用域下的 1
    function foo() {//第二次映射,全局foo已经为1
    	console.log(2)
    }
    console.log(foo)//块级作用域下的 1
}
console.log(foo);//全局作用域下的 1

{
    function foo() {}
    foo = 1;
    function foo() {}
    foo = 2;
    console.log(foo); //私有2
}
console.log(foo);//全局1 

所以真实项目中,不要把function这个操作放在除了函数和对象的大括号中

下面也是一样的原理:

var a = 0;
if (true) {
    a = 1;
    function a() {};
    a = 21;
    console.log(a)//21(function a() {};直线映射给全局一份,之后的是自己私有的)
}
console.log(a); //1

let在声明之前不可使用

var a = 0;
if (true) {
	a = 1; //=>Uncaught ReferenceError: Cannot access 'a' before initialization
	let a = 10;
	a = 21;
	console.log(a)
}
console.log(a);

未声明之前不可以使用变量

// Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a); 
{
	console.log(a);
	var a = 10;
	function a() {}
	console.log(a);
}
console.log(a); 

特殊题目2(函数形参有默认值时)

debugger;
var x=1;
function func(x,y=function anonymous1(){x=2}){
    x=3
    y();
    console.log(x);
}
func(5);//2
console.log(x);//1 

debugger;
var x=1;
function func(x,y=function anonymous1(){x=2}){
    var x=3;//这里带var
    y();
    console.log(x);
}
func(5);
console.log(x);

上面这里func执行时,一直到形参赋值,还是和上面一样的,当满足以下两个条件的时候:

  1. 形参有默认值
  2. 函数体当中有变量声明(var let const function(函数声明还有更特殊的机制)) 就会触发一个特殊机制
  • 函数执行不仅会形成一个私有上下文,而且会把函数体当做一个块级上下文(两个上下文),这个块级上下文的上级上下文就是函数形成的私有上下文
  • 并且私有上下文的操作直到形参赋值就结束了,接下来的代码,全都在下面的块级上下文中进行操作(把函数的大括号当成块级上下文了)
  • 并且如果块级上下文中的某个私有变量和当前私有上下文中的形参变量的名字一样,还会把形参变量的值默认给块级上下文(在块级上下文的变量提升阶段赋值的)

这个块级上下文的变量的来源就是用var等声明的那个变量,并且如果在原来的私有上下文中存在,就会把块级上下文中的值也默认赋值一份,所以块级上下文中的x也是5

即使我传了10,没有使用y的默认值,也会有这个机制

如果随便声明了其他变量,即使是非私有变量,也会有这个机制

但是如果声明是function,会有所不同

如果是function声明变量,想要出现块级作用域,function声明的变量必须与形参中的其中一个一致

机制如下: 需要注意:

  1. 第15步,在形成的块级上下文的变量提升阶段,会默认把上级上下文中存在的x赋值给块级上下文,所以块级上下文的x就是5
  2. 第19步,执行到y(),因为y是在私有上下文中创建的,作用域链是<EC(Y),EC(FUNC)>,所以执行y()修改的是私有下上文中的x
  3. 第21步,打印的是私有上下文中的x

debugger看一下

  1. 块级上下文中的x改为3

//debugger
var x=1;
function func(x,y=function anonymous1(){x=2}){
    var x=3;
    var y=function anonymous2(){x=4};
    y();
    console.log(x);//4
}
func(5);
console.log(x);//1
  1. 改块级上下文

  2. 执行完y 因为y被赋值为了anonymous2anonymous2是在块级上下文中创建的,所以他的作用域就是块级上下文,所以执行的时候修改的块级上下文中的x