JavaScript函数

91 阅读3分钟

一、函数的基本使用

1.1 函数的定义

函数其实就是某段代码的封装,这段代码帮助完成某一个功能,使用函数可以提高代码编写的效率以及代码的重用

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

    • 声明函数 —— 封装 独立的功能

    • 调用函数 —— 享受 封装 的成果

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

    • 声明函数的过程是对某些功能的封装过程
    • 声明函数使用function关键字:这种写法称之为函数的定义
  • 调用函数,也可以称为函数调用:

    • 调用函数是让已存在的函数为我们所用
    • 调用函数通过 函数名() 即可:比如test()
    // 定义一个函数, 在内部计算10和20的和
    function sum() {
      var num1 = 10
      var num2 = 20
      var result = num1 + num2
      console.log("result:", result)
    }
    sum()
    

1.2 函数的参数

  • 函数的参数:

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

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

      • 在函数 内部,把参数当做 变量 使用,进行需要的数据处理

      • 函数调用时,按照函数定义的参数顺序,把 希望在函数内部处理的数据,通过参数传递

  • 形参和实参

    • 形参(参数 parameter):定义 函数时,小括号中的参数,是用来接收参数用的,在函数内部 作为变量使用

    • 实参(参数 argument):调用 函数时,小括号中的参数,是用来把数据传递到 函数内部 用的

  • 注意:函数需要参数而调用时未传入参数,则函数内部的变量值为 undefined

    // name/age/height称之为函数的参数(形参, 形式参数, parmaters)
    function printInfo(name, age, height) {
      console.log(`my name is ${name}`)
      console.log(`age is ${age}`)
      console.log(`height is ${height}`)
    }
    
    // kobe/30/1.98称之为函数的参数(实参, 实际参数, arguments)
    printInfo("kobe", 30, 1.98)
    
    // 两个数求和的函数
    function sum(num1, num2) {
      var result = num1 + num2
      console.log("result:", result)
    }
    
    sum(20, 30)
    sum(123, 321)
    

1.3 函数的返回值

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

    • 使用 return 关键字来返回结果

    • 一旦在函数中执行return操作,那么当前函数会终止

    • 如果函数中没有使用 return语句 ,那么函数有默认的返回值:undefined

    • 如果函数使用 return语句,但是return后面没有任何值,那么函数的返回值也是:undefined

    // 返回值的注意事项
    // 1. 所有的函数, 如果没有写返回值, 那么默认返回undefined
    function foo() {
      console.log("foo函数被执行~")
    }
    
    var result = foo()
    console.log("foo的返回值:", result) // undefined
    
    // 2. 写上return关键字, 但是后面什么内容都没有的时候, 也是返回undefined
    function bar() {
      console.log("bar函数被执行~")
      return
    }
    var result = bar()
    console.log("bar的返回值:", result) // undefined
    
    // 3. 如果在函数执行到return关键字时, 函数会立即停止执行, 退出函数
    function baz() {
      console.log("Hello Baz")
      return
      console.log("Hello World")
      console.log("Hello Why")
    }
    
    baz()
    
    // 函数的具体返回值
    function sum(num1, num2) {
      var result = num1 + num2
      return result
    }
    
    var total = sum(20, 30)
    console.log("total:", total) // 50
    

1.4 函数的练习

// 传入一个数字n, 计算1~n的数字和
function sumN(n) {
  // 1.加对n的判断
  if (n <= 0) {
    console.log(`您传入的${n}是有问题的`)
    return
  }

  // 2.真正对1~n的数字进行计算
  // 1~n的数字和
  var total = 0
  for (var i = 1; i <= n; i++) {
    total += i
  }
  return total
}

var result1 = sumN(5)
var result2 = sumN(10)
console.log(`result1: ${result1}, result2: ${result2}`) // result1: 15, result2: 55

var result3 = sumN(-10) // 您传入的-10是有问题的
console.log("result3:", result3) // result3: undefined
// 从服务器拿到很多的数字
var playCount1 = 13687 // 13687
var playCount2 = 5433322 // 543万
var playCount3 = 8766633333 // 87亿


// 封装一个工具函数: 对数字进行格式化
// 10_0000_0000就是1000000000语法糖
// 语法糖的概念: 一种简写或者特殊的写法, 这种写法相对于原有的写法更加的方便或者阅读性更强
function formatCount(count) {
  var result = 0
  if (count >= 10_0000_0000) { // 超过10_0000_0000值进行转换
    result = Math.floor(count / 1_0000_0000) + "亿"
  } else if (count >= 10_0000) {
    result = Math.floor(count / 1_0000) + "万"
  } else {
    result = count
  }

  return result
}

console.log(formatCount(playCount1)) // 13687
console.log(formatCount(playCount2)) // 543万
console.log(formatCount(playCount3)) // 87亿

