JavaScript匿名函数以及括号运算符

先来看下这道面试题,写这篇博客也是由这道题引起的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var x = 20
var a = {
x: 15,
fn: function () {
var x = 30
return function () {
return this.x
}
}
}
console.log(a.fn())
console.log((a.fn())())
console.log(a.fn()())
console.log(a.fn()() == (a.fn())())
console.log(a.fn().call(this))
console.log(a.fn().call(a))

如果你对这道题完全理解,没有任何疑惑,那么无须继续往下阅读了

匿名函数,意指没有名字的函数,常出现于对象方法、自执行函数、函数回调以及高阶函数(以函数为参数,或以函数为返回值)中,是函数的一种特殊形式。

从文字上可能不太好理解,直接看下面几个示例

对象方法

将一个匿名函数赋值给对象的sayHello属性

1
2
3
4
5
var obj = {
sayHello: function () {
console.log('Hello world!')
}
}

自执行函数

通过括号运算符将匿名函数包括起来,转换成函数表达式,然后再通过括号运算符来调用匿名函数

当声明定义一个函数后,函数是一种对象,存储在堆内存中,函数名其实就是指向函数体的指针,当函数名后直接添加括号时,该函数将会被调用。

1
2
3
4
5
6
7
8
(function () {
console.log('Hello world!')
})()
// 另一种写法,执行结果与上述没有差别
(function () {
console.log('Hello world!')
}())

注意:所有自执行函数都是全局的,也就是说自执行函数内部的this指针指向的是全局对象 window 。

函数回调

在函数回调的情况中,回调的函数有两种,其一是函数有函数名,另外就是匿名函数。

1
2
3
4
5
6
7
8
9
// 回调函数有函数名
$('#btn').click(function sayHello () {
console.log('Hello world!')
})
// 回调函数是匿名函数
$('#btn').click(function () {
console.log('Hello world!')
})

高阶函数

在高阶函数中,匿名函数常常被当做函数返回值,当返回一个匿名函数,匿名函数中又调用了它外层函数的变量时,就会形成闭包。

闭包,就是存在多个嵌套函数时,内层函数能够调用外层函数中的变量。

1
2
3
4
5
6
7
8
9
10
var fn = function () {
var outterWords = 'Hello outter'
// console.log(innerWords)
return function () {
var innerWords = 'Hello inner'
console.log(outterWords)
}
}
console.log(fn()) // 'Hello outter'

在上述代码中,嵌套了两层函数,内层函数能够访问到外层函数作用域中的变量 outterWords ,但是外层函数不能访问内层函数作用域中的 innerWords ,这就是闭包的一种情形。
闭包在JavaScript中是非常重要的,要讲的东西还有很多,这里不再展开,还不清楚的小伙伴可以上网查找相关资料。

这里推荐一个很赞的系列博客文章:深入理解JavaScript原型和闭包

回到高阶函数,上述代码中,其实也就是一个高阶函数,它将一个匿名函数当作返回值返回。

介绍完这几种匿名函数的形式,我们回到文章开头的那道面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var x = 20
var a = {
x: 15,
fn: function () {
var x = 30
return function () {
return this.x
}
}
}
console.log(a.fn())
console.log((a.fn())())
console.log(a.fn()())
// a.fn()返回的值是函数表达式,类似于加了一层括号,然后再进行括号的调用,相当于自执行函数
console.log(a.fn()() == (a.fn())())
console.log(a.fn().call(this))
console.log(a.fn().call(a))

一步步解析:

1
console.log(a.fn())

这部分相信大家都知道,打印的是 a.fn() 返回的匿名函数的函数体

1
2
3
function () {
return this.x
}

接着往下:

1
console.log((a.fn())())

这部分我们首先要注意到,这里用到了两次括号运算符,第一次是将 a.fn() 返回的函数体转换为函数表达式,第二次则是调用这个函数表达式,也就是转换成了自执行函数,自执行函数的 this 指针指向的是全局对象 window 。
所以输出结果为 20

1
console.log(a.fn()())

这部分我一开始也是比较困惑的,因为函数体后面直接加上括号运算符,是会报语法错误的,如下所示:

1
2
3
4
function () {
return this.x
}()
// Uncaught SyntaxError: Unexpected token (

但是,当一个函数返回一个匿名函数,然后再加上括号运算符,则能够正常执行输出的。
其实,当函数返回匿名函数时,返回的是函数表达式,函数表达式后加上括号运算符,则该函数会被调用,函数内部的 this 指向全局对象 window 。所以输出20

1
2
console.log(a.fn()())
// 输出20

知道了前两个的输出,那么自然第三个也明白了,输出 true 。

1
console.log(a.fn().call(this))

这部分主要是 call 方法的运用。call 与 apply 方法的作用是更改函数调用的对象。
上述代码中,this 指向的是全局变量 window ,调用a.fn()返回的匿名函数,输出的是全局的变量x: 20 。

1
console.log(a.fn().call(a))

这部分函数调用的对象为 a ,所以输出的是a内部的变量x: 15 。

扩展延伸:

1
2
3
4
5
6
7
8
9
10
var scope = 'global scope';
function checkScope () {
var scope = 'local scope';
function f() {
return scope;
}
return f;
}
checkScope()();

可以看到这道题与上面那道题的第三个输出是有点相似,区别在于这道题返回的 scope 没有绑定在this指针上,而上面那道题是 this.x 。那么它们的输出结果是一样的吗?答案是否定的。

1
2
3
4
5
6
7
8
9
10
var scope = 'global scope';
function checkScope () {
var scope = 'local scope';
function f() {
return scope;
}
return f;
}
checkScope()(); // 输出'local scope'

区别就是在于有没有 this 指针。我们知道当一个函数返回一个函数体后,加上括号,会被当做函数表达式执行,函数表达式内部的 this 指针是指向全局变量的。如果没有 this 指针,那么执行返回的变量 scope ,首先会顺着作用域链去寻找,也就是外层函数中寻找,找到了 scope ,所以输出’local scope’。有 this 指针,则会在 this 指针指向的对象上去寻找 scope 变量,所以如果这道题是

1
return this.scope

输出的就是 ‘global scope’

总结

这篇文章讲述了匿名函数的几种应用场景:对象方法、自执行函数、函数回调以及高阶函数。
最应该牢记的匿名函数的执行条件:将匿名函数通过 void , ~ , + ,- ,! ,() 等形式来转化为函数表达式,然后通过 () 调用。

1
2
3
void function () {
console.log('Hello world!')
}()

如果匿名函数没有任何关键字、运算符,那么就会报错。
在一个函数中返回一个函数体,加上括号(),将变成函数表达式,如果这个函数表达式中返回一个普通变量,那么这个变量会在它的作用域链中去寻找;
如果这个变量是绑定在 this 指针上,那么会在 this 指向的对象上寻找,如果在对象上找不到,则会顺着这个对象的原型链去寻找。

坚持原创技术分享,您的支持将鼓励我继续创作!