深入了解「函数」

529 阅读5分钟

前言

你是否对函数的理解有些似懂非懂,那么现在就让我们一起来消灭它吧

定义

匿名函数

function (){
  return 1
}

上面的代码直接运行的话会报错声明了一个函数却没有引用,就会报错 Function statements require a function name函数语句需要一个函数名

// 它是匿名函数但是它有 name
let fn = function (){
  return 1
}

以上代码,表示函数在里面申请了一块内存,将地址给了,fn 记录了这个栈中函数的地址

let fn2 = fn 

以上代码表示的是,在中把函数的地址复制了一份,fn2 记录了这个栈中复制的那个地址
那么 fn 和 fn2 都指向了那个函数,故下图打印出来的都是 fn ,匿名函数的 name 它默认和你变量的名字是一样的

截屏2021-12-25 下午2.15.30.png

具名函数

具有名字的函数

// fn3 这个名字,它是一个变量, 它的作用域是所有地方都可以访问到 fn3 
function fn3(){
  return 3
}

// 举例
// 这个的作用域就变了**function fn4(){}**这个就是它的作用域了
let fn5 = function fn4(){}

// console.log(fn4)  超过这个部分就访问不到 fn4 了

箭头函数

// 只有一个参数
const $ = s => document.querySelector(s) 
$('div')

// 两个参数
let fn6 = (x, y) => x+y

// 如果后面有多句,就要用花括号把它们阔起来,同时把 return 的值说清楚
let fn7 = (x, y) => {
  console.log(1)
  return x+y  // 只有一句话情况,就不需要写 return 了
}

词法作用域(静态作用域)

 var global1 = 1
 function fn1(param1){
     var local1 = 'local1'
     var local2 = 'local2')
     function fn2(param2){
             var local2 = 'inner local2'  // 第六行,这个是 local2
         console.log(local1)
         console.log(local2)
     }

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

上面代码分析 截屏2021-12-25 下午2.53.32.png 当我们在 fn2 里面去打印 local1, 它打印的 local1 是根据 词法树来确定的
fn2 里面没有 local1,那我就从 fn1 里去找,于是就找到 local1
通过词法树分析就能知道 local2 是我上面的第六行的 local2 ,与执行循序无关

注意⚠️:词法树只是用来分析这个打印出来的变量 local2 是不是这个作用域内部的那个变量 local2 ,它与值无关,分析的是语义,它们的值是否相等不是我所关心的

面试题

var a = 1
function b(){
  console.log(a)  // 这个 a 是不是我外面的 a? 通过词法树分析 肯定是的,但是 a 的值在任何情况下一定打印出来的是 1 吗?
}

假如我改成以下的代码

var a = 1
function b(){
  console.log(a)
}
a = 2
b()  // 打印出来的是 2

词法作用域,确定的是两个变量的关系,它们的值并不关心

如需深入了解请阅读以下文章

Call Stack

嵌套调用

call-back2.gif

递归

call-back4.gif

以上两个例子 后进先出 ,完整的展示了函数调用栈的执行时机

  • JS引擎在调用一个函数前
  • 需要把函数所在的环境 push 到一个数组里
  • 这个数组叫做调用栈
  • 等函数执行完了,就会把环境 pop 出来
  • 然后 return 到之前的环境,继续执行后续代码

this & arguments

this & arguments,每个函数都有除了箭头函数,arguments 是包含所有参数的伪数组

  1. this 是隐藏的的第一参数,且必须是对象
  function f(){
      console.log(this)
      console.log(arguments)
  }
  // 如果你传的是 null 或 undefined,那么默认打印 window(严格模式下默认是 undefined)
  f.call() // window
  f.call({name:'hone'}) // {name: 'hone'}, []
  f.call({name:'hone'},1) // {name: 'hone'}, [1]
  f.call({name:'hone'},1,2) // {name: 'hone'}, [1,2]

截屏2021-12-25 下午4.47.49.png 那为何这样,因为 this 就是函数与对象之间的羁绊
假如没有 this

let persion = {
  name: 'hone',
  'sayHi':sayHi
}
let sayHI = function (){
  console.log('你好,我叫' + person.name)
}

分析

  • 如果 persion 改名, sayHi 函数就失效了
  • sayHi 函数甚至有可能在另一个文件里面
  • 它具有强耦合性,所以我们不希望 sayHi 函数里出现 persion 引用
class Persion {
  constructor (name){
    this.name = name  // 这里的 this 是 new 强制指定的
  }
  sayHi(){
    console.log('???')
  }
}

分析

  • 这里只有类,还没有创建对象,故不可能获取对象的引用
  • 那么如何拿到对象的 name?,需要一种办法拿到,用参数 以下为假设
    对象
let persion = {
  name: 'hone',
  sayHi: function(persion){
      console.log('Hi, I am ' + persion.name)
  }
}
persion.sayHi(persion)

class Persion{
  constructor(name){
    this.name = name
  }
  sayHi(p){
    console.log('你好,我叫'+ p.name)
  }
}

Python 采用了这种方法

this 是为了解决不用传值,我也知道当前对象传的是啥,那个 this 就是函数前面 . 那个东西,没有 . 那就是 window

// 以下两者等价
persion.sayHi()
person.sayHi.call(person)  // 易于告诉代码的阅读者,这个 this 是啥

aplay

let array = [1, 2, 3, 4, 5]
function sum(){
  let n = 0
  for(let i = 0; n < arguments.length; i++){
    n += arguments[i]
  }
}

// 如果用 call 传值的话 当 arguments 不确定多少时,无法写出来
// 于是就发明了 apply 
sum.apply(undefined, array)

call 和 apply 几乎一模一样,当不确定参数个数时就用 apply,就算知道长度也可以用 apply

bind

