函数实际上是对象。每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定

定义函数的方式

  • 函数表达式
1
2
3
let sum = function(num1, num2){
return num1 + num2
}; // 函数表达式与任何变量初始化语句一样,末尾要有分号
  • 箭头函数
1
2
3
4
5
let sum = (num1, num2) => {
return mun1 + num2
}; // 函数表达式与任何变量初始化语句一样,末尾要有分号

<!-- 箭头函数不能使用 argumentssupernew.target,也不能用作构造函数,也没有 prototype 属性 -->
  • 函数声明
1
2
3
function sum(num1, num2){
return num1 + num2
} // 函数定义最后没有分号
  • 使用构造函数(不推荐)
1
let sum = new Function('num1', 'num2', 'return num1 + num2')

函数名

ECMAScript 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。一般情况下,这个属性保存的是一个字符串化的变量名。如果函数没有名称,会显示成空字符串。如果是 Function 构造函数创建的,会标识成 ‘anonymouse’

1
2
3
4
5
6
7
8
9
function foo(){}
let bar = function(){}
let baz = () => {}

foo.name // foo
bar.name // bar
baz.name // baz
(()=>{}).name // ''
(new Function()).name // anonymouse

如果函数是一个获取函数、设置函数,或者使用 bind() 实例化,那么标识符前面会加上一个前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {}
foo.bind().name // bound foo

let dog = {
years: 1,
get age(){
return this.years;
},
set age(newAge){
this.years = newAge
}
}
let des = Object.getOwnPropertyDescriptor(dog, 'age')
des.get.name // get age
des.set.name // set age

参数

ECMAScript 函数的参数在内部表现为一个数组。在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 argumets 对象,从中取得传进来的每个参数值

arguments 对象是一个类数组,当与命名参数一起使用时,它的值始终会与对应的命名参数同步

1
2
3
4
5
function add(num1, num2){
arguments[1] = 10
console.log(arguments[0] + num2)
}
add(5, 5) // 15

上面的代码,arguments[1]把第二个参数的值重写为 10,num2 也同步修改了,虽然两者的值都是 10,但他们在内存中还是分开的(即访问不同的内存地址),只不过会保持同步而已。如果只传了一个参数,然后把 arguments[1] 设置为某个值,那么这个值并不会反应到第二个命名参数。这是因为 arguments 对象的长度是根据传入的参数个数确定,而非定义函数时给出的命名参数的个数

1
add(5) // NaN

严格模式下,像上面那样修改arguments[1]不会再影响 num2 的值,其次,在函数中尝试重写arguments对象会导致语法错误

默认参数值

ES6 之后,函数可以显式定义默认参数,只需使用 = 赋值即可
使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数,修改命名参数也不会影响 arguments 对象,它始终以调用函数时传入的值为准

1
2
3
4
5
6
7
function myName(name = 'Jeff'){
name = 'John'
return `my Name is ${arguments[0]}`
}

console.log(myName()) // my Name is undefined
console.log(myName('Boll')) // my Name is Boll

默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值

函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。计算默认值的函数只有在调用函数但未传相应参数时才会被调用

给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样,参数初始化顺序遵循暂时性死区规则。参数也存在于自己的作用域中,它们不能引用函数体的作用域

参数收集

可以使用扩展操作符把不同长度的独立参数组合为一个数组,这有点类似与 arguments 对象的构造机制,只不过收集参数的结果是一个 Array 实例

收集参数的前面如果还有命名参数,则只会收集其余的参数。因为收集参数的结果可以变,所以只能把它作为最后一个参数

1
2
3
4
5
6
7
8
9
10
11
12
// 不可以
function getProduct(...values, lastValue){}

// 可以
function ignoreFirst(firstValue, ...values){
console.log(values)
}

ignoreFirst() // []
ignoreFirst(1) // []
ignoreFirst(1,2) // [2]
ignoreFirst(1,2,3) // [2,3]

箭头函数支持收集参数的定义方式