二、arguments参数的使用

  • 在函数有一个特别的对象:arguments对象

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

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

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

    • 如果调用者传入的参数多于函数接收的参数,可以通过arguments去获取所有的参数

    function foo(name, age) {
      console.log("传入的参数", name, age)
    
      // 在函数中都存在一个变量, 叫arguments
      console.log(arguments)
      // arguments是一个对象
      console.log(typeof arguments)
      // 对象内部包含了所有传入的参数
      // console.log(arguments[0])
      // console.log(arguments[1])
      // console.log(arguments[2])
      // console.log(arguments[3])
    
      // 对arguments来进行遍历
      for (var i = 0; i < arguments.length; i++) {
        console.log(arguments[i])
      }
    }
    
    foo("kobe", 30, 1.98, "洛杉矶")
    

    image.png

  • 后续文章会详细介绍具体的使用

    function sum() {
      var total = 0
      for (var i = 0; i < arguments.length; i++) {
        var num = arguments[i]
        total += num
      }
      return total
    }
    
    console.log(sum(10, 20)) // 30
    console.log(sum(10, 20, 30)) // 60
    console.log(sum(10, 20, 30, 40)) // 100
    

三、递归的使用

3.1 函数调用函数

  • 函数内部是可以调用另外一个函数的

    function bar() {
      console.log("bar函数被执行了~")
      console.log("----------")
    }
    
    function foo() {
      // 浏览器默认提供给我们的其他函数
      console.log("foo函数执行")
      console.log("Hello World")
    
      // 调用自己定义的函数
      bar()
    
      // 其他代码
      console.log("other coding")
    }
    
    foo()
    
  • 既然函数中可以调用另外一个函数,那么函数是也可以调用自己呢,但是函数调用自己必须有结束条件,否则会产生无限调用,造成报错

image.png

3.2 递归的思想

  • 递归是一种重要的编程思想:

    • 将一个复杂的任务,转化成可以重复执行的相同任务
  • 注意:递归必须有结束条件

  • pow 函数的实现

    • 递归实现

      • 在数学上:

      image.png

      • 对于函数的调用,可以进行划分:

      image.png

      • 这里需要有一个结束的条件,就是当n已经等于1的时候就不需要拆分了 image.png
    // 需求: 封装一个函数, 函数可以实现x的n次方法
    function pow1(x, n) {
      return x ** n
    }
    
    // console.log(pow1(2, 3))
    // console.log(pow1(3, 3))
    
    // console.log(Math.pow(2, 3))
    // console.log(Math.pow(3, 3))
    
    // 一. for循环实现方式 
    // x² = x * x
    // x³ = x * x * x
    function pow2(x, n) {
      var result = 1
      for (var i = 0; i < n; i++) {
        result *= x
      }
      return result
    }
    
    console.log(pow2(2, 3))
    console.log(pow2(3, 3))
    
    // 二. 递归实现方式(必须有一个结束条件)
    // 缺点: 性能是比较低(占用过多的栈内存)
    // 优点: 写出来的代码非常简洁
    function pow(x, n) {
      if (n === 1) return x
      return x * pow(x, n - 1)
    }
    
    console.log(pow(2, 3))
    console.log(pow(3, 3))
    

3.3 斐波那契

// 什么是斐波那契数列
// 数列: 1 1 2 3 5 8 13 21 34 55  ... x
// 位置: 1 2 3 4 5 6 7  8  9  10  ... n

// 1.斐波那契的递归实现
function fibonacci(n) {
  if (n === 1 || n === 2) return 1
  return fibonacci(n-1) + fibonacci(n-2)
}


// 2.斐波那契的for循环实现
function fibonacci(n) {
  // 特殊的情况(前两个数字)
  if (n === 1 || n === 2) return 1

  // for循环的实现
  var n1 = 1
  var n2 = 1
  var result = 0
  for (var i = 3; i <= n; i++) {
    result = n1 + n2
    n1 = n2
    n2 = result
  }
  return result
}

console.log(fibonacci(5)) // 5
console.log(fibonacci(10)) // 55
console.log(fibonacci(20)) // 6765

四、变量作用域

