8 函数的扩展(ECMAScript6入门)

8.1 函数参数的默认值

8.1.1 ES6之前

1
2
3
4
5
6
function log (x, y) {
if (typeof y === 'undefined') {
y = 'World'
}
console.log(x, y)
}

8.1.2 基本用法

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

  • 阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数或文档
  • 有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不回导致以前的代码无法运行

注意:参数变量是默认声明的,不能用letconst再次声明,否则会报错

1
2
3
4
5
6
7
function log (x, y = 'World') {
console.log(x, y)
}

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

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

案例1:只使用解构赋值默认值

1
2
3
4
5
6
7
8
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

案例二:只使用解构赋值默认值

1
2
3
4
5
6
7
8
9
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}

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

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

案例三:双重默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
st=>start: 开始
cond1=>condition: 判断第二个参数是不是 undefined
op1=>operation: 参数默认值生效
cond2=>condition: 参数默认值中 method 是否是 undefined
op2=>operation: 解构赋值中 method 默认值生效
op3=>operation: 参数默认值不生效
op4=>operation: method 通过结构赋值获取值
op5=>operation: 使用传入的 method 值
cond4=>condition: 判断是不是能够解构
op7=>operation: 解构失败(报错)
op8=>operation: method最终为 undefined
e=>end: 结束

st->cond1
cond1(yes)->op1
cond1(no)->cond4
cond4(yes)->op4->e
cond4(no)->op7->e
cond2(yes)->op7->e
cond2(no)->op
op1->cond2
cond2(yes)->op2
op2->e

1
2
3
4
5
6
function fetch(url, { method = 'GET' } = {}) {
console.log(method);
}

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

案例四:通过对比深入理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 写法一:解构中提供默认值
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]

8.1.4 参数默认值的位置

说明:给函数传递参数时,可以通过空位省略某个参数。但如果函数定义时某个参数有默认值,传递空位给它会报错。
技巧:因为设置了默认值的参数后面有没设置默认值的参数时,前者将不能通过空位省略。因此应该将设置了默认值的参数都排在后面。
注意:传入undefined将触发该参数等于默认值,null则没有这个效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 例一
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]

8.1.5 函数的 length 属性

length属性:函数预期传入的参数个数

  • 某个参数指定默认值以后,length就不计入这个参数了
  • rest 参数也不会计入 length
  • 设置了默认值的参数后面的参数,无论是否设置了默认值,都不再计入 length
1
2
3
4
5
6
7
8
9
10
11
/* 参数指定默认值 */
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

/* rest 参数 */
(function(...args) {}).length // 0

/* 设置了默认值的参数后面的参数 */
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

8.1.5 作用域

说明:参数和其默认值引起的一些作用域问题分析。
参数的作用域:参数声明的过程也是变量声明的过程,参数的作用域和函数内部是一致的
函数局部作用域 -> 全局作用域

  • 参数默认值为其它声明在前面的参数
1
2
3
4
5
6
7
8
9
// 全局变量 x
var x = 1

// 由于前面的参数又声明了一个 x, 因此 y = x 中的 x 是局部声明的这个 x。全局的 x 被屏蔽
function f(x, y = x) {
console.log(y)
}

f(2)// 2
  • 参数默认值为其它全局变量
1
2
3
4
5
6
7
8
9
10
let x = 1;

// 参数赋值默认值这个运算的上下文在函数定义时就形成了
function f(y = x) {
// 内部定义的这个局部变量 x 能屏蔽全局的 x,但无法屏蔽参数默认值对全局 x 的引用,事实上,默认值赋值的过程远在内部声明 x 之前就完成了
let x = 2;
console.log(y);
}

f() // 1
  • 默认参数的变量在函数作用域和全局作用域都没有找到变量
1
2
3
4
5
6
function f(y = x) {
let x = 2;
console.log(y);
}

f() // ReferenceError: x is not defined
  • 暂时性死区
1
2
3
4
5
6
7
8
var x = 1;

// x = x 右值部分寻找 x 时发现函数作用域内存在 x ,但此时 x 还未声明,处于暂时性死区,因而报错
function foo(x = x) {
// ...
}

foo() // ReferenceError: x is not defined
  • 参数的默认值是一个函数
    作为默认值的匿名函数在函数foo定义时被定义,JS静态作用域的特点决定了此时其作用域已经被确定,只能是全局作用域(因为函数定义阶段作用域链中只有全局对象,活动对象在函数调用阶段才会被创建)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var x = 1;
    function foo(x, y = function() {/* 根据静态作用域的特点,这个 x 的作用域在一开始就确定了,x 始终指向全局的那个 x */x = 2; }) {
    var x = 3;
    y();
    // 这个时候访问的 x 是函数作用域中声明的那个 x ,全局的 x 被屏蔽了
    console.log(x);
    }

    foo() // 3

