函数 是可以在指定上下文中执行代码的 特殊对象 。
函数本质上是一个对象,是一个数据类型为 Object 的数据。
函数是类型 Function 的实例。
主要参考资料:
- 《JavaScript 高级程序设计(第4版)》- P287(312/931)
定义函数
定义函数的方式有 5 种:
- 函数声明
- 函数表达式
- 箭头函数
- 立即调用函数表达式
- new Function() (不推荐使用)
函数的主要形式有三种:
- 命名函数(使用函数声明定义)
- 匿名函数(使用函数表达式定义)
- 箭头函数
标准函数
使用关键字 function 定义的函数,被称为标准函数 。
函数声明
函数声明 ,即使用关键字 function 定义一个具名的函数。
使用函数声明定义的函数,被称为命名函数 。
函数的函数名是指向函数对象的指针,是引用函数(对象)的变量。
命名函数的定义会被提升,这个过程被称为函数声明提升。
在执行代码时,JavaScript 引擎会将函数声明提升到源代码树的顶部。
函数拥有一个属性 name ,值为字符串,表示函数名。
示例:
-
使用函数声明定义命名函数。
function namedFunc() { console.log('named function') } // 使用函数声明定义命名函数 const func = namedFunc // 函数是一个对象,可以进行赋值。 console.log('function name:', func.name) namedFunc() func() // 输出: // function name: namedFunc // named function // named function
-
函数声明提升。
console.log(func(19, 0.48)) // 函数声明提升(不建议这样使用) function func(num_01, num_02) { return num_01 + num_02 } // 输出: // 19.48
函数表达式
函数表达式 ,即以表达式的形式使用关键字 function 定义一个函数。
使用函数表达式定义函数时,函数名是可选的:
- 使用函数表达式定义的一个没有名字函数,被称为 匿名函数 。
- 使用函数表达式定义一个具名函数,被称为 命名函数表达式(Named Function Expression) 。
其实当匿名函数第一次被赋值给某个标识符时,匿名函数将拥有函数名,函数名就是该标识符。
函数表达式可以立即调用刚定义的函数,被称为 立即调用函数表达式(IIFE,Immediately Invoked Function Expression) 。
需要对立即调用函数表达式使用括号操作符
()
,将函数表达式包裹起来进行调用。
示例:
-
使用函数表达式定义匿名函数。
const anonymousFunc = function() { console.log('anonymous function') } // 使用函数表达式定义匿名函数 console.log('function name:', anonymousFunc.name) anonymousFunc() // 输出: // function name: anonymousFunc // anonymous function
-
命名函数表达式。
const func = function named() { console.log('named function experession') } // 命名函数表达式 console.log('function name:', func.name) func() // 输出: // function name: named // named function experession
-
立即调用函数表达式
const returnedValue = (function() { return 'immediately invoked function expression' })() // 立即调用函数表达式 console.log('returnedValue:', returnedValue) // 输出: // returnedValue: immediately invoked function expression
箭头函数
ES6 新增了箭头函数(Arrow Function) 。
箭头函数是一个值为函数对象的表达式。
定义箭头函数的同时会根据函数的定义创建一个函数对象。
示例:
- 定义箭头函数。
const arrowFunc = () => { console.log('arrow function') } // 定义箭头函数 console.log('function name:', arrowFunc.name) arrowFunc() // 输出: // function name: arrowFunc // arrow function
箭头函数在一些情况下,可以简写:
- 只有一个参数,且没有设置默认值。
可以省略包裹参数的括号
()
。 - 函数体中只有一条语句或者一个表达式。
可以省略包裹函数体的花括号
{}
,而且箭头函数会隐式返回语句或表达式的值。
示例:
-
只有一个参数,且没有设置默认值。
const func = num => { return num + 20.21 } // 省略括号 ()
-
函数体中只有一条语句或一个表达式。
const func_01 = (str) => console.log(str) // 只有一条语句 const func_02 = (num_01, num_02) => num_01 + num_02 // 只有一个表达式 func_01('string') console.log(func_02(20, 0.49)) // 输出 // string // 20.49
参数
ECMAScript 的函数不关注函数参数的个数和数据类型。
函数的参数在内部表现为一个数组。
函数被调用时会接收一个参数数组,但函数不关注参数数组中的元素。
调用函数时,可以向函数提供任意个数的参数值。
如果调用函数时,没有向命名参数提供具体的值,那么该命名参数的值为 undefined 。
函数的参数是按值传递的。
示例:
- 为函数提供任意个数的参数值。
const func = num => console.log('num:', num) func(20.76, 'arg_02', 'arg_03') // 提供三个参数值 func() // 没有提供参数值 // 输出: // num: 20.76 // num: undefined
默认参数
ECMAScript 从 ES6 开始支持显示定义默认参数。
默认参数,即拥有一个默认值的参数。
在调用函数时:
- 如果没有向默认参数提供具体的值,则默认参数的值为默认值。
- 如果向默认参数提供 undefined 值,则默认参数的值为默认值。
- 如果向默认参数提供除 undefined 之外的值,则默认参数的值为提供的值。
默认参数会按照定义时的顺序依次被初始化,后定义的默认参数可以引用先定义的默认参数。
示例:
-
为函数参数设置默认值。
const func = (str = 'string') => console.log(str) // 默认参数 func() func('new string') // 输出: // string // new string
-
向默认参数提供 undefined 值。
const func = (str_01 = 'string_01', str_02 = 'string_02') => { console.log('str_01:', str_01) console.log('str_02:', str_02) } func(undefined, 'new string') // 输出: // str_01: string_01 // str_02: new string
-
后定义的默认参数引用先定义的默认参数。
const func = (str = 'string', str_01 = str + '_01') => { // 引用先定义的默认参数 console.log('str:', str) console.log('str_01:', str_01) } func() // 输出: // str: string // str_01: string_01
参数的收集与扩展
ES6 新增了扩展操作符 ...
,可以解构和组织数据。
分别在定义函数参数和函数传参时使用扩展操作符,可以收集和扩展参数。
收集参数
在定义函数参数时,对最后的参数使用扩展操作符,可以将超出命名参数个数的参数组织到一个数组中,并将该数组作为最后参数的值。
示例:
const func = (str_01, ...strs) => { // 收集参数
console.log(str_01)
for(const str of strs) {
console.log(str)
}
}
func('str_01', 'str_02', 'str_03', 'str_04')
// 输出:
// str_01
// str_02
// str_03
// str_04
扩展参数
在函数传参时,对可迭代对象使用扩展操作符,可以将可迭代对象中的元素提取出来,作为函数的参数。
在使用扩展操作符传参时,不妨碍在其前后传入其它参数。
示例:
- 在函数传参时,对可迭代对象使用扩展操作符。
const func = (num_01, num_02, num_03) => console.log(num_01 + num_02 + num_03)
const nums = [20, 0.9, 0.01]
func(...nums) // 扩展参数
// 输出:
// 20.91
- 在使用扩展操作符传参的同时,传入其它参数。
const func = (num, str_01, str_02, bool) => { console.log('num:', num) console.log('str_01:', str_01) console.log('str_02:', str_02) console.log('bool:', bool) } const strs = ['string_01', 'string_02'] func(1, ...strs, true) // 传入其它参数 // 输出: // num: 1 // str_01: string_01 // str_02: string_02 // bool: true
引用外部变量
ECMAScript 的函数可以在函数体中使用在函数体外声明的变量。
示例:
- 函数使用在函数体外定义的变量。
let num = 20 const func = () => num + 0.21 // 使用在函数体外定义的变量 console.log(func()) // 输出: // 20.21
内部属性
函数拥有一些内部属性,可以在函数体中使用。
对象 argument
只有标准函数可以使用对象 arguments ,箭头函数不能使用对象 arguments 。
对象 arguments 是一个类数组对象(类似数组的对象),用于存储调用函数时传入的参数值。
对象 arguments 的元素是调用函数时传入的参数值,可以使用中括号
[]
访问对象中的元素。使用 arguments.length 可以获取传入参数的个数。
即使定义函数时没有命名参数,对象 arguments 也会存储调用函数时传入的参数值。
对象 arguments 只存储调用函数时传入的参数值,不存储默认参数的默认值。
示例:
-
使用对象 arguments 获取传入的参数值。
const func = function() { // 没有命名参数 console.log('arguments[0]:', arguments[0]) // 使用对象 arguments 获取传入的参数值 console.log('arguments[1]:', arguments[1]) console.log('count of arg:', arguments.length) // 传入参数的个数 } func('arg_01', 'arg_02') // 输出: // arguments[0]: arg_01 // arguments[1]: arg_02 // count of arg: 2
-
对象 arguments 不存储默认参数的默认值。
const func = function(num, str = 'string') { console.log('arguments[0]:', arguments[0]) console.log('arguments[1]:', arguments[1]) } func(21.08) // 输出: // arguments[0]: 21.08 // arguments[1]: undefined
对象 this
对象 this 在标准函数和箭头函数中有不同的行为:
-
在标准函数中,对象 this 引用将当前函数作为方法调用的上下文对象,即对象 this 引用的对象只有在当前函数被调用时才会确定。
在严格模式下,在全局作用域中调用标准函数,函数的对象 this 的值为 undefined 。
-
在箭头函数中,对象 this 引用定义当前函数的上下文对象。
在严格模式下,在全局作用域中定义箭头函数,函数的对象 this 的值为 undefined 。
示例:
-
标准函数中的对象 this 。
const obj_01 = { key: 'obj_01', func: function() { console.log('this.key:', this.key) } } const obj_02 = { key: 'obj_02', func: obj_01.func // 使对象 obj_02 的方法 func 成为对象 obj_02 的方法 } obj_01.func() // 以对象 obj_01 的方法的形式调用函数 func obj_02.func() // 以对象 obj_02 的方法的形式调用函数 func // 输出: // this.key: obj_01 // this.key: obj_02
-
箭头函数中的对象 this 。
const obj_01 = { key: 'obj_01', getFunc: function() { return () => { console.log('this.key:', this.key) } // 定义箭头函数 } } const obj_02 = { key: 'obj_02', func: null } obj_02.func = obj_01.getFunc() // 对象 obj_02 获取在对象 obj_01 中定义的箭头函数。 obj_02.func() // 对象 obj_02 调用箭头函数 func // 输出: // this.key: obj_01
属性和方法
函数自身拥有一些属性和方法
属性 length
函数的属性 length 存储了函数定义的命名参数的个数。
示例:
const func = (num, str) => console.log(str, num)
console.log('named argument count:', func.length)
// 输出:
// named argument count: 2
方法 apply()
函数的方法 apply() :
-
功能:
以使当前函数的对象 this 引用指定对象的方式,调用当前函数,并以数组的形式,向当前函数传参。(不能更改箭头函数的对象 this ) -
接收两个参数:
-
对象
目标对象。 -
数组 | 函数内部对象 arguments
对象会被解包,提取其中的元素作为当前函数的参数值。
-
示例:
-
使用数组传参。
const obj_01 = { key: 'obj_01', log: function(num_01, num_02) { console.log('this.key:', this.key) console.log('sum:', num_01 + num_02) } } const obj_02 = { key: 'obj_02' } const nums = [19, 0.48] obj_01.log.apply(obj_02, nums) // 使用函数的方法 apply(),使用数组传参 // 输出: // this.key: obj_02 // sum: 19.48
-
使用对象 arguments 传参。
const obj_01 = { key: 'obj_01', log: function(num_01, num_02) { console.log('this.key:', this.key) console.log('sum:', num_01 + num_02) } } const obj_02 = { key: 'obj_02' } const func = function(num_01, num_02) { obj_01.log.apply(obj_02, arguments) // 使用函数的方法 apply(),使用对象 arguments 传参 } func(20, 0.76) // 输出: // this.key: obj_02 // sum: 20.76
方法 call()
函数的方法 call() :
-
功能:
以使当前函数的对象 this 引用指定对象的方式,调用当前函数,并以逐个传参的形式,向当前函数传参。(不能更改箭头函数的对象 this ) -
接收一个参数:
对象,目标对象。 -
更多参数:
作为当前函数的参数值。
示例:
const obj_01 = {
key: 'obj_01',
log: function(num_01, num_02) {
console.log('this.key:', this.key)
console.log('sum:', num_01 + num_02)
}
}
const obj_02 = {
key: 'obj_02'
}
obj_01.log.call(obj_02, 20, 0.35) // 使用函数的方法 call()
// 输出:
// this.key: obj_02
// sum: 20.35
方法 bind()
函数的方法 bind() :
-
功能:
创建一个作为当前函数的副本的、对象 this 引用指定对象的函数。(不能更改箭头函数的对象 this ) -
接收一个参数:
对象,目标对象。 -
返回值:
函数,作为当前函数的副本的、对象 this 引用目标对象的函数。
示例::
const obj_01 = {
key: 'obj_01',
log: function(num_01, num_02) {
console.log('this.key:', this.key)
console.log('sum:', num_01 + num_02)
}
}
const obj_02 = {
key: 'obj_02'
}
const func = obj_01.log.bind(obj_02) // 使用函数的方法 bind()
console.log('func === obj_01.log:', func === obj_01.log)
func(20, 0.45)
// 输出:
// func === obj_01.log: false
// this.key: obj_02
// sum: 20.45
递归
使用命名函数实现递归会有失败的风险:
在命名函数中使用函数名调用命名函数自身可以简单地实现递归。
但当函数名被赋值为 null 后,再调用命名函数,命名函数再也无法通过函数名引用自身,因为函数名的引用为 null 。此时,递归就失效了。
示例:
-
在命名函数中调用命名函数自身。
function factorial(num) { if(num <= 1) { return 1 } else { return num * factorial(num - 1) } } // 阶乘 const func = factorial console.log(func(10)) // 输出: // 3628800
-
将函数名赋值为 null 。
function factorial(num) { if(num <= 1) { return 1 } else { return num * factorial(num - 1) } } const func = factorial factorial = null // 将 factorial 赋值为 null 后,命名函数将失效。 console.log(func(10)) // 报错: // Uncaught TypeError: factorial is not a function
这时需要使用命名函数表达式来解决这个问题。
命名函数表达式使得命名函数的函数名没有被赋值为 null 的可能性。
示例:
- 使用命名函数表达式实现递归。
const func = function factorial(num) { if(num <= 1) { return 1 } else { return num * factorial(num - 1) } } console.log(func(5)) // 输出: // 120
闭包
闭包(Closure) 指的是在函数定义中使用了在包含函数中声明的变量的嵌套定义函数。
闭包调用的作用域链中包含指向包含函数的变量对象的指针,这意味着当包含函数调用执行完毕后,会保留包含函数的变量对象,直到闭包调用执行完毕。
所以闭包函数在执行时比其它函数更占用内存。
过渡使用闭包可能导致内存过渡占用,因此建议在使用闭包时要谨慎,建议仅在必要的时候使用闭包。
对象 this
在函数形式为标准函数的闭包中,单纯地使用对象 this 可能无法正确引用包含函数中的变量。
示例:
const obj_01 = {
key: 'obj_01',
getFunc() {
return function() {
return this.key
} // 函数形式为标准函数的闭包
}
}
const obj_02 = {
key: 'obj_02',
func: null
}
obj_02.func = obj_01.getFunc()
console.log('this.key:', obj_02.func())
// 输出:
// this.key: obj_02
需要创建一个变量存储包含函数的对象 this 的引用,然后闭包使用这个变量正确地获取包含函数中的变量。
示例:
const obj_01 = {
key: 'obj_01',
getFunc() {
const that = this // 存储包含函数的对象 this 的引用
return function() {
return that.key //使用变量 that 获取包含函数中的变量
}
}
}
const obj_02 = {
key: 'obj_02',
func: null
}
obj_02.func = obj_01.getFunc()
console.log('this.key:', obj_02.func())
// 输出:
// this.key: obj_01
尾调用优化
ES6 新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。
栈帧 用于存储一个函数执行的环境。
栈帧保存了该函数的返回地址和局部变量等信息,每个未运行完的函数对应着一个栈帧。
尾调用 ,即包含函数的返回值是对嵌套函数的调用的函数调用方式,或者说包含函数的返回值是嵌套函数的返回值。
示例:
- 尾调用。
function inner() { return 'inner' } function outer() { return inner() } // 尾调用
执行上的例子,会在内存中发生如下操作:
-
在 ES6 优化之前:
- 执行函数 outer ,函数 outer() 的栈帧被推到栈。
- 执行函数 outer 的 return 语句,需要计算函数 inner() 。
- 执行函数 inner() ,函数 inner() 的栈帧被推到栈。
- 执行函数 inner() 的 return 语句,返回值 ‘inner’ ,函数 inner() 的栈帧被弹出栈。
- 回到函数 outer 的 return 语句,返回值 ‘inner’ ,函数 outer() 的栈帧被弹出栈。
-
在 ES6 优化之后:
- 执行函数 outer ,函数 outer() 的栈帧被推到栈。
- 执行函数 outer 的 return 语句,需要计算函数 inner() 。
- JavaScript 引擎发现,此时把函数 outer() 的栈帧弹出栈不会有什么影响,因此函数 outer() 的栈帧被弹出栈。
- 执行函数 inner() ,函数 inner() 的栈帧被推到栈。
- 执行函数 inner() 的 return 语句,返回值 ‘inner’ ,函数 inner() 的栈帧被弹出栈。
可以明显看出,在 ES6 优化之前,每多调用一次嵌套函数,就会增加一个栈帧。而在 ES6 优化之后,无论有多少类似的嵌套函数调用,都只有一个栈帧。
尾调用优化的触发条件:
- 使用严格模式。
- 包含函数的返回值仅仅是对嵌套函数的调用,没有任何额外的操作。
- 嵌套函数不是使用在包含函数中定义的变量的闭包。
示例:
-
不触发尾调用优化。
function outer_01() { return inner_01().toString() // 嵌套函数返回后需要执行函数 toString() } function outer_02() { let str = 'string' // 嵌套函数是一个闭包 function inner_02() { return str + 'inner_02' } return inner_02() }
-
触发尾调用优化。
function outer_01(a, b) { return inner_01(a + b) // 给嵌套函数传参,并在执行嵌套函数前完成参数的计算 } function outer_02(bool) { return bool ? inner_02() : inner_03() // 使用三元表达式 }