JS中的函数

373 阅读24分钟

定义

JS中函数的声明方式有以下三种

匿名函数

匿名函数就是声明的时候不要给名字

function(){
    return 1
}

直接运行会报错,因为声明了一个函数但是又不能引用它,相当于一个废话,只能给它一个引用

var fn =  function(){
    return 1
}

JS中函数是对象,对象是存在堆内存中,一般存就只存函数的地址,所以变量fn就存了函数的地址而不是函数本身

如果是

var fn2 = fn

那么fn2和fn存的都是这个函数的内存地址

fn.name   // fn
fn2.name  // fn

匿名函数但是它有name,name就是存函数地址的变量名

具名函数

顾名思义,具名函数就是具有名字的函数

//  fn3是变量
function fn3(){
  return 3
}

var fn5 = function fn4(){

}

console.log(fn4)     // undefined
fn3()                // 3

我们知道fn3的作用域是全局,但是如果我们把一个具名函数赋值给了一个变量,那么它的作用域就仅仅是这个具名函数内部

image.png

那么fn5.name是多少?

=> 是fn4

箭头函数

只有一个参数

var fn6 = i => i+1

多个参数

// 不能这样写
var fn6 = i,j => i+j

// 要把参数括起来
var fn6 = (i,j) => i+j

函数体是多个语句

// 不能这样
 var fn6 = (i,j) => i+j;console.log(1)
 
// 加括号括起来,同时显示的指定return的值,一个语句return可以省略
 var fn6 = (i,j) => {console.log(1);return i+j}

箭头函数同普通函数的区别在于: this

词法作用域

也叫静态作用域

例子:

var global1 = 1
function fn1(param1){
 var local1 = 'local1'
 var local2 = 'local2')
 function fn2(param2){
     var local2 = 'inner local2'
     console.log(local1)
     console.log(local2)
 }

 function fn3(){
     var local2 = 'fn3 local2'
     fn2(local2)
 }
}

当浏览器拿到这段代码不会马上去执行,它会先做一个『抽象词法树』(先分析词法是否正确 )

image.png

如果要在函数fn2中找变量local1,从右边的词法树下发现fn2下面只有local2找不到local1,于是就去它的『父作用域』去找发现fn1下面有个local1,于是就找到了

对应在代码中的作用域

image.png

比如,在fn2中找变量locla2,发现有local2,就不继续往它的上一层找。如果找不到就往的它上一层(fn1)找,如果还是找不到就再往它的上一层找(widow)

思考:调用fn3的时候,发现里面调用了f2,同时把f3里面的local2传给了fn2调用,那么会不会影响判断fn2里面的local2呢?

=> 是不会的!一个函数里面能够访问哪些变量,在做词法分析的时候已经确定了,与函数调用不调用没有任何关系!

注意:词法分析仅仅只适用于分析作用域中的变量是不是这个变量,而不是分析和这个变量的值是不是这个变量的值

例子:

var a = 1

function b(){
   console.log(a)   // 这里的a就是外面的a,因为它自己作用域内没有a
}

但是函数里面a的值是不是就一定也是外面a的值1呢?

=> 不是

var a = 1

function b(){
   console.log(a)   // a=2
}

a = 2
b()

因此,词法作用域只能确定这个a是不是那个a,但是不能确定这个a的值是不是那个a的值,这就是词法作用域,确定的是变量之间的关系

call stack (调用栈)

stack:栈(先进后出),理解这部分有助于理解函数的调用过程。

1.普通调用,依次执行三个函数

function a(){
    console.log('a')
  return 'a'  
}

function b(){
    console.log('b')
    return 'b'
}

function c(){
    console.log('c')
    return 'c'
}

a()
b()
c()

image.png

通过上面的例子我们可以清晰的看出代码执行中栈内存发生的过程

浏览器拿到这段代码,首先不是运行代码,而是发现有哪些变量声明,然后把他们提到前面去,即使把函数的声明放到最后,浏览器还是会把他们提到最前面,这一步做的是词法解析,接着才去执行

看下下过程:

执行a(),记住a()的位置(在19行离开),进入a里面,记住console.log(a )(它也是一个函数)的位置(第2行),进入函数console.log(a )里面执行打印a,然后再回到之前标记的离开console.log(a )的位置,然后return a(从函数里离开),回到之前标记的a()的离开的位置(在19行离开),接着按照同样的思路执行函数b、c

因此call stack,记的是从哪个位置离开的(进入函数内部之前的位置),当进入新的函数把内容执行完了后,return的时候就是return到call stack 最上面记录的位置,也就是执行函数调用时的位置,return后面的值就是回到标记位置时带回来的值,也是执行函数调用后的结果。

2.函数的嵌套调用

function a(){
    console.log('a1')
    b()
    console.log('a2')
  return 'a'  
}
function b(){
    console.log('b1')
    c()
    console.log('b2')
    return 'b'
}
function c(){
    console.log('c')
    return 'c'
}
a()
console.log('end')

3.函数的递归调用

function fab(n){
    console.log('start calc fab '+ n)
    if(n>=3){
        return fab(n-1) + fab(n-2)
    }else{
        return 1
    }
}

fab(5)

求feb5就要求feb4,求feb4就要求feb3,求feb3就要求feb2和feb1,这样就知道了feb3

再求feb(2),就知道了feb4

feb5等于feb4+ feb3,还得求feb3,求feb3就要求feb2和feb1,这样就知道feb3

feb5也就知道了

start calc fab 5

start calc fab 4

start calc fab 3

start calc fab 2

start calc fab 1

start calc fab 2

start calc fab 3

start calc fab 2

start calc fab 1

总结:

call stack 到底在做什么?由于JS是单线程的,因此它在执行一长串代码的时候就要记住当前的环境(比如在作用域内能访问哪些变量)。当看到函数,就要切换环境(因为它要进入函数,函数的代码并不在这里,存在另外一个内存中),在进入这个函数之前它可能会忘记怎么回来,这时候它就在这个地方做了个记号,但是它要做很多记号,因为函数里面还有函数调用,因此它要把每一层的记号放到一个栈的最上面,这个栈叫调用栈(call stack),只要它进入一层调用栈,那么栈里面就会多一个它进入时的记录,接着就进入新的函数去执行函数体的代码,新的函数里如果还有函数调用,它就把进入第二个函数之前的位置放到栈的最上面,每次回来的时候就回到最近一次离开时的位置,就像是『盗梦空间』一样,如果你现在在第三层梦,要想从梦里走出来, 就要先退出第三层,然后是退出第二层梦,再退出第一层梦,先进后出,这个和队列正好相反,先进的第一层梦,但是最后才从第一层梦退出来

this 和 arguments

在进入函数的时候除了要记录进入时的位置(地址),还要记住传给函数的参数有哪些

this 就是 call 的第一个参数!call 的其他参数统称为arguments(伪数组)

this 是call()隐藏的第一个参数,且一般是对象(如果不是对象,也会帮你转成对象;如果不是对象,就显得很没有意义了)

例子:

 function f(){
      console.log(this)
      console.log(arguments)
  }
  f.call() // window  []
  f.call({name:'lee'}) // {name: 'lee'}, []
  f.call({name:'lee'},1) // {name: 'lee'}, [1]
  f.call({name:'lee'},1,2) // {name: 'lee'}, [1,2]

f.call() === f()

当执行f.call()的时候第一件事把调用时候的位置记录到call stack (调用栈)最上面,接着就可以直接进入函数吗?不可以,好比进入别人门之前还要准备礼物(2个东西),第一个东西就是this,如果 f.call()没有写this就是undefined,浏览器会把它改成window,第二个要准备的东西就是arguments的伪数组,如果没有准备,argumnets就是'[]'空数组

注意: 控制台中浏览器运行的代码的结果中的Window实际上是window;

总结:函数在被调用的一瞬间发生的事

  1. 记录下函数被调用的位置,放到调用栈的最上面
  2. 传this,可传可不传,不传就会默认的变成window(在浏览器中)
  3. 传arguments,不传就是空数组,传什么就把什么放到这个数组里

