参照:http://es6.ruanyifeng.com/#docs/function
1.函数参数的默认值
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World 这有个缺点,y 为空字符则被改为默认值,这需要设置y的默认值
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
2.与解构赋值默认值结合使用
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo()
// TypeError: Cannot read property 'x' of undefined
只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。
3.参数默认值的位置
定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一
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]
4.函数的length属性
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
5.rest参数
es6引入rest数组参数来代替arguments对象,可以省好多事情。
例如可以直接调用数组的方法,而不用转换。
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest
参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
5.严格模式
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。
'use strict';
function doSomething(a, b = a) {
// code
}
第二种是把函数包在一个无参数的立即执行函数里面。
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
6.箭头函数
var f = v = >v;
//等同于
var f = function (v){
return v;
}
如果箭头函数不需要参数或者需要多个参数,就用一个圆括号代表参数部分。
var f = () =>v;
//等同于
var f = function(){
return v;
}
var f = (n1,n2) =>n1+n2;
//等同于
var f = function(n1,n2){
return n1+n2;
}
如果箭头函数代码块部分多余一条语句,就要用大括号将他们括起来,并且用return 语句返回。
var sum = (num1,num2) =>{return num1 + num2;}
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
箭头函数的一个用处是简化回调函数
//正常函数写法
[1,2,3].map(function(x){
return x*x;
});
//箭头函数写法
[1,2,3].map(x => x * x);
另一个例子
//正常函数写法
var result = values.sort(function(a,b){
return a - b;
});
//箭头函数写法
var result = values.sort((a,b)=>a-b);
rest参数与箭头函数结合的例子
const numbers = (...nums) =>nums;
numbers(1,2,3,4,5); // [1,2,3,4,5]
const headAndTail = (head, ...tail) =>[head,tail];
headAndTail(1,2,3,4,5); //[1,[2,3,4,5]]
使用注意点 箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
注意:this对象的指向是可变的,但是在箭头函数中,它是固定的。
箭头函数可以让this指向固定化,这种特性有利于封装回调函数
var Handler = {
id:'123',
init: function(){
document.addEventListener('click',event => this.doSomething(event.type),false);
},
doSomething:function(type){
console.log('Handing' + type + 'for' + this.id);
}
};
上面的init方法中,箭头函数里面的this总是指向handler对象。
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正因为它没有this,所以也不能用作构造函数。
箭头函数转成ES5的代码如下:
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。
由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。
不适用场合:
由于箭头函数使得this从动态变为静态,下面不适合用
1.定义函数的方法,且该方法内部包括this
2.需要动态this的时候
7.双冒号运算符
箭头函数可以绑定this对象,大大减少了显示绑定this对象的写法(call,apply,bind),但是箭头函数并不适用于所有场合,所以现在有一个提案,绑定函数运算符。
两个冒号:(:?;两个冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
如果冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
8.尾调用优化
指某个函数的最后一步是调用另一个函数。
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) {
if (x > 0) {
return m(x)
}
return n(x);
}
尾递归
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
//计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。
改为尾递归:
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
//只保留一个调用记录,复杂度 O(1) 。
递归函数的改写
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量total,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1?
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
第二种方法就简单多了,就是采用ES6的函数默认值。
function factorial (n,total = 1){
if(n === 1) return total;
return factorial(n - 1,n*total);
}
factorial(5); // 120
严格模式
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈,
func.arguments:返回调用时函数的参数。
func.caller:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
function restricted() {
'use strict';
restricted.caller; // 报错
restricted.arguments; // 报错
}
restricted();