函数

函数表达式、函数声明及箭头函数

  1. 函数表达式
    • 必须等到代码执行到它那一行,才会在执行上下文中生成函数定义
  2. 函数声明
    • JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义
      • 这个过程叫函数声明提升
  3. 箭头函数
    • 非常适合嵌入函数
    • 不能使用 arguments、super 和 new.target,也不能用作构造函数
    • 没有 prototype 属性

函数名就是指向函数地指针。

理解参数

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

每个函数在被调用时都会自动创建两个特殊变量:this 和 arguments。内部函数永远不可能直接访问外部函数的这两个变量。

arguments对象

  • 一个类数组对象(但不是 Array 的实例)
  • 可以通过 arguments 对象的 length 属性检查传入的参数个数
  • 可以跟命名参数一起使用
  • 它的值始终会与对应的命名参数同步
    • 修改arguments上的值,会使得命名参数的值会变化
    • 它们在内存中是分开的,只不过会保持同步
  • 有一个callee属性,是一个指向 arguments 对象所在函数的指针
    • 使用 arguments.callee 就可以让函数逻辑与函数名解耦

this

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

  • 在标准函数中,this 到底引用哪个对象必须到函数被调用时才能确定
  • 在箭头函数中,this引用的是定义箭头函数的上下文

函数没有重载,后定义的会覆盖先定义

函数属性

  1. new.target
    • 如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数。
  2. length
    • 保存函数定义的命名参数的个数
  3. prototype
    • 保存引用类型所有实例方法的地方

apply()、call()、bind()区别

第一个参数都是this的值

  • apply()
    • 第二个参数可以是Array实例,也可以是arguments对象
  • call()
    • 除去第一个参数,剩下参数都是逐个传递传给到被调用函数
  • bind()
    • bind()方法会创建一个新的函数实例, 其 this 值会被绑定到传给 bind()的对象

使用 apply()还是 call(),完全取决于怎么给要调用的函数传参更方便,他们真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内 this 值的能力

默认参数及扩展操作符

默认参数

ES6之后,支持显式定义默认参数。给参数传 undefined 相当于没有传值。

默认参数上,实际上和使用 let 关键字顺序声明变量一样,按照定义它们的顺序依次被初始化,后定义默认值的参数可以引用先定义的参数,但反过来不行,会有TDZ,参数也存在于自己的作用域中,它们不能引用函数体的作用域。

扩展操作符

扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数

使用函数实现递归

递归函数通常的形式是一个函数通过名称调用自己

在写递归函数时使用 arguments.callee 可以避免函数名被改名,导致递归出错这个问题

尾调用优化

内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。具体来说,即外部函数的返回值是一个内部函数的返回值。

优化条件

尾调用优化的条件就是确定外部栈帧真的没有必要存在了

  • 代码在严格模式下执行
    • 之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用 f.arguments 和 f.caller,而它们都会引用外部函数的栈帧。
  • 外部函数的返回值是对尾调用函数的调用
  • 尾调用函数返回后不需要执行额外的逻辑
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包

使用闭包实现私有变量

闭包:指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

回顾作用域链概念

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

函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。在定义函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。

作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

在一个函数内部定义的函数会把外部函数的的活动对象添加到自己的作用域链中,外部函数活动对象并不能在它执行完毕后销毁,因为内部函数的作用域链中仍然有对它的引用,外部函数执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到内部函数被销毁后才会被销毁

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

闭包有时会产生内存泄漏

立即调用的函数表达式IIFE

类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式

ES5以前,为了防止变量定义外泄,IIFE 是个非常有效的方式,ES6之后,IIFE 就没有那么必要了,因为块级作用域中的变量无须 IIFE 就可以实现同样的隔离

私有变量

任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量,私有变量包括函数参数、局部变量,以及函数内部定义的其他函数

如果这个函数中创建了一个闭包,则这个闭包能通过其作用域链访问其外部的这 3 个变量。基于这一点,就可以创建出能够访问私有变量的公有方法

特权方法是能够访问函数私有变量(及私有函数)的公有方法

  • 这个模式是把所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有成员的特权方法。这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力。

所带来的问题:必须通过构造函数来实现这种隔离

解决方法:使用静态私有变量实现特权方法可以避免这个问题。

静态私有变量

模块模式