ES6--函数扩展

Posted time_iter

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES6--函数扩展相关的知识,希望对你有一定的参考价值。

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

  function log(x,y=‘world‘) {
console.log(x,y);
  }
   log(‘hello‘);//hello world
   log(‘hello‘,‘sky‘);//hello sky
   log(‘hello‘,‘‘);//hello`

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

function log({x,y=5}) {
    console.log(x,y);
}
console.log({});//undefined,5
console.log({x:1});//1,5
console.log({x:1,y=2});//1,2
console.log();// TypeError: Cannot read property ‘x‘ of undefined

分析:只有当函数的参数是一个对象时,变量x和y才会通过解构赋值而生成。如果函数调用时参数不是对象,变量x和y就不会生成,从而报错。如果参数对象没有y属性,y的默认值5才会生效!

function fetch(url,{ method = ‘GET‘ } = {}) {
    console.log(method);
}
fetch(‘www.baidu.com‘);//GET

分析:
结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。

//一:
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}
//二:
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

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

// 函数没有参数的情况
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]

三:参数默认值的位置
当在一个函数中默认值的参数不是尾参数时,无法只省略该参数,而不省略它后面的参数,只能显式输入undefined!

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

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

五:作用域
如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域

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

分析:参数y的默认值等于x。调用时,由于函数作用域内部的变量x已经生成,所以y等于参数x,而不是全局变量x

let x = 1;

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

f() // 1
分析:函数调用时,y的默认值变量x尚未在函数内部生成,所以x指向全局变量

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

f() // ReferenceError: x is not defined
分析:全局变量x不存在
//1
let foo = ‘outer‘;

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

bar();

//2
let foo = ‘outer‘;
let f = x => foo;

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

bar();

六:应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误

function throwIfMissing() {
  throw new Error(‘Missing parameter‘);
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

分析:
上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。
从上面代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行)

另外,可以将参数默认值设为undefined,表明这个参数是可以省略的

function foo(optional = undefined) { ··· }

七:rest函数
ES6引入rest参数(形式为“…变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中,
rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量
注:1–rest参数之后不能再有其他参数(即只能是最后一个参数)
2–函数的length属性,不包括rest参数

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

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

  return sum;
}

add(2, 5, 3) // 10

八:扩展运算符
扩展运算符(spread)是三个点(…)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列!
注:1–该运算符主要用于函数调用

console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

function add(x, y) {
  return x + y;
}

var numbers = [4, 38];
add(...numbers) // 42

九:扩展运算符的应用
1–合并数组

var arr1 = [‘a‘, ‘b‘];
var arr2 = [‘c‘];
var arr3 = [‘d‘, ‘e‘];

[...arr1, ...arr2, ...arr3]
// [ ‘a‘, ‘b‘, ‘c‘, ‘d‘, ‘e‘ ]

2–与解构赋值结合使用<扩展运算符可以与解构赋值结合起来,用于生成数组>
注:如果将扩展运算符用于数组赋值,只能放在参数的最后一位
3–函数的返回值
javascript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象
4–字符串<扩展运算符还可以将字符串转为真正的数组>
5–实现了Iterator接口的对象<任何Iterator接口的对象,都可以用扩展运算符转为真正的数组>
6–Map和Set结构,Generator函数

十:name属性
1–匿名函数

var func1 = function () {};

// ES5
func1.name // ""

// ES6
func1.name // "func1"

2–有函数名

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

3–Function构造函数返回的函数实例,name属性的值为“anonymous”

(new Function).name // "anonymous"

4–bind返回的函数,name属性值会加上“bound ”前缀

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

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

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

var f = v => v;
等同于:
var f = function(v) {
  return v;
};

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

var f = () => 5;
// 等同于
var f = function (){ return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

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

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

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

var getTempItem = id => ({ id: id, name: "Temp" });

4–箭头函数可以与变量解构结合

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

// 等同于
function full( person ){
  return person.first + ‘ ‘ + person.last;
}

5–箭头函数的一个用处是简化回调函数

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

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

var id = 21;

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

分析:
上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42
注;箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数!
1–多层嵌套

let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]

十二:尾函数调用
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数!
注:尾调用不一定出现在函数尾部,只要是最后一步操作即可
函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)!

尾调用优化的意义:
“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存!

只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化!

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}

分析:上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one

十三:尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数!

1--
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

2--优化后:
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

3--参数默认值
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化!

十四:
怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”!
蹦床函数(trampoline)可以将递归执行转为循环执行!

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

分析:蹦床函数的一个实现,它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题

蹦床函数并不是真正的尾递归优化,下面的代码实现了真正的尾递归优化:

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

分析:上面代码中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层!

以上是关于ES6--函数扩展的主要内容,如果未能解决你的问题,请参考以下文章

ES6扩展——函数扩展之剩余函数

ES6函数的扩展

ES6---函数的扩展之函数的默认值

函数的扩展--ES6

ES6 入门系列 - 函数的扩展

ES6 入门系列 - 函数的扩展