JS基础 - 函数

265 阅读11分钟

函数其实就是某段功能代码的封装,这段代码帮助我们完成某一个功能

默认情况下JavaScript引擎或者浏览器会给我们提供一些已经实现好的函数, 我们也可以编写属于自己的函数

在开发程序时,使用函数可以提高编写的效率以及代码的重用

函数的使用包含两个步骤:

  • 声明函数 —— 封装 独立的功能
  • 调用函数 —— 享受 封装 的成果
    • 声明好函数后,函数默认是不会主动执行
    • 只有我们调用了函数后,函数体中的代码才会依次进行执行

声明函数,在JavaScript中也可以称为定义函数

  • 声明函数的过程是对某些功能的封装过程
  • 我们可以根据自己的需求定义很多自己的函数

调用函数,也可以称为函数调用

  • 调用函数是让已存在的函数为我们所用
  • 这些函数可以是刚刚自己封装好的某个功能函数
  • 也可以去使用默认提供的或者其他三方库定义好的函数

基本使用

声明函数使用function关键字:这种写法称之为函数的定义

注意:

  1. 函数名的命名规则和前面变量名的命名规则是相同的
  2. 函数要尽量做到见名知意 (通常是一些行为(action),所以使用动词定义函数名会更多一些, 但不是绝对的)
  3. 函数定义完后里面的代码是不会执行的,函数必须调用才会执行
// 函数定义
// 函数可以看成自定义了个‘工具’
// 1. 需要给工具一个名字以便于日后使用,这个名称就是函数名
// 2. 函数默认情况不会主动执行,除非进行调用了函数

// foo,bar,baz 一般被称之为“伪变量”(metasyntactic variable)
// 它们本身并没有特别的用途和意义
// 只是约定俗称下,会在测试情况下,被用来作为函数、变量、文件的名称
function foo() {
  console.log('Hello World')
}

// 函数调用
foo()

// 函数可以在任意时候进行任意次的调用
foo()

参数

函数,把 具有独立功能的代码块 组织为一个小模块,在需要的时候 调用

函数的参数,增加函数的 通用性,针对 相同的数据处理逻辑,能够 适应更多的数据

  • 在函数 内部,把参数当做 变量 使用,进行需要的数据处理,所以形参的默认值是undefined
  • 函数调用时,按照函数定义的参数顺序,把 希望在函数内部处理的数据通过参数 传递

形参和实参

  • 形参(形式参数 parameter):定义 函数时,小括号中的参数,是用来接收参数用的,在函数内部 作为变量使用
  • 实参(实际参数 argument):调用 函数时,小括号中的参数,是用来把数据传递到 函数内部 用的

返回值

函数不仅仅可以有参数, 也可以有返回值

  • 使用return关键字来返回结果
  • 一旦在函数中执行return操作,那么当前函数会终止
  • 如果函数中没有使用 return语句 ,那么函数有默认的返回值:undefined
  • 如果函数使用 return语句,但是return后面没有任何值,那么函数的返回值也是:undefined
// 传入一个数字,可以根据数字转化成显示为 亿、万文字显示的文本
function formatNum(num = 0) {
  if ((num / 10000 / 10000).toFixed() > 0) {
    return `${(num / 10000 / 10000).toFixed()}亿`
  } else if ((num / 10000).toFixed() > 0) {
    return `${(num / 10000).toFixed()}万`
  }
  return num
}

// 使用下划线分割数字是普通数字写法的语法糖
// 下划线所在的位置是任意的 可以是1_00_00 也可以是100_00

// 语法糖是对另一种语法的简化写法或特殊写法,这种写法相对于原有写法更为的方便或阅读性更强
// 但他们的底层机制和原理都是一致的
// 也就是说 1_00_00 === 10000 => true
console.log(formatNuM(1_0000_0000))

