函数参数的默认值
基本用法
ES6之前只能采用变通的方法为函数参数指定默认值:
// ES6之前变通的方法为函数参数指定默认值
function fn1(x,y){
/* y = y || "World"; // 短路运算
return x + y; 该方法并不完美,y传入的值为false时无法正确设置默认值*/
// 判断y是否等于false
if(typeof y === "undefined"){
y = "World";
}
return x + y ;
}
let res1 = fn1("hello"); // helloWorld
ES6允许为函数参数设置默认值,直接写在参数定义的后面即可;
// ES6 可以直接将参数默认值写在参数定义的后面
function fn2(x = "name",y = "peanut"){
console.log(x + ":" + y);
}
fn2(); // name:peanut
fn2("name","china"); // name:china
参数变量是默认声明的,不能再使用let、const再次声明;
function test(x = 5){
let x = 1; // 错误
const x = 2; // 错误
}
使用默认参数时,不能存在同名参数;参数默认值是惰性求值(用到了才会计算表达式然后赋值);使用默认值的几个好处:
代码简洁;能一眼看出哪些参数是可以省略的;有利于代码优化,即时未来的版本彻底拿掉这个参数,也不会导致代码无法运行;
与解构赋值默认值结合使用
参数默认值可以和解构赋值结合使用:
// 解构赋值 + 参数默认值
function test({x,y = 1}) {
console.log(x,y);
}
test({}); // undefined 1
test({x : "num"}) //num 1
test({x : 111 , y : 222}); //111 222
/* 注意 : 参数使用的是对象的解构赋值,如果传入的参数不是对象,那么就无法生成对应的x、y
参数 此时会报错 */
test();
双重默认值
在某些情况下,如果参数使用对象解构,那么该参数就不可省略,如果需要省略该参数,就必须使用双重默认值;见以下代码案例:
function fetch(url,{body = '' , method = 'GET' , headers = {}}) {
console.log(method);
}
fetch('www.peanut.run',{}); // GET
// 省略第二个参数会报错
fetch('www.peanut.run'); // error
==========>
可以使用双重默认值,使得第二个参数省略时函数也能正常运行:
function fetch(url,{method = "GET"} = {}) {
console.log(method);
}
fetch('www.peanut.run', {}); // GET
// 此时可以省略第二个参数
fetch('www.peanut.run'); // GET
参数默认值的位置
通常情况下,设置了默认值的参数应该是函数的尾参数;有默认值的参数如果不是尾参数,那么该参数可以省略,但是其后面的参数是不可以省略的;
// 有默认值的参数应该是尾参数
function myFn(x,y = 1,z) {
console.log(x,y,z);
}
myFn(11,,2); // 错误,默认值参数不是尾参数
myFn(11); // 自身可省略,其后面的所有参数都不能省略,否则后面的参数都是undefined,无法正确赋值
函数的length属性
不指定参数默认值,函数的length属性返回的是函数参数的个数;
// 不指定参数默认值,函数的length属性返回的是函数参数的个数
function fl_1(name,age,sex) {}; // 3
指定参数默认值后(如果不是尾参数),默认值参数及其后面的所有参数都将不被计入到length属性中;尾参数指定默认值,尾参数自身不计入length;
// 指定参数默认值后,默认值参数及其后面的所有参数都将不被计入到length属性中
function fl_2(name,age = 18,sex) {}; // 1 age sex不计
rest参数也不会被计入;
// rest也不会被计入
function fl_3(name,age,...parm) {}; // 2
作用域
设置函数参数的默认值之后,一旦函数进行声明初始化时,参数就会形成一个独立的作用域,这个作用域会在函数初始化结束后消失;
// 设置默认值,函数声明初始化时参数会形成独立作用域
let x = 1;
function fun1(y = x) {
let x = 3; // 内部的变量对参数作用域没有影响,参数作用域只会到外部作用域查找
console.log(y);
}
fun1(2); // 2
fun1(); // 1
不管参数的默认值是一个值还是一个函数,其生成作用域查找变量时,都只会到函数外部的作用域中查找;
var p = 1;
function foo(p,y = function(){ p = 2; }){
var p = 3;
y();
console.log(p);
}
foo(); // 3
参数默认值的应用
设置某一个参数不得省略,如果被省略就抛出错误;
// 指定某个参数不可省略,如果省略就抛出错误
function error() {
throw new Error("参数不可省略");
}
function must(z = error()) {};
must(); // "参数不可省略"
将参数默认值设置为undefined,表明该参数可以被省略;
// 将某个参数默认值设为undefined 表明该参数不可省略
function must1(opt = undefined){
console.log(opt);
}
must1(1); // 1
must1(); // undefined
rest参数
ES6引入,形式为“…变量名”,用于获取函数的多余参数;功能与arguments对象类似,区别==arguments对象是伪数组,rest参数则是一个数组,可以使用数组特有的所有方法;rest参数必须是尾参数,rest参数之后不能有其他参数;rest参数不包含在函数的length属性中;
// rest作用与arguments对象类似
// rest参数是数组,arguments对象是伪数组
let test1 = (...nums) =>{
const arr = nums;
// rest参数支持数组所有的方法
arr.push(99);
console.log(arr);
console.log(this.length); // 0
}
test1(1,2,3,4,5); // [1, 2, 3, 4, 5, 99]
严格模式
ES5开始就可以使用“use strict”指定严格模式;ES2016对此做了一点修改,只要函数参数使用了默认值、解构赋值或者扩展运算符,函数内部就不允许显式的设置严格模式;
// 声明严格模式
"use strict";
function myFn(a = 1) {
"use strict"; // 错误
}
不能在函数内部声明严格模式的原因:在执行代码时,函数参数内部的代码优先于函数内部的代码执行,但是函数内的严格模式会应用与函数内部和函数参数,这就导致出现不合理的情况,函数从内部才能得知是否要严格模式执行,但是函数参数优先于函数内部执行;
name属性
name属性返回函数名;ES6之后,匿名函数的name属性也会返回函数名(之前是返回空字符串);将具名函数赋值给变量,name属性返回的仍是函数名;
// ES6之后 匿名函数也会返回函数名
const f = function () {};
console.log(f.name); // f es5返回空字符串
// 具名函数赋值给变量,仍返回函数名
const ff = function myFn() {};
console.log(ff.name); // myFn
箭头函数
基本用法
ES6允许用箭头(=>)定义函数;
() => {};
箭头前圆括号代表函数参数,函数只需要一个参数时可以省略该圆括号;
// 1个返回值 1个参数
let fn1 = a => a;
console.log(fn1(1));
代码块部分只有一个返回值时,可以省略大括号,直接将要返回的结果写在箭头后面;
// 多个参数 代码块多条代码
let fn2 = (aa,bb) =>{
let cc = aa + bb;
console.log(cc);
}
fn2(1,2);// 3
大括号会被解释为代码块,因此返回一个对象时需要在大括号外部嵌套一个圆括号;
let fn3 = (ID) => ({id : ID , name : "peanut"});
console.log(fn3(12)); // {id: 12, name: "peanut"}
可以和函数参数的解构赋值结合使用;
let fn4 = ({first,last}) => {
return first + " " + last;
}
let res1 = fn4({first : "宝",last : "鸡"});
console.log(res1); // 宝 鸡
// 等同于ES5写法:
let fn5 = function (person) {
return person.first + " " + person.last;
}
箭头函数用处之一就是简化回调函数;
// 简化回调函数
// ES5写法:
const arr = [1,2,3];
arr.map(function(x){
return x * x;
})
// ES6箭头函数
arr.map(x => x*x);
注意事项
箭头函数有以下几点需要注意:
函数体内的this对象就是函数定义时所在的对象,而不是调用函数的那个对象;箭头函数不能当做构造函数使用,即不能使用new关键字(报错);不可以使用arguments对象,使用rest参数代替;不可以使用yield命令,因此箭头函数不能使用Generator函数;
// 箭头函数中的this是固定的
function foo() {
setTimeout(() =>{
console.log('id:', this.id);
},100);
}
var id = 12;
foo.call({id : 21}); // 21 指向函数定义时的对象
用处:箭头函数可以让this指向固定化,这一特性很适合封装回调函数;
// 箭头函数能使this指向固定化 适合封装回调函数
// DOM事件的回调函数封装在一个函数中
var handler = {
id : '123',
init : function () {
document.addEventListener('click',
event => this.doSomeThing(event.type),false);
},
doSomeThing : function (type) {
console.log('Handler' + type + 'for' + this.id);
}
}
handler.init(); // Handlerclickfor123
为什么箭头函数不能用作构造函数?
因为箭头函数根本没有自己的this对象,所以不能用作构造函数;没有自己的this对象,因此箭头函数中的this引用的是其外部的this对象;
箭头函数没有自己的this对象,自然也不能用call()、bind()、apply()方法修改this指向;
同this一样,以下三个参数也是指向箭头函数外部的对应变量,箭头函数内部并不存在:
arguments;super;new.target;
箭头函数可以嵌套使用;
// 多重嵌套函数 es5写法:
function insert(value) {
return {
into: function (array) {
return {
after: function (afterValue) {
array.splice(array.indexOf(afterValue) + 1, 0, val
return array;
}
};
}
};
}
// ES6写法
let insert = value => ({into : array => ({after : afterValue => {
array.splice(array.indexOf(afterValue + 1 , 0 , value));
return array;
}})})
绑定this(了解即可)
ES7提出的一个提案:“函数绑定”运算符,用于取代call、bind、apply方法;函数绑定运算符是两个并排的冒号“::”,左边是一个对象,右边是一个函数;该运算符会自动将左边的对象作为上下文执行环境(this),绑定到右边的函数上;
尾调用优化
什么是尾调用?
尾调用是函数式编程的一个重要概念,指某个函数的最后一步是调用另一个函数;
function f(x
){
return g(x
);
}
以下三种情况不属于尾调用:
// 以下三种情况不属于尾调用
// 1.调用其他函数后进行复制操作并返回结果
function f(x){
let y = g(x);
return y;
}
// 2.调用后还有其他操作
function h(x){
return i(x) + 1;
}
// 3.调用后未定义返回值
function k(x) {
l(x);
}
// =====>等同于以下代码
function k(x) {
l(x);
return undefined;
}
尾调用不一定出现在函数末尾:
// 尾调用不一定出现在函数尾部
function f(x) {
if(x > 0){
return m(x); // 尾调用
}
return n(x); // 尾调用
}
尾调用优化
1.首先搞清楚调用帧和调用栈的概念:
函数调用会在内存形成一个调用帧,用来保存调用位置和内部变量等信息;假设在A函数内部调用B函数,那么A函数的调用帧上方还会形成一个B的调用帧。等待B运行结束,将结果返回到A,B的调用帧才会消失;以此类推,如果B的内部还调用C,那么A的上方除了B的调用帧还会出现C的调用帧…;所有的调用帧就形成一个调用栈;
2.尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,直接用内层函数的调用帧取代外层函数即可;
尾调用优化:即只保留内层函数的调用帧。如果所有的函数都是尾调用,那么完全可以做到每次执行时调用帧只有一个,将大大提升性能,这也是尾调用优化的意义所在。
如下所示:
// 尾调用优化的目标是使得每一次调用都只有一个调用帧
function f() {
let m = 1;
let n = 2;
return g(m + n); // 调用g之后,f的调用帧就不需要了
}
f();
// 优化后的形式:
function f() {
return g(3);
}
f();
// 等同于:
g(3);
只有不再用到外层函数的内部变量,内层函数的调用帧才能完全取代外层函数的调用帧,否则无法实现“尾调用优化”:
function addOne(a) {
var one = 1;
function inner(b) {
return b + one; // 无法尾调用优化 inner函数内部引用了外部函数的one变量
}
return inner(a);
}
尾递归
定义:函数调用自身就称为递归,尾调用自身就称为尾递归;递归非常消耗内存,在特定情况下需要保存许多调用帧,这就导致很容易出现栈溢出错误;但是对于尾调用来说,只存在一个调用帧,就不可能出现栈溢出;
// 尾调用优化后的尾递归函数
function fac(n,total = 1) {
if(n === 1){
return total;
}
return fac(n - 1,n * total);
}
fac(5,1);
严格模式
尾调用优化仅在严格模式下开启,正常模式下是无效的;原因:正常模式下由于arguments(返回调用时函数的参数)和caller(返回调用当前函数的函数)变量的存在,这两个变量可以追踪函数的调用栈;尾调用优化时,会修改函数的调用帧,上面的参数也会失真,严格模式能禁用这两个变量,因此尾调用优化仅在严格模式下生效;
// 尾调用优化仅在严格模式下生效
function test() {
"use strict";
test.arguments; // 报错
test.caller; // 报错
}
非严格模式下尾递归优化的实现
正常模式下,如果调用栈过多就会造成栈溢出,在不支持的环境下,如何实现尾调用优化呢?答案是减少调用栈即可;蹦床函数可以将递归执行转换为循环执行:
// 蹦床函数
function trampoline(f){
while(f && f instanceof Function){
f = f();
}
return f;
}
以上代码就是一个蹦床函数的实现,它接收一个参数f,只要f执行后就返回一个函数,就继续执行;简单说就是只返回一个函数,而不是在函数内部调用函数,避免了递归执行,消除调用栈过大;
如以下例子所示:
function sum(x,y){
if(y > 0){
return sum(x + 1,y -1);
}else{
return x;
}
}
sum(1,10000);
sum(1,100000); // 报错 栈溢出
// 利用蹦床函数改写===>
function betterSum(x,y) {
if(y > 0){
return betterSum.bind(null,x + 1 , y - 1); // 绑定this,传入参数但是不执行
}else{
return x;
}
}
betterSum(1,1000000);
蹦床函数真正意义上并没有实现尾递归,真正实现应该是以下形式:
// 尾递归优化的真正实现
function tco(f) {
var active = false; // 表示激活状态
var temp_arr = []; // 保存参数f
var value; // tco函数返回值
return function accumulator(){
temp_arr.push(f); // 将参数加入到数组尾部
if(!active){
active = true;
while(temp_arr.length){
f = f.apply(this,temp_arr.shift()); // 将上面传入数组的f删除并返回赋值给新的f
}
active = false;
return value;
}
}
}
var sum_new = tco(function (x,y){
if(y > 0){
return sum(x + 1,y -1);
}else{
return x;
}
});
sum_new(1,1000000);
函数参数的尾逗号
ES2017允许函数的最后一个参数有尾逗号;在此之前,函数最后一个参数带尾逗号会报错;该规定也使得函数参数与数组和对象的尾逗号规则可以保持一致;