一、什么是自执行函数
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,为什么会这样?
- test函数中的for循环中的变量i由var定义,var会将变量i提升至函数内最顶端,随着for循环执行,i最后的值为10,而将匿名函数添加到arr中,并没有执行,在return arr时,arr中保存的i都是10
var arr = test()
会将test的AO长期保存在内存中,这就意味着它的arr和i在test函数执行完后并没有被销毁- 全局变量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
是不可以使用的,这个时候就只能给函数设置名字