函数的扩展

函数的扩展html

  1. 1.         函数参数的默认值
  2. 2.         rest参数
  3. 3.         扩展运算符
  4. 4.         name属性
  5. 5.         箭头函数
  6. 6.         函数绑定
  7. 7.         尾调用优化
  8. 8.         函数参数的尾逗号

函数参数的默认值node

基本用法python

在ES6以前,不能直接为函数的参数指定默认值,只能采用变通的方法。git

function log(x, y) {es6

  y = y || 'World';github

  console.log(x, y);算法

}数据库

 

log('Hello') // Hello World编程

log('Hello', 'China') // Hello China数组

log('Hello', '') // Hello World

上面代码检查函数log的参数y有没有赋值,若是没有,则指定默认值为World。这种写法的缺点在于,若是参数y赋值了,可是对应的布尔值为false,则该赋值不起做用。就像上面代码的最后一行,参数y等于空字符,结果被改成默认值。

为了不这个问题,一般须要先判断一下参数y是否被赋值,若是没有,再等于默认值。

if (typeof y === 'undefined') {

  y = 'World';

}

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

function log(x, y = 'World') {

  console.log(x, y);

}

 

log('Hello') // Hello World

log('Hello', 'China') // Hello China

log('Hello', '') // Hello

能够看到,ES6的写法比ES5简洁许多,并且很是天然。下面是另外一个例子。

function Point(x = 0, y = 0) {

  this.x = x;

  this.y = y;

}

 

var p = new Point();

p // { x: 0, y: 0 }

除了简洁,ES6的写法还有两个好处:首先,阅读代码的人,能够马上意识到哪些参数是能够省略的,不用查看函数体或文档;其次,有利于未来的代码优化,即便将来的版本在对外接口中,完全拿掉这个参数,也不会致使之前的代码没法运行。

参数变量是默认声明的,因此不能用let或const再次声明。

function foo(x = 5) {

  let x = 1; // error

  const x = 2; // error

}

上面代码中,参数变量x是默认声明的,在函数体中,不能用let或const再次声明,不然会报错。

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

参数默认值能够与解构赋值的默认值,结合起来使用。

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

上面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会经过解构赋值而生成。若是函数foo调用时参数不是对象,变量x和y就不会生成,从而报错。若是参数对象没有y属性,y的默认值5才会生效。

下面是另外一个对象的解构赋值默认值的例子。

function fetch(url, { body = '', method = 'GET', headers = {} }) {

  console.log(method);

}

 

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

// "GET"

 

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

// 报错

上面代码中,若是函数fetch的第二个参数是一个对象,就能够为它的三个属性设置默认值。

上面的写法不能省略第二个参数,若是结合函数参数的默认值,就能够省略第二个参数。这时,就出现了双重默认值。

function fetch(url, { method = 'GET' } = {}) {

  console.log(method);

}

 

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

// "GET"

上面代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,而后才是解构赋值的默认值生效,变量method才会取到默认值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]

参数默认值的位置

一般状况下,定义了默认值的参数,应该是函数的尾参数。由于这样比较容易看出来,到底省略了哪些参数。若是非尾部的参数设置默认值,实际上这个参数是无法省略的。

// 例一

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]

上面代码中,有默认值的参数都不是尾参数。这时,没法只省略该参数,而不省略它后面的参数,除非显式输入undefined。

若是传入undefined,将触发该参数等于默认值,null则没有这个效果。

function foo(x = 5, y = 6) {

  console.log(x, y);

}

 

foo(undefined, null)

// 5 null

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

函数的length属性

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

(function (a) {}).length // 1

(function (a = 5) {}).length // 0

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

上面代码中,length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。好比,上面最后一个函数,定义了3个参数,其中有一个参数c指定了默认值,所以length属性等于3减去1,最后获得2。

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

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

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

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

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

做用域

一个须要注意的地方是,若是参数默认值是一个变量,则该变量所处的做用域,与其余变量的做用域规则是同样的,即先是当前函数的做用域,而后才是全局做用域。

var x = 1;

 

function f(x, y = x) {

  console.log(y);

}

 

