一般来说,函数是一个“子程序”,可以由函数外部(或内部,在递归的情况下)代码调用。与程序本身一样,函数由一系列称为函数体的语句组成。值可以作为参数传递给函数,函数将返回一个值。
在 JavaScript 中,函数是一等对象(first-class objects),因为它们可以传递给其他函数、从函数返回以及分配给变量和属性。它们也可以像任何其他对象一样具有属性和方法。它们与其他对象的区别在于可以调用函数。
函数声明
function
声明创建新函数与给定名称的绑定,还可以使用 function
表达式定义函数。默认情况下,函数是返回 undefined
的。想要返回一个其他的值,函数必须通过一个 return
语句指定返回值。
function add(a, b) {
return a + b
}
function
声明可能出现在一个 if
语句里,但是,这种声明方式在不同的浏览器里可能有不同的效果。因此,不应该在生产环境代码中使用这种声明方式,应该使用函数表达式来代替。
if (false) {
function add(a, b) {
return a + b
}
}
JavaScript 中的 function
声明被提升到封闭函数或全局作用域的顶部,可以在声明之前使用该函数:
hoisted() // 'hoisted'
function hoisted() {
console.log('hoisted')
}
函数表达式
函数表达式可以使用 function
声明,也可以使用 Function
构造函数,也可以使用箭头函数来定义。在函数表达式中可省略函数名称,从而创建匿名函数。
const fn = function () {}
与 function
声明不同,JavaScript 中的函数表达式不会被提升,在创建函数表达式之前不能使用它们。
notHoisted() // TypeError: notHoisted is not a function
let notHoisted = function () {
console.log('notHoisted')
}
如果要在函数体内引用当前函数,则需要创建一个命名函数表达式。该名称仅适用于函数体(作用域)。这可以避免使用已弃用的 arguments.callee
属性来递归调用该函数。
const math = {
factit: function factorial (n) {
return n <= 1 ? 1 : n * factorial(n - 1)
}
}
math.factit(3)
如果函数表达式被命名,则函数的 name
属性将设置为该名称,而不是从语法推断的隐式名称(例如函数分配给的变量)。与 function
声明不同,函数表达式的名称是只读的。
function fn () {
fn = 1
console.log(fn) // 1
}
fn()
!(function fn () {
fn = 1
console.log(fn) // ƒ fn
})()
箭头函数表达式
箭头函数是传统函数表达式的紧凑替代品,更适用于那些本来需要匿名函数的地方,具有一些语义差异和使用方面的故意限制:
- 箭头函数没有自己的
this
、arguments
或super
绑定,因此不应用作方法。 - 箭头函数不能用作构造函数。使用
new
调用它们会抛出TypeError
。他们也无权访问new.target
关键字。 - 箭头函数不能在其主体内使用
yield
,也不能创建为generator
函数。 - 箭头函数的参数和箭头之间不能包含换行符。
const fn = () => {}
箭头函数不会创建自己的 this
,它只会从自己的作用域链的上一层继承 this
。
function Person() {
this.age = 0
setInterval(() => {
++this.age // this -> p: Person {age: 1}
}, 1000)
}
const p = new Person()
鉴于 this
是词法层面上的,严格模式中与 this
相关的规则都将被忽略。
let f = () => {
'use strict'
return this
}
// f() === window
由于箭头函数没有自己的 this
指针,通过 call()
或 apply()
方法调用一个函数时,只能传递参数,他们的第一个参数会被忽略。
const plus = {
base: 1,
add: function (n) {
return ((v) => v + this.base)(n)
},
addThruCall: function (n) {
return ((v) => v + this.base).call({ base: 2 }, n)
}
}
console.log(plus.add(1)) // 2
console.log(plus.addThruCall(1)) // 2
箭头函数不绑定 arguments
对象,支持剩余参数、默认参数和参数内的解构,并且始终需要括号:
const fn = (z, ...[x = 20, y = 30]) => x + y + z
console.log(fn(10)) // 60
console.log(fn(40, 50, 60)) // 150
箭头函数可以通过在表达式前加上 async
关键字来实现 async
。
const asyncFn = async (x) => x * x
asyncFn(10).then(console.log) // 100
箭头函数具有简洁的函数体或通常的块体。在简洁的主体中,仅指定单个表达式,该表达式成为隐式返回值。在块体中,您必须使用显式的 return 语句。使用简洁的正文语法 (params) => { object: literal }
返回对象字面量无法按预期工作。要解决此问题,请将对象字面量括在括号中:
const fn = () => ({ foo: 1 })
尽管箭头函数中的箭头不是运算符,但箭头函数具有特殊的解析规则,与常规函数相比,它们与运算符优先级的交互方式不同。由于 =>
的优先级低于大多数运算符,因此需要括号以避免 callback || ()
被解析为箭头函数的参数列表。
let callback
// callback = callback || () => {}
// SyntaxError: invalid arrow-function arguments
callback = callback || (() => {})
立即调用函数表达式
立即调用函数表达式(IIFE)是一个在定义时就会立即执行的 JavaScript 函数。它是一种设计模式,也被称为自执行匿名函数,主要包含两部分:
- 第一部分是匿名函数,其词法范围包含在
Grouping Operator ()
内。这可以防止访问 IIFE 惯用法中的变量以及污染全局范围。 - 第二部分创建立即调用的函数表达式
()
,JavaScript 引擎将通过该表达式直接解释该函数。
我们可以使用 IIFE 来创建私有和公有变量、方法,避免污染全局命名空间。
!(() => {
let x = 10
})()
IIFE 允许在比较旧的浏览器或者 JavaScript 运行环境没有顶层 await
中使用 await
和 for-await
:
const getFileStream = async (url) => {}
!(async () => {
const stream = await getFileStream('https://domain.com/file.excel')
for await (const chunk of stream) {
console.log({ chunk })
}
})()
在 ES2015 引入 let
和 const
声明和块级作用域之前,通过 var 声明变量,只有函数作用域和全局作用域。
for (var i = 0; i < 2; i++) {
(function(i) {
setTimeout(function() {
console.log(i)
}, 1000)
})(i)
}
构造函数
一个被 function
声明创建的函数是一个 Function
对象,具有 Function
对象的所有属性、方法和行为。直接调用构造函数可以动态创建函数,但会遇到安全性和与 eval()
类似的性能问题。但是,与 eval
不同, Function
构造函数创建仅在全局作用域中执行的函数。
Function('a', 'b', 'return a + b')
除最后一个参数,传递给函数的所有参数都被视为要创建的函数中参数的标识符名称,按照传递的顺序排列。该函数将动态编译为函数表达式,源代码按以下方式组装:
// ƒ anonymous(a,b
// ) {
// return a + b
// }
使用 Function
比使用函数表达式或 function
声明创建函数并在代码中调用它的效率要低,因为此类函数是与代码的其余部分一起解析的。组装源代码的两个动态部分: 参数列表 args.join(',')
和 functionBody
。它们将首先单独解析,以确保它们在语法上都是有效的,可以防止类似注入的尝试。
`ƒ anonymous(${args.join(',')}
) {
${functionBody}
}`
与普通函数表达式不同,名称 anonymous
不会添加到 functionBody
的作用域中,因为 functionBody
只能访问全局作用域。如果 functionBody
不是严格模式,可以使用 arguments.callee
。
闭包
闭包(closure)是一个函数与其周围状态(lexical environment,词法环境)的引用捆绑在一起(封闭)的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
function init () {
let name = 'Mozilla'
return function () {
console.log(name)
}
}
const fn = init()
fn()
实用的闭包
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。因此,通常使用只有一个方法的对象的地方,都可以使用闭包。
function setBaseSize (size) {
return function () {
document.body.style.fontSize = `${size}px`
}
}
document.getElementById('size60').onclick = setBaseSize(60)
document.getElementById('size50').onclick = setBaseSize(50)
我们可以使用闭包来模拟私有方法,私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
const Counter = (function() {
let num = 0
function change (v) {
num += v
}
return {
increment () {
change(1)
},
decrement () {
change(-1)
},
value () {
return num
}
}
})()
Counter.increment()
Counter.increment()
console.log(Counter.value()) // 2
Counter.decrement()
console.log(Counter.value()) // 1
在循环中创建闭包
在 ECMAScript 2015 引入 let
关键字 之前,在循环中有一个常见的闭包创建问题。
var actions = []
function setup () {
for (var i = 0; i < 3; i++) {
actions[i] = function () {
console.log(i)
}
}
}
setup()
for (let i = 0; i < 3; i++) {
actions[i]()
}
结果输出的全是 3
,解决这个问题的一种方案是使用更多的闭包:
// 函数工厂
function insert(action) {
return function () {
console.log(action)
}
}
function setup() {
for (var i = 0; i < 3; i++) {
actions[i] = insert(i)
}
}
工厂函数为每一个回调创建一个新的词法环境,结果正确输出。另外一种解决方案是使用 IIFE:
function setup() {
for (var i = 0; i < 3; i++) {
(function (n) {
actions[n] = function () {
console.log(n)
}
})(i)
}
}
除此之外,还可以利用数组的方法 map
、forEach
的回调函数形成独立的词法环境。ES2015 引入的 let
或 const
关键词可以用来解决这个问题。
性能考量
如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
function P (name) {
this.name = name
this.getName = function () {
return this.name
}
}
上面的代码并没有利用到闭包的好处,相反,当生成实例还会产生更多副本。因此可以避免使用闭包,利用原型继承共享对象方法,但不建议重新定义原型。
function P (name) {
this.name = name
}
P.prototype.getName = function () {
return this.name
}
在用完闭包及时手动解除引用,避免产生内存泄漏。
function fn() {
const o = {
num: 1
}
return function () {
console.log(o.a)
}
}
let f = fn()
f()
f = null