函数默认参数

es5无法设置默认参数,只能通过函数内部先进行判断,如果没有参数则设置一个值。

function test(x,y){
  x = x || "hello ";
  y = y || "word";
  console.log(x+y);
}

test();  //hello word

这样写的话会有一个问题,就是如果我就要参数为空,就会出现问题

test("","test");   // hello test

所以我们还需要加一个判断

function test(x,y){
  x = typeof x === "undefined" ? "hello ": x;
  y = typeof y === "undefined" ? "word": y;
  console.log(x+y);
}

test("","test");  //test

但这样太麻烦了,所以es6提供了函数函数的默认值写法

function test(x="hello ",y="word"){
  console.log(x+y);
}

test();  //hello word
test("","test");  //test

注意事项

  1. 默认参数不能在函数里面二次声明,否则报错
  2. 使用了默认参数,所有参数名必须唯一不能重复,否则报错
  3. 默认参数是惰性的,他会随着每次调用该函数,重新计算,而不是一开始就计算好
function test(x="hello "){
  const x = "test";    //报错
} 

function test(x="hello",x){ //报错
  
} 

let x = 99;   
function test(p=x+1){ //每次都会重新获取x的值
  console.log(p);
}

test(); //100
x = 100;
test(); //101

默认参数与解构混合使用

在函数参数那,我们也可以使用解构来直接获取某些数据,并且可以和默认参数一起使用。

function test({x,y=5}){
  console.log(x,y);  
}

test({});  //undefined,5
test({x:1}); //1,5
test({x:1,y:2});  //1,2
test();  //报错,无法解构

这里我们是对解构的某个值进行默认参数的,事实上我们还可以对整个参数进行默认值设置

function test({x,y=5}={}){
  console.log(x,y);  
}
text();  //undefined,5

这就形成了双重默认值,并且我们还需要理解下他是什么样的流程。

首先我们并没有传参数,所以这里先使用了第一个默认参数{},一个空的对象,然后才会进行结构,运行{x,y=5}这部分。

你不能理解为没有参数,解构这部分也直接被{}空对象替换了,可以理解为,替换的只是解构的外层包裹,内层还是一样的,不会消失不见。

这里写两种写法:

fucntion m1({x=0,y=0} = {}) {
  console.log(x,y);
}
fucntion m2({x,y} = {x:0,y:0}) {
  console.log(x,y);
}

m1和m2的区别就是,第一个初始默认值不同。

m1的第一个默认值是空对象{},再解构的时,如果没有对应的值使用默认值。

m2的第一个默认值是带有参数的{x:0,y:0},然后解构时直接解构第一个默认值。

m1和m2两个函数在没有传参的情况下,结果是相同的。

但是当我们传入一个空对象时,m2由于解构不到参数,会使一个undefined

m1({});  //0,0
m2({});  //undefined,undefined

因为m1的默认值是在解构的时候设置,而m2是之前就预设,此时我们传入的了对象参数,m2的默认值就拿不到了。

那么我们传入一个不相干的参数结果也会是一样的

m1({z:1});  //0,0
m2({z:1});  //undefined,undefined

默认值参数的位置

通常情况下,定义了默认值得参数,一般是可以省略不传的,所以要写在最后面,因为在函数里面,最后那个是可以不写,也不会影响到其他参数的。

function test(x,y,z=1){
  //z可以进行省略
}

function test(x,z=1,y){
  //z无法进行省略,因为位置上会影响到后面的参数
}

触发默认参数

当传入undefined的时候才会触发默认值,而null和空字符串不会触发

function test(x=1){
  console.log(x);
}

test("");  //""
test(null);  //null
test(undefined)  //1

函数length

使用了默认参数后,函数的length返回的是没有设置默认参数的参数lenght。

(function test(x,y){}).length   //2

(function test(x,y=1){}).length  //1

(function test(...arr){}).length  //0

(function test(x,y=1,z){}).length  //1
(function test(x=1,y,z){}).length  //0

通过arguments获取lenght并不会收到影响。

length的含义是这个函数预期传入的参数个数,当我们设置了默认值,那么这个预期就不一定了,所以不计入数量。

当我们把默认值设置在前面时,后面即便不是默认值也不会计入数量了。

作用域

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

var x = 1;
function too(x,y = function(){ x=2 }) {
  var x = 3;
  y();
  console.log(x);
};
too();  //3
console.log(x);  //1

使用了默认参数,所以(x,y = function(){ x=2 })参数会有独立的作用域。

其中y里面的x指向的是他同级作用域下的x,也就是第一个参数x。

而too函数里面,我们又申明了一个x并赋值为3,当y运行的时候,他的这个x指向的是他作用域下的第一个参数x,x赋值为2,而too里面的var x和第一个参数x不在同一个作用域。