f(2) // 2

上面代码中,参数y的默认值等于x。调用时,因为函数做用域内部的变量x已经生成,因此y等于参数x,而不是全局变量x。

若是调用时,函数做用域内部的变量x没有生成,结果就会不同。

let x = 1;

 

function f(y = x) {

  let x = 2;

  console.log(y);

}

 

f() // 1

上面代码中,函数调用时,y的默认值变量x还没有在函数内部生成,因此x指向全局变量。

若是此时,全局变量x不存在,就会报错。

function f(y = x) {

  let x = 2;

  console.log(y);

}

 

f() // ReferenceError: x is not defined

下面这样写,也会报错。

var x = 1;

 

function foo(x = x) {

  // ...

}

 

foo() // ReferenceError: x is not defined

上面代码中,函数foo的参数x的默认值也是x。这时,默认值x的做用域是函数做用域,而不是全局做用域。因为在函数做用域中,存在变量x,可是默认值在x赋值以前先执行了,因此这时属于暂时性死区(参见《let和const命令》一章),任何对x的操做都会报错。

若是参数的默认值是一个函数,该函数的做用域是其声明时所在的做用域。请看下面的例子。

let foo = 'outer';

 

function bar(func = x => foo) {

  let foo = 'inner';

  console.log(func()); // outer

}

 

bar();

上面代码中,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。这个匿名函数声明时,bar函数的做用域尚未造成,因此匿名函数里面的foo指向外层做用域的foo,输出outer。

若是写成下面这样,就会报错。

function bar(func = () => foo) {

  let foo = 'inner';

  console.log(func());

}

 

bar() // ReferenceError: foo is not defined

上面代码中,匿名函数里面的foo指向函数外层,可是函数外层并无声明foo,因此就报错了。

下面是一个更复杂的例子。

var x = 1;

function foo(x, y = function() { x = 2; }) {

  var x = 3;

  y();

  console.log(x);

}

 

foo() // 3

上面代码中,函数foo的参数y的默认值是一个匿名函数。函数foo调用时,它的参数x的值为undefined,因此y函数内部的x一开始是undefined,后来被从新赋值2。可是,函数foo内部从新声明了一个x,值为3,这两个x是不同的,互相不产生影响,所以最后输出3。

若是将var x = 3的var去除,两个x就是同样的,最后输出的就是2。

var x = 1;

function foo(x, y = function() { x = 2; }) {

  x = 3;

  y();

  console.log(x);

}

 

foo() // 2

应用

利用参数默认值,能够指定某一个参数不得省略,若是省略就抛出一个错误。

function throwIfMissing() {

  throw new Error('Missing parameter');

}

 

function foo(mustBeProvided = throwIfMissing()) {

  return mustBeProvided;

}

 

foo()

// Error: Missing parameter

上面代码的foo函数,若是调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。

从上面代码还能够看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(即函数名以后有一对圆括号),这代表参数的默认值不是在定义时执行,而是在运行时执行(即若是参数已经赋值,默认值中的函数就不会运行),这与python语言不同。

另外,能够将参数默认值设为undefined,代表这个参数是能够省略的。

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

rest参数

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

function add(...values) {

  let sum = 0;

 

  for (var val of values) {

    sum += val;

  }

 

  return sum;

}

 

add(2, 5, 3) // 10

上面代码的add函数是一个求和函数,利用rest参数,能够向该函数传入任意数目的参数。

下面是一个rest参数代替arguments变量的例子。

// arguments变量的写法

function sortNumbers() {

  return Array.prototype.slice.call(arguments).sort();

}

 

// rest参数的写法

const sortNumbers = (...numbers) => numbers.sort();

上面代码的两种写法,比较后能够发现,rest参数的写法更天然也更简洁。

rest参数中的变量表明一个数组,因此数组特有的方法均可以用于这个变量。下面是一个利用rest参数改写数组push方法的例子。

function push(array, ...items) {

  items.forEach(function(item) {

    array.push(item);

    console.log(item);

  });

}

 

var a = [];

push(a, 1, 2, 3)

