ES6+中函数的扩展大总结

491 阅读10分钟

在学习了ES6中函数的扩展后我打算做一次总结,内容感觉有点多,下面是目录:

  1. 函数参数的默认值
  2. rest参数
  3. 严格模式
  4. name属性
  5. 箭头函数
  6. 尾调用

参数的默认值:

ES6之前不能直接为函数的参数指定默认值只能采用变通的方法

function log(x,y){
 y = y || "world";
 console.log(x,y);
}
log("Hello"); //Hello world
log("Hello","China")//Hello china;

而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 foo(x = 5){
 let x = 5;//error
 const x = 5;//error
}

使用函数的参数默认值时,不能使用同名的参数

function(x,x=5){//...}//error

另一个注意的地方就是函数的参数是惰性求值,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。

let a = 1;
function test(x = a + 1){
 console.log(x);
}
test();//2;
a = 5;
test();//6;

参数默认值与解构赋值默认值结合使用:

函数的参数默认值可以和解析赋值的默认值结合使用,请看下面的代码

function test({x,y = 5}){
 console.log(x,y);
}
test({});//undefined,5
test({x:1,y:1});//1,1
test({x:1});//1,5
test({y:1})//undefind,1
test()//报错,x未定义
test(1,2)//此时传递的不是一个对象,所以无法解构赋值,不管传递什么参数输出都是 undefined,5

可以看到上面的代码使用解构赋值的默认值来传递参数,当参数是一个对象时x,y才会通过解析赋值来生成(结构赋值我下一篇博客讲),当调用test函数的时候,参数是空的时候,变量x就不会生成,从而报错(Cannot destructure property 'x' of 'undefined' as it is undefined.),当传递的不是对象的时候,无法解构赋值,不管传递什么参数输出都是 undefined,5

参数默认值的位置

通常情况下,定义了参数默认值的参数位置应该是函数的尾参数,这样同意看出来是省略了哪些参数,如果不是尾参数则这个参数是无法省略的

function test(x = 1, y){
  console.log(x,y);
}
test()//1,undefined
test(2)//2,undefined
test(,1)///error
test(undefined,1)//1,1

从上面的代码中可以看到,如果默认参数不是尾参数,这时是无法省略该参数而不省略其后面的参数,除非显示的输入undefined。这里需要注意的是只有显示的输入undefined时才会触发该参数的默认值,null则不会有这个效果。

函数的length属性

函数的length表示该函数预期传入的参数个数,所以当使用参数的默认值的时候,函数的length属性将失真,例如:

(function(a){}).length//1
(function(a = 5){}).length//0
(function(a,b,c = 5){}).length//2

上面的代码中函数的length是函数的参数个数减去函数中的默认参数的个数。

当函数的参数默认值不是尾参数的时候,length属性也不再计入后面的参数,意思就是当函数的参数中包含默认参数的时候,函数的length的值是默认参数前面参数的个数。

(function(a,b = 1,c,d,e,f,g,h){}).length//1

函数中的作用域

当函数使用默认参数的时候,函数进行初始化时,参数会形成一个单独的作用域,这个作用域会在初始化结束后消失,这种语法行为在不设置函数的默认参数的时候是不会出现的。

let x = 1;
function fn(x,y=x){
  console.log(x,y);
}

fn(2)//2,2,

上面的代码中,y的默认值等于x,当调用fn的时候参数会形成一个单独的作用域,此时里面的代码相当于{let x=2//传递的形参;let y=x}在这个作用域里面,默认值x指向第一个参数,而不是全局的变量x,所以输出2

let x = 1;
function fn(y=x){
  console.log(x,y);
}

fn()//1,1

上面的代码中由于在调用的时候没有传递参数,所以函数在初始化的时候内部的代码相当于{let y = x},x指向的是全局作用域的x,所以输出的就是1,1。

而如果像上面的情况下,函数初始化的时候内部的代码相当于{let x = x}由于let和const声明的变量存在暂时性死亡区,所以就会报错

关于暂时性死亡区可以看我的另一篇博客:zhuanlan.zhihu.com/p/127334781

当函数的默认值为一个函数的时候也遵从这样的规则,请看下面的例子:

let foo = "outer";
function bar(func = x =>foo){
  let foo = "inner";
  console.log(func()) 
}
bar()//outer

上面的代码中,函数bar的参数是一个func的默认值是一个函数,这个函数返回foo,当函数初始化的时候会形成单独的作用域,这个作用域里面没有定义foo ,所以函数里的foo 就指向函数外部的foo,最终输出的值为outer.

下面是一个更加复杂的例子:

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

上面的代码中,函数foo的外部声明了一个变量x = 1,当函数初始化的时候,foo的参数形成了一个单独的作用域,作用域的内部声明了x,然后又声明了一个变量y,y的默认值是一个匿名函数,这个匿名函数内部的变量x指向了foo函数的第一个参数x,foo函数的内部又声明了一个内部变量x,改变了与第一个参数x由于不在同一个作用域,所以不是同一个变量,因此执行y后,内部的变量x和外部全局变量x 的值都没变。

如果将 var x = 3的var去掉,函数内部的x就指向第一个参数x,与匿名函数内部的x是一致的,所以最后输出的就是2,而外层变量x依然不受影响。

rest参数

ES6 引入了rest参数(形式为”...变量名“),用于获取函数多余的参数,这样就不需要使用arguments对象了,rest参数搭配的变量是一个数组,该变量将多个多余的参数放入其中。

function add(...values){
   let sum = 0;
   values.forEach(value=>{
    sum + = value; 
 ])
}
add(1,2,3,4,5,6,7

上面的add函数可以接受任意数目的参数。

默认值

ES5开始,函数内部可以设定为严格模式,但是从ES2016做了一些修改,规定只要函数的参数使用了默认值,解析赋值或者扩展运算符,那么函数内部就不能设定为严格模式。


//报错
function(a,b =a ){"use strict"}
//报错
function({a,b} ){"use strict"}

name属性

函数的那么属性用于但会函数的函数名。这个属性从很早之前就开始支持,但是到ES6才写入标准,也做了一些修改。

function foo(){}
foo.name//foo
var  f = function(){}
f.name//f

//如果将一个具名函数赋值给变量则返回的是这个具名函数本来的名字
var bbb = function aaa(){}
bbb.name//aaa

箭头函数

在ES6中新加的就是箭头函数。

//如果使用箭头函数定义下面的函数则:
var f = function(v){return v}

//ES6
var f = v =>v
//或者
var f = v =>{return v}
//或者
var f = (v) => {return v}

在函数参数中,如果函数没有参数或者有多个参数时,需要使用括号,如果有多个括号需要使用括号将参数括起来,在函数体中,如果只有一条语句则可以不适用大括号,那么函数的返回值就是这条语句的返回值。

箭头函数可以简化代码,使函数表达式更加的简单,箭头函数可以与变量的解析结构结合使用,也可以被用作回调函数,也可以结合rest参数,需要注意的是:

1.箭头函数没有this,在函数内使用this时,this指向的是函数定义时所在的对象,而不是使用时所在的对象。
2.不可以当作构造函数,也就是说不可以使用new命令,否则会抛出一个错误。
3.不可以使用arguments对象,该对象在箭头函数体内不存在。如果要用,可以用rest参数代替
4.不可以使用yield命令,因此箭头函数不能用作Generator函数。

上面的代码中setTimeout的参数是一个箭头函数,当调用foo函数的时候100ms后执行箭头函数中的内容,当执行的时候console.log(this.id)中的this指向的是箭头函数定义是所在的对象,也就是foo函数,此时输出的是12(id作为参数传入函数)

如果setTimeout里面的函数是普通函数时this指向的就是window,此时输出的就是42。

this指向的固定化并不是因为箭头函数内部有绑定this的机制,其实就是箭头函数根本就没有this,导致内部的this就是外层代码块的this,正是因为它没有this,所以不能作为构造函数。

尾调用

尾调用是函数式编程的一个重要概念,本身非常的简单,一句话就可以说明白,就是指这个函数的最后一步是调用另一个函数。

function(x){
 return g(x)
}

尾调用之所以与其他的调用不同,就在于其特殊的位置,我们知道,函数调用就在内部形成一个调用记录,又称为调用帧,保存调用的位置和内部变量等信息,如果在函数A的内部调用函数B,那么在函数A的调用帧上方还会形成一个B的调用帧,等到B运行结束,将结果返回到A,B的调用帧才会消失,如果函数B的内部还要调用函数C,那就是还有一个C的调用帧,以此类推,所有的调用都会形成一个调用栈。尾调用由于是函数的最后一步,所以不需要保留外层函数的调用帧,因为调用的位置和内部变量等信息都不会用到了,直接用内层函数的调用帧取代外层函数的即可。

值得注意的是以下的形式不属于尾调用

function f(x){
  let y = g(x); 
  return y;
}
//在g调用之后还有赋值语句

function f(x){
 return g(x) +1
}
//也是属于调用之后还要执行操作
function(){
  g(x);
}
//相当于在调用了g之后还执行了 return undefined;

只有不再用到外层函数的内部变量的时候,内层函数的调用才会取代外层函数的调用帧,否则就无法进行“尾调用优化”,尾调用优化的最好的使用情况就是在递归的时候使用。

说了这么多尾调用之后还要补充最关键的:尾调用的优化只在严格模式下开启,正常模式下是无效的。因为正常情况下函数有 arguments和caller这两个变量,尾调用优化时,函数的调用栈会被改写,这两个变量会失真,严格模式禁用这两个变量,所以尾调用优化仅在严格模式下生效。