事实上在函数有一个特别的对象: arguments对象

  • 默认情况下,arguments对象是所有(非箭头)函数中都可用的局部对象

    • 也只有在所有(非箭头)函数中, 才可以使用arguments对象

    • 在全局是不存在arguments对象的

  • 该对象中存放着所有的调用者传入的参数,从0位置开始,依次存放

  • arguments对象的类型是一个object类型( array-like ),不是一个数组,但是和数组的用法看起来很相似

    • arguments虽然有length属性,也可以通过数字索引去获取对应的值,但是它只是一个对象,并不是数组

    • 因为arguments并不可以调用数组上的一些方法,也就是说判断一个对象是不是数组是根据其构造函数而言的,而不是取决于结构是否类似

  • 函数的形参除了会依次传递给实参,还会将所有的实参全部传递给arguments参数,所以我们可以通过arguments去获取所有的实参,尤其是那些多余的参数(也就是传入的实参多余函数的形参的时候,那些没有被赋值给形参的实参)

// arguments 的结构类似于如下形式 --- 伪代码
{
  '0': 'Klaus',
  '1': '23',
  // arguments的长度
  length: 2,
  // callee指向的是函数自身,可以使用这个属性来进行递归调用
  callee: 函数自身
}

递归

在开发中,函数内部是可以调用另外一个函数的

所以我们可以在函数中,调用函数自身,而这种行为被称之为递归 (Recursion)

递归必须有结束条件,否则会产生无限递归,造成报错(也就是栈内存溢出)

递归其实是一种重要的编程思想: 递归是将一个复杂的任务,转化成可以重复执行的相同简单任务

// 简单模拟Math.pow函数
function pow(num, count) {
  return count === 1 ? num :pow(num, count-1) * num
}

image.png

递归 vs 循环

一般情况下,可以使用递归实现的功能,也可以使用循环来进行实现

递归的优点: 可以使代码更为的简洁,可读性更强

递归的缺点: 每调用一次函数就会在栈中单独新开一个函数执行上下文,所以递归的性能是很低的,占据了过多的栈内存空间

递归实现斐波那契数列

function fibonacci(num) {
  if (num === 1 || num === 2) {
    return 1
  } else {
    return fibonacci(num - 1) + fibonacci(num - 2)
  }
}

循环实现斐波那契数列

function fibonacci(num) {
  if (num === 1 || num === 2) {
    return 1
  } else {
    // 名为n1的游标指向 fibonacci(num - 2)
    let n1 = 1
    // 名为n2的游标指向 fibonacci(num - 1)
    let n2 = 1
    let res = 0

    for (let i = 3; i <= num; i++) {
      res = n1 + n2
      // 求和 游标后移 更新游标的指向
      n1 = n2
      n2 = res
    }

    return res
  }
}

作用域

作用域(Scope)表示一些标识符的作用有效范围,也就是变量在那个范围内是有效的,可以被正常访问

函数的作用域表示在函数内部定义的变量,只有在函数内部可以被访问到

变量类型说明作用域
局部变量(Local Variables)定义在函数内部或代码块(ES6+)的变量函数内部或代码块(ES6+)
全局变量(Global Variables)定义在所有函数之外声明的变量(也就是在script中声明的)在变量定义后的任何范围内都可以正常访问
在变量定义之前访问获取到的值为undefined
通过var声明的全局变量等价于在window对象上添加一个属性
外部变量(Outer Variables)在当前作用域内访问一个外层作用域中的变量的时候,这个变量就被称之为外部变量或自由变量

JS引擎在查找变量的时候,会遵循如下顺序:

  1. 在自己当前作用域中查找,找到就使用,没有找到就去上层作用域中进行查找
  2. 在上层作用域查找,找到就使用,没有找到就再去上层作用域中进行查找
  3. 依次类推,直到全局作用域,找到就使用,没有找到就去GO(windows对象)上进行查找
  4. 如果GO中,依旧没有,报错

函数表达式

在JavaScript中,函数只是一种特殊的可以执行的值, 其值类型是Function,本质是一种特殊的对象

const fun = () => {}
console.log(typeof fun) // => function
// 函数声明
function foo() {}

// 函数表达式
const baz = () => {}
// 函数表达式的本质是将函数赋值给了一个变量
// 所以函数名 定义了没有意义 一般会省略
const baz =  function foo() {
  console.log('Hello World')
}

