不要随便使用形参默认值

296 阅读3分钟

首先来看一段代码

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

正常来说,我们希望打印出来的应该是2和1,但是我们在浏览器上可以看到实际打印的结果是3和1

image.png

出现这样结果的原因是什么呢?

这是因为,当函数中存在形参赋默认值的时候,如果函数体内使用了var/let/const来声明变量,在代码执行的时候,会把上图的函数解析为如下的函数(这样只是好理解,并不是解析成这样,实际上这里还有一个特殊处理,我没想好怎么用代码表示,下面分析的时候会讲)

function func(x,y){
    if(y === undefined) y = function () { x = 2 }
    (function(){
        var x = 3
        y()
        console.log(x)
    })()
}

一个函数会被割裂成两个函数,生成两个执行上下文,一个为函数执行上下文(VO-FN),一个为块级执行上下文(VO-BK),函数执行上下文(VO-FN)的代码字符串到形参赋值就截止了,函数体中的代码字符串属于块级执行上下文(VO-BK),在块级执行上下文(VO-BK)中执行的y函数,其作用域链是由执行时创建的函数执行上下文(VO-Y)指向函数创建时赋值的作用域即函数执行上下文(VO-FN),并不影响块级执行上下文(VO-BK)中的x,所以第一个打印的结果是3而不是2

口说无凭,来打个debugger来逐步看看代码的效果

image.png

在执行函数体的这一步,我们可以看到,左边变量出现了两个执行上下文,而且都有一个x为5,这就是我之前说的没想好怎么用代码表示的特殊处理,如果块级执行上下文(VO-BK)中出现了和函数执行上下文(VO-FN)同名的变量,那么函数执行上下文(VO-FN)会把该变量赋值给块级执行上下文中的同名变量,这里我们加上var y = 10试试看

image.png

可以看到,一旦我们在函数体中加上了var y = 10,那么块级执行上下文(VO-BK)中就出现了y并且值为函数执行上下文(VO-FN)中的同名变量的值

当然,不要使用let和const声明同名变量,这个特殊处理只在代码执行阶段生效,如果你使用let和const声明同名变量,代码在词法解析阶段就会直接报错,这个特殊处理,只能使用var来触发

代码往下执行,我们可以看到,x和y修改的都是块级执行上下文(VO-BK)中的变量

image.png

当然,再往下执行,y会因为不是函数而报错,所以我们删掉var y = 10,重新执行一次

image.png

函数y执行完,我们会发现,被修改的是函数执行上下文(VO-FN)中的x,而不是块级执行上下文(VO-BK)中的x,这是因为函数本身的作用域链机制,y函数是在函数执行上下文(VO-FN)中创建的,所以y函数上的[[scope]]属性就会指向VO-FN,所以node.js在y函数执行生成的函数执行上下文(VO-Y)中没找到变量x,自然依照执行上下文生成时初始化的作用域链,去函数执行上下文(VO-FN)中找。

这样分析一遍之后,大家就明白为什么结果是3和1而不是2和1了

所以,不要随便使用形参默认值