8.1.6 应用

可以指定某一个参数不得省略,如果省略就抛出一个错误

1
2
3
4
5
6
7
8
9
10
11
12
function throwIfMissing() {
throw new Error('Missing parameter');
}

// 参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行)
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}

// foo 函数执行时默认值 throwIfMissing() 才执行,mustBeProcided取得的 是 throwIfMissing() 的返回值
foo()
// Error: Missing parameter

8.2 rest 参数

说明:ES6引入rest参数,形式为...变量名,用于获取函数的多余参数

  • 相比 arugments ,rest参数的写法更自然也更简洁
  • rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量
  • 函数的length属性,不计入rest参数

用途:这样就不需要使用arguments对象了
注意:rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错

1
2
3
4
5
6
7
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

8.3 扩展运算符

8.3.1 含义

扩展运算符:用于将数组变为参数序列
说明:相当于rest参数的逆运算

1
2
3
4
5
6
7
8
9
10
function push(array, ...items) {
array.push(...items);
}

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

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

8.3.2 替代数组的 apply 方法

说明:使用apply方法的一种用途就是让数组中的每个元素分别作为实参传递到方法中,但这个需求使用扩展运算符实现更加直观简洁。
举个栗子:下面说三个例子

  • Math.max()
  • Array.prototype.push()
  • new Date()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 案例一:Math.max() */
// ES5的写法
Math.max.apply(null, [14, 3, 77])

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

/* 案例二:Array.prototype.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);

/* 案例三:new Date() */
// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);

8.3.3 扩展运算符的应用

8.3.3.1 合并数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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' ]

8.3.3.2 与解构赋值结合

说明:可以达到从数组中到获取子数组的目的
注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错

1
2
3
4
5
6
// ES5
a = list[0]// 获取 list 数组第一个值
rest = list.slice(1)// 获取 list 数组其余的值

// ES6
[a, ...rest] = list
1
2
3
4
5
6
7
8
9
10
11
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 // []
1
2
3
4
5
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错

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

8.3.3.3 函数的返回值

说明:JS函数如果想返回多个值,只能以数组或对象的方式返回。其中数组形式的返回值用扩展运算符处理非常合适。

1
2
var dateFields = readDateFields(database);
var d = new Date(...dateFields);

8.3.3.4 字符串

说明:扩展运算符可以将字符串转为真正的数组
注意:能够正确处理32位的 Unicode字符

  • 正确获取字符串的 length

    1
    2
    3
    4
    5
    [...'hello']
    // [ "h", "e", "l", "l", "o" ]

    'x\uD83D\uDE80y'.length // 4,这种方式,JavaScript会将32位Unicode字符,识别为2个字符
    [...'x\uD83D\uDE80y'].length // 3
  • 正确 split 字符串

    1
    2
    3
    4
    5
    6
    7
    let str = 'x\uD83D\uDE80y';

    str.split('').reverse().join('')
    // 'y\uDE80\uD83Dx'

    [...str].reverse().join('')
    // 'y\uD83D\uDE80x'

8.3.3.5 实现 Iterator 接口的对象

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

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

8.3.3.6 Map和Set结构,Generator 函数

说明:扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如

  • Map 结构
  • Set 结构
  • Generator 函数返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Map 结构 */
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);

// map.keys() 返回一个便利器对象
let arr = [...map.keys()]; // [1, 2, 3]

/* Set 结构 */
var go = function*(){
yield 1;
yield 2;
yield 3;
};

// go() 返回一个遍历器对象
[...go()] // [1, 2, 3]

8.4 name属性

说明:函数的name属性,返回该函数的函数名

es5和es6的对比 es5 es6
是否加入到标准
匿名函数赋值给一个变量, name 属性值 空字符串 变量名
具名函数赋值给一个变量,name 属性值 函数本来的名字 函数本来的名字
(new Function).name anonymous anonymous
bind返回的函数,name属性值 “bound 函数名” “bound 函数名”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 匿名函数赋值给一个变量 */
var func1 = function () {};

// ES5
func1.name // ""

// ES6
func1.name // "func1"

/* Function构造函数返回的函数实例 */
(new Function).name // "anonymous"

/* bind返回的函数 */
function foo() {};
foo.bind({}).name // "bound foo"

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

8.5 箭头函数

说明:是一种特殊的函数,相比传统的函数

  • 表达更加简洁
  • 绑定this,没有传统函数this指向变化带来的困扰

