函数
函数 是 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))
}
}