baz() // success
foo() // error
函数声明函数表达式
单独的语句表达式
会作用域提升,可以在定义前使用作用域提升的是函数所赋值给的那个变量(前提是那个变量是使用var定义的)
所以不可以在定义前被调用

头等函数

头等函数(first-class function;第一级函数)是指在程序设计语言中,函数被当作头等公民(或被称之为一等公民)。

这意味着,函数可以作为别的函数的参数、函数的返回值,赋值给变量,匿名函数或存储在数据结构中

通常我们对作为头等公民的编程方式(或者叫编程范式),称之为函数式编程

JavaScript就是符合函数式编程的语言,这个也是JavaScript的一大特点

回调函数

  1. 将一个函数作为参数传入另一个函数中
  2. 在另一个函数的执行的某一个时刻,会对我们所传入的那个函数进行调用

那么我们就将作为参数传入的那个函数,称之为回调函数

回调函数的特点:

  1. 自己主动定义的
  2. 自己并没有去调用它
  3. 在某一个时刻,该函数会被执行

高阶函数

高阶函数(Higher-order function)必须至少满足两个条件之一:

  • 接受一个或多个函数作为输入
  • 输出一个函数

匿名函数

匿名函数(anonymous function)指没有给函数具体名称的函数

如果在传入一个函数时,我们没有指定这个函数的名称或者没有将函数表达式赋值给某个具体的变量的时候,那么这个函数称之为匿名函数

立即执行函数

Immediately-Invoked Function Expression(IIFE 立即调用函数表达式)表达的含义是一个函数定义完后会被立即执行

// 立即执行函数是那些定义完直接使用的函数
// IIFE全称叫立即函数调用表达式
// 也就是这里的函数需要是一个表达式,而不可以使用函数声明
// 而在函数表达式中,显示定义函数名是没有任何意义的(也就是这里的foo是没有任何作用的)
// 所以实际在使用IIFE的时候,使用的一般都是匿名函数
(function foo() {
  console.log(233)
})()

foo() // error
// IIFE和普通函数一样
// 可以有参数 也可以有自己的返回值
const userName = (name => {
  console.log('Hello World')
  return name
})('Klaus')

console.log(userName)
// IIFE会创建一个单独的函数执行上下文
// 也就意味着有自己的作用域,可以避免污染全局变量(也就是将全局变量的值进行覆盖)
(function() {
  let msg = 'Hello World'
  console.log(msg) // succsee
})()

console.log(msg) // error

IIFE的其它写法

// 基本写法1 --- 函数调用小括号写在外部,函数表达式使用小括号包裹以表示其是一个整体
// 如果一条语句以 [ 或 ( 作为语句开头的时候,前一条语句必须以分号结尾
// 否则JS引擎在解析的时候,会将两条语句作为一条语句来进行解析,从而报错
(() => { console.log('Hello World') })();

// 基本写法2 --- 函数表达式和调用整体使用小括号进行包裹,以表示其是一个整体
((() => console.log('Hello World'))())

// 基本写法3 --- 在函数前加上正号,负号,叹号以便于JS引擎可以将函数是识别为表达式
// 不推荐
!function() {
  console.log('Hello World')
}()

+(() => console.log('Hello World'))()

// 下面这种定义方式是错误的
// 因为其前边的函数定义会被识别为函数声明,而不是函数表达式
// 而IIFE中,函数的使用需要使用函数表达式
// function foo() {}()

IIFE的应用示例

const btns = document.getElementsByClassName('btn')

// 不使用let/const情况下,解决事件点击的时候i变量值问题
for (var i = 0; i < btns.length; i++) {
  // 使用IIFE开启函数作用域,形成闭包
  // 这样在执行时候,就可以去自己的作用域中查找到正确的i值
  // 而不是使用全局那个被污染的i值
  (function(i) {
    btns[i].addEventListener('click', () => {
      console.log(`按钮${i + 1}被点击了`)
    })
  })(i)
}

函数中常见的代码风格

image.png

debug

bug --- 代码出现了错误和问题

debug --- 找出这个问题并进行修复

image.png

image.png