注意,rest参数以后不能再有其余参数(即只能是最后一个参数),不然会报错。

// 报错

function f(a, ...b, c) {

  // ...

}

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

(function(a) {}).length  // 1

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

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

扩展运算符

含义

扩展运算符(spread)是三个点(...)。它比如rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。

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

// 1 2 3

 

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

// 1 2 3 4 5

 

[...document.querySelectorAll('div')]

// [<div>, <div>, <div>]

该运算符主要用于函数调用。

function push(array, ...items) {

  array.push(...items);

}

 

function add(x, y) {

  return x + y;

}

 

var numbers = [4, 38];

add(...numbers) // 42

上面代码中,array.push(...items)和add(...numbers)这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。

扩展运算符与正常的函数参数能够结合使用,很是灵活。

function f(v, w, x, y, z) { }

var args = [0, 1];

f(-1, ...args, 2, ...[3]);

替代数组的apply方法

因为扩展运算符能够展开数组,因此再也不须要apply方法,将数组转为函数的参数了。

// ES5的写法

function f(x, y, z) {

  // ...

}

var args = [0, 1, 2];

f.apply(null, args);

 

// ES6的写法

function f(x, y, z) {

  // ...

}

var args = [0, 1, 2];

f(...args);

下面是扩展运算符取代apply方法的一个实际的例子,应用Math.max方法,简化求出一个数组最大元素的写法。

// ES5的写法

Math.max.apply(null, [14, 3, 77])

 

// ES6的写法

Math.max(...[14, 3, 77])

 

// 等同于

Math.max(14, 3, 77);

上面代码表示,因为JavaScript不提供求数组最大元素的函数,因此只能套用Math.max函数,将数组转为一个参数序列,而后求最大值。有了扩展运算符之后,就能够直接用Math.max了。

另外一个例子是经过push函数,将一个数组添加到另外一个数组的尾部。

// ES5的写法

var arr1 = [0, 1, 2];

var arr2 = [3, 4, 5];

Array.prototype.push.apply(arr1, arr2);

 

// ES6的写法

var arr1 = [0, 1, 2];

var arr2 = [3, 4, 5];

arr1.push(...arr2);

上面代码的ES5写法中,push方法的参数不能是数组,因此只好经过apply方法变通使用push方法。有了扩展运算符,就能够直接将数组传入push方法。

下面是另一个例子。

// ES5

new (Date.bind.apply(Date, [null, 2015, 1, 1]))

// ES6

new Date(...[2015, 1, 1]);

扩展运算符的应用

1)合并数组

扩展运算符提供了数组合并的新写法。

// ES5

[1, 2].concat(more)

// ES6

[1, 2, ...more]

 

var arr1 = ['a', 'b'];

var arr2 = ['c'];

var arr3 = ['d', 'e'];

 

// ES5的合并数组

arr1.concat(arr2, arr3);

// [ 'a', 'b', 'c', 'd', 'e' ]

 

// ES6的合并数组

[...arr1, ...arr2, ...arr3]

// [ 'a', 'b', 'c', 'd', 'e' ]

2)与解构赋值结合

扩展运算符能够与解构赋值结合起来,用于生成数组。

// ES5

a = list[0], rest = list.slice(1)

// ES6

[a, ...rest] = list

下面是另一些例子。

const [first, ...rest] = [1, 2, 3, 4, 5];

first // 1

rest  // [2, 3, 4, 5]

 

const [first, ...rest] = [];

first // undefined

rest  // []:

 

const [first, ...rest] = ["foo"];

first  // "foo"

rest   // []

若是将扩展运算符用于数组赋值,只能放在参数的最后一位,不然会报错。

const [...butLast, last] = [1, 2, 3, 4, 5];

// 报错

 

const [first, ...middle, last] = [1, 2, 3, 4, 5];

// 报错

3)函数的返回值

JavaScript的函数只能返回一个值,若是须要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。

var dateFields = readDateFields(database);

var d = new Date(...dateFields);

上面代码从数据库取出一行数据,经过扩展运算符,直接将其传入构造函数Date。

