函数参数的默认值
规则
- 参数变量是默认声明的,所以不能用let或const再次声明
let oPlus = (x,y=5) => {
let x = 1; // 语法错误
return x + y;
}
3.当传入的参数是undefined时,如果有默认参数,将会调用默认值,传入参数为null时,不会调用默认值
let oPlus = (x,y='world') => {
return x+y;
}
console.clear();
console.log(oPlus('hello ',undefined)); //'hello world'
console.log(oPlus('hello ')); //'hello world'
console.log(oPlus('hello ',null)); //'hello null'
4.非尾部的参数设置默认值,实际上这个参数是没法省略的
let oPlus = (x='world',y) => {
return x+y;
}
console.clear();
console.log(oPlus()); // 报错
console.log(oPlus(,)); // 1 undefined
console.log(oPlus(null)); // null undefined
console.log(oPlus(undefined)); 1 undefined
代码示例CODEPEN
// es5中默认参数的写法
function oPlus(x,y) {
if (typeof y === 'undefined') {
y = 'world'
}
return x + y;
}
// es6中默认值写法
let oPlus = (x,y='world') => {
return x+y;
}
console.clear();
console.log(oPlus()); // 'undefinedworld'
console.log(oPlus('hello ')); //'hello world'
console.log(oPlus(undefined)); // 'undefinedworld'
console.log(oPlus(null)); // 'nullworld'
console.log(oPlus(undefined,undefined)); // 'undefinedworld'
console.log(oPlus(null,null)); // 0
console.log(oPlus(undefined,'ok')); // 'undefinedok'
console.log(oPlus(null,'ok')); // 'nullok'
console.log(oPlus(null,undefined)); // 'nullworld'
console.log(oPlus(undefined,null)); // NaN
与解构赋值默认值一起使用
规则
- 默认参数仍然可以进行二次默认参数设置,比如 {x,y=5} = {}或者 {x,y} = {x:1,y:5}
这两种写法稍微有些不同,个人更倾向于第一种
// {x,y=5} = {} 等同于 {x,y} = {y:5} 双重默认值
let destructFunc = ({x,y=5} = {}) => {
console.log(x,y);
}
console.clear();
destructFunc(); // undefined 5
destructFunc({x:1}) // 1 5
destructFunc({x:2,y:10}) // 2 10
let foo = ({x,y=5}) => {
console.log(x,y);
}
foo(); // 报错
foo({x:1}); // 1 5
foo({}) // undefined 5
函数的length
规则
- 函数length的含义指的是预计传入函数的参数个数,如果用...rest表示参数的话,其length = rest.length;
- 参数设置默认值后,自身以及后续参数都不纳入函数length统计范围;
示例代码
(x,y=5) => {} // length: 1
(x=5,y) => {} // length: 0
(x,y=5,z) => {} // length: 1
({x,y=5}) =》 {} // lenght: 1
({x,y}) =》 {} // lenght: 1
({x=1,y}) =》 {} // lenght: 1
({x,y=5} = {}) =》 {} // lenght: 0
作用域
规则
默认参数的作用域规则也符合一般变量的作用域规则,即首先是函数作用域,其次是全局作用域
示例代码
let x = 1;
let foo = (y=x) => {
let x = 3;
console.log(y);
}
foo();//1
// 参数默认值是函数
let strfoo = 'bar';
let sfunc = (func = () => strfoo) => {
let strfoo = 'ccc';
console.log(func());
}
// 传值的变量是全局变量是
let sx = 1;
let sxfoo = (x, y= () => { sx=x }) => {
console.log('inner',x);
x = 4;
y();
console.log(sx);
}
sfunc(); // 'bar'
sxfoo(5); // 'inner' 5 \n 4
console.log(sx); // 4
原理
从上面可以看出,sxfoo执行时,分以下几步:
一、执行参数部分
- 先通过函数传参,显示指明第一个参数x等于5;
- 由于没有传值给参数y,此时其值为undefined,自动赋值y =() => {sx = x} ,然后初始化此匿名箭头函数,顺序也是从参数到函数体。由于该匿名函数此时只是进行一系列函数初始化并未执行,因此sx只是绑定了window'x'一个引用
二、执行函数体部分
- 执行函数体代码,x仍等于5,此时程序向控制台输出 inner 5
- x= 4 将 全局变量x即window['x']设置为4,
- 执行y() 此时sx= x实际执行,赋值sx等于window['x']即4
- 程序执行到最后一句console.log(sx),向控制台输出 4
rest
规则
- rest是一个真正的数组,可以使用数组对象的一切方法
- rest参数后不能再有参数,否则会报错
- 函数的length属性不包括rest参数
示例代码
// rest可以使用一个真正的数组方法
let arrPush = (array,..items) => {
items.forEach( item => array.push(item));
}
let a = [];
arrPush(a,1,2,3,4);
// 函数长度
let restFunc = (y,...rest) => {};
let restFunc1 = (...rest) => {}
console.log(reestFunc.length,restFunc1.length); // 1 0;
扩展运算符
1.扩展运算符可以将数组变成以逗号为分隔的字符串 2.如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
代码示例CODEPEN
// 将数组转化为逗号分隔的字符串
let a = [1,2,3]
console.log(...a) // 1 2 3
console.log(1,2,3);
// 合并数组
let b = [4,5]
a.push(...b)
console.log(a) // 1 2 3 4 5
// 替代apply
console.log(Math.max(...a)); // 3
console.log(Math.max.apply(null,a)); // 3
// 与解构赋值一起使用
const [first,...second] = [1,2,3,4];
console.log(first,second);// 1,[2,3,4]
const [ifirst,...isecond] = [];
console.log(ifirst,isecond); // undefined [];
const [...iisecond,iifirst] // 报错
// 处理函数的返回值,函数的返回值只有一个,如果想多个值,可以返回数组
let foo = () => [1,2,3];
console.log(...foo());
// 将字符串转化为真正的数组
console.log(...'helloworld');
/* 任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。
...document.getElementsByTagName document.childNodes
上面代码中,arrayLike是一个类似数组的对象,但是没有部署Iterator接口,扩展运算符就会报错。这时,可以改为使用Array.from方法将arrayLike转为真正的数组。
*/
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];
// Map和Set结构,Generator函数都可以使用扩展运算符
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
var go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
函数name
规则
将函数名称标准化了,纠正了es5,匿名函数赋值,函数名称为空的现象
代码示例
let foo = () => {} // function name is 'foo'
var foo = function() {} // function name is ''
function foo() {} // function name is foo
严格模式
规则
- 参数使用了默认参数,解构赋值,扩展符的不能在函数体内部使用‘use strict’
- 在全局下使用严格模式,即将‘use strict’显示声明在所有函数体外
- 把函数包在一个无参数的立即执行函数里面,可以规避第一条规则
const doSomething = (() => {
'use strict'
return function(value=43) {
return value;
}
})
原理
函数执行的顺序是先执行参数部分,再执行函数体,如果在函数内部常规声明严格模式,由于参数部分先执行,此时严格模式还没生效,而到了函数体部分,严格模式又生效了,这样参数部分和函数体部分就出现不一致的验证的规则,增加了程序的复杂度,es6标准索性禁止设置严格模式。
箭头函数
规则
- 箭头函数的this指的是它定义时的上下文环境,而不是运行时的上下文环境;
- 箭头函数不能当作构造函数,因为它没有new命令,否则会报错;
- 箭头函数不能使用arguments,因为箭头函数没有arguments这个值,可以用rest替代;
- 不可以用作yield函数,因为箭头函数不能用作Generator函数;
- 除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new target;
- 由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。
代码示例
// 简单的箭头函数
let foo = () => {}
// 作为回调函数被执行
[].map(v => v+1);
[].map(v => { return v+1;})
//等同于
[].map(function(v) { return v+1})
// 结合扩展符
let a = [1,2,3];
let oitem = (...numbers) => {
let sum = 0;
numbers.forEach(n => sum+=n);
return sum;
}
console.log(oitem(...a)); //6
//和变量解构一起使用
let oput = ({first,second}) => `${first}${second}`;
console.log(oput({
first: 'cuc',
second: 'baidu'
})); // 'cucbaidu'
// 箭头函数的this指向
console.clear();
window['s2'] = 0;
class Timer {
constructor() {
this.s1 = 0;
this.s2 = 0;
this.count = 0;
setInterval( () => {
this.s1++;
},1000);
setInterval(function() {
this.s2++;
this.count++;
},1000);
}
}
let timer = new Timer();
console.log(timer.s2); // 0
setTimeout(() => console.log('s1',timer.s1,window['s1']),2100); // 2 undefined
setTimeout(() => console.log('s2',timer.s2,window['s2']),2100); // 0 2
console.log(window['s2']); // 0
/*
声明了一个类,类里面一个使用箭头函数作为回调函数,一个使用普通函数作为回调函数。
箭头函数绑定定义时的上下文环境也就是Timer,而普通函数绑定的是运行时的上下文环境,即window
*/
尾调用
什么是尾调用
- 尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数;
- 尾调用只发生在严格模式下。
- 最有最后一步调用的是函数,才可以称为尾调用
function f(x) {
return g(x);
}
function f(x) {
if (x<0) {
return m(x);
} else {
return n(x);
}
}
只有最后一步调用的是函数才是尾调用
原理
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
尾递归
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。 CODEPEN
// hack尾递归方式一
function currying(func,n) {
return function(m) {
return func.apply(this,m,n);
}
}
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
const factorial = currying(tailFactorial, 1);
console.log(factorial(5)); // 120
// hack 尾递归方式二
function factorial(n,total = 1) {
if (n===1) {
return total;
}
return factorial(n-1,total);
}
console.log(factorial(5)) // 120
// hack 尾递归方式三
let tco = (f) => {
let value = null;
let active = false;
let accumArr = [];
return function() {
accumArr.push(arguments);
while(accumArr.length) {
active = true;
value = f.apply(this,accumArr.shift());
}
active = false;
return value;
}
}
let sum = tco((n,m) => {
if (m>0) {
return sum(n+1,m-1);
} else {
return n;
}
});
console.log(sum(1,1000));