之所以会写这篇文章,主要源于笔者在重构老项目的时候发现了一个bug,导致某个插件不生效了,在review加search code加断点调试之后,发现了原因:一个同名的变量将插件方法给覆盖了,ohmyGad。

正文

1.变量是如何被覆盖的

在一般情况下,js代码都是自上而下执行的,对于同一个变量,我们可以通过如下方式来修改:

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

2.变量提升

上面的覆盖过程大家都很好理解,那么看如下的操作呢?

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

这个时候console.log()都会输出undefined而不会报错,这是为什么呢?这里就是变量提升起到的作用。我们在用var或者函数声明的方式定义一个变量时,这个变量的定义会提升到方法体的最顶端,即如下所示:

var a = undefined;
var b = undefined;
console.log(a)
// ..
console.log(b)

因此我们得出一条结论:

函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部。

值得注意的是,我们使用let,const定义变量的时候,并不会发生提升,因为它存在局部(块)作用域的概念,会出现暂时性死区,所以在它们之前打印变量将报错。如果对暂时性死区或者对es6不太了解的朋友可以参考我的另一篇文章:

一文快速掌握 es6+新特性及核心语法

对let和const以及es6的新特性有详细的介绍。

3.更近一步——变量提升的优先级

直接剖出问题:

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

此时代码会打印什么呢?答案是会打印1。这个问题也是我之前面试一些求职者的过程中错误高发区,这里隐藏着一个概念:函数声明提升的优先级高于变量声明的提升。浏览器底层的实现过程是这样的:当js解析器在遇到函数声明时,会优先将其提升到定义体顶部,其次再是var声明的变量,这样就导致函数a被变量a给覆盖的情况,所以最终将打印1。

4.函数参数作用域与作用域链

作用域就是变量和函数的可访问范围,当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain),来保证对执行环境有权访问的变量和函数的顺序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象。然后会一层层向外查找,直到发现第一个指定的变量为止。

在了解完以上概念之后,我们来看看下面这个问题:

var a = {
    name: 'xuxi'};
function b(a){
a.age = 12;
a = { num: 1};
return a
}
var a1 = b(a);
console.log(a, a1)

上面代码打印的是什么呢?其实这个是我今天出的面试题,还是因为一个朋友之前问了我这个问题,我觉得有必要总结一下。虽然今天的候选人没有答出来,但是我相信在给他解释完之后他应该不虚此行(说过了,不好意思)。

这块主要还是函数内部作用域和引用类型的一个问题。具体过程如下:

(1)我们根据之前介绍的作用域和作用域链的概念可以知道,在函数体内,变量会就近查找,而函数参数会存在于函数体内部作用域中,所以当我们把全局变量a当作入参传递给函数时,又由于全局a是引用类型,此时只是引用了它的地址,那么我们通过a.age设置属性时,全局a也会改变。

 (2)第二步是将a赋予了一个新的值,此时的a根据就近查找其实是参数a,本质上是将参数a赋予了一个新的对象,这个时候和全局变量的a没有任何关系了,此时函数最后会返回一个新的对象。

综上两步分析,我们就会明白为什么打印a时输出的是{name: 'xuxi', age: 12},打印a1会输出{num: 1}了。

总结

函数声明提升,变量作用域以及作用域链这块一直是学习javascript的基础也是重点,所以希望这篇文章可以让大家更好的掌握它。 如果想了解更多webpack,node,gulp,css3,javascript,nodeJS,canvas等前端知识和实战,欢迎在公众号《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

更多推荐

欢迎关注下方公众号,获取更多前端知识精粹和学习社群:在公众号点击进群,可以加入vue学习小组,一起学习前端技术;

回复 学习路径,将获取笔者多年从业经验的前端学习路径的思维导图

趣谈前端

Vue、React、小程序、Node 

 

前端 算法|性能|架构|安全