4)字符串

扩展运算符还能够将字符串转为真正的数组。

[...'hello']

// [ "h", "e", "l", "l", "o" ]

上面的写法,有一个重要的好处,那就是可以正确识别32位的Unicode字符。

'x\uD83D\uDE80y'.length // 4

[...'x\uD83D\uDE80y'].length // 3

上面代码的第一种写法,JavaScript会将32位Unicode字符,识别为2个字符,采用扩展运算符就没有这个问题。所以,正确返回字符串长度的函数,能够像下面这样写。

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'

上面代码中,若是不用扩展运算符,字符串的reverse操做就不正确。

5)实现了Iterator接口的对象

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

var nodeList = document.querySelectorAll('div');

var array = [...nodeList];

上面代码中,querySelectorAll方法返回的是一个nodeList对象。它不是数组,而是一个相似数组的对象。这时,扩展运算符能够将其转为真正的数组,缘由就在于NodeList对象实现了Iterator接口。

对于那些没有部署Iterator接口的相似数组的对象,扩展运算符就没法将其转为真正的数组。

let arrayLike = {

  '0': 'a',

  '1': 'b',

  '2': 'c',

  length: 3

};

 

// TypeError: Cannot spread non-iterable object.

let arr = [...arrayLike];

上面代码中,arrayLike是一个相似数组的对象,可是没有部署Iterator接口,扩展运算符就会报错。这时,能够改成使用Array.from方法将arrayLike转为真正的数组。

6MapSet结构,Generator函数

扩展运算符内部调用的是数据结构的Iterator接口,所以只要具备Iterator接口的对象,均可以使用扩展运算符,好比Map结构。

let map = new Map([

  [1, 'one'],

  [2, 'two'],

  [3, 'three'],

]);

 

let arr = [...map.keys()]; // [1, 2, 3]

Generator函数运行后,返回一个遍历器对象,所以也可使用扩展运算符。

var go = function*(){

  yield 1;

  yield 2;

  yield 3;

};

 

[...go()] // [1, 2, 3]

上面代码中,变量go是一个Generator函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历获得的值,转为一个数组。

若是对没有iterator接口的对象,使用扩展运算符,将会报错。

var obj = {a: 1, b: 2};

let arr = [...obj]; // TypeError: Cannot spread non-iterable object

name属性

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

function foo() {}

foo.name // "foo"

这个属性早就被浏览器普遍支持,可是直到ES6,才将其写入了标准。

须要注意的是,ES6对这个属性的行为作出了一些修改。若是将一个匿名函数赋值给一个变量,ES5的name属性,会返回空字符串,而ES6的name属性会返回实际的函数名。

var func1 = function () {};

 

// ES5

func1.name // ""

 

// ES6

func1.name // "func1"

上面代码中,变量func1等于一个匿名函数,ES5和ES6的name属性返回的值不同。

若是将一个具名函数赋值给一个变量,则ES5和ES6的name属性都返回这个具名函数本来的名字。

const bar = function baz() {};

 

// ES5

bar.name // "baz"

 

// ES6

bar.name // "baz"

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

(new Function).name // "anonymous"

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;

};

若是箭头函数不须要参数或须要多个参数,就使用一个圆括号表明参数部分。

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

因为大括号被解释为代码块,因此若是箭头函数直接返回一个对象,必须在对象外面加上括号。

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

箭头函数能够与变量解构结合使用。

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

 

// 等同于

function full(person) {

  return person.first + ' ' + person.last;

}

箭头函数使得表达更加简洁。

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

const square = n => n * n;

上面代码只用了两行,就定义了两个简单的工具函数。若是不用箭头函数,可能就要占用多行,并且还不如如今这样写醒目。

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

// 正常函数写法

[1,2,3].map(function (x) {

  return x * x;

});

 

// 箭头函数写法

[1,2,3].map(x => x * x);

另外一个例子是

// 正常函数写法

var result = values.sort(function (a, b) {

  return a - b;

});

 

// 箭头函数写法

var result = values.sort((a, b) => a - b);

下面是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命令,所以箭头函数不能用做Generator函数。

