javascript笔记之深入理解ES6参数作用域

参数默认值是变量

1
2
3
4
5
6
7
var x = 1;

function f(x, y = x) {
console.log(y);
}

f(2) // 2

上面代码中,参数y的默认值等于x,在调用函数的时候,形成一个单独的参数作用域。es标准中参数是从左到右顺序初始化,所以x先于y初始化,当y初始化的时候指向了第一个参数x,而不是指向全局变量x,所以输出2。

例子2

1
2
3
4
5
6
7
8
var x = 1;

function f(y = x) {
let x=2;
console.log(y);
}

f() // 1

在上面的代码中,参数y的默认值等于x。在调用函数f的时候,形成一个单独的参数作用域。es标准中参数是从左到右顺序初始化,此时参数作用域内并没有变量x,在作用域链上查找x在全局作用域,所以y指向全局作用域x。所以函数作用域内x和参数作用域的y没有关系,所以输出1。

如果此时全局变量x不存在,则会报错

1
2
3
4
5
6
function f(y = x) {
let x=2;
console.log(y);
}

f() // ReferenceError: x is not undefined

下面的写法也会报错

1
2
3
4
5
6
var x=1;
function f(x = x) {
// ...
}

f() // ReferenceError: x is not undefined

上面代码中,参数是从左到右顺序初始化的时候,x并不存在,无法使用,不能完成初始化,类似let x=x;

参数默认值为函数

例2

1
2
3
4
5
6
7
8
let foo = 'outer';

function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}

bar(); // outer

在上面的代码中,参数func的默认值是一个函数。在调用函数的时候,形成一个单独的参数作用域。在这个作用域里面定义了一个变量y指向匿名函数,在匿名函数中的变量foo在参数作用域没有找到变量foo,所以foo指向了外层的全局作用域的foo,所以输出1。

例3

1
2
3
4
5
6
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}

bar() // ReferenceError: foo is not defined

上面代码中,函数外层并没有定义变量foo,所以就报错了。

例4

1
2
3
4
5
6
7
8
9
10
var x = 1;
function foo(x, y = function() { x = 2; }) {
// 这里只能使用var声明,如果是let/const会报错,因为参数中已经有x变量了
var x = 3;
y();
console.log(x);
}

foo() // 3
x // 1

在上面的代码中,参数作用域里首先声明了变量x,然后声明了变量y,y的默认值是一个匿名函数。这个匿名函数内部的变量x,指向参数作用域的第一个参数x。

函数作用域又定义了一个内部变量x,函数作用域与参数作用域分属不同的作用域,所以两个x不是同一个变量,因此y()执行之后,改变的是参数作用域的x,所以全局作用域的x和函数作用域的x都没有改变。

让我们稍微改变下例4的代码。

例5

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

foo() // 2
x // 1

此时的函数作用域x指向了参数作用域的x,函数y内部的x也指向函数作用域的x,这个时候输入的就是2了。

转换为es5

将例4的代码转为es5的就比较清楚参数作用域的范围了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//这里是全局作用域
var x=1
function foo(x, y) {
// 这里相当于参数作用域
if (typeof y == 'undefined') {
y = function() { x = 2; }; // 参数x屏蔽了全局作用域的x,所以x指向参数中的x
}

return function() {
// 这里相当于函数作用域,重新声明了一个变量x,屏蔽了参数作用域的x
// 这里只能使用var声明,如果是let/const会报错
var x = 3;
y();
console.log(x);
}.apply(this, arguments);
}

将例5的代码转为es5的就比较清楚参数作用域的范围了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//这里是全局作用域
var x=1
function foo(x, y) {
// 这里相当于参数作用域
if (typeof y == 'undefined') {
y = function() { x = 2; }; // 参数x屏蔽了全局作用域的x,所以x指向参数中的x
}

return function() {
// 这里相当于函数作用域,此时函数作用域没有变量x,向上层参数作用域查找x
// 所以这里x就是参数作用域的x
x = 3;
y();
console.log(x);
}.apply(this, arguments);
}

结论

由上面es5的代码可以看出,参数作用域可以访问自己和外层的变量,无法访问函数体内的变量,这样理解起来参数作用域就容易了。

如果还理解不了,就在大脑里把有默认值参数的函数转换为es5的形式,也就豁然开朗。

应用

利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

1
2
3
4
5
6
7
8
9
10
function throwIfMissing() {
throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}

foo()
// Error: Missing parameter

上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。

从上面代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(注意函数名throwIfMissing之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。

参考

ES6入门教程

Note 6. ES6: Default values of parameters