所以y是无法修改到var x变量的,他只能修改第一个参数x。

所以:

function too(x,y = function(){ x=2 }) {
  y();
  console.log(x);
};
too();  //2

而我们运行too函数时,输出3,是因为,var x在第一个参数x后面声明的,就近原则,var x是最快获取到的,所以输出3,这里也可以了解下变量提升,而y这个函数,他本身也只能修改第一个参数,后面的var x他改不了,所以,最终结果是个3

如果我们不在too里面声明一个x,而是直接用参数x,那么结果又不同了

function too(x,y = function(){ x=2 }) {
  x = 3;
  y();
  console.log(x);
};
too();  //2

这里就会输出为2,因为x是同一个x,同一个作用域。

应用

我们可以将默认值来指定某一个值是必填的,如果省略则进行报错

function err(name){
  throw new Error(name+"参数为必填参数");
}

function test(x=err("x")){
  return x;
}

test();  //报错:x参数为必填参数

另外我们可以给默认参数设置为undefined表示这个参数是可以省略的。

function test(option = undefined){
}

rest

rest用于处理特别多的参数的时候,以前我们用arguments对象获取所有参数,现在我们可以使用rest方法更加便捷的获取。

function add(...values) {
  let sum = 0;

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

  return sum;
}

add(1,2,3);  //6

通过...values的方式,values成为了一个保存所有参数的数组,然后我们遍历这个数组,进行求和,然后返回结果。

values这个变量名是可以自定义的。

例子:数字排序

//es5写法
function sortNumber() {
  return Array.prototype.slice.call(arguments).sort();
}

//es6
const sortNumber = (...numbers) => numbers.sort();

例子:改写push

function push(arr,...items) {
  items.forEach(funtion(item){
    arr.push(item);
  });

  return arr;
};

push([],1,2,3,4);   //[1,2,3,4]

注意

rest必须写在最后面,作为最后一个参数,否则报错,并且使用了rest后,lenght也无法检测到参数数量

function test(x,...vals,y){ ... }   //报错

(function test(x,...vals){}).length;   //1

(function test(...vals){}).length;   //0

严格模式

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

function test(){
  'use strict'
  ...
}

es6做了点修改,如果函数使用了默认参数,解构赋值,rest扩展运算符,那么函数内部就不能显示设定为严格模式,否则就会报错。

但是可以通过一个方式规避:

全局严格模式

'use strict'

function test(){
  ...
}

函数包裹

const test = (function(){
  'use strict'
  return function(x = 1) {
    ...
  }
})()

name

函数的name属性返回的该函数的函数名,但是在es5中,如果我们创建一个变量,并给这个变量赋值一个匿名函数,然后我们再来获取其name,得到的是空,而es6返回的是这个变量名

var f = function(){};

f.name;  //es5  ""
f.name;  //es6   "f";

如果被赋值这个这函数本身就是一个具名函数,那么name最终返回的是这个具名函数的名字。

var f = function test(){};

f.name;   "test";

如果是Function构造函数返回的函数示例,返回的是:anonymous

(new Function).name   //'anonymous'

如果是使用bind方法生成的新函数,会带有bound前缀

function test() {};

test.bind({}).name;  //'bound test'

(function(){}).bind({}).name;   //bound

箭头函数

es6允许使用箭头来定义函数,箭头函数大大的简化的函数的书写,在一些回调上显得更加简洁明了。

var f = ()=>{};

//等同于
function f() {};

箭头函数还有一些省略写法:

var f1 = val => {};   //单参数
var f2 = (val1,val2) => {};  //多参数和无参数用圆括号

var f3 = val => val;   //省略return
//等同于
funciton(val) {
  return val;
}

var f4 => ({key:val});   //如果要return一个对象,可以用圆括号包裹用以区分

箭头函数本身也是函数,所以参数上,我们也能使用解构,rest扩展运算符,默认值,这里就不重复说明了。

箭头函数使得表达更加简洁,以前要写好几行的代码,现在可以缩短为一行

const isEven = n => n % 2 == 0;
const square = n => n * n;

注意事项

  1. this对象是函数定义时所在的对象,而不是使用时所在的对象
  2. 不可以当做构造函数使用,所以不能使用new
  3. 不可以使用arguments对象,可以使用rest参数代替
  4. 不可以使用yield命令,因此箭头函数不能作为generator函数

this对象是函数定义时所在的对象,而不是使用时所在的对象

function foo(){
  setTimeout(()=>{
    console.log('id:',this.id);
  },100);
}

var id = 1;

foo.call({id:2});   //id:2