上面四点中,第一点尤为值得注意。this对象的指向是可变的,可是在箭头函数中,它是固定的。

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。

箭头函数可让setTimeout里面的this,绑定定义时所在的做用域,而不是指向运行时所在的做用域。下面是另外一个例子。

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('s2: ', timer.s2), 3100);

// s1: 3

// s2: 0

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

箭头函数可让this指向固定化,这种特性颇有利于封装回调函数。下面是一个例子,DOM事件的回调函数封装在一个对象里面。

var handler = {

  id: '123456',

 

  init: function() {

    document.addEventListener('click',

      event => this.doSomething(event.type), false);

  },

 

  doSomething: function(type) {

    console.log('Handling ' + type  + ' for ' + this.id);

  }

};

上面代码的init方法中,使用了箭头函数,这致使这个箭头函数里面的this,老是指向handler对象。不然,回调函数运行时,this.doSomething这一行会报错,由于此时this指向document对象。

this指向的固定化,并非由于箭头函数内部有绑定this的机制,实际缘由是箭头函数根本没有本身的this,致使内部的this就是外层代码块的this。正是由于它没有this,因此也就不能用做构造函数。

因此,箭头函数转成ES5的代码以下。

// ES6

function foo() {

  setTimeout(() => {

    console.log('id:', this.id);

  }, 100);

}

 

// ES5

function foo() {

  var _this = this;

 

  setTimeout(function () {

    console.log('id:', _this.id);

  }, 100);

}

上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有本身的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: 1

var t2 = f().call({id: 3})(); // id: 1

var t3 = f()().call({id: 4}); // id: 1

上面代码之中,只有一个this,就是函数foo的this,因此t一、t二、t3都输出一样的结果。由于全部的内层函数都是箭头函数,都没有本身的this,它们的this其实都是最外层foo函数的this。

除了this,如下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

function foo() {

  setTimeout(() => {

    console.log('args:', arguments);

  }, 100);

}

 

foo(2, 4, 6, 8)

// args: [2, 4, 6, 8]

上面代码中,箭头函数内部的变量arguments,实际上是函数foo的arguments变量。

另外,因为箭头函数没有本身的this,因此固然也就不能用call()、apply()、bind()这些方法去改变this的指向。

(function() {

  return [

    (() => this.x).bind({ x: 'inner' })()

  ];

}).call({ x: 'outer' });

// ['outer']

上面代码中,箭头函数没有本身的this,因此bind方法无效,内部的this指向外部的this。

长期以来,JavaScript语言的this对象一直是一个使人头痛的问题,在对象方法中使用this,必须很是当心。箭头函数”绑定”this,很大程度上解决了这个困扰。

嵌套的箭头函数

箭头函数内部,还能够再使用箭头函数。下面是一个ES5语法的多重嵌套函数。

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]

下面是一个部署管道机制(pipeline)的例子,即前一个函数的输出是后一个函数的输入。

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)

// 12

若是以为上面的写法可读性比较差,也能够采用下面的写法。

const plus1 = a => a + 1;

const mult2 = a => a * 2;

 

mult2(plus1(5))

// 12

箭头函数还有一个功能,就是能够很方便地改写λ演算。

// λ演算的写法

fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

 

// ES6的写法

var fix = f => (x => f(v => x(x)(v)))

               (x => f(v => x(x)(v)));

上面两种写法,几乎是一一对应的。因为λ演算对于计算机科学很是重要,这使得咱们能够用ES6做为替代工具,探索计算机科学。

函数绑定

箭头函数能够绑定this对象,大大减小了显式绑定this对象的写法(call、apply、bind)。可是,箭头函数并不适用于全部场合,因此ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。虽然该语法仍是ES7的一个提案,可是Babel转码器已经支持。

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

foo::bar;

// 等同于

bar.bind(foo);

 

foo::bar(...arguments);

// 等同于

bar.apply(foo, arguments);

 

const hasOwnProperty = Object.prototype.hasOwnProperty;

function hasOwn(obj, key) {

  return obj::hasOwnProperty(key);

}