为什么要用f.call()的形式调用函数而不是f()

=> 因为f() 是阉割版的f.call()

如果用f.call() ,就会知道this是什么,如果用f()这种形式调用函数,想知道this是什么就会变得模糊

例如:

 function f(){
      console.log(this)
      console.log(arguments)
  }
  
  f()  // window []
  f(1) // window [1]

f():如果是这样调用函数,就会很疑惑this为什么是window

f(1): :如果是这样调用函数为什么arguments变成了[1]

为什么this必须是对象?

 function f(){
      console.log(this)
      console.log(arguments)
  }
  
 f.call(10,1)   // Number{10}

我们发现即使是指定this为数字10,也会被new Number()了一下,变成了Number对象而不是数字10,为什么会变成这样?

因为this就是函数与对象之间的羁绊

假如这个世界上没有this会怎么样?会不会变得很奇怪?什么情况下才会需要this?

例子:

var person = {
      name: 'lee',
      sayHi: function(person){
          console.log('Hi, I am' + person.name)
      },
      sayBye: function(person){
          console.log('Bye, I am' + person.name)
      },
      say: function(person, word){
          console.log(word + ', I am' + person.name)
      }
  }
 

世界上没有this的代码的写法:这样写就很烦,能不能直接访问"点"前面的东西,这样就不用传person参数,点前面是什么,就把它作为参数传进去?

person.sayHi(person)
person.sayBye(person)
person.say(person, 'How are you')

JS为了满足部分人想偷懒的想法,允许了下面这种写法

person.sayHi()
person.sayBye()
person.say('How are you')

"我没传参数给你,但是定义函数的时候却收到了参数",这就很矛盾了

var person = {
  name: 'lee',
  sayHi: function(person){
      console.log('Hi, I am' + person.name)
  },
  sayBye: function(person){
      console.log('Bye, I am' + person.name)
  },
  say: function(person, word){
      console.log(word + ', I am' + person.name)
  }
}

于是就有了下面的解决办法:

单独的给第一个参数一个关键字this,既然不传person,那函数里面也不要接受person了

这时候函数里面也不能用person.name了,得用一个关键字代替你传进来的参数person,于是就有了this,this相当于一个占位符,我不知道你传了什么参数进来,我只根据点前面的对象来判断this,点前面是person,那么this就是person

var person = {
  name: 'lee',
  sayHi: function(){
      console.log('Hi, I am' + this.name)
  },
  sayBye: function(){
      console.log('Bye, I am' + this.name)
  },
  say: function(word){
      console.log(word + ', I am' + this.name)
  }
}

至此,JS就做到了this可以访问点前面的东西,这就是一个语法糖

person.sayHi()       // 以person为this来调用sayHi
person.sayBye()
person.say('How are you')

上面这种写法的目的不就是为了指定this吗?为什么不像下面这样写,下面这种写法同上面这种写法是一样的

// 没有语法糖的写法
person.sayHi.call(person) // 以person为this来调用sayHi
person.sayBye.call(person)
person.say.call(person, 'How are you')

再来捋一下:

var person = {
 name: 'lee',
 sayHi: function(){
     console.log('Hi, I am' + this.name)
 },
 sayBye: function(){
     console.log('Bye, I am' + this.name)
 },
 say: function(word){
     console.log(word + ', I am' + this.name)
 }
}

person.sayHi() // Hi,i am lee

// window.name='xxx'

var fn = person.sayHi

fn()

这里直接写fn()效果是不是同person.sayHi() 一样呢?

=> 不一样!这时JS分析不出来你想要使用哪个对象来调用sayHi()

因此执行fn()的结果就是Hi I am

注意下面的函数同person和sayHi都没有关系,它就只是一个函数,函数只有输入和输出,没有所属的对象的说法

function(){
  console.log('Hi, I am' + this.name)
}

如果加一句window.name='xxx'

结果 Hi, I am xxx,也就是说fn()里的this就是window,打印出来的就是window.name

如果运行person.sayHi.call({name:'yyy'})

结果: Hi, I am yyy

如果运行person.sayHi.call({name:'zzz'})