常见用途:简化回调函数

8.5.1 基本用法

参数 => 表达式

参数部分 说明
没有参数 ()
1个参数 (参数)参数
>1个参数 (参数1, 参数2, ...)
表达式部分 结构 返回值
单个语句 语句 表达式本身的运算结果
多个语句 {语句1; 语句2; return 语句3} return的部分
直接返回对象 (对象字面量) 对象字面量
  • 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /* 没有参数 */
    var f = () => 5;// var f = function () { return 5 };

    /* 单个参数 */
    var f = v => v;
    // var f = function (v) {
    // return v;
    // }

    /* 多个参数 */
    var sum = (num1, num2) => num1 + num2;
    // var sum = function(num1, num2) {
    // return num1 + num2;
    //};
  • 如果箭头函数的代码块部分多于一条语句,就要使用{}将它们括起来,并且使用return语句返回

    1
    2
    3
    4
    var sum = (num1, num2) => {
    const pi = 3.14;
    return (num1 + num2);
    }
  • 由于{}被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上()

    1
    var getTempItem = id => ({id: id, name: "Temp"});
  • 变量解构结合使用

    1
    2
    3
    4
    const full = ({first, last}) => first + ' ' + last;
    // function full(person) {
    // return person.first + ' ' + person.last;
    // }
  • 与 rest 参数结合使用

    1
    2
    3
    4
    5
    6
    7
    /**
    * 将参数转换为数组
    * @param {可变参数}
    */

    const numbers = (...nums) => nums;

    // 将

8.5.2 使用注意点

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

箭头函数的this

说明:this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
用途:箭头函数可以让this指向固定化,这种特性很有利于封装回调函数
注意:由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向

原理说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ES6
function foo() {
setTimeout(() => {
// 箭头函数里面根本没有自己的this,而是引用外层的this
console.log('id:', this.id);
}, 100);
}

// ES5
function foo() {
var _this = this;

setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}

改变 this 无效

1
2
3
4
5
6
7
(function() {
return [
// 箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']

8.5.3 嵌套的箭头函数

管道函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 管道机制:前一个函数的输出是后一个函数的输入 */
const pipeline = (...funcs) =>
val => funcs.reduce((a, b) => b(a), val);

/**
* @param {...Function} funcs 需要进行管道运算的一个一个函数
*/

//var pipeline = function (...funcs) {
// /**
// * @val {Number} 初始值
// */
// return function (val) {
// return funcs.reduce(function (a, b) {
// return b(a)
// }, val)
// }
//}

const plus1 = a => a + 1;// 返回增加1后的值
const mult2 = a => a + 2;// 返回增加2后的值

const addThenMult = pipeline(plus1, mult2);
addThenMult(5)// 12

ℷ演算

1
2
3
4
5
6
// λ演算的写法
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)));

8.6 函数绑定

函数绑定运算符:对象::函数名
功能:该运算符会自动将左边的对象,作为上下文环境(即 this 对象),绑定到右边的函数上面,然后返回

  • 如果左边(对象)为空,右边是一个对象的方法,则等于将该方法绑定到该对象上面
  • 某些情况下可以采用链式写法

用途:用来取代call、apply、bind调用
兼容性:只是一个ES7的提案,但Babel转码器已经支持

取代 bind、 apply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 取代 bind */
contextObj::bar
// bar.bind(contextObj)

/* 取代 apply */
contextObj::bar(...arguments)
// bar.apply(contextObj, arguments)

const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* 判断对象中是否存在某个值
* @param {obj} 对象
* @param {key} 属性名
*/

function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
// return hasOwnProperty.call(obj);
}

简写的情形

1
2
3
4
5
var method = obj::obj.foo;
// var method = ::obj.foo;

let log = ::console.log;
// var log = console.log.bind(console.log)

链式写法

1
2
3
4
5
6
7
/* 例子1 */
import { map, takeWhile, forEach } from "iterlib"

getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

8.7 尾调用优化

8.7.1 什么是尾调用?

答:指函数的最后一步是调用另一个函数
注意:尾调用不一定出现在函数尾部,只要是最后一步操作即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 正确的尾调用 */
function f(x){
return g(x);
}

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

/* 容易被误解为尾调用的情形 */
// 情况一:调用函数g之后,还有赋值操作
function f(x){
let y = g(x);
return y;
}

// 情况二:调用后还有加运算
function f(x){
return g(x) + 1;
}

// 情况三:最后还有一个隐含的 return undefined 的操作
function f(x){
g(x);
}

8.7.2 尾调用优化

