跳到主要内容

函数

函数 是 JS 的基本组成部分。其他语言中所说的子例程 subroutine 或过程 procedure 就是函数。

函数是一个 JS 代码块,函数定义可以包含一组标识符,即形参( parameter )。函数调用会为这些形参提供实参( argument )。除了实参,函数调用还有一个 调用上下文( invocation context ),也就是 this 值。

如果对象的属性为函数,则称为该对象的 方法 ;如果函数是通过一个对象调用的,则这个对象就是函数的调用上下文,即 this 值。设计用来构造一个对象的函数称为 构造函数

JS 函数也是对象 ,可以通过程序操控。可以将函数赋值给变量,也可以在函数上设置属性。

JS 可以嵌套在函数里,内嵌的函数可以访问定义在函数作用域的任何变量。因此 JS 函数也是闭包

定义函数

有三种定义函数的方式:函数声明、函数表达式,以及 ES6 中的箭头函数。

还可以用 Function() 构造函数定义新函数。还有一些特殊函数,比如生成器函数、异步函数。

函数声明

函数声明由 function 关键字定义:

function add(x, y) {
return x + y
}

如果函数没有通过 return 语句返回值,则会自动返回 undefined

函数声明会被 "提升" 到脚本、函数或代码块的顶部,因此可以在声明之前就调用函数:

print("hello")    // 打印 "hello"

function print(x) {
console.log(x)
}

函数表达式

函数表达式与函数声明类似,它的函数名是可选的:

const square = function(x) { return x * x }

// 定义的表达式可以直接使用
let squaredTen = (function(x) { return x * x })(10)

// 可以定义函数名,对递归有用
const factorial = function fact(x) {
if (x <= 1) {
return 1
} else {
return x * fact(x - 1)
}
}

和函数声明不同的是,函数表达式呈现的是个值,所以不会被 "提升" 到顶部。在函数表达式定义之前是不可以使用函数的。

实际上,函数名是函数的一个局部变量,不管是函数声明还是函数表达式的函数名都会作为函数的 name 属性。如果函数表达式没有指定函数名,则会选择用变量名代替:

const factorial = function(x) {
if (x <= 1) {
return 1
} else {
return x * factorial(x - 1)
}
}
factorial.name // => "factorial"

let fn = factorial
fn.name // => "factorial"

箭头函数

在 ES6 中,可以用箭头函数定义函数,它的特点就是语法简洁:

const add = (x, y) => { return x + y }

如果函数体只有一个 return 语句,则箭头后面可以直接跟上返回值:

const add = (x, y) => x + y

由于对象字面量也是用大括号包裹,容易与箭头函数的大括号发生歧义。为了避免歧义可以给对象字面量包裹圆括号:

const fn = (x) => ({ value: x })

如果箭头函数只有一个参数,则可以省略圆括号:

const double = x => x * 2

但是如果没有参数,则必须把圆括号写出来。

箭头函数的参数与箭头之间不能换行,会被 JS 自动加上分号。

箭头函数和其他方式定义的函数有个极其重要的区别:它们会从定义自己的环境中继承 this 值,而非像以其他方式定义的函数那样定义自己的调用上下文。且箭头函数没有 prototype 属性。

调用函数

函数调用

通过函数表达式跟上圆括号包裹的参数列表来调用一个函数:

const double = x => x * 2
double(2) // => 4

在 ES2020 中,可以在函数表达式后再加上 ?. 进行条件调用:

double?.(2) // 若 double 不为空则会被调用

对于非严格模式下的函数调用,调用上下文 this 值是全局对象;严格模式下是 undefined 。一般不会在函数调用中使用 this 值。

JS 运行的时候会有个调用栈。如果有函数调用,则会创建一个调用上下文(或执行上下文)推到调用栈上,如果这个函数调用了别的函数,则会继续创建一个新的调用上下文到前一个上下文顶上。当函数执行结束后才会推出该函数的调用上下文。因此,应当注意递归函数的调用次数,上万次的调用很有可能导致 "最大调用栈溢出" 。

方法调用

方法调用和函数调用的区别在于调用上下文。方法调用的函数表达式 obj.method (或者用中括号)本身也是个属性访问表达式 ,而在 method 调用的时候,obj 会成为调用上下文,即 this 值。

方法和 this 值是面向对象编程范式的核心。

基于方法调用的返回值还可以继续调用其他方法。如果自己写的方法没有返回值,可以考虑返回 this 值。这样可以实现方法调用链的编程风格:

new Square().size(100, 100).outline("red").fill("yellow")

this 值不具有变量那样的作用域机制。除了箭头函数,嵌套函数不会继承包含函数的 this 值:

let obj = {
m() {
let self = this
f()
function f() {
this === obj // => false ,this 为全局对象或 undefined
self === obj // => true ,外部作用域中的 self 值
}
}
}
obj.m()

