4.Function扩展


ES6对Function做一些扩展,引入很多新特征,下面来逐个介绍

1.函数参数默认值

函数定义时,可以指定默认参数,在未传入参数或参数为undefined时,会使用默认的参数

function Obj(id = 107, name = 'jack'){
  this.id = id;
  this.name = 'jack'
}
const obj = new Obj() // {id: 107, name: 'jack'}

function add(x, y = 1){
  return x + y
}
add(1) // 2
add(1, 3) // 4

但是要小心,参数变量是默认声明的,不能用let或const再次声明,因为其中调用时会发生隐式解构,即let [arg1,...,arg2] = [].prototype.slice.call(arguments,0)

function fn(x = 5){
  // 隐式解构:let [x=5] = [].prototype.slice.call(arguments,0)
  let x = 1 // error
}
// 下面这个可以
function fn(x=7) {
  if (x) {
    let x = 1 // 不在一个语句块里
    console.log(x) // 1
  }
  console.log(x) // 7
}

惰性求值

let x = 99
function foo(p = x + 1){
  alert(p)
}
foo() // 100
x = 100
foo() // 101

指定了默认值后,函数的length属性会失真,因为length属性含义为函数预期传入的参数个数

(function (a, b, c = 5) {}).length // 2
(function (a, b = 1, c) {}).length // 1 只记录默认值前面的
2.rest参数

rest参数形式为...args,会将获取函数的多余参数并将其放进数组中

function(a,...b){}
// 等价于
function(){
  let args = [...arguments]
  let a = args.shift()
  let b = args
}
function add(n, ...values) {
  let sum = 0
  for (let val of values) {
    sum += val
  }
  return n * sum
}
add(10, 1, 2, 3) // 60
3.name属性

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

function fn(){ ... }
fn.name // 'fn'

var bar = function foo(){ ... }
bar.name // 'foo'

// 特殊的
var fn = function(){ ... }
fn.name // 'fn'

// bind()返回的函数会加上‘bound’前缀
function fn(){ ... }
fn.bind({}).name // 'bound fn'
☆4.箭头函数

4.1 箭头函数特征

箭头函数是ES6对函数进行的最大扩展

var fn = n => n+1
// 等价于
var fn = function(n){
  return n+1
}

var fn = (a,b)=>a+b

var fn = (a,b)=>{
  return a+b
}

若直接返回一个对象有必要时需要加个括号

var getId = id =>({id: id,name:'jack'})

若箭头函数只有一行语句,且不需要返回值,可以采用void(慎用,会被同事特殊关怀滴~)

var fn = arr => void arr.push(1)
var arr = [3,2]
fn(arr) // [3,2,1]
4.2 箭头函数和参数解构结合
var obj ={
  id: 107,
  name: 'jack',
  age: 18
}
var getIdAndName = ({id, name}) =>name + ':'+ id

getIdAndName(obj) // jack:107
4.3 箭头函数与rest参数结合
let numbersToArr = ...num => num
numbersToArr(1,2,3) // [1,2,3]

let fn = (head,...end) =>[head, end]
fn(1,2,3) //[1,[2,3]]
☆4.4 箭头函数注意点

作用域里无this,this实际是来自于作用域链访问到的非箭头函数作用域中的this
不可以new
不存在arguments对象
不可以yield

详细说明一下箭头函数的this

//  用Babel编译ES6代码来解释this
// ES6
function fn() {
  setTimeout(() => {
    console.log('id:', this.id)
  }, 100)
}

// ES5
function fn() {
  var _this = this
  setTimeout(function () {
    console.log('id:', _this.id)
  }, 100)
}

由于箭头函数作用域中没有自己的this,故call、apply、bind对其无效

function fn() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id)
      };
    };
  };
}

var f = fn.call({id: 1}) // f为return 的箭头函数,这时候this为上文所说的_this,故箭头函数没有真正意义上的'this',也意味着bind、call、apply对它无效!