说明:JS 引擎在处理尾调用函数的时候会比普通函数有更好的内存性能
调用帧:函数调用在内存中形成的一个调用记录,保存调用位置和内部变量等信息
调用栈:如果A调用了B,非尾调用会在A的调用帧上方形成一个B的调用帧,依次类推,形成一个调用栈;尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,只要直接用内层函数的调用帧,取代外层函数的饿调用帧
注意:只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

Alt text

1
2
3
4
5
6
7
8
// 不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}

8.7.3 尾递归

尾递归:尾递归调用自身
说明:因为同时保存成千上百给调用帧,递归容易造成栈溢出,但尾递归不回,因为只存在一个调用帧
注意:ES6第一次明确规定,所有ECMAScript的实现,都必需部署尾调用优化

8.7.3.1 计算 n 的阶乘

尾调用

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 递归计算阶乘
* @param {Number} n 几的阶乘
* @param {Number} total 当前一次乘积值
* @return {Number} 返回的值并不使用,也没有意义,只是为了构成尾递归
*/

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

factorial(5, 1)// 120

非尾调用

1
2
3
4
functon factorial (n) {
if (n === 1) return 1;
return n * factional(n - 1);
}

8.7.3.2 fibonacci递归算法

尾调用

1
2
3
4
5
6
7
8
9
10
11
/**
* 递归计算 fibonacii 数列的和
* @param {Number} 在几以下的 fibonacci 数列
* @param {Number} ac1 fibonacii 的第一个值
* @param {Number} ac2 fibonacii 的第二个值
* @return {Number} fibonacii 数列的和
*/

function fibonacci (n, ac1 = 1, ac2 = 1) {
if (n <=1) return ac2;
return fibonacci(n -1, ac2, ac1 + ac2);
}

非尾调用

1
2
3
4
function fibonacii (n) {
if (n <= 1) return 1;
return fibonacci(n - 1) + gibonacci(n - 2)
}

8.7.4 递归函数的改写

说明:把所有用到的内部变量改写成函数的参数,确保最后一步只调用自身
技巧:解决尾递归参数不太易读的问题

  1. 在尾递归函数之外,再提供一个正常形式的函数
  2. 将函数柯里化(currying),意思是将多参数的函数转换成单参数的形式
  3. 采用ES6的函数默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 尾递归函数
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}

/* 1. 在尾递归函数之外,再提供一个正常形式的函数 */
function factorial1(n) {
return tailFactorial(n, 1);
}
factorial1(5) // 120

/* 2. 将函数柯里化(currying),意思是将多参数的函数转换成单参数的形式 */
/**
*/

function currying(fn, n) {
return function (m) {
return fn.call(this, m, n);
};
}
const factorial2 = currying(tailFactorial, 1);

factorial2(5) // 120
/* 3. 采用ES6的函数默认值 */
function factorial3(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

factorial3(5) // 120

8.7.5 严格模式

说明:ES6的尾递归优化值在严格模式下开启,正常模式是无效的
原理:因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。严格模式下,尾调用优化发生,函数的调用栈会改写,因此上面两个变量就会失真

  • func.arguments:返回调用时函数的参数
  • func.caller:返回调用当前函数的那个函数
1
2
3
4
5
6
function restricted() {
"use strict";
restricted.caller; // 报错
restricted.arguments; // 报错
}
restricted();

8.7.6 尾递归优化的实现

说明:正常模式下,或者那些不支持该功能的环境中,可以自己实现尾递归优化。这里提供两种方式

  1. 利用蹦床函数,将递归执行转为循环执行(需要改写尾递归函数本身)
  2. 真正的自己实现的尾递归优化(不需要改写递归函数本身)

8.7.6.1 利用蹦床函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 递归函数
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1);
} else {
return x;
}
}
// function sum(x, y) {
// if (y > 0) {
// return sum(x + 1, y - 1);
// } else {
// return x;
// }
//}

// 蹦床函数
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}

trampoline(sum(1, 100000))
// 100001

8.7.6.2 真正实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

8.8 函数参数的尾逗号

说明:允许函数的最后一个参数有尾逗号
注意:目前数定义和调用时,都不允许有参数的尾逗号
支持:ES7的一个提案
用途:在定义函数时,如果参数单独占一行,增加参数时,之前最后一个参数后面要加一个逗号,版本管理系统就会显示,添加逗号的那一行也发生了变动。如果之前最后一个参数后面就有,,就没有这个问题了

1
2
3
4
5
6
7
8
9
function clownsEverywhere(
param1,
param2,
)
{ /* ... */ }


clownsEverywhere(
'foo',
'bar',
);