原生js:自执行函数(IIFE)

165 阅读3分钟

一、什么是自执行函数

IIFE(Immediately Invoked Function Expression):立即地调用函数表达式

常见的2种形式:

      ;(function () {
        console.log(1)
      })()
      // W3C推荐
      ;(function () {
        console.log(2)
      }())

二、自执行函数传参

      ;(function (a, b) {
        console.log(a + b)
      })(1, 2)

三、自执行函数的返回值通过全局变量接收

      const res = (function (a, b) {
        return a + b
      })(1, 2)

四、只有表达式才可以被执行符号执行

这样写会报错

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

如果执行符号中传了参数,js引擎会认为这是一个表达式,不会报错,但不会自执行。 ()会被认为是执行符号,(1)则会被认为是一个表达式,返回值是1

      function test() {
        console.log('test')
      }(1)

如何将函数声明变成表达式:

      // 括号中包裹内容,这就是一个表达式
      ;(function () {
        console.log(1)
      })()
      // 括号包裹内容后变成表达式
      ;(function () {
        console.log(2)
      })()
      // 函数前面加上 ! + - 都可以将函数声明变成函数表达式
      !function () {
        console.log(3)
      }()
      // && || 语句也可以将函数声明变成函数表达式
      1 && function () {
        console.log(4)
      }()
      // 函数表达式可以直接在后面加上执行符号执行
      const fn = function () {
        console.log(5)
      }()

表达式面试题:

      var a = 10
      if (function b() {}) {
        a += typeof(b)
      }
      console.log(a)

打印10undefined,为什么?

(function b() {})是一段表达式,表达式没有函数提升,并且表达式会自动忽略函数名,typeof(b)中的b是undefined

五、一次性函数

      let aa = function a() {
        console.log('a函数执行了')
        aa = function () {
          return false
        }
      }
      aa()
      aa() // 无效

六、立即执行函数的使用

1、案例一

      function test() {
        var arr = []
        for (var i = 0; i < 10; i++) {
          arr[i] = function () {
            document.write(i + ' ')
          }
        }
        return arr
      }

      var arr = test()
      for (var i = 0; i < arr.length; i++) {
        arr[i]()
      }

页面上会打印10个10,为什么会这样?

  1. test函数中的for循环中的变量i由var定义,var会将变量i提升至函数内最顶端,随着for循环执行,i最后的值为10,而将匿名函数添加到arr中,并没有执行,在return arr时,arr中保存的i都是10
  2. var arr = test()会将test的AO长期保存在内存中,这就意味着它的arr和i在test函数执行完后并没有被销毁
  3. 全局变量arr接收到的局部arr中i已经是10了,全局for循环中执行arr的每个函数,打印10次10

如何在页面打印0-9?

方法一:使用自执行函数

每次循环时,传入到自执行函数中的i分别是0-9,而形参i在自执行函数执行完便销毁掉下一次循环重新接收i,所以数组保存的函数中的i为0-9

      function test() {
        var arr = []
        for (var i = 0; i < 10; i++) {
          ;(function (i) {
            arr[i] = function () {
              document.write(i + ' ')
            }
          })(i)
        }
        return arr
      }

      var arr = test()
      for (var i = 0; i < arr.length; i++) {
        arr[i]()
      }

方法二:调用函数时传入参数,不使用i

      function test() {
        var arr = []
        for (var i = 0; i < 10; i++) {
          arr[i] = function (num) {
            document.write(num + ' ')
          }
        }
        return arr
      }

      var arr = test()
      for (var i = 0; i < arr.length; i++) {
        arr[i](i)
      }

方法三:使用let替换var

let具有块级作用域,循环中定义的变量i不会被提升,每次循环时i的值都会被重新定义,效果和自执行函数一样

      function test() {
        var arr = []
        for (let i = 0; i < 10; i++) {
          arr[i] = function () {
            document.write(i + ' ')
          }
        }
        return arr
      }

      var arr = test()
      for (var i = 0; i < arr.length; i++) {
        arr[i]()
      }