如果是箭头函数,则会从上一层作用域上继承 this 值:

let obj = {
m() {
let self = this
let f = () => {
this === obj // => true ,this 继承了上层作用域中的 this 值
self === obj // => true ,外部作用域中的 self 值
}
f()
}
}
obj.m()

或者还可以对嵌套函数使用 bind() 方法,来生成一个绑定了对象的函数:

const fn = f.bind(this)
fn()

构造函数调用

在函数前使用 new 关键字则为构造函数调用。如果没有参数列表,可以省略函数表达式后面的圆括号:

new Object  // 等价于 `new Object()`

构造函数(或称为构造函数)调用是用来初始化一个对象的。它会创建一个新的空对象,这个对象继承构造函数的 prototype 属性。同时,这个对象会被用作函数的调用上下文,即 this 值(即便构造函数为对象方法)。

箭头函数没有 prototype 属性,不能作为构造函数调用。

构造函数正常情况下不使用 return 返回。如果使用 return 返回了原始值,或者没有使用 return 语句,构造函数调用会隐式返回创建的新对象;如果使用 return 返回了一个对象,则会返回这个对象。

间接调用

JS 函数也是对象,函数自身也有方法,其中有 call()apply() 方法。这两个方法用来在调用函数的同时指定其 this 值。它们的第一个参数即为指定的 this 值。

隐式函数调用

一些 JS 语言特性看起来不像函数调用,但实际会导致某些函数被调用:

  • 如果对象有的获取方法或设置方法,则访问设置该属性会调用这些方法;

  • 在隐式转换中,会调用对象的 toString()valueOf() 方法;

  • 在遍历可迭代对象的元素时,也会设计一系列方法的调用;

  • 带标签的模板字面量是一种伪装的函数调用;

  • 代理对象的行为完全由函数控制,对象的几乎任何操作都会导致一个函数被调用。

实参 & 形参

JS 的函数定义不会指定函数形参类型,调用时也不会进行实参个数及类型检查。

可选形参 & 默认值

当调用函数传入的实参少于形参时,额外的形参会保持 undefined 。在 ES6 及后,可以为形参设置默认值:

function getPropertyNames(obj, arr = []) {
for (let prop in obj) {
arr.push(prop)
}
return arr
}

在设置有可选参数的函数时,一定要把可选参数放在参数列表的末尾,这样调用时才可以省略。实参和形参的参数匹配始终是从头开始的。

剩余形参

在 ES6 中,可以在尾部通过 ... 定义剩余参数,它会接收实参中多余的参数:

function max(...nums) {
let maxValue = -Infinity
nums.forEach(num => {
if (num > maxValue) {
maxValue = num
}
})
return maxValue
}
max(10, 5, 6, 9) // => 10

剩余形参必须是最后一个参数,其值始终为数组。数组可能为空,但永远不可能为 undefined(不要给剩余形参定义默认值,没用且不合法)。

像上面代码例子中可以接收任意数量的函数称为可变参数函数,或 变长数组

Arguments 对象

在 ES6 前是通过函数内 arguments 类数组对象来实现变长数组的:

function max() {
let maxValue = -Infinity
for (let i = 0; i < arguments.length; i += 1) {
if (arguments[i] > maxValue) {
maxValue = arguments[i]
}
}
return maxValue
}

Argumnets 对象在 JS 诞生之初就有了,它效率低且难优化,特别是在非严格模式下。现代代码中应当避免使用 arguments 对象。在严格模式下,arguments 会被当成保留字,不能用作自定义标识符。

展开 & 解构语法

在实参中使用展开语法:

let odds = [7, 3, 9]
let evens = [2, 8, 4]
Math.max(...odds, ...evens) // => 20

在形参中使用解构语法:

function vectorAdd([x1, y1], [x2, y2]) {
return [x1 + x2, y1 + y2]
}
vectorAdd([1, 2], [3, 4]) // => [4, 6]

function vectorMultiply({x, y}, n) {
return { x: x * n, y: y * n }
}
vectorMultiply({ x: 1, y: 2 }, 2) // => { x: 2, y: 4 }

可以将参数列表整合为对象作为实参,形参中对其需要的属性进行解构。

在 ES2018 中,解构对象也可以使用剩余形参,此时剩余形参是一个对象,包含所有未解构的属性。

函数作为命名空间

在函数体内声明的变量在函数外部不可见。根据这个特性,可以把函数用作临时的命名空间,来保证其中定义的变量不会污染全局命名空间。

可以在一个表达式中定义并调用匿名函数,来做到无声明执行代码块:

(function() {
// 要执行的代码...
})() // 外面这对圆括号也可以写在里面

闭包

与多数现代编程语言一样,JS 使用词法作用域。函数执行时使用的是定义函数时生效的变量作用域,而非调用函数时生效的变量作用域。因此,JS 函数对象的内部状态除了包括函数代码,还要包括对函数定义所在作用域的引用。函数对象与作用域组合起来解析函数变量的机制,称为 闭包