结果: Hi, I am zzz

以上就说明

function(){
  console.log('Hi, I am' + this.name)
}

函数是独立的存在,同sayHiperson是没有任何关系,它就是和call有关系,call传什么this进来,函数里面的this就是什么

this 是 call 的第一个参数,this 是参数,所以,只有在调用的时候才能确定

如果不用call的方式调用函数,而是用用阉割版的方式调用:person.sayHi()会不小心的把person当做this

因此只要用call的方式调用函数,就不会有那么多模糊不清的东西了

person.sayHi() 等价于 person.sayHi.call(person)
fn()           等价于 fn.call()

person.sayHi() 第一眼并不知道this是什么

person.sayHi.call(person) 第一眼知道this是person

fn() 第一眼并不知道this是什么

fn.call() 不传第一个参数this就是是undefined,浏览器环境下会变成window

this的意义就是让函数有一个可依托的对象

完整的代码:

var person = {
      name: 'lee',
      sayHi: function(person){
          console.log('Hi, I am' + person.name)
      },
      sayBye: function(person){
          console.log('Bye, I am' + person.name)
      },
      say: function(person, word){
          console.log(word + ', I am' + person.name)
      }
  }
  
  // 世界上没有this的代码的写法
  person.sayHi(person)
  person.sayBye(person)
  person.say(person, 'How are you')

  // 想偷懒,能不能变成下面这样,能不能直接访问"点"前面的东西,这样就不用传person参数,点前面是什么,就把它作为参数传进去?
  
  person.sayHi()
  person.sayBye()
  person.say('How are you')

  // 那么源代码就要改了
  var person = {
      name: 'lee',
      sayHi: function(){
          console.log('Hi, I am' + this.name)
      },
      sayBye: function(){
          console.log('Bye, I am' + this.name)
      },
      say: function(word){
          console.log(word + ', I am' + this.name)
      }
  }
  // 如果你不想吃语法糖
  person.sayHi.call(person)
  person.sayBye.call(person)
  person.say.call(person, 'How are you')

  // 还是回到那句话:this 是 call 的第一个参数
  // this 是参数,所以,只有在调用的时候才能确定
  person.sayHi.call({name:'haha'})  // 这时 sayHi 里面的 this 就不是 person 了
  // this 真的很不靠谱

  // 新手疑惑的两种写法
  var fn = person.sayHi
  person.sayHi() // this === person
  fn()  // this === window

再来看一个例子:

function sum(x,y){
   return x + y
}

sum.call(undefined,1,2)    // 3
sum.call(1,4)              // NaN

call的第一个参数永远是this,如果不想传this就给个undefined/null来占位

sum.call(1,4)这种写法就变成了this是1,4是x,y是undefined,因此结果就是NaN

apply

apply就是call的另外一个版本

function sum(){
   var n = 0
   for(var i=0;i<arguments.length;i++){
      n+ = arguments[i]
   }
   return n
}

sum(1,2,3,4)       // 10

但是有一种场景是没有办法用call来写,求所有参数的和(不知道有多少参数)

var a = [1,2,3,4,5,6,7,8]             // 不知道有多少个
sum.call(undefined,a[0],a[1],a[2]...) // 这里我没法写怎么办

即使知道有多少个,也不能傻傻的写sum.call(undefined,a[0],a[1],a[2]...a[7]) 这时bind就应运而生了,解决参数不固定或者参数太长

var a = [1,2,3,4,5,6,7,8]             
sum.apply(undefined,a)         // 36 

因此不管a有多少项,都能求出和,bind的第二个参数就是数组

总结:

fn.call(asThis, p1,p2) 是函数的正常调用方式

当你不确定参数的个数时,就使用 apply

fn.apply(asThis, params)

bind

call 和 apply 是直接调用函数,而 bind 则是返回一个新函数(并没有调用原来的函数),这个新函数会 call 原来的函数,call 的参数由你指定。

var view = {
   element: 'div',
   bindEvents: function(){
       // 1处的this
      this.element.onclick =  function(){    // 一般来说this是view,意外情况就是用call显示的指定
      // 2 处的this
         this.onClick()                      // 这里的this是包裹它的function被call的时候传的第一个参数
      }//.call()
   },
   onClick: function(){

   }
}

