函数
函数
函数概述
每个函数都是 Function
类型的实例,而 Function
也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。
由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。
函数的声明
// function 命令
function namedFunction(x, y) { return x + y }
// 函数表达式
// 函数表达式声明函数时,如果 function 命令后不带有函数名
let expression = function (x, y) { return x + y }
// 函数表达式声明函数时,如果 function 命令后带有函数名
// > 函数名仅在函数体内部有效,可在函数体内部调用自身
// > 方便排查错误。显示函数调用栈时,将显示函数名
let expression = function expression(x, y) {
console.log(typeof expression) // function
return x + y
}
// Function 构造函数 - 不推荐使用
// 使用 Function 构造函数声明函数会被解释两次,会影响性能。
// > 第一次,将其当作常规 ECMAScript 代码
// > 第二次,解释传给构造函数的字符串
let newFunction = new Function('x', 'y', 'return x + y')
// 等同于 function newFunction(x, y) { return x + y }
函数声明提升:函数声明会在任何代码执行之前先被读取并添加到执行上下文。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。
函数重复声明:如果同一个函数被多次声明,后面的声明会覆盖前面的声明。
函数返回:函数体内部的
return
语句,表示返回。如果没有的话,该函数不返回任何值,或者说返回undefined
。函数没有重载,可以借助函数对的
length
属性(返回函数预期传入的参数个数,即函数定义之中的参数个数)以便实现面向对象编程的“方法重载”(overload)。// 使用 function 命令 namedFunction() // 函数声明提升,不会报错 function namedFunction(x, y) { return x + y } // 使用函数表达式 expression() // 报错: Uncaught TypeError: expression is not defined var expression = function (x, y) { return x + y } // 等同于 // var expression // expression() // expression = function (x, y) { return x + y } // 使用 function 命令和 var 赋值语句声明同一个函数时, // 因为存在函数提升,最后会采用 var 赋值语句的定义 var expression = function () { return 'expression' } function expression() { return 'function expression' } expression() // 'expression'
函数的属性和方法
name
属性 :返回函数的函数名。function namedFunction(x, y) { return x + y } namedFunction.name // 'namedFunction' var expression = function (x, y) { return x + y } expression.name // '' ,ES5 环境下返回空字符串 expression.name // 'expression' ,ES6 环境下返回具名函数名 let expression = function expression(x, y) { return x + y} expression.name // 'expression' ,ES5 环境下返回具名函数名 expression.name // 'expression' ,ES6 环境下返回具名函数名 // Function 构造函数返回的函数实例,name 属性的值为 anonymous (new Function).name // "anonymous" // bind 返回的函数,name 属性值会加上 bound 前缀 function namedFunction(x, y) { return x + y } namedFunction.bind({}).name // 'bound namedFunction' (function(){}).bind({}).name // 'bound '
length
属性 :返回函数预期传入的参数个数,即函数定义之中的参数个数。length
属性可以用于判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)。function namedFunction(x, y) { return x + y } namedFunction.length // 2
toString()
方法 :返回函数源码的字符串(包含换行符、注释在内)。function namedFunction(x, y) { return x + y } namedFunction.toString() // 'function namedFunction(x, y) { return x + y }' // 原生函数,返回 'function (){[native code]}' Math.sqrt.toString() // 'function sqrt() { [native code] }'
call(thisArg[, arg1, arg2, ...argN])
方法 :调用一个函数,并将其this
值设置为提供的值。apply(thisArg [, argsArray])
方法 :调用一个函数并将其this
的值设置为提供的thisArg
。参数可用以通过数组对象传递。bind(thisArg[, arg1[, arg2[, ...argN]]])
方法 :创建一个新的函数,该函数在调用时,会将this
设置为提供的thisArg
。
函数参数
ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含的值。参数数组为空或者数组元素超过函数要求,都不会有影响。
函数参数不是必需的,JavaScript 允许省略参数。但是没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入 undefined
。
function namedFunction(x, y) { return [x, y] }
namedFunction(1, 2, 3) // [1, 2]
namedFunction(1, 2) // [1, 2]
namedFunction(1) // [1, undefined]
namedFunction() // [undefined, undefined]
namedFunction(undefined, 1) // [undefined, 1]
函数如果有同名的参数,则取最后出现的那个值。如果使用 arguments
对象,则可以获取对应的值。
function namedFunction(x, x) { return [x, x, arguments[0]] }
namedFunction(1, 2) // [2, 2, 1]
namedFunction(1) // [undefined, undefined, 1]
arguments 对象
使用 function
关键字定义(非箭头)函数时,可以在函数内部访问 arguments
对象,从中取得传进来的每个参数值。
arguments
对象是一个类数组对象(但不是Array
的实例),因此可以使用中括号语法访问其中的元素,通过arguments.length
可以获取参数个数。但无法使用数组专有的方法,需要转换成真正的数组。let args = Array.prototype.slice.call(arguments) // 或者 let args = [] for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]) }
arguments
对象可以跟函数命名参数一起使用。arguments
对象的值始终会与对应的命名参数同步,它们在内存中还是分开的,只不过会保持同步而已。严格模式下,arguments
对象与函数参数不具有联动关系。如果只传了一个参数,将
arguments[1]
设置为某个值,这个值并不会反映到第二个命名参数。因为arguments
对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的。arguments
对象存在一个callee
属性,返回它所对应的原函数。通过arguments.callee
达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。
使用箭头函数时,传给函数的参数不能使用 arguments
关键字访问,只能通过定义的命名参数访问。
reset 参数(剩余参数)
reset
参数(剩余参数,形式为 ...变量名
)用于获取传入函数的多余参数,并组成一个数组。
function foo(x, ...resetArr) { return resetArr }
foo(1, 2, 3) // [2, 3]
// 使用 rest 参数代替 arguments 对象示例
function foo(...resetArr) { return resetArr }
foo(1, 2, 3) // [1, 2, 3]
// 与解构赋值默认值结合使用
function f(...[a, b, c]) {
return [a, b, c]
}
foo(1, 2, 3) // [1, 2, 3]
reset
参数与 arguments
对象的区别:
reset
参数只包含没有对应形参的实参,而arguments
对象包含了传给函数的所有实参。reset
参数是一个真实的Array
实例,arguments
对象是类数组对象arguments
对象存在一些附加的属性(如callee
属性)
设置参数默认值
在 JavaScript 中,函数参数的默认值是 undefined
。函数默认参数允许在没有值或 undefined
被传入时使用默认形参。
- 参数变量是默认声明的,所以不能用
let
或const
再次声明。 - 使用参数默认值时,函数不能有同名参数。
- 参数默认值不是在定义时执行,而是在运行时执行计算默认值表达式的值。如果参数已赋值,默认值中的函数就不会执行。
- 参数传入
undefined
,将触发该参数等于默认值,null
则没有这个效果。 - 参数默认值设为
undefined
,表明这个参数是可以省略的.
// ES5 之前,设置函数参数默认值
function log(x, y) {
y = (typeof y !== 'undefined') ? y : 0
return [x, y]
}
log(1) // [1, 0]
log(1, 2) // [1, 2]
// ES6 设置函数参数默认值
function log(x, y = 0) {
return [x, y]
}
log(1) // [1, 0]
log(1, 2) // [1, 2]
// 参数变量是默认声明的,所以不能用 let 或 const 再次声明。
function log(x = 0) {
let x = 1 // 报错。Uncaught SyntaxError: Identifier 'x' has already been declared
const x = 2 // 报错。Uncaught SyntaxError: Identifier 'x' has already been declared
}
// 使用参数默认值时,函数不能有同名参数。
function foo(x, x, y = 1) {}
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context
function log(x, x, y) {} // 不报错
// 参数默认值不是在定义时执行,而是在运行时执行计算默认值表达式的值。如果参数已赋值,默认值中的函数就不会执行。
let x = 99
function foo(val = x + 1) {
return val
}
foo() // 100
x = 100
foo() // 101
// 传入 undefined,将触发该参数等于默认值,null 则没有这个效果。
function foo(x = 5, y = 6) {
return [x, y]
}
foo(undefined, null) // [5, null]
// 参数默认值设为 undefined,表明这个参数是可以省略的
function foo(optional = undefined) {}
函数设置默认值与解构赋值默认值结合使用
function foo({ x = 0, y = 0 } = {}) {
return [x, y]
}
function bar({ x, y } = { x: 0, y: 0 }) {
return [x, y]
}
// 函数没有参数的情况
foo() // [0, 0]
bar() // [0, 0]
// x 和 y 都有值的情况
foo({ x: 3, y: 8 }) // [3, 8]
bar({ x: 3, y: 8 }) // [3, 8]
// x 有值,y 无值的情况
foo({ x: 3 }) // [3, 0]
bar({ x: 3 }) // [3, undefined]
// x 和 y 都无值的情况
foo({}) // [0, 0];
bar({}) // [undefined, undefined]
foo({ z: 3 }) // [0, 0]
bar({ z: 3 }) // [undefined, undefined]
函数指定了默认值以后,函数的 length
属性(返回该函数预期传入的参数个数),将返回没有指定默认值的参数个数。
- 某个参数指定默认值以后,预期传入的参数个数就不包括该参数。
rest
参数(剩余参数)不会计入length
属性。- 如果设置了默认值的参数不是尾参数,
length
属性不再计入后面的参数个数。
// 某个参数指定默认值以后,预期传入的参数个数就不包括该参数
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
// rest 参数(剩余参数)不会计入 length 属性。
(function(...args) {}).length // 0
// 如果设置了默认值的参数不是尾参数,length 属性不再计入后面的参数个数。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing() {
throw new Error('Missing parameter')
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided
}
foo()
值传递
- 如果函数参数是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。在函数体内修改参数值,不会影响到函数外部。
- 如果函数参数是引用类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,此时不会影响到原始值。
let fooVal = 1
function foo(val) { val = 2 }
foo(fooVal)
console.log(fooVal) // 1
let fooObj = { foo: 1 }
function foo(val) { val.foo = 2 }
foo(fooObj)
console.log(fooObj.foo) // 2
let fooArr = [1, 2, 3]
function foo(val) { val = [4, 5, 6] }
foo(fooArr)
console.log(fooArr) // [1, 2, 3]
箭头函数
ES6 允许使用“箭头”(=>
)定义函数。
let foo = () => 'foo'
let foo = val => val
let foo = (x, y) = x + y
let foo = ({ key, value }) => { key, value }
let foo = id => { id: id, name: 'foo' } // 报错
let foo = id => ({ id: id, name: 'foo' }) // 正常执行
// 执行函数没有返回值,或者说返回为 undefined
// 函数意图返回一个对象,由于引擎任务大括号是代码块,所以执行了 name: 'foo' ,
// 此时 name 可以被解释为语句标签,英因此实际执行语句为 'foo' ,然后函数执行结束,没有返回值。
let foo = () => { name: 'foo' }
let formatArr = [1, 2, 3].map(val => val * val)
let sortArr = [3, 2, 1].sort((a, b) => a - b)
let foo = (x, ...rest) => [x, rest]
foo(1, 2, 3, 4, 5) // [1, [2, 3, 4, 5]]
使用箭头函数注意事项:
箭头函数没有自己的
this
对象。箭头函数内部的this
是定义时上层作用域中的this
。由于箭头函数没有自己的
this
,不能使用call()
、apply()
、bind()
方法改变this
的指向。function Timer() { this.s1 = 0 this.s2 = 0 // 箭头函数 // this 指向定义时所在的作用域(即 Timer 函数) setInterval(() => this.s1++, 1000) // 普通函数 // this 指向运行时所在的作用域(即全局对象) setInterval(function () { this.s2++ }, 1000) } var timer = new Timer() setTimeout(() => console.log('s1: ', timer.s1), 3100) setTimeout(() => console.log('s2: ', timer.s2), 3100) // s1: 3 // s2: 0
在箭头函数之中不存在的指向外层函数的对应变量:
arguments
、super
、new.target
。如果要使用arguments
对象,可以使用rest
参数代替。不可以当作构造函数使用。不可以对箭头函数使用
new
命令,否则会抛出一个错误。不可以使用
yield
命令,因此箭头函数不能用作Generator
函数。
不适合使用箭头函数的场景:
- 定义对象的方法,且该方法内部包括
this
,不应使用箭头函数。对象的属性建议使用传统的写法定义。 - 需要动态
this
的时候,不应使用箭头函数。 - 函数体复杂,或者函数内部有大量的读写操作,不单纯是为了计算值,不应使用箭头函数,使用普通函数,可以提高代码可读性。
递归
递归函数通常的形式是一个函数通过函数名调用自身。
arguments
对象上的属性 callee
指向正在执行的函数的指针,可以在函数内部递归调用。
在严格模式下,arguments.callee
不能访问,可以使用命名函数表达式。
// 使用 arguments.callee 进行递归调用
function factorial(num) {
if (num <= 1) {
return 1
} else {
return num * arguments.callee(num - 1)
}
}
// 使用函数名进行递归调用
function factorial(num) {
if (num <= 1) {
return 1
} else {
return num * factorial(num - 1)
}
}
// 箭头函数进行递归
let factorial = x => (x == 0 ? 1 : x * factorial(x - 1))
闭包
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。主要用于
读取外层函数内部的变量,并将这些变量始终保持在内存中。
闭包能够返回外层函数的内部变量的原因:
- 闭包用到了外层变量,导致外层函数不能从内存释放。
- 只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。
function createIncrementor(start) { return function () { return start++ } } var inc = createIncrementor(5) inc() // 5 inc() // 6 inc() // 7
封装对象的私有属性和私有方法。
function Person(name) { var _age function setAge(n) { _age = n } function getAge() { return _age } return { name: name, getAge: getAge, setAge: setAge, } } var p1 = Person('张三') p1.setAge(25) p1.getAge() // 25
立即调用的函数表达式(IIFE)
立即调用的函数表达式(IIFE)的写法:
(function(){ /*code*/ }());
(function(){ /*code*/ })();
注:两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。
let foo = function foo() { return 'foo' }()
foo // 'foo'
(function(){ return 'foo' }()); // 'foo'
(function(){ return 'foo' })(); // 'foo'
函数定义后立即调用的方法就是不要让 function
出现在行首,让引擎将其理解成一个表达式。则立即调用的函数表达式可以有以下写法。
true && function(){ /* code */ }();
0, function(){ /* code */ }();
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
使用立即调用的函数表达式,主要目的:
- 不必为函数命名,避免污染全局变量
- IIFE 内部形成一个单独的作用域,可以封装一些外部无法访问的私有变量
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
尾调用优化
尾调用概述
尾调用:外部函数的返回值是一个内部函数的返回值。即某个函数的最后一步是调用另一个函数。
function outerFunction() {
return innerFunction() // 尾调用
}
- 在 ES6 优化之前,执行示例会在内存中发生如下操作:
- 执行到
outerFunction
函数体,第一个栈帧被推到栈上。 - 执行
outerFunction
函数体,到return
语句。计算返回值必须先计算innerFunction
。 - 执行到
innerFunction
函数体,第二个栈帧被推到栈上。 - 执行
innerFunction
函数体,计算其返回值。 - 将返回值传回
outerFunction
,然后outerFunction
再返回值。 - 将栈帧弹出栈外。
- 执行到
- 在 ES6 优化之后,执行这个例子会在内存中发生如下操作。
- 执行到
outerFunction
函数体,第一个栈帧被推到栈上。 - 执行
outerFunction
函数体,到达return
语句。为求值返回语句,必须先求值innerFunction
。 - 引擎发现把第一个栈帧弹出栈外也没问题,因为
innerFunction
的返回值也是outerFunction
的返回值。 - 弹出
outerFunction
的栈帧。 - 执行到
innerFunction
函数体,栈帧被推到栈上。 - 执行
innerFunction
函数体,计算其返回值。 - 将
innerFunction
的栈帧弹出栈外。
- 执行到
尾调用优化的条件为确定外部栈帧真的没有必要存在了。涉及的条件如下:
代码在严格模式下执行。
要求严格模式主要因为在非严格模式下函数调用中允许使用
f.arguments
和f.caller
,而它们都会引用外部函数的栈帧。意味着不能应用优化了,因此尾调用优化要求必须在严格模式下有效,以防止引用这些属性。外部函数的返回值是对尾调用函数的调用。
尾调用函数返回后不需要执行额外的逻辑。
尾调用函数不是引用外部函数作用域中自由变量的闭包。
// 有优化:栈帧销毁前执行参数计算
function outerFunction(a, b) {
return innerFunction(a + b)
}
// 有优化:尾调用不一定出现在函数尾部,只要是最后一步操作即可
function outerFunction(x) {
if (x > 0) {
return innerFunctionA(x)
}
return innerFunctionB(x)
}
// 有优化:两个内部函数都在尾部
function outerFunction(condition) {
return condition ? innerFunctionA() : innerFunctionB()
}
// 无优化:尾调用没有直接返回
function outerFunction() {
let innerFunction = innerFunction()
return innerFunction
}
// 无优化:尾调用返回后必须转型为字符串
function outerFunction() {
return innerFunction().toString()
}
// 无优化:尾调用是一个闭包
function outerFunction() {
let foo = 'bar'
function innerFunction() { return foo }
return innerFunction()
}
// 无优化:尾调用返回好进行计算
function outerFunction() {
return innerFunction() + 1
}
// 无优化:尾调用没有返回
function outerFunction() {
innerFunction()
// 相当于
// innerFunction()
// return undefined
}
尾递归
尾递归:函数尾调用自身。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。
// 阶乘函数时间复杂度为 O(n)
function factorial(n) {
if (n === 1) return 1
return n * factorial(n - 1)
}
// 使用函数默认值改写阶乘函数,确保最后一步只调用自身
// 使用尾调用优化,阶乘函数时间复杂度为 O(1)
function factorial(n, total = 1) {
if (n === 1) return total
return factorial(n - 1, n * total)
}
// 使用柯里化改写阶乘函数,确保最后一步只调用自身
// 使用尾调用优化,阶乘函数时间复杂度为 O(1)
// 注:柯里化(currying)将多参数的函数转换成单参数的形式
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n)
}
}
function tailFactorial(n, total) {
if (n === 1) return total
return tailFactorial(n - 1, n * total)
}
const factorial = currying(tailFactorial, 1)
尾递归优化的实现
蹦床函数(trampoline):将递归执行转为循环执行。
- 接受一个
函数 f
作为参数。只要函数 f
执行后返回一个函数,则继续执行。 - 返回一个函数,然后执行该函数,而不是函数里面调用函数。避免了递归执行,从而就消除了调用栈过大的问题。
function trampoline(f) { while (f && f instanceof Function) { f = f() } return f }
// 示例 function sum(x, y) { if (y > 0) { return sum.bind(null, x + 1, y - 1) } else { return x } } trampoline(sum(1, 100000))
- 接受一个
非严格模式下,尾递归优化真实实现
- 设置状态变量
active
。默认情况下,变量是不激活的;进入尾递归优化的过程,该变量就会激活。 - 每一轮递归函数返回的都是
undefined
,避免了递归执行。 accumulated
数组存放每一轮函数执行的参数,总是有值的,保证了accumulator
函数内部的while
循环总是会执行。- 巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。
function tco(f) { var value var active = false var accumulated = [] return function accumulator() { accumulated.push(arguments) if (!active) { active = true while (accumulated.length) { value = f.apply(this, accumulated.shift()) } active = false return value } } }
// 示例 var sum = tco(function (x, y) { if (y > 0) { return sum(x + 1, y - 1) } else { return x } }) sum(1, 100000)
- 设置状态变量