4.1 什么叫做作用域

  • 在JavaScript(ES5之前)中没有块级作用域的概念,但是函数可以定义自己的作用域。

    • 作用域(Scope)表示一些标识符的作用有效范围(所以也有被翻译为有效范围的)

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

    // 1.作用域的理解:message在哪一个范围内可以被使用, 称之为message的作用域(scope)
    // 全局变量: 全局作用域
    var message = "Hello World"
    if (true) {
      console.log(message)
    }
    function foo() {
      console.log("在foo中访问", message)
    }
    foo()
    
    // 2.ES5之前是没有块级作用域(var定义的变量是没有块级作用域)
    {
      var count = 100
      console.log("在代码块中访问count:", count) // 100
    }
    console.log("在代码块外面访问count:", count) // 100
    // for循环的代码块也是没有自己的作用域
    for (var i = 0; i < 3; i++) {
      var foo = "foo"
    }
    console.log("for循环外面访问foo:", foo) // foo
    console.log("for循环外面访问i:", i) // 3
    
    // 3.ES5之前函数代码块是会形成自己的作用域
    // 意味着在函数内部定义的变量外面是访问不到的
    function test() {
      var bar = "bar"
    }
    
    test()
    // console.log("test函数外面访问bar:", bar)
    
    // 函数有自己的作用域: 函数内部定义的变量只有函数内部能访问到
    function sayHello() {
      var nickname = "kobe"
      console.log("sayHello函数的内部:", nickname)
    
      function hi() {
        console.log("hi function~")
        console.log("在hi函数中访问nickname:", nickname)
      }
      hi()
    }
    sayHello()
    // console.log("sayHello外面访问nickname:", nickname) // 访问不到
    

4.2 全局/局部/外部变量

  • 外部变量和局部变量的概念:

    • 定义在函数内部的变量,被称之为局部变量(Local Variables)

    • 定义在函数外部的变量,被称之为外部变量(Outer Variables)

  • 全局变量

    • 在函数之外声明的变量(在script中声明的),称之为全局变量

    • 全局变量在任何函数中都是可见的

    • 通过var声明的全局变量会在window对象上添加一个属性(了解)

    // 1.全局变量(global variable): 在全局(script元素中)定义一个变量, 那么这个变量是可以在定义之后的任何范围内被访问到的, 那么这个变量就称之为是一个全局变量.
    var message = "Hello World"
    
    // 在函数中访问message
    function sayHello() {
      // 外部变量(outer variable): 在函数内部去访问函数之外的变量, 访问的变量称之为外部变量
      console.log("sayHello中访问message:", message) // Hello World
    
      // 2.局部变量(local variable): 在函数内部定义的变量, 只有在函数内部才能进行访问, 称之为局部变量
      var nickname = "kobe"
    
      function hi() {
        console.log("hi function~")
        // message也是一个外部变量
        console.log("hi中访问message:", message) // Hello World
        // nickname也是一个外部变量
        console.log("hi中访问nickname:", nickname) // kobe
      }
      hi()
    }
    
    sayHello()
    
  • 关于块级作用域、作用域链、变量提升、AO、VO、GO等概念后续文章会详细补充

4.3 变量的访问顺序

  • 优先访问自己函数中的变量,没有找到时,在外部中访问

五、函数表达式

5.1 概念

函数表达式(Function Expressions):

  • 注意,function 关键字后面没有函数名

    • 函数表达式允许省略函数名
    // 函数的声明(声明语句)
    foo()
    function foo() {
      console.log("foo函数被执行了~")
    }
    
    // 函数的表达式
    // console.log(message) // undefined
    // var message = "hello world"
    
    // console.log(bar)
    bar()
    var bar = function() {
      console.log("bar函数被执行了~")
    }
    
  • 无论函数是如何创建的,函数都是一个值(这个值的类型是一个对象)。

5.2 函数声明 vs 函数表达式

  • 语法不同:

    • 函数声明:在主代码流中声明为单独的语句的函数

    • 函数表达式:在一个表达式中或另一个语法结构中创建的函数

  • JavaScript创建函数的时机是不同的:

    • 函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。

    • 函数声明被定义之前,它就可以被调用

      • 这是内部算法的原故

      • 当 JavaScript 准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数

  • 开发中如何选择

    • 需要声明一个函数时,首先考虑函数声明语法

    • 它能够为组织代码提供更多的灵活性,因为可以在声明这些函数之前调用这些函数

六、函数式编程中概念

6.1 函数作为一等公民

  • 函数可以赋值给变量

  • 函数也可以传递给另外一个函数

  • 函数也可以作为另外一个函数的返回值

  • 函数也可以存储在别的数据结构中

    // 函数作为一等(头等)公民
    // 1.函数可以被赋值给变量(函数表达式写法)
    var foo1 = function() {
      console.log("foo1函数被执行~")
    }
    // foo1()
    
    // 2.让函数在变量之间来回传递
    var foo2 = foo1
    foo2()
    
    
    // 3.函数可以另外一个函数的参数
    function bar(fn) {
      console.log("fn:", fn)
      fn()
    }
    bar(foo1)
    
    
    // 4.函数作为另外一个函数的返回值
    function sayHello() {
      function hi() {
        console.log("hi kobe")
      }
      return hi
    }
    
    var fn = sayHello()
    fn() // hi kobe
    
    
    // 5.将函数存储在另外一个数据结构中
    var obj = {
      name: "xiaoming",
      eating: function() {
        console.log("eating")
      }
    }
    obj.eating()
    