var t1 = f.call({id: 2})()() // id: 1
var t2 = f().call({id: 3})() // id: 1
var t3 = f()().call({id: 4}) // id: 1

再举个栗子

function fn() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}
var id = 21
fn() // 21
fn.call({ id: 42 }) // 42


function Timer() {
  this.s1 = 0
  this.s2 = 0
  // 箭头函数
  setInterval(() => this.s1++, 1000)
  // 普通函数
  setInterval(function(){this.s2++}, 1000)
}

var timer = new Timer()

setTimeout(() => console.log(timer.s1), 3100) // 3  this绑定timer
setTimeout(() => console.log(timer.s2), 3100) // 0  this帮定window

下面的例子能帮助你很好的理解箭头函数里的this

var obj = {
  name: 'jack',
  sayName:()=>alert(this.name)
}

obj.sayName() // undefined, WTF???


// 冷静详细分析一下
// 箭头函数的this实际上是__this Babel转译
obj = {
  __this:this // 这里的this指向的是window
  sayName:function(){
    alert(__this.name)
  }
}
// 在全局执行obj.sayName()
// 1.创建全局上下文gContext
gContext = {
  VO:{
    obj:{ ... },  // 因为obj是gContext中访问的,this是顺作用域链查询为window
    this: window
  }
  Scope:[VO],
}
// 2.objContext
objContext = {
  VO:{
    __this: this, 
    name: 'jack',
    sayName:function(){ alert(_this.name) } 
  },
  Scope: [gContext.VO,VO]  // 在全局时Scope加入gContext.VO
}
// 3.sayNameReference
sayNameReference = {
  base: obj, 
  name: sayNameReference
}
sayName.[[scope]] = objContext.Scope
// 4.sayName执行时创建sayNameContext
sayNameContext = {
  VO:{
    arguments:{
      length:0
    }
  },
  Scope: [gContext.VO,objContext.VO,VO]
}

// 卧槽,终于分析完了,这时候函数执行alert(__this.name),就会查找_this
// 就会顺着sayNameContext.Scope作用域链查询,由于作用域链是栈结构,后进先出,故从数组屁股开始找 
// sayNameContext.VO --> objContext.VO 找到_this,而_this保存的又是this,继续查找 
// --> gContext.VO 找到this为window,TMD,终于给老子找到了!!!

考你一下有没有认真听课

var button = document.getElementById('press')
button.addEventListener('click', () => {
  this.classList.toggle('on')
});
// babel转译
button = {
  __this: this, // 这里的_this指向什么?
  onClick:function(){
    __this.classList.toggle('on')
  }
}
5.尾调用优化

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

function fn(x){
  return g(x)
}

函数调用会在内存形成一个调用记录,又称调用帧(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧,等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个调用栈(call stack)
而由于尾调用是函数的最后一步,故不需保留外层函数的调用帧,直接访问内层函数的调用帧即可。
这就是尾调用优化,即必要时(闭包除外),删除外部函数的调用帧,只保留内层函数的调用帧。

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

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

// 等同于
g(3)
5.1 尾递归

在尾部调用自身即为尾递归,由于递归需要同时保存成千上百个调用帧,很耗费内存,很容易栈溢出(stack overflow),而尾递归由于只会存在下一个内部函数的调用帧,所以不会栈溢出

function factorial(n) {
  if (n === 1) return 1
  return n * factorial(n - 1) // 每次都还保留n这个变量,故调用帧保存下来
}

factorial(5) // 120

最多(即递归到最后一次函数执行时,调用帧叠加最多)需要保存n个调用帧,因为外层调用帧不能删除优化,还要用到n这个外部变量,空间复杂度O(n),现在做尾调优化

function factorial(n, total) {
  if (n === 1) return total
  return factorial(n - 1, n * total) // 不保留其他变量,调用帧在尾调开始时就结束了
}

factorial(5, 1) // 120

这时候,每次递归执行时只需保存当前的调用帧即可,外部函数的调用帧已经优化删除了,空间复杂度O(1)

5.2 尾递归优化改写

头发还多的话去找找看吧

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值