JavaScript - 函数

72 阅读8分钟

定义一个函数

JavaScript 函数是执行特定任务的代码块,会在某些代码调用它时被执行。其本身是一种对象。

一个 JavaScript 函数用function关键字定义,后面跟着函数名和圆括号。

具名函数

function 函数名(形式参数1, 形式参数2){
  语句
  return 返回值
}

例如:

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

匿名函数

具名函数去掉函数名就是匿名函数,如下所示:

let a = function(x, y){ 
  return x+y 
}

也叫函数表达式,(函数没有名字,变量a容纳了函数的地址)

箭头函数

箭头左边是输入的函数,右边是输出的函数

let f1 = x => x*x 
let f2 = (x,y) => x+y   // 圆括号不能省
let f3 = (x,y) => {return x+y}   // 花括号不能省
let f4 = (x,y) => ({name:x, age: y})   //直接返回对象会出错,需要加个圆括号

构造函数

let f = new Function('x', 'y', 'return x+y')

很少有人用构造函数来定义一个函数,但是能让知道函数是谁构造的

所有函数都是 Function 构造出来的,包括 Object、Array、Function 也是

函数调用

函数在被调用时才会执行,函数自身是不会执行的。如下

let fn = () => console.log('hi')
let fn2 = fn
fn2()

想要fn2执行,第三行如果输入函数自身fn2,函数不会有任何结果,因为 fn 没有执行(被调用)。输入fn()才会打印出 hi。有圆括号才是调用。

  • 对于上述代码需要明确的是:
    • fn 保存了匿名函数的地址
    • 这个地址被复制给了 fn2
    • fn2() 调用了匿名函数
    • fnfn2 都是匿名函数的引用而已
    • 真正的函数既不是 fn 也不是 fn2

函数要素

每个函数都有这些要素

  • 调用时机
  • 作用域
  • 闭包
  • 形式参数
  • 返回值
  • 调用栈
  • 函数提升
  • arguments(除了箭头函数)
  • this(除了箭头函数)

调用时机

JavaScript - 函数的执行时机 - 掘金 (juejin.cn)

作用域

每个函数都会默认创建一个作用域,作用域是指变量可以生效的范围。

  • 作用域可分为全局作用域和局部作用域,相应的变量也可分为全局变量和局部变量
  • 在全局作用域中定义的变量可以在任何地方使用,在局部作用域中定义的变量只能在这个局部作用域内部使用。
  • 在顶级作用域声明的变量是全局变量,window 的属性是全局变量,其他都是局部变量

例如:

function fn(){
  let a = 1
}
fn()
console.log(a)

这里的 a 是局部变量,只作用于函数fn里面,console.log(a)是打印不出 a 的

let a = 1
function fn(){
  console.log(a)
}
fn()
console.log(a)

这里的 a 是全局变量,可以在任何地方使用

注意:

  • 即使window的属性写在某个函数里面也是全局变量

image.png

  • Object / parseInt 可以直接用是因为他们在window上

image.png

函数可嵌套,作用域也可嵌套

例如:

function f1(){
  let a = 1
  
  function f2(){
    let a = 2
    console.log(a)
  }

  console.log(a)
  a = 3
  f2()
}
f1()

会打印出 12

规则:

  • 如果多个作用域有同名变量 a
  • 那么查找 a 的声明时,就向上取最近的作用域,简称「就近原则」
  • 查找 a 的过程与函数执行无关,但 a 的值与函数执行有关

注:和函数执行没有关系的作用域叫做静态作用域,也叫词法作用域

再例如:

function f1(){
  let a = 1
  function f2(){
    let a = 2
    function f3(){
      console.log(a)
    }
    a = 22
    f3()
  }
  console.log(a)
  a = 100
  f2()
}
f1()

会打印出 122

闭包

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,如果一个函数用到了外部的变量,那么这个函数加这个变量就叫做闭包。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

例如:

function f1(){
  let a = 1
  function f2(){
    let a = 2
    function f3(){
      console.log(a)
    }
    a = 22
    f3()
  }
  console.log(a)
  a = 100
  f2()
}
f1()

函数 f3let a = 2中的 a 组成了闭包

形式参数

形式参数的意思就是非实际参数,可认为是变量声明

例如:

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

add(1,2)

其中 xy 就是形式参数,因为并不是实际的参数。调用 add 时,12 是实际参数,会被赋值给 xy

上面代码近似等价于下面代码

function add(){
  var x = arguments[0]
  var y = arguments[1]
  return x+y
}

add(1,2)

形式参数只是给参数取名字,可多可少

例如:

image.png

返回值

返回值是指函数执行完毕后返回的值。

  • 每个函数都有返回值,这个返回值可以通过关键字 return 进行设置
  • 函数执行完了后才会返回,即只有执行了以后才会有返回值
  • 只有函数有返回值
  • 若未显式地设置函数的返回值,那函数会默认返回一个undefined
  • 在函数中,一旦执行完成return语句,那么整个函数就结束了,后续语句将不再执行

