ES6学习4(函数的拓展)

来源:互联网 发布:福州专业淘宝美工培训 编辑:程序博客网 时间:2024/05/19 06:38

函数参数的默认值

基本用法

在ES6之前,不能直接为函数的参数指定默认值,于是只能使用变通的方法。在函数内部手动检测参数是否有值,不过这样的方法有时并不保险。
ES6允许直接写上默认值。

function log(x, y = 'World') {  console.log(x, y);}log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', '') // Hello

这样写不仅简化了写法,更加可靠,而且还有两个好处,只要一看函数的参数表,马上就知道哪些参数是必须的,哪些不用传也行,不传时默认值是什么。将来包装这个函数或写对外接口时,可以直接剪掉这个参数,也不会有什么问题。

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

如果只使用解构赋值的默认值,那就意味着还是必须要传一个参数才行。

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

这里如果传入的参数不是一个对象,那么解构赋值就不会执行,x,y也就不会生成。如果同时使用函数参数的默认值,就可以不传参数了。

function foo({x, y = 5}={}) {    console.log(x, y);}foo({}) // undefined, 5foo({x: 1}) // 1, 5foo({x: 1, y: 2}) // 1, 2foo() // undefined, 5

参数默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

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]

函数的length属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。

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

这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入length属性。

(function(...args) {}).length // 0

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

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

作用域

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

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

内部的x并未生成:

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

如果给y赋值时x并不存在则会报错:

function f(y = x) {  let x = 2;  console.log(y);}f() // ReferenceError: x is not defined

由于函数的参数其实也是隐式的使用let声明的,所以下面这样写会触发暂时性死区,是读不到外面作用域中的x的:

var x4 = 1;function f4(x4 = x4) {  console.log(x4);}f4(); // ReferenceError: x is not defined

如果参数的默认值是一个函数,该函数的作用域是其声明时所在的作用域:

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

上面代码中,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。这个匿名函数声明时,bar函数的作用域还没有形成,所以匿名函数里面的foo指向外层作用域的foo,输出outer。
这里使用babel转码的时候会转为这样:

var foo = 'outer';function bar() {  var func = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function (x) {    return foo;  };  var foo = 'inner';  console.log(func()); }bar();

这时输出的就是inner了,因为func是在函数里定义的。不知道这个问题会怎么解决。

应用

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

function throwIfMissing() {  throw new Error('Missing parameter');}function foo1(mustBeProvided = throwIfMissing()) {  return mustBeProvided;}foo1();

rest参数

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

function add(...values) {  let sum = 0;  for (var val of values) {    sum += val;  }  return sum;}console.log(add(2, 5, 3)); // 10

与arguments不同,这里的value真的是一个数组,所以数组特有的方法都可以用于这个变量。注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。函数的length属性,不包括rest参数。

拓展运算符

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

function push(array, ...items) {  array.push(...items);}

扩展运算符与正常的函数参数可以结合使用。

function f(v, w, x, y, z) {     console.log(v+","+w+","+x+","+y+","+z);}var args = [0, 1];f(-1, ...args, 2, ...[3]);

应用

由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。

// ES5的写法Math.max.apply(null, [14, 3, 77])// ES6的写法Math.max(...[14, 3, 77])

还有一些其他的应用:

var arr1 = ['a', 'b'];var arr2 = ['c'];var arr3 = ['d', 'e'];// ES6的合并数组[...arr1, ...arr2, ...arr3];//与解构赋值结合使用const [first, ...rest] = [1, 2, 3, 4, 5];first // 1rest  // [2, 3, 4, 5]const [first, ...rest] = [];first // undefinedrest  // []:const [first, ...rest] = ["foo"];first  // "foo"rest   // []

对于字符串,拓展运算符可以将其转化为真正的数组,并且可以正确的识别32位的Unicode字符。因此,正确返回字符串长度的函数,可以像下面这样写。

function length(str) {  return [...str].length;}length('x\uD83D\uDE80y') // 3

凡是涉及到操作32位Unicode字符的函数,都有这个问题。因此,最好都用扩展运算符改写。

let str = 'x\uD83D\uDE80y';str.split('').reverse().join('')// 'y\uDE80\uD83Dx'[...str].reverse().join('')// 'y\uD83D\uDE80x'

任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。

var nodeList = document.querySelectorAll('div');var array = [...nodeList];

name属性

var func1 = function () {};// ES5func1.name // ""// ES6func1.name // "func1"
const bar = function baz() {};// ES5bar.name // "baz"// ES6bar.name // "baz"

