博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ES6学习笔记之Function
阅读量:6701 次
发布时间:2019-06-25

本文共 9056 字,大约阅读时间需要 30 分钟。

函数的扩展

rest 参数

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

const foo = (...values) = {  console.log(values)}foo(1,2,3)  // [1,2,3]复制代码

arguments 对象是使用function声明函数时自动生成的对象, 包含了函数的参数,但结构复杂。在箭头函数中被rest代替,不可使用,否则报错。

// arguments变量的写法function f1() {  console.log(arguments)}const f11 = () => {  console.log(arguments)}// rest参数的写法const f2 = (...numbers) => {  console.log(numbers)};复制代码

注意:rest 参数只能是最后一个参数,否则会报错。

const foo = (a, ...rest, b) => {}//  Uncaught SyntaxError: Rest parameter must be last formal parameter复制代码

函数参数的默认值

特点

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

function log(x, y = 'World') {  console.log(x, y);}log('Hello') // Hello Worldlog('Hello', undefined) // Hello Worldlog('Hello', '') // Hellolog('Hello', null) // Hello nulllog('Hello', 'China') // Hello China复制代码

这种写法有两个好处:

  1. 就算不用阅读文档也可以直观的看出哪些参数可以忽略;
  2. 有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
  • 只有在这个参数没有设置或者设置为undefined的时候,默认值才会生效,跟解构赋值很相近。所以定义默认值的参数,最好是函数的尾参数。不然会出现一下情况:
function f(x = 1, y) {  return [x, y];}f() // [1, undefined]f(2) // [2, undefined])f(, 1) // 报错f(undefined, 1) // [1, 1]复制代码

另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

let x = 99;function foo(p = x + 1) {  console.log(p);}foo() // 100x = 100;foo() // 101复制代码

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

当参数是一个对象时:

function foo({x, y = 5}) {  console.log(x, y)}foo({}) // undefined 5foo({x: 1}) // 1 5foo({x: 1, y: 2}) // 1 2foo() // TypeError: Cannot read property 'x' of undefined复制代码

如果调用函数时没有给参数就会报错.

通过提供函数参数的默认值,就可以避免这种情况。

function foo({x, y = 5} = {}) {  console.log(x, y);}foo() // undefined 5复制代码

要注意函数解构设置默认值的写。

以下有两种写法:

// 写法一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]复制代码

作用域

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

let x = 1;let y = 3;function f1(x, y = x) {  console.log(x, 'x')  x = 3  console.log(y, 'y');}f1(2) // 2复制代码
  1. 函数头内 xy 的值不受外界影响。函数体内的值也优先为头部的值
  2. y 在函数头就已经赋值,所以在运行时即便x 改变,也不会受影响。

严格模式

从 ES5 开始,函数内部可以设定为严格模式。