1
2
3
let getSum = (...values) => {
return values.reduce((x, y) => x + y,0)
}

函数内部

arguments.callee

arguments.callee是一个指向 arguments 对象所在函数的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function factorial(num) {
if(num <= 1){
return 1
} else {
return num * factorial(num - 1)
}
}
// 此时函数逻辑与函数名是紧密耦合的。如果遇到以下情况,将会导致问题
let trueFactorial = factorial;
factorial = function(){
return 0
}
console.log(trueFactioral(5)) // 0
console.log(factorial(5)) // 0
// trueFactorial 变量指向 factorial 函数,factorial 函数被重写为一个返回 0 的函数,trueFactorial 函数执行调用 factorial 函数时,返回 0,这显然不是我们期待的结果
// 使用 arguments.callee 改写后,函数与函数名解耦,trueFactorial() 就可以正确计算
function factorial(num) {
if(num <= 1){
return 1
} else {
return num * arguments.callee(num - 1)
}
}
console.log(trueFactioral(5)) // 120
console.log(factorial(5)) // 0

在严格模式下访问 arguments.callee 会报错,此时可以使用命名函数表达式来达到目的

1
2
3
4
5
6
7
const factorial = (function f(num){
if(num <= 1){
return 1
}else{
return num * f(num - 1)
}
})

这里创建了一个命名函数表达式 f(),然后将它赋值给了变量 factorial。即使把函数赋值给另一个变量,函数表达式的名称 f 也不变,因此递归调用不会有问题

this

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时通常称其为 this值
在箭头函数中,this 引用的是定义箭头函数的上下文

在严格模式下,调用函数时如果没有指定上下文对象,则 this 值不会指向 window。除非使用 apply() 或 call() 把函数指定给一个对象,否则 this 的值会变成 undefined

caller

函数对象上的属性 caller,引用的是调用当前函数的函数,如果是在全局作用域中调用的则为 null

1
2
3
4
5
6
7
8
9
10
11
12
function outer(){
inner()
}
function inner(){
console.log(inner.caller)
}
outer() // outer 函数的源代码

// 如果要降低耦合度,可以通过 arguments.callee.caller 来引用同样的值
function inner(){
console.log(arguments.callee.caller)
}

new.target

ES6新增,用来检测函数是否使用 new 关键字调用。如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 的值是被调用的构造函数

函数属性与方法

每个函数都有两个属性:length 和 prototype

length 属性保存函数定义的形参的个数

函数还有三个方法:apply()、call() 和 bind()

call() 和 apply() 的作用一样,只是传参的形式不同。第一个参数都是 this 值,剩下的参数 apply() 可以是 Array 的实例,也可以是 arguments 对象;call() 必须将参数一个一个的列出来
bind() 方法会创建一个新的函数实例,新函数的 this 值指向 bind() 的第一个参数,其余参数为函数调用时传入的参数,同 call()

尾调用优化

尾调用:即外部函数的返回值是一个内部函数的返回值(ES6新增)。比如:

1
2
3
function outer(){
return inner() // 尾调用
}

以上代码,在 ES6 优化之前,执行时在内存中的操作如下:
1、执行到 outer 函数体,第一个栈帧被推到栈上
2、执行 outer 函数体到 return 语句,计算返回值必须先计算 inner
3、执行到 inner 函数体,第二个栈帧被推到栈上
4、执行 inner 函数体,计算其返回值
5、将返回值传回 outer,然后 outer 再返回值
6、将栈帧弹出栈外

ES6 优化后,操作如下:
1、执行到 outer 函数体,第一个栈帧被推到栈上
2、执行 outer 到 return 语句,计算返回值必须先求值 inner
3、引擎发现把第一个栈帧弹出栈外也没问题,因为 inner 的返回值也是 outer 的返回值
4、弹出 outer 的栈帧
5、执行到 inner 函数体,栈帧被推到栈上
6、执行 inner 函数体,计算其返回值
7、将 inner 的栈帧弹出栈外

