ES6 函数的扩展
函数参数的默认值
ES6之前不能直接为函数的参数指定默认值,ES6允许为函数的参数设置默认值,即直接写在参数定义的后面
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
// ES5 为函数指定默认值的替代方式
// ES6以后
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
参数变量是默认声明的,所以不能用let
或const
再次声明 即使用参数默认值时,函数不能有同名参数。
参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
与解构赋值配合使用
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 报错
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
// 写法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]
// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
注意此时函数传参进来,采用的应该也是对象的解构赋值
参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一
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]
函数的length属性会返回,没有指定默认值的参数个数。
length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。
如果设置了默认值的参数不是尾参数,那么length
属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则
应用
利用参数默认值,可以指定某一个参数不得被省略
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
// 注意这里函数后面有()代表函数会执行
return mustBeProvided;
}
foo()
// Error: Missing parameter
rest参数
ES6 引入 rest 参数(形式为 …变量名),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
// arguments变量的写法
function sortNumbers() {
return Array.from(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
函数的length
属性,不包括 rest 参数。
严格模式
从 ES5 开始,函数内部可以设定为严格模式。
function doSomething(a, b) {
'use strict';
// code
}
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。
// 报错
function doSomething(value = 070) {
'use strict';
return value;
}
两种方法可以规避这种限制
- 设定全局性的严格模式
- 把函数包在一个无参数的立即执行函数里面
name属性
返回函数名
需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的name
属性,会返回空字符串,而 ES6 的name
属性会返回实际的函数名。
bind 返回的函数,name 值会加上 bound 前缀
箭头函数
ES6 允许使用箭头来定义函数
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
如果箭头函数不需要参数或者需要多个参数,就使用一个圆括号代表参数部分
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return
语句返回。
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
配合解构使用
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
注意事项
- 箭头函数没有自己的 this 对象
对于普通函数来说,内部的this
指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this
对象,内部的this
就是定义时上层作用域中的this
。也就是说,箭头函数内部的this
指向是固定的,相比之下,普通函数的this
指向是可变的。即对箭头函数使用call、apply也不会改变其this指向!
总之,箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。 - 不可以当作构造函数,即不可以对箭头函数使用 new 命令
- 不可以使用 arguments 对象,该对象不存在,只能用 rest 参数来代替
- 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
除了this
,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.target
另外,由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
箭头函数的不适用场合
- 定义对象的方法
globalThis.s = 21;
const obj = {
s: 42,
m: () => console.log(this.s)
};
obj.m() // 21
// 等同于
globalThis.s = 21;
globalThis.m = () => console.log(this.s);
const obj = {
s: 42,
m: globalThis.m
};
obj.m() // 21
JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m
,这导致箭头函数内部的this
指向全局对象
- 需要动态 this 的时候
- 另外,如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。
嵌套的箭头函数
箭头函数还有一个功能,就是可以很方便地改写 λ 演算。
// λ演算的写法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
// ES6的写法
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));
尾调用 优化
尾调用概念
尾调用 是函数式编程的一个重要概念,就是指某个函数的最后一步式调用另一个函数
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);
}
//等价于
function f(x){
g(x);
return undefined;
}
尾调用不一定要出现在函数尾部,只要是最后一步操作即可
尾调用优化
函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息
如果在函数A
的内部调用函数B
,那么在A
的调用帧上方,还会形成一个B
的调用帧。等到B
运行结束,将结果返回到A
,B
的调用帧才会消失。如果函数B
内部还调用函数C
,那就还有一个C
的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。类似尾部递归优化。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。
注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。
尾递归
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
// 正常递归
function factorial(n,total) {
if (n === 1) return total;
return factorial(n - 1,n * total);
}
factorial(5) // 120
// 尾递归
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。
但有时改写会不那么直观,两个方法解决这个问题
-
在尾递归函数以外,再提供一个正常形式的函数
function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } function factorial(n) { return tailFactorial(n, 1); } factorial(5) // 120
函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。
function currying(fn, n) { return function (m) { return fn.call(this, m, n); }; } function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } const factorial = currying(tailFactorial, 1); factorial(5) // 120
上面代码通过柯里化,将尾递归函数
tailFactorial
变为只接受一个参数的factorial
。 -
采用ES6的函数默认值
function factorial(n, total = 1) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5) // 120
-
总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
- func.arguments 返回调用函数的参数
- func.caller 返回当前调用的那个函数
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
尾递归优化的实现
正常模式下的手动实现——采用“循环”换掉“递归”。
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)
// 蹦床函数,将递归操作改为循环操作
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
蹦床函数并不是真正的尾递归优化,下面的才是
function tco(f) {
var value;
var active = false; // 关键!!!!!!
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
// 100001
函数参数的尾逗号
ES2017 允许函数的最后一个参数有尾逗号
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
); // 注意调用也要加,
Function.prototype.toString()
ES2019 对函数实例的toString()
方法做出了修改
toString()
方法返回函数代码本身,以前会省略注释和空格。
修改后的toString()
方法,明确要求返回一模一样的原始代码。包括注解空格
catch 命令的参数省略
JavaScript 语言的try...catch
结构,以前明确要求catch
命令后面必须跟参数,接受try
代码块抛出的错误对象。
ES2019 做出了改变,允许 catch 语句省略参数
总结
本文主要介绍了ES6有关函数的扩展,包括函数参数的默认值,rest参数(…),严格模式(‘use strict’),name属性,箭头函数,尾调用及其优化,Function.prototype.toString(),catch命令的参数省略等等。