ES6语法总结-函数的扩展
Posted DA_GAMER
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES6语法总结-函数的扩展相关的知识,希望对你有一定的参考价值。
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 {
// ...
}
以上是关于ES6语法总结-函数的扩展的主要内容,如果未能解决你的问题,请参考以下文章