ES6函数作用域

ES6 中的函数(非箭头函数)跟 ES5 的函数是有本质区别的,核心在于作用域,先看一道题:

1
2
3
4
5
6
7
let x = 0
function b(y = x, x) {
x = 3
console.log(x, y)
var y = 4
}
b()

应该打印出什么呢?大家不妨亲自运行一下,看看跟自己想的是否一致。这里总结了两点 ES6 函数中比较特殊的地方:

ES6 函数可能存在两个作用域

在 ES5 中,函数只有一个作用域,但是在 ES6 中,如果给函数参数设置了默认值,那么就会创建两个作用域:

  • 函数参数作用域
  • 函数体作用域

请看示例:

1
2
3
4
5
6
7
8
9
var x = 1
function foo(x, y = function() { x = 2 }) {
console.log(x)
var x = 3
y()
console.log(x)
}
foo()
console.log(x)

在这一题中,符合函数参数设置了默认值的条件,于是就存在了三个作用域:

  • 全局作用域
  • 函数参数作用域
  • 函数体作用域

在每个作用域下变量的值如下:

1
2
3
-> {x: 3} // 函数体作用域
-> {x: undefined, y: function() { x = 2; }} // 函数参数作用域
-> {x: 1} // 全局作用域

当 y 函数执行的时候,它虽然属于函数体作用域,却是在函数参数作用域下定义的,按照 JS 词法作用域的定义,会先在当前作用域下查找变量,如果找不到再去上级作用域中查找,所以 x = 2 只会影响函数参数 x ,不会影响到函数体内的 x 也不会影响到全局的 x。

出现两个作用域的时候,有一点需要注意:在函数体作用域内,如果出现与函数参数同名的变量,其变量提升的初始值与同名的函数参数相同而不是 undefined。例如:

1
2
3
4
5
6
7
var a = 1
function fn(a = 2) {
console.log(a) // 打印 1 而不是 undefined
var a = 3
}

fn(a)

如果把 var 改成 let/const,则会报错:

1
2
3
4
5
6
7
var a = 1
function fn(a = 2) {
console.log(a)
let a = 3 // 词法分析阶段直接报错:SyntaxError: Identifier 'a' has already been declared
}

fn(a)

如果同名变量是函数:

1
2
3
4
5
6
7
var a = 1
function fn(a = 2) {
console.log(a) // 打印 function a
function a() {}
}

fn(a)

因为函数声明在变量提升的时候会赋值为函数的堆内存地址。

但是下面的代码就有意思了:

1
2
3
4
5
6
7
var a = 1
function fn(a = 2) {
console.log(a)
var a = 3
function a() {}
}
fn(a)

不同 JS 引擎下的结果是不一致的,在 Chrome 下打印 1,在 Firefox 下打印 function a,大家可以试试看,希望有大佬可以解释下原因。

ES6 函数参数可能存在暂时性死区

暂时性死区这个概念是 ES6 中提出来的,大家对其印象和应用场景可能仅限于下面的代码:

1
2
3
4
5
function f() {
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 1;
}
f()

但是在函数参数中也可能会出现这种问题,也就是开头的那段代码:

1
2
3
4
5
6
7
let x = 0 // 全局作用域下的 x
function b(y = x, x) { // 函数参数作用域下定义了 x 和 y
x = 3 // 函数体作用域下定义了 y
console.log(x, y)
var y = 4
}
b()

函数参数作用域下也存在类似于 let/const 这种变量提升的机制,即 y 和 x 两个变量被收集起来了,属于 uninitialized 的状态,然后再按顺序赋值,如果没有默认值就赋 undefined,如果有默认值就赋默认值,而如果在变量 initialized 之前被其他变量用到就会报错,这就解释了为什么上面的代码会报错了。