浅析前端之变量提升var、let(看了这篇,面试题就不愁了)

677 阅读5分钟

说起变量提升前端的小伙伴肯定都不陌生,写这篇文章的目的也是为了让自己巩固一下自己学到的知识,也送给对这些知识不是那么熟悉的同学,与之共勉~

首先我们先来看一道题热热身

var a = 12,
    b = 13,
    c = 14;
function fn(a) {
    console.log(a, b, c);
    var b = c = a = 20;
    console.log(a, b,  b)
}
fn(a);
console.log(a, b, c)

答案是什么先不公布,首先我们要知道在JS代码从上到下执行之前做了什么;

JS代码执行的之前,会有一个预编译的过程,预编译有两个任务

  1. 对关键字带有var的进行提前声明,但是不定义,值为undefied;
  2. 对于带有function的进行声明 + 定义;

那么我们看看这道题是如何执行的
代码执行首先js预编译

var a = undefined, b = undefined, c  = undefined;
var fn = function () {
    console.log(a, b, c);
    var b = c = a = 20;
    console.log(a, b,  b)
}

这个时候 函数function作为字符串存储在开辟的堆内存之中,并没有执行,并且将这个堆内存的地址赋值给了fn,预编译完成;

2.js代码开始从上往下执行;
a = 12, b = 13, c = 14;
fn(a) 函数执行;
将预编译阶段的堆内存中的代码从上到下的执行;

fn (a) {
    函数执行分为两步;
    1.先检验是否有参数传递,如果有先执行形参赋值;
    2.如果没有,就检查是否有带var的关键字,如果有就进行变量提升,如果都没有,代码从上往下继续执行;
    
    函数fn 有形参a 所以先声明a = 12;
    var b = undefined; 
    然后代码从上往下执行;
    console.log(a, b, c) 
    a  = 12;b = undefined; c不是fn函数中的私有变量,所以去上级作用域中找,找到window,发现了c = 14;
    所以第一次console的结果是12 undefined 14;
    b = c = a = 20; b和a都是fn中的私有变量,c是window的全局变量,所以c = 20 = window.c = 20;
    console.log(a, b, c) 
    此时fn中的私有变量a, b, 以及window下的c都已经修改为20; 所以结果是20 20 20 ;
} 
console.log(a, b, c)
a, b, c是window下的全局变量, 12,13,20 ;

这个分析不知道大家有没有对变量提升有了一个清晰的理解? 那我们接下来看下一道题

let i = 2;
let fn = function (n) {
    i*= 2;
    return function (m) {
        i-= (n--) + (++m);
        console.log(i)
    }
};
let f = fn(1);
f(2);
fn(3)(4);
f(5);
console.log(i);

这道题和上一道题差不多,就是var 变成了 let;那么同学们也知道,只有带var关键字的会进行变量提升,带let的并不会;那这道题是不是就简单很多了呢? 还是老规矩,你们先自己尝试把答案写出来,然后看看分析过程,答案是否一致;

代码执行首先js预编译

开辟一个堆内存,把匿名函数当作字符串存入一个地址A中;
function (n) {
     i*= 2;
    return function (m) {
        i-= (n--) + (++m);
        console.log(i)
    }
}

代码从上往下执行; let i = 2; let fn = 堆内存地址; let f = fn(1)执行的返回结果;

fn(1)执行;
fn(n) {
    n = 1; 形参赋值;
    i*= 2; (i是上局作用域下的变量2*=2 = 4; 此时全局下的i = 4);
    return function (m) { (此时继续声明一个堆内存,把这个匿名函数当作字符串存入这个地址,并且返回地址B,也就是f = B)
        i-= (n--) + (++m);
        console.log(i)
    }
};

f(2)继续执行; f(2)执行就相当于B(2)执行
f(2) {
  m = 2; (形参赋值);
  i-= (n--) + (++m); (i去上级作用域找,找到全局下的i = 4, n也是去上级作用域A下面找 n = 1;全局i = 4-= 1+3 = 0;此时上级作用域A中的n = 0; 当前m = 3);
  console.log(i = 0);
};
fn(3)(4)执行;开辟一个新的堆内存AA执行并且将AA中的返回值BB执行
fn(3) {
    n = 3; 形参赋值;
    i*= 2; 0*=2 = 0;
    return BB 并且将BB执行
    BB(4){
        m = 4;
        i-= (n--) + (++m); (0-= 3 + 5) = -8; 
        console.log(i = -8)
    }
};
f(5)执行 也就是B执行
f(5) {
    m = 5;
    i-= (n--) + (++m); (-8 -= 0 + 6 = -14)
    console.log(i = -14)
};
console.log(i = -14)

怎么样,是不是很简单?那么我们趁热打铁,再来一道;

let n = 10, obj = {n: 20};
let fn = obj.fn = (function() {
    this.n++;
    n++;
    return function(m) {
        n+=10+ (++m);
        this.n += n;
        // console.log(this.n)
        console.log(n);
    }
})(obj.fn);
fn(10);
obj.fn(10);
console.log(n, obj.n)

这道题就说一下思路,这里就有一点不一样就是多了this;那么我们来看一下;

let n = 10; obj = {n: 20} )这里的obj等于一个堆内存;
fn = obj.fn = 一个自执行函数的返回值 = 一个堆内存地址A,参数是obj.fn;
自执行函数执行
没有形参接受,所以没有形参赋值;
this.n++; 自执行函数中的this默认是window; window下没有n = NAN;
n++; 去上级作用域找n = 10 ++ = 11; 全局n = 11;
return A

fn(10) {
    m = 10;
    n+= 10 + (++m); 11+= 10 + 11= 32; m =11;
    this.n += n; = NAN += 32 = NAN;
    console.log(n = 32)
};
obj.fn(10) {
    m = 10;
    n+= 10 + (++m); 32+= 10 + 11= 53; m =11;
    this.n += n; 20 += 53 = 73;
    console.log(n = 53)
};
console.log(n = 53, obj,n = 73; window.n = NAN)

怎么样?是不是很简单?let 和 var的区别就是带var的会变量提升,let 不会,并且var的变量会添加到window上,let 也不会; 我也不知道我说的详细不详细,反正我把我知道的都描述出来了,有什么不对的希望大家可以指出