【JS】变量提升相关题目

349 阅读9分钟

本文所用到的执行环境版本信息:

Chrome浏览器:5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36

题1

var a;
console.log('1 ', a); // log1
var a = 12;
function fn() {
  console.log('2 ', a); // log2
  var a = 13;
}
fn();
console.log('3 ', a); // log3

在Chrome Console中的执行结果如下:

image.png

解释:

  1. log1:fn外部的变量a通过var重复声明,而且对a的赋值在log1之后,因此log1是undefined
  2. log2:在fn内部也用var声明了变量a,而且由于var声明的变量在函数fn的范围内提升,因此当log2访问变量a时,首先在函数fn的作用域内就找到了变量a,而对这个a的赋值是在log2之后,所以log2是undefined
  3. log3:发生在fn外部,由于函数作用域的存在,log3是访问不到fn内部声明的变量a的,它只能访问到fn外部声明的变量a,而且已经被赋值为12,因此log3是12

题2

var a;
console.log('1 ', a); // log1
var a = 12;
function fn() {
  console.log('2 ', a); // log2
  a = 13;
}
fn();
console.log('3 ', a); // log3

在Chrome Console中的执行结果如下:

image.png

解释:

  1. log1:fn外部的变量a通过var重复声明,而且对a的赋值在log1之后,因此log1是undefined
  2. log2:和题1不同的是,fn内部并没有用var声明变量a,当log2寻找变量a时,在fn的作用域内找不到a的声明,就沿着作用域链到fn的外部作用域寻找,此时找到了值为12的变量a,因此log2是12
  3. log3:因为fn内部没有声明变量a,因此fn内部一切对a的操作,修改的都是外部声明的变量a,在fn执行结束时,变量a的值已经被修改为13。log3访问到的也是这个外部声明的a,因此log3是13

题3

console.log('1 ', a); // log1
a = 12;
function fn() {
  console.log('2 ', a); // log2
  a = 13;
}
fn();
console.log('3 ', a); // log3

在Chrome Console中的执行结果如下:

image.png

解释:

和题2的不同在于,此时在全局作用域范围内,也没有任何对变量a的声明了,log1中直接去访问变量a时,就报错了。

如果我把a = 12这一句拿到log1之前的话:

a = 12;
console.log('1 ', a); // log1
function fn() {
  console.log('2 ', a); // log2
  a = 13;
}
fn();
console.log('3 ', a); // log3

在Chrome Console中的执行结果如下:

image.png

可见,直接对a的赋值,会自动在window上创建a属性,此后对a的操作,都是对window.a的操作:

image.png

顺便复习一个特性:不管在全局、函数、块作用域内a = 12这样的语句都会导致在全局对象window上创建a属性,实际上也就是执行了window.a = 12而只有在全局作用域中var a这样的语句会导致在全局对象window上创建a属性,即window.a

下面是个例子:

image.png

ab都是直接赋值,结果都在window上创建了window.awindow.b

而当在函数内部使用了var声明时,就不会在window上创建属性了:

image.png

var a声明在全局,因此会挂到window上,var b声明在fn内部,则不会挂到window上。

题4

console.log('1 ', foo); // log1
{
  console.log('2 ', foo); // log2
  function foo() {}
  foo = 1;
  console.log('3 ', foo); // log3
}
console.log('4 ', foo); // log4

在Chrome Console中的执行结果如下:

image.png

IE11的执行结果和Chrome的相同,而在IE10及以下中的执行结果如下:

image.png

不同点在于log1和log4,以及window.foo的值。

解释:

首先说在chrome中,块级作用域内声明的函数foo,会导致在全局作用域有一个变量名为foo的声明提升。而我们熟知的函数声明语句会被提升到作用域的顶部,依然局限在其所在的块级作用域内。也就是说题4的代码相当于下面的代码:

var foo // 产生一个全局的var foo 声明
console.log('1 ', foo); // log1
{
  /*
   函数声明语句被提升到其所在的块级作用域顶部,
   同时,这个函数体也会赋值给由于声明提升而产生的全局变量foo,
   可以认为此时的块级作用域内有两个变量foo,一个是局部的,一个是全局的,
   此后在块级作用域内所操作的变量foo,所用的都是局部的foo。
  */
  function foo() {} 
  console.log('2 ', foo); // log2
  /*
   在块级作用域内所操作的变量foo,是局部的foo,这样一改导致局部变量foo
   不再是一个函数,而成了1
  */
  foo = 1; 
  console.log('3 ', foo); // log3
}
// 离开块级作用域后,访问到的就是全局的foo,它依然是个函数
console.log('4 ', foo); // log4

在Chrome Console中的执行结果如下:

image.png

可见是和题4的结果一样的。

而为什么题4在IE10及以下的执行结果又和Chrome中的不同呢?我们现在把语句块去掉:

console.log('1 ', foo); // log1

  console.log('2 ', foo); // log2
  function foo() {}
  foo = 1;
  console.log('3 ', foo); // log3

console.log('4 ', foo); // log4

在Chrome Console中的执行结果如下:

image.png

怎么样,这就和IE10及以下的结果一样了。因此可以理解为,在IE10及以下中,直接无视语句块{}的存在。所有语句都处于全局作用域中,由于函数声明的提升,log1和log2都是foo() {},此后变量foo被改为1,因此log3和log4都是1window.foo也是1

再看下面的变体,

image.png

