第10章 函数
定义函数的几种方式:
- 函数声明
- 函数表达式
- 箭头函数
- Function构造函数(不推荐)
# 箭头函数
箭头函数由于其极简语法而受人喜欢,此外this穿透的特性也令人称道。
不过有很多场合不适用,原因如下:
- 不能使用arguments、super和new.target
- 没有自己的this,不能用作构造函数
- 没有prototype属性
# 函数名
所有函数对象都会暴露一个只读的name属性,它保存的是一个函数标识符,一般情况下,是函数的变量名。特殊情况有
- Function构造函数创建
- 箭头函数
- foo.bind().name
- getter,setter函数
# 函数的参数
函数不关心传入的参数个数和数据类型,解释器非常宽容。原因在于函数内部的参数是一个类数组,即arguments
,函数调用时会访问arguments对象,从中取得传进来的每个参数值。
注意,箭头函数内无法使用arguments关键字获取到参数。
const foo = function () {
console.log(arguments[0])
}
foo(1) // 1
const bar = () => {
console.log(arguments[0])
}
bar(1) // {}
2
3
4
5
6
7
8
9
10
# 没有重载
在Java中,一个函数可以有两个定义,质押签名(接收参数的类型和数量)不同就行。ECMAScript中函数没有签名,自然也没有重载。
如果定义了2个同名函数,则后定义的会覆盖先定义的。但不建议使用这样的编程方式,每个函数最好有其准确的命名,传参,返回,并说明用途。
# 默认参数值
ES6之后,可以显示定义默认参数。如
function add(a, b = 1) {
return a + b
}
console.log(add(1)) // 2
console.log(add(1, 10)) // 11
2
3
4
5
6
此外,默认参数值不限于原始值或对象类型,也可以使用调用函数返回的值。并且只有在函数被调用时才会求值,不会在函数定义时求值,
let count = 1
function countAdd() {
return count++
}
function add(a, b = countAdd()) {
return a + b
}
console.log('count', count) // 1
console.log(add(1)) // 2
console.log('count', count) // 2
console.log(add(1)) // 3
console.log('count', count) // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
# 默认参数作用域与暂时性死区
默认参数等同于在函数顶部使用let关键字声明了变量,因此也存在暂时性死区。
暂时性死区:即前面定义的参数不能引用后面的定义的。
function add1(a, b = 1, c = b) {
return a + b + c
}
console.log(add1(1)) // 3
// ---------------------------
function add2(a, b = c, c = 1) {
return a + b + c
}
console.log(add2(1)) // 报错
2
3
4
5
6
7
8
9
10
11
12
13
# 参数扩展与收集
# 扩展参数
es6之前,通过apply方法来扩展参数
getSum.apply(null,[1,2,3])
有了扩展运算符之后,这个操作就非常简便了。
getSum(...[1,2,3])
getSum(...[1,2],...[3,4])
2
# 收集参数
函数内进行参数收集时,也可以使用扩展运算符。
function getSum(...values){}
注意,收集参数的前面如果还有其他参数,则只会收集其余的参数。
function getSum(...values,lastValue){} // 错误
function getSum(firstValue,...values){} // 正确
2
箭头函数不支持arguments对象,但支持收集参数。
const getSum = (...values)=>{}
# 函数声明与函数表达式
函数声明的特点在于,存在函数声明提升。
console.log(sum(10,10))
function sum(a,b){
return a+b
}
2
3
4
注意eslint规则不推荐在函数定义之前调用。
函数表达式则不能在定义之前调用。
# 函数作为值
也叫做回调函数。因为函数名就是变量,所以函数可以用在任何使用变量的地方。
回调的最大用处在于可以处理异步任务或复杂流程。
# 函数内部
# arguments
arguments.callee 表示arguments对象所在的函数指针,即函数名。通常在递归函数中,我们会写同名的函数调用,此时可以使用arguments.callee代替,达到解耦的目的。
function dfs(num) {
if (num <= 1) {
return 1
} else {
return num + dfs(num - 1)
}
}
console.log(dfs(4)) // 10
let fn = dfs
dfs = null
console.log(fn(4)) // 报错
2
3
4
5
6
7
8
9
10
11
12
13
14
// 只需要改一行代码就不会报错了
...
return num + arguments.callee(num - 1)
...
2
3
4
# this
this通常指调用函数的对象,即不是看函数在哪里定义,而是看调用时函数所属哪个对象。
如果函数不属于任何对象,则该this指向window。
可以通过call,apply,bind来改变this指向。
# caller
caller指向的是调用当前函数的函数,如果是全局作用域中调用,则是null。
function outer() {
inner()
}
function inner() {
console.log(inner.caller) // [Function: outer]
}
outer()
2
3
4
5
6
7
8
9
# new.target
用于检测函数是否使用new关键字调用。如果是,则new.target指向被调用的构造函数,如果否,则为undefined。
function Target() {
console.log(new.target)
}
new Target()
2
3
4
# 函数属性与方法
# length
该属性保存函数定义的命名参数的个数。
function add1(num1, num2) {}
function add2(num1, num2, num3) {}
console.log(add1.length) // 2
console.log(add2.length) // 3
2
3
4
# prototype
该属性保存引用类型所有实例的方法,如toString()、valueOf()等。
# apply(thisArg,values)
第一个参数是函数内部的this指向thisArg,第二个参数是参数数组。
# call(thisArg,value1,value2,...)
用法同apply,区别在于参数是一个个传入的。
# bind(thisArg)
也可以改变this指向,但不会调用函数,而是返回一个新的函数实例。
# 尾调用优化
尾调用,即外部函数的返回值是一个内部函数的返回值。比如
function outer(){
return inner()
}
2
3
在es6种,新增了内存管理优化机制,来进行尾调用优化,即无论调用多少次嵌套函数,都只有一个栈帧,这样就不会导致栈内存溢出。
# 尾调用优化的条件
下面的条件缺一不可
- 代码在严格模式下进行。
- 外部函数的返回值是对尾调用函数的调用。
- 尾调用函数返回后不需要执行额外的逻辑。
- 尾调用函数不是引用外部函数作用域中自由变量的闭包。
下面是几个不符合尾调用优化的案例:
// 无优化:尾调用没有返回
function outerFunction() {
innerFunction()
}
// 无优化:尾调用没有直接返回
function outerFunction() {
let inner = innerFunction()
return inner
}
// 无优化:尾调用返回后转为字符串
function outerFunction() {
return innerFunction().toString()
}
// 无优化:尾调用是一个闭包
function outerFunction() {
let foo = 'bar'
function innerFunction() {
return foo
}
return innerFunction()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
下面是几个符合尾调用优化的案例:
'use strict'
function outerFunction(a, b) {
return innerFunction(a + b)
}
function outerFunction(a, b) {
if (a < b) {
return a
}
return innerFunction(a + b)
}
function outerFunction(condition) {
return condition ? innerFunctionA() : innerFunctionB()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 尾调用优化案例
下面通过改写斐波那契数列来书写尾调用优化。
普通递归实现:
function fib(n) {
if (n <= 2) return 1
return fib(n - 2) + fib(n - 1)
}
// 1 1 2 3 5 8
console.log(fib(6)) // 8
2
3
4
5
6
7
8
然而传入60,就爆栈了。
下面是尾调用优化实现。
function fib(n) {
return fibImpl(0, 1, n)
}
function fibImpl(a, b, n) {
if (n === 0) {
return a
}
return fibImpl(b, a + b, n - 1)
}
console.log(fib(60)) // 1548008755920
2
3
4
5
6
7
8
9
10
11
# 闭包
闭包是指那些引用了另一个函数作用域中变量的函数。经典写法是
function outer() {
let foo = 'bar'
let inner = function () {
return 'this is ' + foo
}
return inner
}
2
3
4
5
6
7
如何理解闭包呢?可以从作用域链的创建和使用进行理解。
函数调用时会产生执行上下文和作用域链,同时会产生一个活动对象,包括该函数的arguments和实参。
除了活动对象,还有一个全局的变量对象,它贯穿全局执行上下文,直到代码全部执行完毕。
对比下普通函数和闭包的作用域链。
// 普通函数
function add(value1, value2) {
return value1 + value2
}
let result = add(1, 2)
2
3
4
5
add()函数是在全局上下文中调用的,关系如下:
- 函数活动对象:arguments、value1、value2。只在函数执行期间存在,函数执行完毕后,函数活动对象被销毁。
- 全局变量对象:add、result、this。在代码执行期间始终存在。
下面看看闭包的作用域链。
// 闭包
function createCompare(propertyName) {
return function (obj1, obj2) {
let value1 = obj1[propertyName]
let value2 = obj2[propertyName]
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
}
let compare = createCompare('name')
let result = compare({name:'nick'},{name:'matt'})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
函数可以创建作用域,函数内部又定义了函数时,内部函数会把外部函数的活动对象添加到自己的作用域链中,即我们常说的函数内部可以访问外层的变量,但外部函数不能访问内层的变量,也即变量搜索原则。
createCompare的作用域链关系如下:
- 全局变量对象:closure、result
- 函数活动对象:arguments、propertyName
匿名函数的作用域链关系如下:
- 全局变量对象:closure、result(同上)
- 函数活动对象:arguments、propertyName(同上)
- 闭包活动对象:arguments、obj1、obj2
闭包的副作用是,createCompare函数执行完毕后,由于匿名函数保持对活动对象的引用,导致活动对象不能销毁。只有匿名函数被销毁时,createCompare活动对象才会销毁。
// 解除对函数的引用,就可以释放内存了
compare = null
2
所以,回到闭包定义,必须是函数内的内部函数,访问或者使用了外层函数中的变量(参数或函数内定义的变量),然后该内部函数持续被某处引用,导致被访问的变量被困在闭包中,无法被垃圾回收机制回收。
# 闭包中的this
箭头函数中的this会穿透作用域,即this指向箭头函数所在作用域的this。
普通函数调用时,有如下表现:
- 如果在全局函数中调用,则this在非严格模式下等于window,严格模式下是undefined。
- 如果作为某个对象的方法调用,则this等于这个对象。
- 匿名函数没有绑定对象,this等于window。
区别下面3个场景:
window.identity = 'The Window'
let object = {
identity: 'My Object',
getIdentityFunc() {
return this.identity
},
}
console.log(object.getIdentityFunc()) // My Object
// =====================================
window.identity = 'The Window'
let object = {
identity: 'My Object',
getIdentityFunc() {
return function(){
return this.identity
}
},
}
console.log(object.getIdentityFunc()()) // The Window
// =====================================
window.identity = 'The Window'
let object = {
identity: 'My Object',
getIdentityFunc() {
let that = this
return function(){
return that.identity
}
},
}
console.log(object.getIdentityFunc()()) // My Object
// =====================================
window.identity = 'The Window'
let object = {
identity: 'My Object',
getIdentityFunc() {
return () => {
return this.identity
}
},
}
console.log(object.getIdentityFunc()()) // My Object
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
注意第3种场景,是把this保存到闭包可以访问的变量中,从而可以访问到外层作用域的this。
再来看看下面的例子。
window.identity = 'The Window'
let object = {
identity: 'My Object',
getIdentityFunc() {
return this.identity
},
}
console.log(object.getIdentityFunc()) // My Object
console.log((object.getIdentityFunc)()) // My Object
let getIdentity = object.getIdentityFunc
console.log(getIdentity()) // The Window
2
3
4
5
6
7
8
9
10
11
12
13
14
第二个:规范认为object.getIdentityFunc
和(object.getIdentityFunc)
是相等的。
第三个:真正执行调用的是getIdentity
,它不属于对象方法的调用,所以this指向window。
注意下面这个
console.log((object.getIdentityFunc = object.getIdentityFunc)()) // The Window
执行了一次赋值,再调用赋值后的结果。赋值表达式的值是函数本身,也就是不属于对象的方法了,此时调用,this指向window。
第二和第四个写法基本不会在代码中出现,但要注意js语法稍有不同,也可能影响this的值。
# 内存泄漏
由于闭包中的变量被困在内存中,如果不手动处理释放,是不能被垃圾回收机制回收的,这容易导致内存泄漏。
常见的例子是,在旧版本IE中,把HTML元素保存在闭包中,就宣布了该元素不能被销毁,如下:
function assignHandler(){
let element = document.getElementById('hello')
element.onclick = ()=>console.log(element.id)
}
2
3
4
这就是一个闭包。element元素的事件处理程序创建了一个循环引用。匿名函数引用着assignHandler()的活动对象,阻止了对element的引用计数归零。
这里注意几点:
- 什么是循环引用?我理解这里指的是,输出
element.id
时,程序需要找到element
,发现它是一个DOM节点,这时候就是二次引用了element
。所以解决方式是另外定义一个变量来保存element.id
即可。 - 由于闭包会引用包含函数的活动对象,其中就有
element
,所以完成onclick
事件监听之后,需要设置element
为null,就解除了对DOM对象的引用。
function assignHandler(){
let element = document.getElementById('hello')
let id = element.id
element.onclick = ()=>console.log(id)
element = null
}
2
3
4
5
6
# 立即调用函数表达式
简称IIFE,写法是
;(function () {
// 块级作用域
})()
2
3
在ES6之前,就是用这种方式来模拟块级作用域的。块级作用域内的变量不能被外界访问,很好的实现变量和模块隔离。
下面是ES6的块级作用域。
{ // 大括号 + let/const生成块级作用域
let i = 0
i += 1
console.log(i) // 1
}
console.log(i) // ReferenceError: i is not defined
2
3
4
5
6
7
8
for (let i = 0; i < 3; i++) { // 循环 + let/const生成块级作用域
console.log(i) // 0 1 2
}
console.log(i) // ReferenceError: i is not defined
2
3
4
一个经典的例子是,如何锁住参数值。
for (var i = 0; i < 2; i++) {
setTimeout(() => {
console.log(i) // 2 2
}, 100)
}
for (let i = 0; i < 2; i++) {
setTimeout(() => {
console.log(i) // 0 1
}, 100)
}
2
3
4
5
6
7
8
9
10
11
# 私有变量
严格地说,JS没有私有成员的概念,所有对象属性都是公开的。不过,私有变量是存在的。这是因为有作用域的存在,变量的访问和设置是有规则的。
# 特权方法
利用闭包可以创建特权方法,用来访问函数私有变量和私有函数。有2种方式来创建。
构造函数实现。
function Car(name) { this.getName = function () { return name } this.setName = function (value) { name = value } } const car = new Car('mini') console.log(car.name) // undefined console.log(car.getName()) // mini car.setName('jeep') console.log(car.getName()) // jeep
1
2
3
4
5
6
7
8
9
10
11
12
13
14私有作用域实现。
;(function () { let $ = {} let name = '' $.getName = function () { return name } $.setName = function (value) { name = value } window.$ = $ })(window) window.$.setName('mini') console.log(window.$.getName()) // mini console.log(window.name) // undefined window.$.setName('jeep') console.log(window.$.getName()) // jeep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 模块模式
看看下面的代码结构
let singleton = function () {
// 私有变量和私有函数
let privateVariable = 10
let privateFunction = function () {
return false
}
// 特权方法和属性
return {
publicProperty: true,
publicMethods() {
privateVariable++
return privateFunction()
},
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这就是模块模式,它在单例对象的基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。
举个例子:
let application = function () {
// 私有变量
let components = new Array()
// 初始化
components.push(new BaseComponent())
// 公共接口
return {
getComponentCounts() {
return components.length
},
registerComponent(component) {
if (typeof component === 'object') {
components.push(component)
}
},
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对外暴露的是公共接口,私有变量则无法访问。
此外,还可以在此基础上对其进行增强。
let application = function () {
let components = new Array()
components.push(new BaseComponent())
// 创建局部变量保存实例
let app = new BaseComponent()
app.getComponentCount = function () {
return components.length
}
app.registerComponent = function (component) {
if (typeof component === 'object') {
components.push(component)
}
}
return app
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17