6.2 函数式编程

  • 函数可以作为头等公民的编程语言,叫做函数式编程的语言
  • JavaScript就是一种函数式编程语言

6.3 函数的回调

  • 将一个作为另外一个函数的参数传入到另外一个函数中

  • 在另外一个函数中,对于传入的函数进行调用的过程,就叫做函数的回调

    // 1.函数回调的概念理解
    function foo(fn) {
      // 通过fn去调用bar函数的过程, 称之为函数的回调
      fn()
    }
    function bar() {
      console.log("bar函数被执行了~")
    }
    foo(bar)
    
    
    // 2.函数回调的案例
    function request(url, callback) {
      console.log("根据URL向服务器发送网络请求")
      console.log("需要花费比较长的时间拿到对应的结果")
      var list = ["javascript", "javascript学习", "JavaScript高级编程"]
      callback(list)
    }
    
    function handleResult(res) {
      console.log("在handleResult中拿到结果:", res)
    }
    request("url", handleResult)
    
    
    // 3.函数回调的案例重构
    function request(url, callback) {
      console.log("根据URL向服务器发送网络请求")
      console.log("需要花费比较长的时间拿到对应的结果")
      var list = ["javascript", "javascript学习", "JavaScript高级编程"]
      callback(list)
    }
    
    // 传入的函数是没有名字, 匿名函数
    request("url", function(res) {
      console.log("在handleResult中拿到结果:", res)
    })
    

6.4 匿名函数

  • 传入一个函数时,如果没有给函数名,也没有定义对应的变量的函数,就叫做匿名函数

6.5 高阶函数

  • foo可以接受另外一个函数参数,那么foo就称之为是一个高阶函数

  • 如果一个函数有返回另外一个函数,那么这个函数也叫做高阶函数

    function foo(fn) {
        fn()
    }
    
    foo(function() {
    
    })
    

七、立即执行函数

7.1 概念、语法及作用

  • 什么是立即执行函数?

    • 专业名字:Immediately-Invoked Function Expression(IIFE 立即调用函数表达式)

    • 表达的含义是一个函数定义完后被立即执行

      • 第一部分是定义了一个匿名函数,这个函数有自己独立的作用域

      • 第二部分是后面的(),表示这个函数被执行了

        // 立即执行函数
        (function() { 
          console.log("立即执行函数被调用~")
        })()
        
        // 立即执行函数的参数和返回值
        var result = (function(name) {
          console.log("函数立刻被执行~", name)
          return "Hello World"
        })("kobe")
        console.log(result) // Hello World
        
  • 作用

    • 会创建一个独立的执行上下文环境,可以避免外界访问或修改内部的变量,也避免了对内部变量的修改
    // 应用场景一: 防止全局变量的命名冲突
    
    // 在立即执行函数中定义的变量是有自己的作用域的
    (function() {
      var message = "Hello World"
      // console.log(message)
    })()
    // console.log(message) // 报错:message未定义
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
    
      <button class="btn">按钮1</button>
      <button class="btn">按钮2</button>
      <button class="btn">按钮3</button>
      <button class="btn">按钮4</button>
    
      <script>
        // 获取所有的按钮监听点击
        // 没有使用立即执行函数
        // var btnEls = document.querySelectorAll(".btn")
        // for (var i = 0; i < btnEls.length; i++) {
        //   var btn = btnEls[i];
        //   btn.onclick = function() {
        //     console.log(`按钮${i+1}发生了点击`) // 按钮5发生了点击,此处会出错
        //   }
        // }
    
        // 使用立即执行函数
        var btnEls = document.querySelectorAll(".btn")
        for (var i = 0; i < btnEls.length; i++) {
          var btn = btnEls[i];
          (function(m) {
            btn.onclick = function() {
              console.log(`按钮${m+1}发生了点击`)
            }
          })(i)
        }
    
        console.log(i) // 4
    
      </script>
    
    </body>
    </html>
    

7.2 其他写法补充

  • 立即执行函数必须是一个表达式(整体)不能是函数声明

    • 下面的这种写法会报错,因为是一个函数声明,不是一个函数表达式;

    • 当圆括号出现在匿名函数的末尾想要调用函数时,它会默认将函数当成是函数声明。

      // 错误的写法
      // () -> 优先级的()
      function foo() {
      }()
      
  • 当圆括号包裹函数时,它会默认将函数作为表达式去解析,而不是函数声明。

    // 常见的写法
    (function() {
      console.log("立即执行函数被调用~")
    })()
    
    // 其他写法
    // 匿名函数
    (function() {
      console.log("立即执行函数被调用")
    }())
    
  • 下面是一个函数表达式,所以可以执行

    // +(正号)-(负号)!(取反)
    +function foo() {}()