优化前,每多调用一次嵌套函数,就会多增加一个栈帧,而优化后无论调用多少次嵌套函数,都只有一个栈帧。这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做

尾调用优化的条件

尾调用优化的条件就是确定外部栈帧真的没有必要存在了。需满足如下条件:

  • 代码在严格模式下运行
  • 外部函数的返回值是对尾调用函数的调用
  • 尾调用函数返回后不需要执行额外的逻辑
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包
1
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
// 以下为不满足尾调用优化的例子
"use strict";

// 尾调用没有返回
function outer(){
inner()
}
// 尾调用没有直接返回
function outer(){
let innerResult = inner()
return innerResult
}
// 尾调用返回后必须转型为字符串
function outer(){
return inner().toString()
}
// 尾调用是一个闭包
function outer(){
let foo = 'bar';
function inner(){ return foo }
return inner()
}

// 以下为符合尾调用优化条件的例子
"use strict";
// 栈帧销毁前执行参数计算
function outer(a, b){
return inner(a + b)
}
// 初始返回值不涉及栈帧
function outer(a,b){
if(a < b){
return a
}
return inner(a + b)
}
// 两个内部函数都在尾部
function outer(condition){
return condition ? innerA() : innerB()
}

尾调用优化的代码

下面是一个通过递归计算斐波那契数列的函数:

1
2
3
4
5
6
function fib(n){
if(n < 2){
return n
}
return fib(n - 1) + fib(n - 2)
}

由于返回语句中有一个相加的操作,所以这个函数不符合尾调用优化的条件
可以使用如下方法重构,以满足优化条件:

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";

function fib(b){
return fibImpl(0, 1, n);
}

function fibImpl(a, b, n){
if(n === 0){
return a
}
return fibImpl(b, a + b, n - 1)
}

闭包

作用域链

在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用 arguments 和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止

函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。

1
2
3
4
5
6
7
8
9
10
function compare(value1, value2){
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 10)

上面的代码,全局的变量对象包含 this、result 和 compare;在定义 compare() 函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的 [[Scope]] 中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的 [[Scope]] 来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。在 compare() 函数执行上下文的作用域链中会有两个变量对象:局部变量对象和全局变量对象。作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象

作用域链

this 和 arguments 是不能直接在内部函数中访问的,如果想访问包含作用域中的 this 或 arguments 对象,需要将其引用先保存到闭包能访问的另一个变量中

闭包原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createComparisonFunction(propertyName) { 
return function(object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];

if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
}
// 创建比较函数
let compare = createComparisonFunction('name');
// 调用函数
let result = compare({ name: 'Nicholas' }, { name: 'Greg' });

上面代码的作用域链如下:
闭包作用域链
在 createComparisonFunction() 返回匿名函数后,它的作用连被初始化为包含 createComparisonFunction() 的活动对象和全局变量对象。这样,匿名函数就可以访问到 createComparisonFunction() 可以访问的所有变量。因为匿名函数的作用域链中仍然有对它的引用,所以createComparisonFunction() 的活动对象并不能在它执行完毕后销毁。createComparisonFunction() 执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁

1
2
// 接触对函数的引用,这样就可以释放内存了
compare = null

把 compare 设置为等于 null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放,作用域链也会被销毁,其他作用域(除全局作用域之外)也可以销毁

因为闭包会保留他们包含的作用域,所以比其他函数更占内存。过度使用可能导致内存占用过度,因此建议仅在十分必要时使用

this 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentity () {
return this.identity;
}
};

object.getIdentity(); // 'My Object'

(object.getIdentity)(); // 'My Object'

(object.getIdentity = object.getIdentity)(); // 'The Window'

以上代码,第一次是正常调用,返回 ‘My Object’;第二次调用时虽然加了括号,但其实与第一次调用是引用的,都是先成员访问,再函数调用;第三次执行了一次赋值,然后再调用赋值后的结果,赋值表达式的返回结果是值本身,此时函数的 this 不再与任何对象绑定,所以返回的是 ‘The Window’