闭包值得关注的时候,是定义函数与调用函数不在同一个作用域的时候。最常见的就是一个函数返回了在它内部定义的嵌套函数:

let scope = "global scope"
function fn() {
let scope = "local scope"
return function() {
return scope
}
}
fn()() // => "local scope"

每次函数调用都会创建一个新的闭包,闭包之间的内部变量互不共享(可称为私有变量):

function counter() {
let n = 0
return {
count() { return n++ }
reset() { n = 0 }
}
}

function counter() {
let n = 0
return () => {
return n++
}
}

let a = counter(), b = counter()
a.count() // => 0
a.count() // => 1
b.count() // => 0

可以通过闭包给对象添加私有属性:

function addPrivateProperty(obj, name) {
let value
obj[`get${name}`] = () => value
obj[`set${name}`] = v => { value = v }
}
let obj = {}
addPrivateProperty(obj, "Name")
obj.setName("Talaxy")
obj.getName() // => "Talaxy"

在 ES6 及后,引入了块级作用域。一对 {} 即为一个块级作用域。let 和 const 声明的变量会限制在块级作用域里。

在写闭包的时候应当注意,this 是 JS 关键字而非变量。箭头函数会继承包含它们的函数的 this 值,而用 function 声明的函数并非如此。如果闭包内需要使用其包含函数的 this 值,除了用 bind() 方法,还可以将 this 值赋值给变量:

let obj = {
value: 10,
m1() {
const self = this // 让嵌套函数可以访问外部 this 值
return function() { return self.value }
},
m2() {
return () => this.value // 箭头函数会自动继承 this 值
}
}

obj.m1()() // => 10
obj.m2()() // => 10ssss

函数属性、方法、构造函数

对函数使用 typeof 会返回 "function" 。函数也是特殊的对象,有自己的属性和方法。

函数属性

  • length 函数在参数列表中声明的形参个数(剩余形参除外);

  • name 定义函数时的名字,如果是匿名函数,则为第一次赋值的变量名或属性名;

  • prototype 函数自己的原型对象(除了箭头函数,所有函数都有原型属性)。

函数方法

  • call(thisArg, arg1, arg2, ...) & apply(thisArg, args) 间接调用函数。

    调用函数时,会使用 thisArg 做为函数的调用上下文,即 this 值:

    let obj = {}
    function setName(name) { this.name = name }
    setName.call(obj, "Talaxy")
    obj.name // => "Talaxy"

    两个函数功能一致,只是调用函数的参数传入形式不一样。call() 传入参数列表,apply() 传入参数数组。

  • bind(thisArg, arg1, arg2, ...) 返回一个新函数,其会绑定指定的 this 值。

    调用新函数时,thisArg 会永远作为函数的调用上下文,即 this 值:

    let obj = { name: "Talaxy" }
    function getName() { return this.name }
    let getObjName = getName.bind(obj)
    getObjName() // => "Talaxy"

    第一个参数后可以指定要绑定的参数,再调用的时候则不需要提供已经绑定的参数:

    let add = (x, y) => x + y
    let f = add.bind(null, 1)
    let g = f.bind(null, 3)
    f(2) // => 3
    g() // => 4

    对箭头函数进行 bind() 绑定不会覆盖 this 值,它们只会继承 this 值(但是参数可以绑定)。对绑定过的函数也不能进行 this 值覆盖。

    绑定函数的 name 属性为原函数名加上 "bound " 前缀。

  • toString() 返回一个表示当前函数源代码的字符串。

    大部分实现都返回函数完整的源代码。如果是内置函数或绑定函数,通常会用 [native code] 表示函数体。如果是 Function() 构造出来的函数,函数名会显示为 anonymous

Function() 构造函数

Function() 允许在运行时动态创建编译 JS 函数。它的前几个参数为形参列表,最后一个参数为函数体代码字符串(无法指定函数名,因此也为匿名函数):

const add = new Function("x", "y", "return x + y")

频繁调用该构造函数会影响程序性能。

Function() 构造的函数不使用词法作用域,而是始终编译为如同顶级作用域一样:

let scope = "global"
function fn() {
let scope = "local"
return new Function("return scope")
}
fn()() // => "global"

eval() 类似,Function() 构造函数会有重要的安全隐患。一般的浏览器已经默认禁用了这些特性,开发中也不应当使用到这样的代码。

函数式编程

书上 P196 页,暂不做笔记。

高阶函数

高阶函数即操作函数的函数,它接受函数作为参数,或者返回一个新函数:

// 返回一个计算 `f(g(x))` 的函数
function compose(f, g) {
return function(...args) => {
f.call(this, g.apply(this.args))
}
}

参考

Function - MDN

this - MDN

箭头函数 - MDN

剩余参数 - MDN

Scope(作用域)- MDN

闭包 - MDN