element被点击的时候理应被调用,谁调用?

=> 浏览器调用,浏览器调用这个函数的时候用的是call的形式

 function(){    
         this.onClick()    // div 
  }.call()

浏览器用call()的方式调用了这个函数,那浏览器给call传的第一个参数是什么?

我们查下文档:

target.onclick = functionRef;

functionRef是函数名或函数表达式.该函数接收一个MouseEvent对象作为其唯一参数。在函数中,this将是触发事件的元素。

所以,this为被点击的元素也就是那个div

那问题来了:2处的this是div就不对了,我们想要2处的this是1处的this(也就是view怎么办?)

于是就有人提出在1处的地方使用一个变量_this记住1处的this,再到2处使用这个变量问题不就解决了吗

var view = {
   element: 'div',
   bindEvents: function(){
       var _this = this
       // 1处的this
      this.element.onclick =  function(){   
      // 2 处的this
         _this.onClick()                     
      }
   },
   onClick: function(){
        this.element.addClass('active')
   }
}

既然都是用变量(this是参数),又何必用_this变量存另外一个变量,而且弄一个"假猴"来绕晕自己,为什么不明明白白的指定

var view = {
   element: 'div',
   bindEvents: function(){
       // 1处的this
      this.element.onclick =  function(){   
      // 2 处的this
        view.onClick()    // 这样就看出来了                 
      }
   },
   onClick: function(){
        this.element.addClass('active')
   }
}

当然使用_this来存this也是一种方法(this是无法确定的东西,不到万不得已不要使用this)

官方给了另外的解决办法

var view = {
   element: 'div',
   bindEvents: function(){
       // 此处的2个this在同一作用域下,都是指view
      this.element.onclick =  this.onClick.bind(this)
   },
   onClick: function(){
        this.element.addClass('active')
   }
}

也就是说

this.element.onclick =  this.onClick.bind(this) 

等于

 var _this = this
 this.element.onclick =  function(){   
         _this.onClick.call(_this)  // 把_this当做自己的this                    
 }

捋一下思路:

我们真正想要的是onClick,但是又不得不写一个新的函数,在新的函数里call onClick

function(){   
   _this.onClick.call(_this)                   
 }

如果能这样写就好了:

this.element.onclick =  this.onClick(this) 

但是不能这样写,这样onClick被调用的时候的this就是那个div了

怎么办呢?就是用一个函数把另外我们想要调用的函数包起来,这样在调用的时候就可以指定它里面的this

var view = {
   element: 'div',
   bindEvents: function(){
      var _this = this
      this.element.onclick =  function(){_this.onClick.call(_this) }
   },
   onClick: function(){
        this.element.addClass('active')
   }
}

看下bind做了什么

当我们写了下面的代码

this.element.onclick =  this.onClick.bind(this) //A处

bind实际上返回了一个新的函数并调用了这个返回了的函数(浏览器调用 ),你在A处给bind传了什么参数,那么B处就是什么参数(A处的this就是_this也就是view)

return function(){
    this.onClick.call(this) // B处
}

再看下bind的伪代码

var view = {
   element: 'div',
   bindEvents: function(){
      this.onClick.bind = function(x,y,z){
        var oldFn = this // 也就是外面的this.Click
        return function(){ //浏览器会调用这个函数
            oldFn.call(x,y,z)
        }
      }
      this.element.onclick =  this.onClick.bind(this)
   },
   onClick: function(){
        this.element.addClass('active')
   }
}

总结:

this.element.onclick =  this.onClick.bind(this)
  • this永远是call()的第一个参数
  • bind 是返回一个新函数(并被浏览器自动的去调用),并没有直接调原来的函数onClick,这个新函数会 call 原来的函数onClick,call 的参数由你指定,传给bind的第一个参数会作为onClick.call()的第一个参数,bind的其他参数会作为onClick.call()的其他参数

科里化 & 高阶函数

返回函数的函数

柯里化

在数学中

