第十章 函数
函数实际上是对象。每个函数都是
Function
类型的实例,而Function
也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定
定义函数的方式
- 函数表达式
1 | let sum = function(num1, num2){ |
- 箭头函数
1 | let sum = (num1, num2) => { |
- 函数声明
1 | function sum(num1, num2){ |
- 使用构造函数(不推荐)
1 | let sum = new Function('num1', 'num2', 'return num1 + num2') |
函数名
ECMAScript 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。一般情况下,这个属性保存的是一个字符串化的变量名。如果函数没有名称,会显示成空字符串。如果是 Function 构造函数创建的,会标识成 ‘anonymouse’
1 | function foo(){} |
如果函数是一个获取函数、设置函数,或者使用 bind()
实例化,那么标识符前面会加上一个前缀
1 | function foo() {} |
参数
ECMAScript 函数的参数在内部表现为一个数组。在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 argumets 对象,从中取得传进来的每个参数值
arguments 对象是一个类数组,当与命名参数一起使用时,它的值始终会与对应的命名参数同步
1 | function add(num1, num2){ |
上面的代码,arguments[1]
把第二个参数的值重写为 10,num2 也同步修改了,虽然两者的值都是 10,但他们在内存中还是分开的(即访问不同的内存地址),只不过会保持同步而已。如果只传了一个参数,然后把 arguments[1]
设置为某个值,那么这个值并不会反应到第二个命名参数。这是因为 arguments
对象的长度是根据传入的参数个数确定,而非定义函数时给出的命名参数的个数
1 | add(5) // NaN |
严格模式下,像上面那样修改arguments[1]
不会再影响 num2
的值,其次,在函数中尝试重写arguments
对象会导致语法错误
默认参数值
ES6 之后,函数可以显式定义默认参数,只需使用 =
赋值即可
使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数,修改命名参数也不会影响 arguments 对象,它始终以调用函数时传入的值为准
1 | function myName(name = 'Jeff'){ |
默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值
函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。计算默认值的函数只有在调用函数但未传相应参数时才会被调用
给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样,参数初始化顺序遵循暂时性死区规则。参数也存在于自己的作用域中,它们不能引用函数体的作用域
参数收集
可以使用扩展操作符把不同长度的独立参数组合为一个数组,这有点类似与 arguments 对象的构造机制,只不过收集参数的结果是一个 Array 实例
收集参数的前面如果还有命名参数,则只会收集其余的参数。因为收集参数的结果可以变,所以只能把它作为最后一个参数
1 | // 不可以 |
箭头函数支持收集参数的定义方式
1 | let getSum = (...values) => { |
函数内部
arguments.callee
arguments.callee
是一个指向 arguments
对象所在函数的指针
1 | function factorial(num) { |
在严格模式下访问 arguments.callee 会报错,此时可以使用命名函数表达式来达到目的
1 | const factorial = (function f(num){ |
这里创建了一个命名函数表达式 f(),然后将它赋值给了变量 factorial。即使把函数赋值给另一个变量,函数表达式的名称 f 也不变,因此递归调用不会有问题
this
在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时通常称其为 this值
在箭头函数中,this 引用的是定义箭头函数的上下文
在严格模式下,调用函数时如果没有指定上下文对象,则 this 值不会指向 window。除非使用 apply() 或 call() 把函数指定给一个对象,否则 this 的值会变成 undefined
caller
函数对象上的属性 caller,引用的是调用当前函数的函数,如果是在全局作用域中调用的则为 null
1 | function outer(){ |
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 | function outer(){ |
以上代码,在 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 | // 以下为不满足尾调用优化的例子 |
尾调用优化的代码
下面是一个通过递归计算斐波那契数列的函数:
1 | function fib(n){ |
由于返回语句中有一个相加的操作,所以这个函数不符合尾调用优化的条件
可以使用如下方法重构,以满足优化条件:
1 | ; |
闭包
作用域链
在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用 arguments 和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。
1 | function compare(value1, value2){ |
上面的代码,全局的变量对象包含 this、result 和 compare;在定义 compare() 函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的 [[Scope]] 中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的 [[Scope]] 来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。在 compare() 函数执行上下文的作用域链中会有两个变量对象:局部变量对象和全局变量对象。作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象
this 和 arguments 是不能直接在内部函数中访问的,如果想访问包含作用域中的 this 或 arguments 对象,需要将其引用先保存到闭包能访问的另一个变量中
闭包原理
1 | function createComparisonFunction(propertyName) { |
上面代码的作用域链如下:
在 createComparisonFunction() 返回匿名函数后,它的作用连被初始化为包含 createComparisonFunction() 的活动对象和全局变量对象。这样,匿名函数就可以访问到 createComparisonFunction() 可以访问的所有变量。因为匿名函数的作用域链中仍然有对它的引用,所以createComparisonFunction() 的活动对象并不能在它执行完毕后销毁。createComparisonFunction() 执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁
1 | // 接触对函数的引用,这样就可以释放内存了 |
把 compare 设置为等于 null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放,作用域链也会被销毁,其他作用域(除全局作用域之外)也可以销毁
因为闭包会保留他们包含的作用域,所以比其他函数更占内存。过度使用可能导致内存占用过度,因此建议仅在十分必要时使用
this 对象
1 | window.identity = 'The Window'; |
以上代码,第一次是正常调用,返回 ‘My Object’;第二次调用时虽然加了括号,但其实与第一次调用是引用的,都是先成员访问,再函数调用;第三次执行了一次赋值,然后再调用赋值后的结果,赋值表达式的返回结果是值本身,此时函数的 this 不再与任何对象绑定,所以返回的是 ‘The Window’