学习ES6中的函数扩展小记

函数的扩展

1. 函数参数的默认值

注意:
1. 参数变量是默认声明的,不能用 let 和 const 再次声明
2. 使用参数默认值时,函数不能有同名参数
3. 参数默认值不是传值的,而是每次都重新计算默认值表达式的值。(参数默认值是惰性求值的)

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

// 只对对象进行了解构赋值,参数本身并没有默认值,不传递参数会导致报错
function foo({x, y = 5}) {
  console.log(x, y);
}

// 这种即对对象进行了解构赋值,又对函数参数进行了默认值设置。不传递参数则默认参数为一个空对象
function foo({x, y = 5} = {}) {
  console.log(x, y);
}

区分二者的差别:

// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

参数默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾参数。
1. 容易看出到底省略了哪些参数
2. 如果非尾部的参数设置了默认值,实际上这个参数是无法省略的

函数的 length 属性

函数的 length 属性,返回的是没有指定默认值的参数的个数,指定默认值的参数,将不计算在内。同样的 rest 参数也不会计入 length 属性

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

作用域(重点理解)

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域就会消失(不设置默认值时,不会出现)

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

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

foo() // 2
x // 1

应用

利用参数默认值,可以指定某个参数不得省略,如果省略就抛出一个错误。比如绑定一个默认值为返回错误的函数,或者将某个参数的默认值设置为 undefined 代表可以省略

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

2. rest 参数

用于获取函数的多余参数。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

注意,rest 参数之后不能再有其他参数,否则会报错(它只能是最后一个参数),且函数的 length 属性,不包括 rest 参数

3. 严格模式

ES5 开始,函数内部可以设置严格模式。在 ES6 中,规定了只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显示设定严格模式,否则会报错、

原因:函数执行的时候,先执行函数参数,再执行函数体,但是是否进行‘严格模式’是进入函数体之后才知道的,但是参数此时却已经优先执行了

有两种方法,可以避免出现这种情况:
1. 设定全局的严格模式
2. 把函数包在一个无参数的立即执行函数里面

'use strict';

function doSomething(a, b = a) {
  // code
}
const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

4. name 属性

返回对应函数的函数名

function foo() {}
foo.name  // "foo"

// 把具名函数赋值给变量,则依旧返回的是这个具名函数原本的名字
var bar = function baz () {}
bar.name  // "baz"

// Function 构造函数返回的函数实例,name 属性的值为 anonymous
(new Function).name  // "anonymous"

// bind 返回的函数,name 属性值会加上 bound 前缀
function foo () {}
foo.bind({}).name  // "bound foo"

5. 箭头函数

如果箭头函数的代码块部分多于一条语句,就是使用大括号将他们括起来,并且使用 return 语句返回

var sun = (num1, num2) => {return num1 + num2}

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错

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

如果箭头函数只有一行语句,且不需要返回值:

let fn = () => void doesNotReturn()

箭头函数和解构赋值结合使用也是相当的方便

箭头函数使用注意点:
1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象(相当于在箭头函数中,this 对象是固定的,所以经常会出现在定时器中使用)
2. 不可以当做构造函数,即不可以使用 new 命令,否则会报错(构造函数本身不可以是箭头函数,但是普通构造函数内部是可以使用箭头函数的)
3. 不可以使用 arguments 对象,该对象在函数体内不存在,这里可以使用 rest 来代替(如果使用了 arguments,则指向的是外部函数的 arguments)
4. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
5. 由于箭头函数没有自己的 this,所以无法使用 call()、apply()、bind() 这些方法来改变 this 的指向(如果使用了,则这种绑定改变无效)

实际上箭头函数根本没有自己的 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

这里的 t1、t2、t3 都是指向函数 foo 的 this

6. 双冒号运算符

ES6提案,提出了“函数绑定”运算符,用来取代 call、apply、bind 调用。双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定到该对象上面

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

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

7. 尾调用优化

尾调用,即是指某个函数的最后一步是调用另一个函数。(尾调用不一定出现函数尾部,只要是最后一步操作即可)

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

尾调用的意义:
1. 函数调用会在内存形成一个“调用记录”,又称“调用帧”,它会保存调用位置和内部变量等信息。
2. 如果在函数 A 的内部调用函数 B,那么在 A 的调用帧上方,会形成一个 B 的调用帧。知道 B 运行结束,将结果返回 A,函数 B 的调用帧才会消失。
3. 如果多个函数嵌套调用,就会出现多个调用帧,形成了一个“调用栈”
4. 尾调用的意义在于,他是函数执行的最后一步,所以不需要保留外层函数的调用帧(即调用位置、内部变量等信息都不会再用到),直接使用内层函数的调用帧,取代外层函数的调用帧就可以了。
5. 如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存,这就是“尾调用”的意义

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

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

// inner 用到了外层函数的 addOne 的内部变量 one,所以无法进行尾调用优化
function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}

函数调用自身,成为递归。如果尾调用自身,就称为尾递归(重点理解写法)

尾递归优点:递归非常耗费性能,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误。但是尾递归不会出现这种情况

对于尾递归的改写:确保把所有用到的变量,改写成函数的参数

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的,因为正常模式下,函数内部有两个变量,可以跟踪函数的调用栈:(严格模式可以禁止他们)
1. func.arguments:返回调用时函数的参数
2. func.caller:返回调用当前函数的那个函数

function restricted() {
  'use strict';
  restricted.caller;    // 报错
  restricted.arguments; // 报错
}
restricted();

总结:
递递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。

8. 函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号(此前函数定义和调用时,都不允许参数后面出现逗号)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值