一句话介绍bind

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

模拟

// 伪代码1
var view = {
  element: ${'#div'),
  bindEvents: function(){
    // 在调用这个绑定事件的时候 view.bindEvevts,一般来说这个 this 肯定是这个 view,但是也不能肯定,因为用户可以用 call 调用,所以无法确定
    this.element.onclick = function(){
      this.onClick() // 这个函数是如何被调用的?不知道    
    }
  },
  onClick: function(){
  }
}

这个 element 被点击的时候,这个函数理应被调用,被浏览器调用,浏览器用的是 call ,那浏览器 call 的第一个参数是什么? 看文档

截屏2021-12-25 下午7.30.23.png 文档告诉我,我的 call 的第一个参数,是触发事件的元素
那么浏览器在调用 function(){ this.onClick() } ,它会 call 一下,那 call 给我们的第一个参数是被点击的那个元素,也就是上面这个伪代码的 div,this.onClick()这个 this 就是 div,那该如何? 我没有办法拿到 this

// 伪代码2
// 用这种???
var view = {
  element: ${'#div'),
  var _this = this
  bindEvents: function(){
    this.element.onclick = function(){
      _this.onClick()  //  _this.onClick.call(_this)  
    }
  },
  onClick: function(){
  }
}

// 以上代码直接写成,不就行了
var view = {
  element: ${'#div'),
  bindEvents: function(){
    this.element.onclick = function(){
      view.onClick()   
    }
  },
  onClick: function(){
  }
}

不推荐伪造 this 的这种写法,于是有了另一种写法

// 伪代码3
var view = {
  element: ${'#div'),
  bindEvents: function(){
    this.element.onclick = this.onClick().bind(this) // 这个意思就是上面伪代码2 的意思
  },
  onClick: function(){
    this.element.addClass('active')
  }
}

你明明要用 onClick 但是你不得不写一个新的函数,在新的函数里面去 call 这个 onClick

this.element.onclick = this.onClick().bind(this) // 现在的 this 和外面的 this是一样的
// 相当于
function(){
  this.onclick.call(this)
}

bind 的作用就是往 onClick 后面加一个 call,在调这个 bind 新的函数出来的时候加

// 模拟 bind
this.onClick.bind = function(x, y, z){
  var oldFn = this // 这个 this 是外面的 this.onClick
  return function(){
    oldFn.call(x, y, z) // 你给 bind 传的是啥,它就全部复制到这里
  }
}

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

当用户点击这个 div ,浏览器就会调用 function(){ oldFn.call(x, y, z) },这个函数会调用之前的函数 this.onClick 然后在后面加个 call ,call 的参数就是 this

看完还是有点晕= = ,这里推荐 冴羽 的一篇 JavaScript 深入之 bind 实现

// 以下代码为 冴羽 博客里的,链接在上方已给出
var foo = {
    value: 1
}

function bar() {
    console.log(this.value);
}

// 返回了一个新函数bar.bind(foo) 名为 bindFoo
var bindFoo = bar.bind(foo)   // bar.bind(foo)()

// 新函数会 call 原来的函数
bindFoo() // 1  相当于  bar.call(foo)  这个里面的参数由我指定

使用 .bind 可以让 this 不被改变

function f1(p1, p2){
  console.log(this,p1,p2)
}
let f2 = f1.bind({name: 'hone'})
// 那么 f2 就是 f1 绑定了 this 之后的函数
f2() // 等价于 f1.call({name: 'hone'})

.bind 还可以绑定其他参数

let f3 = f1.bind({name: 'hone'}, 'hi')
f3() // 等价于 f1.call({name: 'hone'}, 'hi')

函数柯里化

什么叫柯里化

关于x 和 y 的函数

// z 根据 x, y来动
z = f(x, y) = x + 2y 
// g 根据 y 来动
g = f(x=1)(y) = 1 + 2y   

g 叫做 z 的偏(Partial)函数, g 只是 z 的一部分

柯里化:把一个函数其中一个参数固定下来,得到一个新的函数,将函数的个数变少,输出一个新的函数

🌰 例子

// 柯里化之前
function sum(x, y){
  return x+y
}

// 柯里化之后
function addOne(y){
  return sum(1, y) 
}

addOne(4) // 5

function addTwo(){
  return sum(2, y)   
}

addTwo(4) // 6

addOne 就是把 sum 的 x 固定成 1,然后再加上后面的数

柯里化 可以用来做 惰性求值,就是你在调我第一个函数的时候,其实我啥也没做,在最后真正调的时候我才去做

推荐文章

  1. www.yinwang.org/blog-cn/201…
  2. zhuanlan.zhihu.com/p/31271179

高阶函数

认识

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

  1. 接受一个或多个函数作为输入:forEach sort map filter reduce
  2. 输出一个函数:lodash.curry
  3. 不过它也可以同时满足两个条件:Function.prototype.bind

作用:可以将函数任意的组合

🌰 举例

let sum = 0
let arr = [1, 2, 3, 4, 5, 6]
for(let i = 0; i < arr.length; i++){
  if(arr[i] % 2 === 0){
    sum += array[i]
  }
}

// 以上代码可以写成
arr.filter(n => n % 2 === 0)
   .reduce((sum, item) => {return sum +item}, 0)

let arr1 = [1, 4, 6, 2, 3, 8, 7, 9, 5]
arr1.filter( n => n % 2 === 1)
    .sort((a, b) => {return a-b},0)

回调和构造函数

回调

名词形式:被当做参数的函数就是回调
动词形式:调用这个回调
回调和异步没有任何关系
图中所示为同步回调 截屏2021-12-25 下午10.09.22.png

setTimeout(fn,1000) 这个就是异步回调

构造函数

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

箭头函数

👉🏻 链接