06_JavaScript函数

122 阅读6分钟

JavaScript函数

1. 函数使用的步骤

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

默认情况下JavaScript引擎或者浏览器会给我们提供一些已经实现好的函数

我们也可以编写属于自己的函数

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

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

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

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

    • 声明好函数后,函数默认是不会主动执行
    • 只有我们调用了函数后,函数体中的代码才会依次进行执行

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

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

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

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

函数的作用:

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

2. 声明和调用函数

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

注意:

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

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

// 函数调用
foo()

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

3. 函数的参数

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

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

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

形参和实参

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


    // 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}`)
    }

    // why/18/1.88称之为函数的参数(实参, 实际参数, arguments)
    printInfo("why", 18, 1.88)
    printInfo("kobe", 30, 1.98)

    // 另外一个案例也做一个重构
    function sum(num1, num2) {
      var result = num1 + num2
      console.log("result:", result)
    }

    sum(20, 30)
    sum(123, 321)

4. 函数的返回值

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

  • 使用return关键字来返回结果
  • 一旦在函数中执行return操作,那么当前函数会终止
  • 如果函数中没有使用 return语句 ,那么函数有默认的返回值:undefined
  • 如果函数使用 return语句,但是return后面没有任何值,那么函数的返回值也是:undefined

Snipaste_2022-11-13_14-28-24.png


    // var result = prompt("请输入一个数字:")
    // 1.理解函数的返回值
    // function sayHello(name) {
    //   console.log(`Hi ${name}`)
    // }

    // var foo = sayHello("Kobe")
    // console.log("foo:", foo)

    // 2.返回值的注意事项
    // 注意事项一: 所有的函数, 如果没有写返回值, 那么默认返回undefined
    // function foo() {
    //   console.log("foo函数被执行~")
    // }

    // var result = foo()
    // console.log("foo的返回值:", result)

    // 注意事项二: 我们也可以明确的写上return
    // 写上return关键字, 但是后面什么内容都没有的时候, 也是返回undefined
    // function bar() {
    //   console.log("bar函数被执行~")
    //   return
    // }
    // var result = bar()
    // console.log("bar的返回值:", result)

    // 注意事项三: 如果在函数执行到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)


数字格式化的函数练习


    // 从服务器拿到很多的数字
    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))
    console.log(formatCount(playCount2))
    console.log(formatCount(playCount3))
****

arguments参数(JS高级再学习)

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

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

    • 也只有在所有(非箭头)函数中, 才可以使用arguments对象
    • 在全局是不存在arguments对象的
  • 该对象中存放着所有的调用者传入的参数,从0位置开始,依次存放

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

    • arguments虽然有length属性,也可以通过数字索引去获取对应的值,但是它只是一个对象,并不是数组
    • 因为arguments并不可以调用数组上的一些方法,也就是说判断一个对象是不是数组是根据其构造函数而言的,而不是取决于结构是否类似
  • 函数的形参除了会依次传递给实参,还会将所有的实参全部传递给arguments参数,所以我们可以通过arguments去获取所有的实参,尤其是那些多余的参数(也就是传入的实参多余函数的形参的时候,那些没有被赋值给形参的实参)

    // 1.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("why", 18, 1.88, "广州市")


    // 2.arguments的案例
    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))
    console.log(sum(10, 20, 30))
    console.log(sum(10, 20, 30, 40))


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

函数中调用函数

    function bar() {
      console.log("bar函数被执行了~")
      console.log("----------")
    }

    function foo() {
      // 浏览器默认提供给我们的其他函数
      console.log("foo函数执行")
      console.log("Hello World")
      // alert("Hello Coderwhy")

      // 调用自己定义的函数
      bar()

      // 其他代码
      console.log("other coding")
    }

    foo()

5. 递归

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

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

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


    function bar() {

    }

    // 递归调用
    // 默认情况下会产生无限调用的情况
    function foo() {
      console.log("foo函数被执行了")
      foo()
    }

    foo()

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


// 简单模拟Math.pow函数
    // 需求: 封装一个函数, 函数可以实现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(2, 3))  == 2 * pow(2,2)
    console.log(pow(3, 3))

    function pow(x, n) {
      return n === 1 ? x : x * pow(x, n-1)
    } 
    console.log(pow(2, 3))

Snipaste_2022-11-13_16-47-49.png

递归的代码第一次接触会有点绕,对于初次接触函数的同学,可以先跳过去。

6.递归 vs 循环

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

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

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


    // 什么是斐波那契数列
    // 数列: 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))
    console.log(fibonacci(10))
    console.log(fibonacci(20))

7. 作用域

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

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

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