在foo函数内,有一个定时器,定时器的回调是一个箭头函数,按照第一条的说明,this对象是定义时所在的对象,也就是说this指向的是foo。

然后我们给全局window上定义了一个id变量,值为1;

然后我们将foo的this指向了一个对象,于是我们得到的是id:2

如果是普通函数,定时器里面的函数this指向的是window,按道理就要输出id:1

箭头函数可以让this指向固定化,这种特性非常有利于封装回调。

简单来讲,箭头函数没有自己的this,他的this永远是外层代码块的this,正因为没有this,所以不能用作构造函数。

像arguments、super、new.target这三个变量,在箭头函数里面也是没有的,但是可以获取到外层代码块的变量。

function foo(){
  setTimeout(()=>{
    console.log("arguments:",arguments);
  },100)
}

foo(1,2,3,4);   //arguments:,[1,2,3,4]

另外由于箭头函数没有自己的this,所以我们也不能改变箭头函数自己的this指向,所以call、apply、bind这些改变this指向的语法也无法使用。

绑定this

箭头函数绑定this对象,大大的减少了显示绑定this对象的写法,如:call、apply、bind,但是箭头函数并非适合所有场合,所以es7提出了“函数绑定”运算符,用来取代这三个方法,但是目前还是提案,不过bable转码器已经支持了。

函数绑定运算法是两个冒号:::,双冒号左边是一个对象,右边是一个函数,改运算符会将左边的对象做为this的上下文环境。

foo::bar
//等同于
bar.bind(foo);

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

如果左边的为空,右边是一个对象的方法,那么就表示右边这个对象作为上下文。

::obj.foo;
//等同于
obj::obj.foo;

这样的话,双冒号返回的的还是原对象,类似于返回了一个this一样,可以拿来做链式写法。

var s = {
  test:()=>{
    console.log(1);
  }
}

s.test()::test()::test()

这个是我自己想的,可能会有问题,也找不到地方测试。

尾部调用

指的是函数最后一步调用另一个函数,这个最后一步可以不是写在最后面,但他必须是函数最后一步。且无其他运算,如果你最后一步调用一个函数再操作,那就算不得。

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;
};

所以第三种也不算,他不是最后一步。

尾调用优化

尾调用之所以和其他不同,是因为js的函数运行,如果a函数里面运行了b函数,那么内存中a的运行区域上还有b函数,b如果有c,那么就会继续堆,从而形成一个调用栈,只有当c运行完了,c才会从内存中消失,然后运行b,所以,如果相关联的太多了,就会出现栈溢出。

尾部调用的话,说明他的这段函数已经不需要继续保存了,所以a会先消失,而不是要等后面所有的b、c函数运行完毕再消失。

如果是递归的话,a函数无限调用自身,传统的话太多就会直接溢出了,而使用尾部调用优化,那么就等于a运行完毕了,再开一个a,原来的消失,相对来说性能会更好。

如何实现优化呢

  1. 尾部调用的函数内部不要有外部的变量,可以用参数的形式传,但是不要直接在里面调用
  2. es6的尾调用优化只有在函数开启严格模式下才有效

为什么只能在严格模式下呢,是因为正常函数下,有两个变量可以跟踪函数的调用栈,导致原来的无法内存区域依旧保持存在。

  1. func.arguments 返回调用时函数的参数
  2. func.caller 返回调用当前函数的那个函数

而严格模式会禁用这两个变量,所以尾部优化只有在严格模式下有效

function test(){
  "use strict"
  test.caller;    //报错
  test.arguments;   //报错
}

test();

尾调用优化实现

尾部优化只有在严格模式下有效,那么其他环境怎么办呢?

答案就是自己动手了,从原来上讲,优化的原理就是减少栈的堆叠,或者说是调用。

这是一个正常的递归函数:

function sum(x,y){
  if(y>0){
    return sum(x+1,y-1);
  }else {
    return x;
  }
}

sum(1,100000);  //报错,溢出

优化1:蹦床函数

//蹦床
function trampoline(f){
  while(f && f instanceof Function) {
    f = f();
  }else {
    return f;
  }
}

//重复函数
function sum(x,y){
  if(y>0){
    return sum.bind(nyll,x+1,y-1);
  }else {
    return x;
  }
}

trampoline(sum(1,100000));   //100001

蹦床函数并不是真正的尾部优化

尾部优化

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

函数参数的尾逗号

es7有一个提案,就是函数的参数,支持最后一个参数后面带一个逗号。这样可以使函数参数和数组参数的尾逗号规则保持一致。

function test(x,y,) {};   //现在这样写还是会报错的
分类: ES6 标签: functiones6

评论

暂无评论数据

暂无评论数据

目录