若是双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

var method = obj::obj.foo;

// 等同于

var method = ::obj.foo;

 

let log = ::console.log;

// 等同于

var log = console.log.bind(console);

因为双冒号运算符返回的仍是原对象,所以能够采用链式写法。

// 例一

import { map, takeWhile, forEach } from "iterlib";

 

getPlayers()

::map(x => x.character())

::takeWhile(x => x.strength > 100)

::forEach(x => console.log(x));

 

// 例二

let { find, html } = jake;

 

document.querySelectorAll("div.myClass")

::find("p")

::html("hahaha");

尾调用优化

什么是尾调用?

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

function f(x){

  return g(x);

}

上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。

如下三种状况,都不属于尾调用。

// 状况一

function f(x){

  let y = g(x);

  return y;

}

 

// 状况二

function f(x){

  return g(x) + 1;

}

 

// 状况三

function f(x){

  g(x);

}

上面代码中,状况一是调用函数g以后,还有赋值操做,因此不属于尾调用,即便语义彻底同样。状况二也属于调用后还有操做,即便写在一行内。状况三等同于下面的代码。

function f(x){

  g(x);

  return undefined;

}

尾调用不必定出如今函数尾部,只要是最后一步操做便可。

function f(x) {

  if (x > 0) {

    return m(x)

  }

  return n(x);

}

上面代码中,函数m和n都属于尾调用,由于它们都是函数f的最后一步操做。

尾调用优化

尾调用之因此与其余调用不一样,就在于它的特殊的调用位置。

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

尾调用因为是函数的最后一步操做,因此不须要保留外层函数的调用帧,由于调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就能够了。

function f() {

  let m = 1;

  let n = 2;

  return g(m + n);

}

f();

 

// 等同于

function f() {

  return g(3);

}

f();

 

// 等同于

g(3);

上面代码中,若是函数g不是尾调用,函数f就须要保存内部变量m和n的值、g的调用位置等信息。但因为调用g以后,函数f就结束了,因此执行到最后一步,彻底能够删除 f(x) 的调用帧,只保留 g(3) 的调用帧。

这就叫作“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。若是全部函数都是尾调用,那么彻底能够作到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

注意,只有再也不用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,不然就没法进行“尾调用优化”。

function addOne(a){

  var one = 1;

  function inner(b){

    return b + one;

  }

  return inner(a);

}

上面的函数不会进行尾调用优化,由于内层函数inner用到了外层函数addOne的内部变量one。

尾递归

函数调用自身,称为递归。若是尾调用自身,就称为尾递归。

递归很是耗费内存,由于须要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来讲,因为只存在一个调用帧,因此永远不会发生“栈溢出”错误。

function factorial(n) {

  if (n === 1) return 1;

  return n * factorial(n - 1);

}

 

factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多须要保存n个调用记录,复杂度 O(n) 。

若是改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function factorial(n, total) {

  if (n === 1) return total;

  return factorial(n - 1, n * total);

}

 

factorial(5, 1) // 120

还有一个比较著名的例子,就是计算fibonacci 数列,也能充分说明尾递归优化的重要性

若是是非尾递归的fibonacci 递归方法

function Fibonacci (n) {

  if ( n <= 1 ) {return 1};

 

  return Fibonacci(n - 1) + Fibonacci(n - 2);

}

 

Fibonacci(10); // 89

// Fibonacci(100)

// Fibonacci(500)

// 堆栈溢出了

若是咱们使用尾递归优化过的fibonacci 递归算法

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {

  if( n <= 1 ) {return ac2};

 

  return Fibonacci2 (n - 1, ac2, ac1 + ac2);

}

 

Fibonacci2(100) // 573147844013817200000

Fibonacci2(1000) // 7.0330367711422765e+208

Fibonacci2(10000) // Infinity

因而可知,“尾调用优化”对递归操做意义重大,因此一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,全部ECMAScript的实现,都必须部署“尾调用优化”。这就是说,在ES6中,只要使用尾递归,就不会发生栈溢出,相对节省内存。

递归函数的改写

