1、函数参数的默认值
根本用法
function log(x, y = 'World') {console.log(x, y);
}
log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello
- 应用参数默认值时,函数不能有同名参数
// 不报错
function foo(x, x, y) {// ...}
// 报错
function foo(x, x, y = 1) {// ...}
// SyntaxError: Duplicate parameter name not allowed in this context
- 参数默认值不是传值的,而是每次都从新计算默认值表达式的值。也就是说,参数默认值是惰性求值的
let x = 99;
function foo(p = x + 1) {console.log(p);
}
foo() // 100
x = 100;
foo() // 101
与解构赋值默认值联合应用
参数默认值的地位
- 定义了默认值的参数,应该是函数的尾参数。因为这样比拟容易看进去,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的
// 例一
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]
函数的 length
属性
- 函数的
length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真 - 如果设置了默认值的参数不是尾参数,那么
length
属性也不再计入前面的参数了
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
作用域
- 一旦设置了参数的默认值,函数进行申明初始化时,参数会造成一个独自的作用域(context)。等到初始化完结,这个作用域就会隐没。这种语法行为,在不设置参数默认值时,是不会呈现的
var x = 1;
function f(x, y = x) {console.log(y);
}
f(2) // 2
- 上面代码中,函数
f
调用时,参数y = x
造成一个独自的作用域。这个作用域外面,变量x
自身没有定义,所以指向外层的全局变量x
。函数调用时,函数体外部的局部变量x
影响不到默认值变量x
。如果此时,全局变量x
不存在,就会报错
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
- 上面这样写,也会报错。参数
x = x
造成一个独自作用域。理论执行的是let x = x
,因为暂时性死区的起因,这行代码会报错”x 未定义“
var x = 1;
function foo(x = x) {// ...}
foo() // ReferenceError: x is not defined
- 如果参数的默认值是一个函数,该函数的作用域也恪守这个规定。函数
bar
的参数func
的默认值是一个匿名函数,返回值为变量foo
。函数参数造成的独自作用域外面,并没有定义变量foo
,所以foo
指向外层的全局变量foo
,因而输入outer
。如果外层没有什么foo
就会报错
let foo = 'outer';
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
利用
- 利用参数默认值,能够指定某一个参数不得省略,如果省略就抛出一个谬误
function throwIfMissing() {throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {return mustBeProvided;}
foo() // Error: Missing parameter
- 能够将参数默认值设为
undefined
,表明这个参数是能够省略的
2、rest 参数
- ES6 引入 rest 参数(模式为
... 变量名
),用于获取函数的多余参数,这样就不须要应用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中 - 留神,rest 参数之后不能再有其余参数(即只能是最初一个参数),否则会报错
- 函数的
length
属性,不包含 rest 参数
3、严格模式
- 从 ES5 开始,函数外部能够设定为严格模式
function doSomething(a, b) {
'use strict';
// code
}
- ES2016 做了一点批改,规定只有函数参数应用了默认值、解构赋值、或者扩大运算符,那么函数外部就不能显式设定为严格模式,否则会报错
4、name 属性
- 函数的
name
属性,返回该函数的函数名 - ES6 对这个属性的行为做出了一些批改。如果将一个匿名函数赋值给一个变量,ES5 的
name
属性,会返回空字符串,而 ES6 的name
属性会返回理论的函数名
var f = function () {};
// ES5 f.name // ""// ES6 f.name //"f"
- 如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的
name
属性都返回这个具名函数本来的名字
const bar = function baz() {};
// ES5 bar.name // "baz"
// ES6 bar.name // "baz"
Function
构造函数返回的函数实例,name
属性的值为anonymous
bind
返回的函数,name
属性值会加上bound
前缀
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound"
5、箭头函数
- 如果箭头函数只有一行语句,且不须要返回值,能够采纳上面的写法,就不必写大括号了
let fn = () => void doesNotReturn();
应用留神点
1. 函数体内的 this 对象,就是定义时所在的对象,而不是应用时所在的对
2. 不能够当作构造函数,也就是说,不能够应用 new 命令
3. 不能够应用 arguments 对象
4. 不能够应用 yield 命令
不适宜场合
- 定义对象的办法,且该办法外部包含 this。
cat.jumps()
办法是一个箭头函数,这是谬误的。调用cat.jumps()
时,如果是一般函数,该办法外部的this
指向cat
;如果写成下面那样的箭头函数,使得this
指向全局对象,因而不会失去预期后果。这是因为对象不形成独自的作用域,导致jumps
箭头函数定义时的作用域就是全局作用域
const cat = {
lives: 9,
jumps: () => {this.lives--;}
}
- 须要动静
this
的时候,也不应应用箭头函数
var button = document.getElementById('press');
button.addEventListener('click', () => {this.classList.toggle('on');
});
下面代码运行时,点击按钮会报错,因为 button
的监听函数是一个箭头函数,导致外面的 this
就是全局对象。如果改成一般函数,this
就会动静指向被点击的按钮对象
嵌套的箭头函数
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]
- 管道用法
6、尾调用
- 尾调用(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);
}
- 尾调用不肯定呈现在函数尾部,只有是最初一步操作即可
尾调用优化
- 尾调用之所以与其余调用不同,就在于它的非凡的调用地位。其余函数调用会在内存造成一个“调用记录”,又称“调用帧”,保留调用地位和内存变量等信息。如果在函数 A 的外部调用函数 B,那么在 A 的调用帧上方,还会造成一个 B 的调用帧。等到 B 运行完结,将后果返回到 A,B 的调用帧才会隐没。以此类推,就造成了调用栈
- 尾调用因为是函数的最初一步操作,所以不须要保留外层函数的调用帧,因为调用地位、外部变量等信息都不会再用到了,只有间接用内层函数的调用帧,取代外层函数的调用帧就能够了
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {return g(3);
}
f();
// 等同于
g(3);
- “尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么齐全能够做到每次执行时,调用帧只有一项,这将大大节俭内存
尾递归
- 函数调用本身,称为递归。如果尾调用本身,就称为尾递归
- 递归十分消耗内存,因为须要同时保留成千上百个调用帧,很容易产生“栈溢出”谬误(stack overflow)。但对于尾递归来说,因为只存在一个调用帧,所以永远不会产生“栈溢出”谬误
严格模式
- ES6 的尾调用优化只在严格模式下开启,失常模式是有效的
尾递归优化的实现
- 就是采纳“循环”换掉“递归”
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(…)
// 改写为蹦床函数
function trampoline(f) {while (f && f instanceof Function) {f = f();
}
return f;
}
function sum(x, y) {if (y > 0) {return sum.bind(null, x + 1, y - 1); // 这里会返回一个新的函数,并传入参数
} else {return x;}
}
- 蹦床函数并不是真正的尾递归优化,上面的实现才是
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;
}
return undefined;
};
}
var sum = tco(function(x, y) {if (y > 0) {return sum(x + 1, y - 1) // 始终都是 undefined,所以防止了递归
}
else {return x}
});
sum(1, 100000) // 100001
7、函数参数的尾逗号
8、Function.proptype.toString()
toString()
办法返回函数代码自身,以前会省略正文和空格
function /* foo comment */ foo () {}
foo.toString()
// function foo() {}
- 批改后的
toString()
办法,明确要求返回截然不同的原始代码
function /* foo comment */ foo () {}
foo.toString()
// "function /* foo comment */ foo () {}"
9、catch 命令的参数省略
- ES2019 做出扭转能够省略
try {// ...} catch {// ...}