ES6学习笔记之函数扩展

一. 函数参数的默认值

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

function log(x , y = 'world'){
   console.log(x, y);
}

log('hello'); //  Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

function Point(x = 0, y = 0){
    this.x = x;
    this.y = y;
}
var p = new Point();
p // {x : 0, y: 0}

Tips:参数变量是默认声明的,所以不能用let或const再次声明。

//下面代码中,参数变量x是默认声明的,在函数体中,不能用let或const再次声明,否则会报错。
function foo(x = 5){
    let x = 1;  //error
    const x =2; //error
 }

参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

let x = 99;
function foo(p = x+1){
    console.log(p);
}

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

Tips:参数p的默认值是x + 1,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。

二 .与解构赋值默认值结合使用

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

Tips:上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。

//下面代码指定,如果没有提供参数,函数foo的参数默认为一个空对象。
function foo({x, y = 5} = {}){
    console.log(x, y);
}

foo(); //undefined 5

  function fetch(url, { body = '', method = 'Get', headers = {}}){
  console.log(method);
}

fetch('http://example.com', {});
//'GET'

fetch('http://example.com');
//报错

Tips: 上面代码中,如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。

function fetch(url, {method = 'GET'} = {}){
  console.log(method);
}

fetch('http://example.com');
//'GET'

//写法一
function m1({x = 0, y = 0} = {}){
    return [x, y];
 }

 //写法二
 function m2({x , y} = { x : 0, y : 0}){
       return [x, y]
  }

Tips:上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。

//函数没有参数
m1() // [0, 0]
m2() // [0, 0]

//x和y都有值的情况
m1({x : 3, y : 8}) // [3, 8]
m2({x: 3,  y : 8}) // [3, 8]

//x有值,y都无值的情况
m1({x : 3}) // [3, 0]
m2({x : 3}) // [3, undefined]

//x和y都无值
m1({}) // [0, 0]
m2({}) //[undefined, undefined]

m1({z : 3}) // [0, 0]
m2({z : 3}) // [undefined, undefined]

三. 参数默认值的位置

如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

//例一
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]

Tips:上面代码中,有默认值的参数都不是尾参数。

//如果传入undefined,将触发该参数等于默认值,null则没有这个效果。
 function foo(x = 5, y =6){
    console.log(x, y);
 }

foo(undefined, null)
//5 null

Tips: 上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。

四. 函数的length属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

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

五. 作用域

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

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

Tips:上面代码中,参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。

let x = 1;
function f(y = x){
  let x = 2;
  console.log(y);
}
f() // 1

*Tips: 上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。*

//如果此时,全局变量x不存在,就会报错。
function f(y = x){
  let x = 2;
  console.log(y);
 }

 f() // ReferenceError: x is not defined

//这样写,也会报错。
function foo(x = x){
    // ...
}
foo() // ReferenceError: x is not defined

*Tips:上面代码中,参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。*

如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。

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

 function bar(func = () => foo){
      let foo = 'inner';
      console.log(func());
 }
 bar() //ReferenceError: foo is not defined

六. rest参数

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

function add(...values){
let sum = 0;
for(var val of values){
sum += val
}
return sum;
}

add(2, 5, 3);

rest 参数就是一个真正的数组,数组特有的方法都可以使用。

function push(array, ...items){
    items.forEach(function(item){
        array.push(item);
        console.log(item);
  });
}

var a =[];
push(a, 1, 2, 3);

注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

//报错
function(a, ...b, c){
    //...
}

函数的length属性,不包括 rest 参数。

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

七. name属性

函数的name属性,返回该函数的函数名。

*Tips:如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。 *

var f = function() {};

//ES5
f.name // ''''

//ES6
f.name // "f"

八 .箭头函数

ES6 允许使用“箭头”(=>)定义函数。

var f = v => v;

等同于

var f = function(v){
     return v;
  }

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

 var f = () => 5;
 var f = function(){return 5};
 var sum = (num1, num2) => num1 + num2;
  var sum = function(num1, num2){
      return num1 + num2;
  };

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var sum = (num1, num2) => { return num1 + num2}

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

//报错
let getTempItem = id => {id : id, name : "Temp"}

//不报错
let getTempItem = id => ({id : id, name : "Temp"})

箭头函数可以与变量解构结合使用。

const full = ({first, last}) => first + ' ' + last;

等同于

function full(person){
    return person.first + ' '+person.last;
}

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命令

//在箭头函数中,this对象的指向是可变的
function foo(){
    setTimeout(() => {
    console.log('id', this.id);
    }, 100);
}

var id = 21;
foo.call({id : 42});
//id :42

Tips:上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。

function Timer(){
    this.s1 = 0;
    this.s2 = 0;
    setInterval(() => this.s1++, 1000);
    setInterval(function(){
          this.s2++;
    }, 1000);

 var timer = new Timer();
 setTimeout(() => console.log('s1: ', timer.s1), 3100);
  //s1 : 3

 setTimeout(() => console.log('s2: ', timer.s2), 3100);
  //s2 : 0

*Tips:Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100毫秒之后,timer.s1被更新了3次,而timer.s2一次都没更新。 *

九.尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

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

Tips:情况一是调用函数g之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内

尾调用不一定出现在函数尾部,只要是最后一步操作即可。

//函数m和n都属于尾调用
function f(x) {
   if(x > 0){
     return m(x);
   }
  return n(x);
}

推荐阅读更多精彩内容

  • 函数参数的默认值 基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。 上面代码检查函数l...
    呼呼哥阅读 520评论 0 0
  • 第一章 块级作用域绑定 let 和 const 都是不存在提升,声明的都是块级标识符都禁止重声明 每个const声...
    NowhereToRun阅读 894评论 0 2
  • 1.函数参数的默认值 (1).基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
    赵然228阅读 172评论 0 0
  • 参数函数的默认值 对应的布尔值为 false ,则赋值不起作用 ES6将允许将默认值直接写在参数定义的后面 阅读代...
    冰豹阅读 92评论 0 2
  • 三,字符串扩展 3.1 Unicode表示法 ES6 做出了改进,只要将码点放入大括号,就能正确解读该字符。有了这...
    eastbaby阅读 716评论 0 7