比较特殊的是bind返回的函数:

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

箭头函数

基本用法:

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

由于{}代表着代码块,要直接返回对象的函数要使用({})的形式。

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

与变量解构同时使用:

const full = ({ first, last }) => first + ' ' + last;// 等同于function full(person) {  return person.first + ' ' + person.last;}

利用这个特性可以简化回调函数:

// 正常函数写法[1,2,3].map(function (x) {  return x * x;});// 箭头函数写法[1,2,3].map(x => x * x);

几个比较巧妙的用法:

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]]

注意事项

函数体内的this对象就是定义时所在的对象,而不是使用时所在的对象。
一般来说,this对象的指向是可变的,比如使用call或函数实际执行时的环境发生了变化。但是箭头函数并不是这样的,它的this对象在定义函数时就已经确定了,不会发生改变。

var s2 = 0;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);setTimeout(() => console.log('s2in: ', timer.s2), 3100);setTimeout(() => console.log('s2out: ', s2), 3100);// s1: 3// s2in: 0// s2out: 3

这里setInterval执行时是处于全局作用域的,普通函数的this这时是全局变量,可以看到timer的s2一直没有被更新,因为执行时this是全局变量,但是timer的s1是一直被更新的,因为这个函数中的this一开始就被绑定在了timer。
箭头函数中this的固定并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。
如果我们看一下babel对箭头函数的处理就会更加清楚:

// ES6function foo() {  setTimeout(() => {    console.log('id:', this.id);  }, 100);}// ES5function foo() {  var _this = this;  setTimeout(function () {    console.log('id:', _this.id);  }, 100);}

箭头函数里根本就没有this。
所以当箭头函数嵌套的时候,所有的this都是最外层箭头函数所在作用域中的this:

function foo() {  return () => {    return () => {      return () => {        console.log('id:', this.id);      };    };  };}var f = foo.call({id: 1});var t1 = f.call({id: 2})()(); // id: 1var t2 = f().call({id: 3})(); // id: 1var t3 = f()().call({id: 4}); // id: 1

由于没有自己的this,箭头函数自然也就不能用做构造函数,也就不能用call()、apply()、bind()这些方法去改变this的指向。
arguments、super、new.target在箭头函数中也是不存在的,指向外层函数的对应变量
除此之外,箭头函数还不可以使用yield命令,因此箭头函数不能用作Generator函数。

嵌套箭头函数

function insert(value) {  return {into: function (array) {    return {after: function (afterValue) {      array.splice(array.indexOf(afterValue) + 1, 0, value);      return array;    }};  }};}insert(2).into([1, 3]).after(1); //[1, 2, 3]

使用箭头函数改写以后:

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]
const pipeline = (...funcs) =>  val => funcs.reduce((a, b) => b(a), val);const plus1 = a => a + 1;const mult2 = a => a * 2;const addThenMult = pipeline(plus1, mult2);addThenMult(5)

函数绑定

函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

foo::bar;// 等同于bar.bind(foo);foo::bar(...arguments);// 等同于bar.apply(foo, arguments);var method = obj::obj.foo;// 等同于var method = ::obj.foo;

尾调用优化

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

function f(x){  return g(x);}function f(x) {  if (x > 0) {    return m(x)  }  return n(x);}

以下3种情况都不是尾调用:

// 情况一function f(x){  let y = g(x);  return y;}// 情况二function f(x){  return g(x) + 1;}// 情况三function f(x){  g(x);}

函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

尾递归

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
比如说著名的fibonacci数列计算,如果使用非尾递归:

function Fibonacci (n) {  if ( n <= 1 ) {return 1};  return Fibonacci(n - 1) + Fibonacci(n - 2);}Fibonacci(10); // 89// Fibonacci(100)// Fibonacci(500)// 堆栈溢出了

换一种想法,从头开始计算,使用尾递归:

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {  if( n <= 1 ) {return ac2};  return Fibonacci2 (n - 1, ac2, ac1 + ac2);}Fibonacci2(100) // 573147844013817200000Fibonacci2(1000) // 7.0330367711422765e+208Fibonacci2(10000) // Infinity

这样调用栈就不会溢出了。

严格模式

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
func.arguments:返回调用时函数的参数。
func.caller:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

尾递归优化的实现

对于不支持尾调用优化的环境,我们也可以自己实现尾调用优化,主要的思想就是将嵌套的调用转换为循环

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
0 0