function foo(a, b) {  'use strict';  // code}复制代码

然而 ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符(rest),那么函数内部就不能显式设定为严格模式,否则会报错。

// 报错function foo(a, b = a) {  'use strict';  // code}// 报错const foo = function ({a, b}) {  'use strict';  // code};// 报错const foo = (...a) => {  'use strict';  // code};const obj = {  // 报错  foo({a, b}) {    'use strict';    // code  }};复制代码

两种方法可以规避这种限制:

1.设定全局性的严格模式

'use strict';function foo(a, b = a) {  // code}复制代码

2.把函数包在一个无参数的立即执行函数里面。

const foo = (function () {  'use strict';  return function(value = 42) {    return value;  };}());复制代码

箭头函数

基本用法

特点:

  1. 使用=>来定义函数
  2. 参数数量有且只有一个的时候,可以不使用括号
  3. 如果函数体内直接返回某个值的时候可以不用写大括号和return,就代表返回了这个值。(返回对象时需要用圆括号括起来,否则会被识别成代码块。)
const f1 = (x) => {  return x + 1}const f2 = x => x + 1const f3 = x => ({ x })function f4 (x) {  return  { x }}复制代码

嵌套的箭头函数

箭头函数内部,还可以再使用箭头函数。下面是一个 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]复制代码

箭头函数有几个使用注意点

  1. 函数体内的this对象,就是定义时所在的对象(他的外层对象),而不是使用时所在的对象。
  2. 不可以当作构造函数,因我 他没有自己的this对象。
  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
var a=11function f1(){  this.a=22;  let b=function(){    console.log(this.a);  };  b();}function f2(){  this.a=22;  let b=()=>{console.log(this.a)}  b();}var x=new f1();   // 11var y=new f2();   // 22复制代码

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

所以上面代码相当于这样:

function f2(){  this.a=22;  let _this = this  let b=()=>{console.log(_this.a)}  b();}复制代码

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

const foo () =>{  console.log(arguments)}foo() // Uncaught ReferenceError: arguments is not definedfunction foo () {  return () => {    console.log(arguments)  }}foo()()复制代码

另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

(function() {  return [    (function () { return this.x }).bind({ x: 'inner' })()  ];}).call({ x: 'outer' });// ['inner'](function() {  return [    (() => this.x).bind({ x: 'inner' })()  ];}).call({ x: 'outer' });// ['outer']复制代码

函数的 length 属性 和 name 属性

函数的length 属性将返回该函数预期传入的参数个数。

某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest 参数也不会计入length属性。

如果 默认参数不是尾参数,那么默认参数后面的参数也不计入 length

const f1 = (a,b) => {}f1.length // 2const f2 = (a, ...rest) => {}f2.length // 1const f3 = (a, b=1, c) => {}f3.length // 1复制代码

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

function foo() {}foo.name // "foo"复制代码

1.如果将一个匿名函数赋值给一个变量.

var f = function () {};// ES5f.name // ""// ES6f.name // "f"复制代码

2.如果将一个具名函数赋值给一个变量. 都返回函数原本的名字。

const bar = function baz() {};// ES5bar.name // "baz"// ES6bar.name // "baz"复制代码

3.Function构造函数返回的函数实例,name属性的值为anonymous

const foo = new Functionfoo.name  //   "anonymous"复制代码

4.bind返回的函数,name属性值会加上bound前缀。

function foo() {};foo.bind({}).name // "bound foo"(function(){}).bind({}).name // "bound "复制代码

尾调用

简介

尾调用(Tail Call)是函数式编程的一个重要概念,是指某个函数的最后一步是调用另一个函数。

function f(x){  return g(x);}复制代码

以下三种情况,都不属于尾调用:

const g = (x) => {}// 情况一function f(x){  let y = g(x);  return y;}// 情况二function f(x){  return g(x) + 1;}// 情况三function f(x){  g(x);}//  等同于function f(x){  g(x);  return  undefined}复制代码

尾调用优化

调用帧:函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。

调用栈: 如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。

等到B运行结束,将结果返回到A,B的调用帧才会消失。
如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。
所有的调用帧,就形成一个“调用栈”(call stack)。

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

function g (x) {  console.log(x)}function f() {  let m = 1;  let n = 2;  return g(m + n);}f();// 等同于function f() {  return g(3);}f();// 等同于g(3);复制代码

“尾调用优化”的意义: 如果所有函数都是尾调用,那么就可以做到每次执行时,调用帧只有一项,这将大大节省内存.

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

function addOne(a){  var one = 1;  function inner(b){    return b + one;   // 这里还要使用 外层函数的 one  }  return inner(a);}复制代码

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但是如果使用尾调用优化,使每次只存在一个调用帧,就不会发生“栈溢出”错误。

function f1(n) {  if (n === 1) return 1;  return n * f1(n - 1);}f1(5) // 120// 尾递归function f2(n, total) {  if (n === 1) return total;  console.log(n - 1, n * total)  return f2(n - 1, n * total);}f2(5,1) // 120复制代码

这个函数更具有数学描述性:

如果输入值是1 => 当前计算数1 * 上一次计算的积total 如果输入值是x => 当前计算数x * 上一次计算的积total 计算f2(5, 1)的时候,其过程是这样的:

  • f2(5, 1)
  • f2(4, 5)
  • f2(3, 20)
  • f2(2, 60)
  • f2(1, 120)
  • 120

整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的sum(5,0)。

这能有效的防止堆栈溢出。

普通递归改写需要在最后一步调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。这样做的缺点就是不太直观。很难看出这些参数是干什么的。 有两个方法可以解决这问题: 1. 内部要调的参数给个默认值

function f2(n, total=1) {  if (n === 1) return total;  return f2(n - 1, n * total);}f2(5) // 120复制代码

2. 用另外一个函数来返回这个函数。

function f2(n, total) {  if (n === 1) return total;  return f2(n - 1, n * total);}function f3 (n) {  return  f2(n, 1)}f3(5) // 120复制代码

严格模式

以上只是尾调用优化的写法,但是并没有实现真正的优化。

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

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

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

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

尾递归优化的实现

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

那就只有自己是实现递归优化了。

原理:尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。

下面是一个直接写的尾递归:

function sum(x, y) {  if (y > 0) {    return sum(x + 1, y - 1);  } else {    return x;  }}sum(1, 1000)  // 1001sum(1, 100000)// Uncaught RangeError: Maximum call stack size exceeded(…)复制代码

一旦指定sum递归 100000 次,就会报错,提示超出调用栈的最大次数。

有两种方法避免: 1.使用蹦床函数(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函数的每次执行,都会返回自身的另一个版本。trampoline(sum(1, 100000))// 100001复制代码

2.蹦床函数并不是真正的尾递归优化,下面的实现才是

function tco(f) {  var value;  var active = false;  var accumulated = [];  // console.log(1)  return function accumulator() {      // console.log(2, arguments)    accumulated.push(arguments);    if (!active) {      active = true;      while (accumulated.length) {        // console.log(3)        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, 10000)// 100001复制代码

转载地址:http://rcwlo.baihongyu.com/

你可能感兴趣的文章
[svc]visio绘制模具
查看>>
springmvc入门基础之注解和参数传递
查看>>
iOS10 CoreData新特性
查看>>
absolute绝对定位的非绝对定位用法
查看>>
小白全栈
查看>>
struts2中struts.xml配置文件详解【未整理】
查看>>
基于Linux的智能家居的设计(5)
查看>>
身份识别协议枚举工具ident-user-enum
查看>>
正则则表达式大全(收集)
查看>>
手把手教你完成第一个vivado项目
查看>>
webpack-Module Resolution(模块解析)
查看>>
linux日志logger命令详解
查看>>
SQL SERVER 如果判断text类型数据不为空
查看>>
mongodb安全权限设定
查看>>
glib 散列表
查看>>
javascript模拟C# Stringbuilder
查看>>
解析Linux系统关于用户权限、组
查看>>
Android 如何判断一个应用在运行
查看>>
分组背包题目
查看>>
获取GridView TemplateField的数据
查看>>