尾递归的实现,每每须要改写递归函数,确保最后一步只调用自身。作到这一点的方法,就是把全部用到的内部变量改写成函数的参数。好比上面的例子,阶乘函数 factorial 须要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样作的缺点就是不太直观,第一眼很难看出来,为何计算5的阶乘,须要传入两个参数5和1?

两个方法能够解决这个问题。方法一是在尾递归函数以外,再提供一个正常形式的函数。

function tailFactorial(n, total) {

  if (n === 1) return total;

  return tailFactorial(n - 1, n * total);

}

 

function factorial(n) {

  return tailFactorial(n, 1);

}

 

factorial(5) // 120

上面代码经过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。

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

function currying(fn, n) {

  return function (m) {

    return fn.call(this, m, n);

  };

}

 

function tailFactorial(n, total) {

  if (n === 1) return total;

  return tailFactorial(n - 1, n * total);

}

 

const factorial = currying(tailFactorial, 1);

 

factorial(5) // 120

上面代码经过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial 。

第二种方法就简单多了,就是采用ES6的函数默认值。

function factorial(n, total = 1) {

  if (n === 1) return total;

  return factorial(n - 1, n * total);

}

 

factorial(5) // 120

上面代码中,参数 total 有默认值1,因此调用时不用提供这个值。

总结一下,递归本质上是一种循环操做。纯粹的函数式编程语言没有循环操做命令,全部的循环都用递归实现,这就是为何尾递归对这些语言极其重要。对于其余支持“尾调用优化”的语言(好比Lua,ES6),只须要知道循环能够用递归代替,而一旦使用递归,就最好使用尾递归。

严格模式

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。

这是由于在正常模式下,函数内部有两个变量,能够跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,所以上面两个变量就会失真。严格模式禁用这两个变量,因此尾调用模式仅在严格模式下生效。

function restricted() {

  "use strict";

  restricted.caller;    // 报错

  restricted.arguments; // 报错

}

restricted();

尾递归优化的实现

尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,有没有办法也使用尾递归优化呢?回答是能够的,就是本身实现尾递归优化。

它的原理很是简单。尾递归之因此须要优化,缘由是调用栈太多,形成溢出,那么只要减小调用栈,就不会溢出。怎么作能够减小调用栈呢?就是采用“循环”换掉“递归”。

下面是一个正常的递归函数。

function sum(x, y) {

  if (y > 0) {

    return sum(x + 1, y - 1);

  } else {

    return x;

  }

}

 

sum(1, 100000)

// Uncaught RangeError: Maximum call stack size exceeded(…)

上面代码中,sum是一个递归函数,参数x是须要累加的值,参数y控制递归次数。一旦指定sum递归100000次,就会报错,提示超出调用栈的最大次数。

蹦床函数(trampoline)能够将递归执行转为循环执行。

function trampoline(f) {

  while (f && f instanceof Function) {

    f = f();

  }

  return f;

}

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

而后,要作的就是将原来的递归函数,改写为每一步返回另外一个函数。

function sum(x, y) {

  if (y > 0) {

    return sum.bind(null, x + 1, y - 1);

  } else {

    return x;

  }

}

上面代码中,sum函数的每次执行,都会返回自身的另外一个版本。

如今,使用蹦床函数执行sum,就不会发生调用栈溢出。

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

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

函数参数的尾逗号

ES7有一个提案,容许函数的最后一个参数有尾逗号(trailing comma)。

目前,函数定义和调用时,都不容许有参数的尾逗号。

function clownsEverywhere(

  param1,

  param2

) { /* ... */ }

 

clownsEverywhere(

  'foo',

  'bar'

);

若是之后要在函数的定义之中添加参数,就势必还要添加一个逗号。这对版本管理系统来讲,就会显示,添加逗号的那一行也发生了变更。这看上去有点冗余,所以新提案容许定义和调用时,尾部直接有一个逗号。

function clownsEverywhere(

  param1,

  param2,

) { /* ... */ }

 

clownsEverywhere(

  'foo',

  'bar',

);

 转载于   阮一峰的 博客

相关文章
相关标签/搜索