变量类型说明作用域
局部变量(Local Variables)定义在函数内部或代码块(ES6+)的变量函数内部或代码块(ES6+)
全局变量(Global Variables)定义在所有函数之外声明的变量(也就是在script中声明的)在变量定义后的任何范围内都可以正常访问 在变量定义之前访问获取到的值为undefined 通过var声明的全局变量等价于在window对象上添加一个属性
外部变量(Outer Variables)在当前作用域内访问一个外层作用域中的变量的时候,这个变量就被称之为外部变量
    // 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)
    }
    console.log("在代码块外面访问count:", count)
    
    // for循环的代码块也是没有自己的作用域
    for (var i = 0; i < 3; i++) {
      var foo = "foo"
    }
    console.log("for循环外面访问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)
 // 1.全局变量(global variable): 在全局(script元素中)定义一个变量, 那么这个变量是可以在定义之后的任何范围内被访问到的, 那么这个变量就称之为是一个全局变量.
    var message = "Hello World"
    
    // 在函数中访问message
    function sayHello() {
      // 外部变量(outer variable): 在函数内部去访问函数之外的变量, 访问的变量称之为外部变量
      console.log("sayHello中访问message:", message)

      // 2.局部变量(local variable): 在函数内部定义的变量, 只有在函数内部才能进行访问, 称之为局部变量
      var nickname = "coderwhy"

      function hi() {
        console.log("hi function~")
        // message也是一个外部变量
        console.log("hi中访问message:", message)
        // nickname也是一个外部变量
        console.log("hi中访问nickname:", nickname)
      }
      hi()
    }


    sayHello()

Snipaste_2022-11-14_09-25-56.png

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

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

    function sayHello() {
      // var message = "Hello Coderwhy"

      function hi() {
        // var message = "Hi Kobe"
        console.log(message)
      }
      hi()
    }

    sayHello()

8. 函数表达式

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

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

    // 函数的声明(声明语句)
    foo()
    function foo() {
      console.log("foo函数被执行了~")
    }

    // 函数的表达式
    // console.log(message) // undefined
    // var message = "why"

    // console.log(bar)
    bar()
    var bar = function() {
      console.log("bar函数被执行了~")
    }

9. 头等函数

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

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

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

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


    // 函数作为一等(头等)公民
    // 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()


    // 5.将函数存储在另外一个数据结构中
    var obj = {
      name: "why",
      eating: function() {
        console.log("eating")
      }
    }
    obj.eating()
    function bar1() {
      console.log("bar1函数被执行~")
    }
    function bar2() {
      console.log("bar2函数被执行~")
    }
    function bar3() {
      console.log("bar3函数被执行~")
    }
    // 事件总线的封装
    var fns = [bar1, bar2, bar3]

    // 函数式编程: 使用函数来作为头等公民使用函数, 这种编程方式(范式).
    // JavaScript支持函数式编程.


10. 回调函数

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

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

回调函数的特点:

  1. 自己主动定义的
  2. 自己并没有去调用它
  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)
    })

11. 高阶函数

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

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

12. 匿名函数

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

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

13. 立即执行函数

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


 // 1.普通函数的使用过程
    // function foo() {
    //   console.log("foo函数被执行~")
    // }
    // foo()
    // foo(); // ()[]{}

    // 2.定义函数, 定义完这个函数之后, 会要求这个函数立即被执行
    // {} 代码块/对象类型
    // () 控制优先级(2+3)*5/函数的调用/函数的参数
    // [] 定义数组/从数组-对象中取值/对象的计算属性
    // 立即执行函数(常用的写法)
    (function() { 
      console.log("立即执行函数被调用~")
    })()

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


   // 应用场景一: 防止全局变量的命名冲突
    
    // 立即执行函数和普通的代码有什么区别?
    // 在立即执行函数中定义的变量是有自己的作用域的
    (function() {
      var message = "Hello World"
      // console.log(message)
    })()
    // console.log(message)
    // var message = "Hello World"
    // console.log(message)

  <button class="btn">按钮1</button>
  <button class="btn">按钮2</button>
  <button class="btn">按钮3</button>
  <button class="btn">按钮4</button>
  
  <script>
    // 慢慢来, 反而是快的

    // 1.获取一个按钮监听点击
    // 1.拿到html元素
    // var btnEl = document.querySelector(".btn")
    // console.log(btnEl)
    // // 2.监听对应按钮的点击
    // btnEl.onclick = function() {
    //   console.log("点击了按钮1")
    // }

    // 2.获取所有的按钮监听点击
    // 没有使用立即执行函数
    debugger
    // var btnEls = document.querySelectorAll(".btn")
    // for (var i = 0; i < btnEls.length; i++) {
    //   var btn = btnEls[i];
    //   btn.onclick = function() {
    //     console.log(`按钮${i+1}发生了点击`)
    //   }
    // }

    // 使用立即执行函数
    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)

早期是没有let 的 现在可以通过let解决这个问题 或者执行函数


    // 1.常见的写法
    // (function() {
    //   console.log("立即执行函数被调用~")
    // })()


    // 2.错误的写法
    // () -> 优先级的()
    // function foo() {
    // }()

    // 3.其他写法
    // 匿名函数
    (function(fn) {
      console.log("立即执行函数被调用")
    }());

    // +(正号)-(符号)!(取反) - 了解
    +function foo() {}()


14. 函数中常见的代码风格

Snipaste_2022-11-14_16-00-02.png

    // 1.foo和()之间不需要有空格
    // 2.多参数,后面加上一个空格
    // 3.()和{之间有一个空格
    // 4.{和其他函数定义在同一行中
    function foo(m, n) {

    }

    foo(20, 30)

    if (true) {
    } else {

    }

    for (var i = 0; i < 10; i++) {

    }

    // 模板字符串(可以换行)
    var message = `
       哈哈哈哈哈${100}
    `

    // 图里面建议:
    function sum(num1, num2) {
      return num1 + num2
    }