当把块级作用域内的函数声明改为函数赋值时,window.foo的值就变成了1,在IE10中也是如此。虽然在语句块内部,但var foo不受其影响,声明依然提升到全局作用域顶部,由于此时是函数赋值的形式,因此在块级作用域内foo函数体也不会提升到log2之前,因此log1和log2都是undefined。此时等于块内块外都只有一个变量foo,把foo改成1后,log3和log4也都变成了1。这种情况下,语句块形同虚设。

题5

console.log('1 ', foo); // log1
{
  console.log('2 ', foo); // log2
  function foo() {1}
  console.log('3 ', foo); // log3
  foo = 1;
  console.log('4 ', foo); // log4
  function foo() {2}
  console.log('5 ', foo) // log5
}
console.log('6 ', foo); // log6

在Chrome Console中的执行结果如下:

image.png

在IE10及以下的执行结果:

image.png

对于IE10及以下的结果,很简单,直接忽略语句块{}的存在,结果很好理解。

对于Chrome中的情况,我们先按题4中的思路来分析,结果应该是这样:

image.png

唯一不对的地方就是log6和window.foo的结果。按说语句块内的foo = 1应该修改的是局部的foo,而不会影响全局的foo。

可是还有一个因素,就是语句块内函数声明语句所在的位置,也会影响全局foo的值

如果题目真的如上图中我们理解的这样的话,两次函数声明语句都在修改局部foo之前,此时对这个局部foo的修改的确不会影响全局foo的值,从log6的输出结果也可以看出,widow.foo依然是foo() {2}

可题5中,function foo() {2}这条声明语句出现在了foo = 1;之后,这就会导致对局部foo的这次修改,也会修改到全局foo上。这就是为何最终log6输出的全局foo也变成了1

也就是说这里存在三条规则:

  1. 语句块内的函数声明语句,会在全局作用域内提升出来一个var变量声明(也就是log1的输出结果)
  2. 语句块内的函数声明语句,会提升到该语句块顶部(也就是log2,log3的输出结果)
  3. 出现在(语句块内的)函数声明语句之前的,对该局部变量名的修改,也会同步改动到全局的那个变量名上(也就是log6的输出结果)。而出现在函数声明语句之后的修改,则改动只在局部变量上,而不会影响全局的那个变量

如果我们把foo = 1这一句放到function foo() {2}之后,

image.png

可见,此时foo = 1就不会影响全局的那个foo(log6的输出,以及window.foo)

题6

var x = 1;
function func(x, y = function anonymous() {x = 2}) {
  x = 3;
  y();
  console.log('1 ', x);
}
func(5);
console.log('2 ', x);

在Chrome Console中的执行结果如下:

image.png

解释:

func函数内部直接操作的变量x,就是参数中的xy的默认函数anonymous()中直接操作的变量x,顺着作用域链向外找,会发现也是参数中的x。因此log1是2,外部的log2始终访问的是全局的x,因此是1

题7

var x = 1;
function func(x, y = function anonymous() {x = 2}) {
  var x = 3;
  y();
  console.log('1 ', x);
}
func(5);
console.log('2 ', x);

在Chrome Console中的执行结果如下:

image.png

解释:

和题6的不同在于,此时func函数内部也用var声明了变量x,相当于在func内有了两个变量x,可以理解为func参数中的xy是在一个函数内的作用域A中,而func函数体内的3条语句又是在一个作用域B中,而B在A之内。

看下面这个变体:

image.png

看得出anonymous函数内log1和log2访问到的都是参数中的x,而log3访问到的才是func函数体内声明的x。也就是说对于参数中声明的函数anonymous而言,它访问的是参数中声明的变量。因为参数所在的作用域A是在func函数体内作用域B之外,因此func体内可以访问参数中的变量,但形参函数anonymous访问不到func体内声明的变量。

再看下面这个变体:

var x = 1;
function func(x, z = 'arg z', y = function anonymous() {console.log('1 ', x, z);  x = 2; z = 'change arg z'; console.log('2 ', x, z)}) {
  var x = 3;
  var z = 'zzz';
  y();
  console.log('3 ', x, z);
}
func(5);
console.log('4 ', x);

在Chrome Console中的执行结果如下:

image.png

我增加了参数z,也在func内声明了变量z,然后在anonymous函数体内和func函数体内打印z,可以看到,anonymous函数中的zfunc函数中的z没有任何关系。再看下图,

image.png

我在func函数体内声明的变量t,在形参函数anonymous内是访问不到的,也就是说形参函数anonymous只能访问func参数中声明的变量。

归纳起来就是:

  1. func内(作用域B)要是声明了和参数(作用域A)同名的变量,则func内所能访问到的都是func内声明的这个变量,因为作用域B嵌套在作用域A之内,屏蔽了作用域A也就是参数中的同名变量
  2. 形参函数(属于作用域A)内部只能访问func参数(作用域A)中声明的变量,因为外层的作用域A,访问不到其内层的作用域B。形参函数内自行声明的除外

题8

var x = 1;
function func(x, y = function anonymous() {x = 2}) {
  var x = 3;
  var y = function anonymous() {x = 4};
  y();
  console.log('1 ', x);
}
func(5);
console.log('2 ', x);

在Chrome Console中的执行结果如下:

image.png

根据题7中的总结,func中声明了和参数同名的变量时,访问的就是func中声明的变量,而与参数无关。此题中y()执行的就是var y = function anonymous() {x = 4};这个函数,而且它改的x也是func内声明的var x = 3;这个x,因此log1是4