ECMAScript-6-Primer-III

Posted by Gloomymoon on 2018-03-30

ECMAScript 6 Primer III

7 函数的扩展

7.1 函数参数的默认值

ES6允许为函数的参数设置默认值,直接写在参数定义的后面,而不用采用变通的方式。

1
2
3
4
5
6
7
function log(x, y = 'World'){
console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

这是非常简洁自然的写法。由于参数变量是默认生命的,所以不能用letconst再次声明。使用参数默认值时,函数不能有同名参数。

1
2
3
4
5
6
7
8
9
10
// 不报错
function foo(x, x, y) {
// ...
}

// 报错
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值结合起来使用。

1
2
3
4
5
function foo({x, y = 5} = {}) {
console.log(x, y);
}

foo() // undefined 5

参数默认值的位置
通常情况下,定义了默认值的参数应该是函数的尾参数,如果非尾部的参数设置默认值,这个参数是没法省略的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 例一
function f(x = 1, y) {
return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]

如果传入undefined,将触发该参数等于默认值,null则没有这个效果。

函数的length属性
制定了默认值后,函数的length属性,将失真,仅返回没有指定默认值的参数个数。

同理,后文的rest参数也不会计入length属性。如果设置了默认值的参数不是尾参数,那么length也不会计入后面的参数。

1
2
3
4
5
6
7
8
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

(function(...args) {}).length // 0

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域,初始化结束后这个作用域会消失。这种语法行为,在不设置参数默认值时是不会出现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var x = 1;

function f(x, y = x) { //默认值变量x指向第一个参数x
console.log(y);
}

f(2) // 2

function f(y = x) { //变量x本身没有定义,所以指向外层的全局变量x
let x = 2;
console.log(y);
}

f() // 1

如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。

应用
利用参数默认值,可以指定某一个参数不能省略,否则抛出一个错误。

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

7.2 rest参数

ES6引入了rest参数,用于获取函数的多余参数,rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

1
2
3
4
5
6
7
8
9
10
11
function add(...values) {
let sum = 0;

for (var val of values) {
sum += val;
}

return sum;
}

add(2, 5, 3) // 10

注意rest参数后不能再有其他参数。函数的length属性,不包括rest参数。

7.3 严格模式

从ES5开始,函数内部可以设定为严格模式。

ES2016做了一点修改,只要函数参数使用了默认值、解构赋值或者扩展运算符,那么函数内部就不能显示设定为严格模式。

两种方法可以规避这种限制。第一种是设定全局的严格模式,第二种是把函数包在一个无参数的立即执行函数里,这个函数中设定严格模式。

7.4 name属性

函数的name属性,返回函数的函数名。

7.5 箭头函数

ES6允许使用箭头(=>)定义函数。

1
2
3
4
5
6
var f = v => v;

//箭头函数等同于:
var f = function(v) {
return v;
};

如果箭头函数的代码块部分多于一条语句,要使用大括号将他们括起来,并使用return语句返回。由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。

使用注意

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数。
  • 不可以使用arguments对象,该对象在函数体内不存在。应该使用rest参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。

由于箭头函数没有自己的this,所以也不能使用call()apply()bind()这些方法来改变this的指向。

嵌套的箭头函数
箭头函数内部还可以再使用箭头函数。

7.6 双冒号运算符

由于箭头函数并不适用于所有场合,所以现在有一个提案,提出了函数绑定运算符,用来取代callapplybind调用。

函数绑定运算符是并排的两个冒号(::),左边是一个对象,右边是一个函数,该运算符将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

1
2
3
4
5
6
7
8
9
10
11
12
foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

1
2
3
4
5
6
7
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;

let log = ::console.log;
// 等同于
var log = console.log.bind(console);

7.7 尾调用优化

什么是尾调用?
尾调用(Tail Call)就是指某一个函数的最后一步是调用另一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function f(x){
return g(x);
}

//下面三种都不是尾调用
// 情况一
function f(x){
let y = g(x);
return y;
}

// 情况二
function f(x){
return g(x) + 1;
}

// 情况三
function f(x){
g(x);
}

尾调用优化
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,只保留内部函数的调用帧,将大大节省内存。。

尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”(stack overflow)错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//非尾递归的Fabonacci数列实现:
function Fibonacci (n) {
if ( n <= 1 ) {return 1};

return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出

//尾递归优化过的Fibonacci数列实现:
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};

return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

递归函数的改写
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。

一般的方法是把所有用到的内部变量改写成函数的参数。但这样做的缺点就是不太直观,一种方法是可以在尾递归函数外在提供一个正常行驶的函数。

函数式编程有一个概念,叫做柯里化(currying),指将多参数的函数转换成单参数的形式。因此柯里化也可以帮助递归函数的改写。

第二种方法是采用ED6的函数默认值。

1
2
3
4
5
6
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

factorial(5) // 120

严格模式
ES6的尾调用优化只在严格模式下开启,在正常模式是无效的。

尾递归优化的实现
那么在正常模式下或不支持严格模式的环境中,如何优化递归呢,答案是采用“循环”换掉“递归”,自己实现优化。

7.8 函数参数的尾逗号

ES2017允许函数的最后一个参数有尾逗号。使函数参数与数组和对象的尾逗号规则保持一致。