z = f(x,y) = x + 2y      // z是关于x和y的函数
g = f(x=1)(y) = x + 2y   // g是关于y的函数,因为x已经被确定了

g和z的关系:g是z的偏函数,只是它的一部分

柯里化:把一个函数其中的一个参数固定下来得到一个新的函数

例子:

function sum(x,y){
   return x+y
}
function addOne(y){
   return sum(1,y)   // 把sum的x固定为1
}
function addTwo(y){
   return sum(2,y)   // 把sum的x固定为1
}
addOne(4)   // 5
addTwo(4)   // 6

addOneaddtwo 做的事情就是柯里化

柯里化的意义是什么?

以早期的模板引擎为例(给一个模板字符串,再给一个data,就能返回一个html)

var  Handerber = function(template,data){
   return template.replace('{{name}}',data.name)
}
Handerber('<h1>Hi! I am {{name}}</h1>',{name:'lee'})

// 运行结果: <h1>Hi! I am lee</h1>

这样存在一个问题,如果有10000个不同的name就得重复写10000次模板字符串

Handerber('<h1>Hi! I am {{name}}</h1>',{name:'zhang'}) Handerber('<h1>Hi! I am {{name}}</h1>',{name:'wang'})

这个模板会被经常使用,是否可以做到复用这个模板?

var  Handerber = function(template,data){
   return template.replace('{{name}}',data.name)
}
var template = '<h1>Hi! I am {{name}}</h1>'
Handerber(template,{name:'zhang'})

// 运行结果: <h1>Hi! I am zhang</h1>

函数式编程中不会频繁的声明变量,而是声明一个科里化的函数,它需要被调用2次

第一次:只是返回一个函数

function Handerbar2(template){
   return function(data){
      return template.replace('{{name}}',data.name)
   }
}

var t = Handerbar2('<h1>Hi! I am {{name}}</h1>')
t

t就是一个函数

ƒ (data){
      return template.replace('{{name}}',data.name)
}

第二次: 模板才会和data结合在一起

function Handerbar2(template){
   return function(data){
      return template.replace('{{name}}',data.name)
   }
}

var t = Handerbar2('<h1>Hi! I am {{name}}</h1>')
t({name:'wang'})

// '<h1>Hi! I am wang</h1>'

以上就是科里化在模板引擎中的使用

当然也可以用于惰性求值,第一次的var t = Handerbar2('<h1>Hi! I am {{name}}</h1>') 其实什么也没做

只有在第二次调用的时候才真正的生效t({name:'wang'})

一般用于对模板进行很重的操作,只有在需要的时候才第二次调用,这就叫惰性求值

科里化在惰性求值的时候还是有意义的

总结:

//柯里化之前
function sum(x,y){
  return x+y
}
//柯里化之后
function addOne(y){
  return sum(1, y)
}
//柯里化之前
function Handlebar(template, data){
  return template.replace('{{name}}', data.name)
}
//柯里化之后
function Handlebar(template){
  return function(data){
      return template.replace('{{name}}', data.name)
  }
}

高阶函数

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

a. 接受一个或多个函数作为输入

b. 输出一个函数

c. 不过它常常同时满足两个条件

举例:接受一个或多个函数作为输入

array.sort(function(a,b){a-b})   array.sort.call(array,fn)    
array.forEach(function(a){})     array.forEach.call(array,fn)
array.map(function(){})          array.map.call(array,fn)
array.filter(function(){})       array.filter.call(array,fn)
array.reduce(function(){})       array.reduce.call(array,fn)

备注:

fn为对数组进行处理的函数(对数组所有的操作都放在fn里)

举例:输出一个函数

fn.bind.call(fn,{},123) 

备注:

bind接受一个函数fn作为输入,会返回一个新的函数,这个新的函数会调用fn(浏览器自动去调用),bind的第二个参数会作为fn.call()的第一个参数(也就是指定fn的this),参数1、2、3就是fn接收的其他参数

举例:既输入函数又输出函数

=> bind

高阶函数有什么用呢?

=> 它可以将函数任意的组合

例子:找出数组中所有的偶数并求和