2、案例二:点击li打印对应的下标

    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
    </ul>
    <script>
      var lis = document.querySelectorAll('li')
      for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = function () {
          console.log(i)
        }
      }
    </script>

每次打印都是5,为什么?

点击事件的执行函数实际上是一个闭包,闭包中使用了上级作用域下的变量i,就会将变量i长期保存在上级作用域下,当开始点击时,i随着for循环早已变成5

如何解决闭包带来的这种问题:

方法一:使用自执行函数

      for (var i = 0; i < lis.length; i++) {
        ;(function (i) {
          lis[i].onclick = function () {
            console.log(i)
          }
        })(i)
      }

自执行函数可以在每次循环结束时,将i销毁,下一次循环时重新创建i

方法二:每次循环时将i保存到元素身上

      for (var i = 0; i < lis.length; i++) {
        lis[i].index = i
        lis[i].onclick = function () {
          console.log(this.index)
        }
      }

方法三:闭包中嵌套闭包

      for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = (function (i) {
          return function () {
            console.log(i)
          }
        })(i)
      }

每次循环执行时,通过执行符号()将当前i传到回调中,回调中返回一个函数,该函数可以拿到回调的形参

3、案例三:for循环中的定时器

      for (var i = 0; i < 5; i++) {
        setTimeout(() => {
          console.log(i)
        }, 100)
      }

为什么打印5个5?

js的事件循环机制导致setTimeout中的函数被放到任务队列中,随着for循环的执行,该函数在任务队列中有5个;而当for循环执行完毕,此时i为5,任务队列中的函数依次进入到主线程中执行,所以打印5个5

如何做到打印0-4?

使用自执行函数可以做到:闭包中的i在每次循环时被保存下来,在循环结束时也不会被销毁

      for (var i = 0; i < 5; i++) {
        ;(function (i) {
          setTimeout(() => {
            console.log(i) // 使用父级函数的局部变量,导致该局部变量不会被销毁
          }, 100)
        })(i)
      }

扩展,setTimeout第三个参数就是定时器中函数的实参,因此可以这样写:

      for (var i = 0; i < 5; i++) {
        setTimeout(function(i){
          console.log(i)
        }, 100, i)
      }

4、案例四:插件的定义

利用自执行函数可以防止全局变量的污染

      ;(function () {
        function Test(name) {
          this.name = name
        }
        Test.prototype = {}
        window.Test = Test
      })()

      const test = new Test('测试')

定义一个计算器的插件:

      ;(function () {
        function Compute({ x, y }) {
          this.x = x
          this.y = y
        }
        Compute.prototype = {
          constructor: Compute,
          plus: function () {
            return this.x + this.y
          },
          minus: function () {
            return this.x - this.y
          },
          mul: function () {
            return this.x * this.y
          },
          div: function () {
            return this.x / this.y
          }
        }
        window.Compute = Compute
      })()
      const compute = new Compute({ x: 1, y: 2 })
      console.log(Compute.prototype, compute.__proto__)
      console.log(compute.minus())
      console.log(compute.mul())
      console.log(compute.div())

七、非匿名自执行函数的函数名是只读的

1、自执行函数中操作全局变量

      var b = 10
      ;(function () {
        b = 20
        console.log(b) // 20
      })()
      console.log(b) // 20

2、自执行函数中暗示全局变量

      ;(function () {
        b = 20
        console.log(b) // 20
      })()
      console.log(b) // 20

3、自执行函数如果有函数名,那么这个名字在函数执行的过程中不可以更改

      var b = 10
      ;(function b() {
        b = 20 // 无效
        console.log(b) // ƒ b(){}
      })()
      console.log(b) // 10

预编译阶段,b先是undefined,再是函数b。由于函数b是自执行函数,那么在整个代码执行的过程中,给b赋值的操作就自动被忽略

4、你会不会觉得既然我用了自执行函数了,那为什么我还要设置函数名?其实在严格模式下arguments.callee是不可以使用的,这个时候就只能给函数设置名字