例如:

function hi(){ console.log('hi') }

hi()

没写 return,所以返回值是 undefined

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

hi()

返回值为 console.log('hi') 的值,console.log('hi') 的值是 undefined

image.png

调用栈

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

image.png

递归函数的调用栈

例如写一个4的阶乘

function f(n){
  return n !== 1 ? n* f(n-1) : 1
}

fn(4)

对于fn(4)我们可以理解为

f(4)
= 4 * f(3)
= 4 * (3 * f(2))
= 4 * (3 * (2 * f(1)))
= 4 * (3 * (2 * (1)))
= 4 * (3 * (2))
= 4 * (6)
= 24

先递进再回归,它的调用栈如下图所示(压4次,弹4次)

image.png

调用栈最多有多长?

  • Chrome 12578
  • Firefox 26773
  • Node 12536

爆栈:如果调用栈中压入的帧过多,程序就会崩溃

函数提升

函数提升指不管把具名函数声明在哪里,它都会跑到第一行

例如:

(先使用,后声明。但是没有BUG)

image.png

注意:

let fn = function(){}

这是赋值,右边的匿名函数声明不会提升

arguments

arguments 是一个包含所有普通参数的伪数组。

  • 每个函数都有,除了箭头函数
  • 每次调用函数时,都会对应产生一个 arguments
  • 我们应该尽量不对 arguments 内的元素进行修改,修改 arguments 会让代码变得令人疑惑

如何传 arguments

  • 调用 fn 即可传 arguments
  • fn(1,2,3) 那么 arguments 就是 [1,2,3] 伪数组

image.png

this

对于 this 可以理解为JS 通过 this 做到让函数获取对象的引用。谁调用函数,谁就是this。除了箭头函数,任何函数都有 this

关于 this 的规则:

  • 如果不给任何的条件,那么 this 默认指向 window
  • 如果传一个对象, this 就是这个对象
  • 如果传的不是对象,JS会把传的东西封装成对象;如果加上use strict。那么传的this是什么就是什么
  • 如果传undefined, 那么 this 是 window

如何传 this:

  • fn.call() 传 this

例如:

image.png

上面说过如果传的不是对象,JS会把传的东西封装成对象;但是如果加上use strict。那么传的this是什么就是什么

image.png

关于 call

目前可以用 fn.call(xxx, 1,2,3) 传 this 和 arguments,而且 xxx 会被自动转化成对象

(this 是第一个参数; arguments是后面的参数)

image.png

this 的调用

上面说过可以用call来传 this ,在传输时要注意需要自己手动把“参数”传到函数里,作为 this ;传什么,this 就是什么。一般不要空写,不建议使 this 自动把“参数”传到函数里。

例一:

let person = {
  name: 'frank',
  sayHi(){
    console.log(`你好,我叫` + this.name)
  }
}

person.sayHi.call(person)

person.sayHi.call(person)最好不要写成person.sayHi.call()

例二:

image.png

例三:

image.png

为什么要多写一个 undefined ?

  • 因为第一个参数要作为 this
  • 但是代码里没有用 this
  • 所以只能用 undefined 占位
  • 其实用 null 也可以

this 的两种使用方法

  • 隐式传递
    • fn(1,2) 等价于fn.call(undefined, 1, 2)
    • obj.child.fn(1)等价obj.child.fn.call(obj.child, 1)
      • (如果是对象的属性的函数调用,this是函数前面的这部分)
  • 显示传递
    • fn.call(undefined, 1,2)
    • fn.apply(undefined, [1,2])

绑定 this

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

例如:

image.png f2 就是 f1 绑定了 this 之后的新函数;f2()等价于 f1.call({name:'frank'})

  • .bind 还可以绑定其他参数

例如:

image.png 在这里{name:'frank'}是隐藏参数(或者说this), 'hi'p1

箭头函数的 this 和 arguments

箭头函数没有 arguments 和 this ,可以理解为新的语法里用箭头函数干掉了 arguments 和 this

  • 箭头函数的this就是一个普通变量。里面的 this 就是外面的 this

image.png

  • 箭头函数没有 arguments

image.png

this总结

(假设 fn 是一个普通函数, arrow 是一个箭头函数)

  • 在 new fn() 调用中,fn 里的 this 指向新生成的对象,这是 new 决定的
  • 在 fn() 调用中, this 默认指向 window,这是浏览器决定的
  • 在 obj.fn() 调用中, this 默认指向 obj,这是 JS 的隐式传 this
  • 在 fn.call(xxx) 调用中,this 就是 xxx,这是开发者通过 call 显式指定的 this
  • 在 arrow() 调用中,arrow 里面的 this 就是 arrow 外面的 this,因为箭头函数里面没有自己的 this
  • 在 arrow.call(xxx) 调用中,arrow 里面的 this 还是 arrow 外面的 this,因为箭头函数里面没有自己的 this

资料来源:饥人谷