// 循环的方式
array = [1,2,3,4,5,6,7,8]

var sum = 0
for(var i=0;i<array.length;i++){
    if(array[i]%2===0){
        sum += array[i]
    }
}

// 面向对象的方式

array.filter(function(n){n%2===0})  // [2,4,6,8]
     .reduce(function(pre,next){return pre+next},0) // 0作为callback中pre的初始值
     
// 函数式的方式
reduce(filter(function(n){n%2===0}),function(pre,next){return pre+next},0)

例子:找出数组中的所有单数并排序

array = [1,2,3,4,5,6,7,8]

sort(filter(array,function(n){n%2===1}),function(a,b){return a-b})

这些函数在underscore.js或者lodash里都能找到该怎么写

React中每个组件可以是一个纯函数,组件和组件可以相互转换和使用,有了高阶函数就可以把各种函数组合起来,函数接受函数作为参数然后返回一个新的函数,这个新的函数可以作为另一个函数

回调函数(callback)

把回调当做名词:被当做参数的函数就是回调(隐含条件: 回调函数要被call一下,这个被call的过程叫callback回调)

把回调当做动词:调用这个回调(调用这个被当做参数的函数)

注意回调跟异步没有任何关系

例子

array.sort(function(a,b){a-b})

上面的函数function(a,b){a-b}就是一个回调函数,因为它是另一个函数的参数。但是只有当它在被调用时(对数组进行排序)才叫回调callback

被调用(call)同时带回来值(back)才叫callback

例子:

// 1.异步的回调
setTimeout(fn,1000)

// 2. 同步的回调
array.sort(function(a,b){a-b})

有的时候异步的时候会用到回调,有的时候回调会用到异步,异步和回调只是2种现象同时出现的而已,异步和回调之间并不存在任何关系,彼此是独立存在的,就像是男医生和女医生『医生和男女之间并不存在什么关系』

函数里的各种名字

A函数接收函数作为参数,A函数叫高阶函数

A函数返回一个函数,A这个函数也叫高阶函数

A函数被当做参数,A就是回调函数

A函数返回的函数参数要比原函数少一个参数A就叫柯里化函数

返回对象的函数叫构造函数

函数式编程里的概念很多,但是可以用语言清晰的描述出来;但是面向对象里很多概念用文字都很难去定义,比如:封装、继承、多态

构造函数

返回对象的函数就是构造函数 ,一般首字母大写

Number(1)String(2)

自己写个构造函数

function Empty(){
   return {}
}

一般不用自己写 return ,也不用写 “{}”

function Empty(){
   
}
var empty = new Empty()  //相当于Empty.call({})

因为当我们写new语法糖的时候,会自动帮我们写几行代码

function Empty(){
   return {}     // new帮我们写好了
}

如果我们想要加属性怎么办? => 通过this

function Empty(){
   this.name = 'lee'
   return this     // new帮我们写好了
}

this默认是一个空对象

当我们写var empty = new Empty()

相当于Empty.call({})

那么this = {}

也就是说,当我们用new调用构造函数的时候,new语法糖帮我们做了下面的事:

1.自动往构造函数里添加了this并且这个this就是"{}" 2.帮我们自动return这个this

总结:

1.new的去糖形式

// Empty就是构造函数
function Empty(){
   this.name = 'lee'
   return this
}

var empty = Empty.call({})

2.new的语法糖形式

function Empty(){
   this.name = 'lee'
}

var empty = new Empty()

上面2种写法是等价的

箭头函数

箭头函数同普通函数的区别:箭头函数里没有自己的this (JS仿佛在说不要用this了,可能官方都觉得this不好用)

例1

setTimeout(function(){
   console.log(this)
}.bind({name:'lee'}),1000)

// 1秒后输出 {name:'lee'}

image.png

上面标记的函数是"旧函数",旧的函数通过bind调用后返回新的函数fn,新的函数会调用旧的函数,调用旧的函数的时候旧的函数会被call 即function(){console.log(this)}.call({name:'lee'}),call的时候传的第一个参数是{name:'lee'},因此this就是call的第一个参数{name:'lee'}

例 2

setTimeout(function(a){ // 1处
   console.log(this)
   setTimeout(function(a){ // 2处
      console.log(this)
   },1000)
}.bind({name:'lee'}),1000)

// 1秒后输出 {name:'lee'}
// 2秒后输出  window

1处的this同2处的this是不一样的,就好比1处的参数a同2处的参数a是不一样的。2处的this就是call这个函数时传的第一个参数,同a一样也是参数,既然是参数就是动态的,被调用的时候传的什么就是什么,这个函数被调用的时候没有传this,因此这个this就是window

如何让2处的this也是{name:'lee'}呢? => 继续bind

setTimeout(function(a){  // 1处
   console.log(this)
   setTimeout(function(a){ // 2处
      console.log(this)
    }.bind(this),1000)          // 3处
}.bind({name:'lee'}),1000)

3处的this就不用写{name:'lee'},因为此处的this还没有进入到函数里面,3处的this就是1处的this

3处的this:把函数外面的this当做函数里面的this

image.png

如上图所示旧的函数被bind以后会返回新的函数,新的函数被调用的时候,回去调用旧函数(在旧函数后面加call()),call()的第一个参数就是bind的第一个参数,所以这个旧函数被call的时候里面的this就是外面的this,因此bind就充当了函数里外this交换的媒介

接下来用箭头函数来重写就不需要bind了

setTimeout(function(a){  // 1处
   console.log(this)
   setTimeout(()=>console.log(this),1000)     // 2处  
}.bind({name:'lee'}),1000)

由于箭头函数内本身没有自己的this,因此它里面的2处this也就是外面的1处的this

箭头函数中的this就是个变量而已,自身找不到(因为自己没有this)就按照作用域的规则往上找,到它的父作用域找

当我们希望一个函数里面的this和它外面的this一样的时候,就用箭头函数,每次进入一个函数的时候,第一:会将位置记录到call stack里,不用指定this

function本身一定有this,每次进入一个函数的时候,第一:会将位置记录到call stack里,第二:一定要强制的确定一个this

看一个奇怪的例子:

image.png

即使是用call的形式强制的指定this,也是被拒绝的,箭头函数里面的this就是外面的this,也就是window。箭头函数内的this就相当于一个变量,而function中的this是一个参数,它的值是要根据函数被call(asThis,asArg)的时候传的值来确定的。变量要遵循的是词法作用域,词法作用域的规则是:先从自己的作用域找,找不到就往它的父作用域找

拓展:函数参数的作用域问题

参数形成单独作用域

```
let x = 1;
function fun(x, y = x) {
  console.log(y);
}
fun(2);
```
  • 参数 y 的默认值等于变量 x
  • 调用函数 fun 时,参数形成一个单独的作用域
  • 在这个作用域中,默认值变量指向第一个参数 x,而不是全局环境的 x

有默认值的形参创建的作用域也会沿着作用域链查找变量

function fun(y = x) {
  let x = 2;
  console.log(y);
}
fun(); // ReferenceError: x is not defined
  • 调用函数 fun 时,参数 y=x 形成一个单独的作用域
  • 在这个作用域里,没有定义 x,所以沿着作用域链在全局寻找变量 x
  • 由于全局环境中也没有定义变量 x,所以会报错
  • 函数调用时,函数体内部的局部变量 x 影响不到参数默认值变量 x

避免暂时性死区(TDZ

let x = 1;
function fun(x = x) {}
fun(); // Uncaught ReferenceError: x is not defined

  • 参数 x = x 形成一个单独作用域
  • 在这个作用域中,执行的是 let x = x,这就是形成暂时性死区的原因

如果参数的默认值是一个函数,该函数的作用域也遵守上面的规则

let foo = "outer";
function bar(func = () => foo) {
  let foo = "inner";
  console.log(func());
}
bar(); // outer
  • 函数 bar 的参数 func 的默认值是一个匿名函数,返回值为变量 foo
  • 形参形成的单独作用域里,并没有定义变量 foo,所以指向外层的全局变量 foo

函数参数默认值的作用域问题