函数
1. 定义函数
1.1 函数声明
- 函数声明语句会被提升至当前作用域顶部,因此函数调用代码可以出现在函数声明之前。
- 如果没有
return
语句或者return
后没跟表达式,则函数返回undefined
。
function printprops(o) {
for (let p in o) {
console.log(`${p}: ${o[p]}\n`);
}
}
1.2 函数表达式
- 函数表达式与函数声明类似,区别在于函数声明一定要有函数名,函数表达式函数名是可选的,一般会没有函数名,并将函数表达式赋值给变量或常量。
const square = function(x) {
return x * x;
}
- 函数表达式可以有函数名,一般在调用自己(递归)时使用,但不建议,因为可以用赋值的变量来调用,更加简洁。
const f = function fact(x) {
if (x <= 1) return 1;
else return x * fact(x - 1);
}
// 等价于
const f = function(x) {
if (x <= 1) return 1;
else return x * f(x - 1);
}
- 函数表达式不能在定义之前被调用,因为在被求值之前是不存在的。
1.3 箭头函数
ES6 提出的箭头函数更加简洁,不需要 function
关键字和函数名:
const sum = (x, y) => { return x + y; }
如果只有一个 return
语句,则可以省略 return
关键字及花括号:
const sum = (x, y) => x + y;
如果只有一个参数,则可以省略参数列表的圆括号:
const polynomial = x => x * x + 2 * x + 3;
注意
- 无参情况下圆括号不能省略。
- 函数体只有一个
return
语句且返回对象字面量时,要么不省略 return 和 花括号,要么把对象字面量用圆括号包裹后返回。
const f = x => { return { value: x } }
const g = x => ({ value: x}) // 圆括号包裹
特性
- 箭头函数从环境继承
this
的值,而不是像其它函数那样定义自己的调用上下文。 - 箭头函数没有
prototype
属性,因此不能作为构造函数。
1.4 嵌套函数
重要的是理解变量作用域规则:作用域链逐层寻找。
2. 调用函数
2.1 函数调用
- 在一次调用中,每个实参表达式都会被求值,结果作为实参,并赋值给函数中定义的命名形参。
- 在 ES2020 中,可以使用条件式调用函数,从而只在函数不是
null
或undefined
时调用函数。
const a = sum?.(1, 2);
- 非严格模式下函数调用上下文(this 值)是全局对象(globalThis),严格模式下,调用上下文是
undefined
。对于箭头函数来说,总是继承自身定义所在环境的this
值。
2.2 方法调用
函数作为对象的属性就是方法,调用与普通函数类似,区别在于调用上下文,方法调用的 this
就是该调用对象,在方法体内可以通过 this
引用该对象。
let calculator = {
operand1: 1,
operand2: 1,
add() {
this.result = this.operand1 + this.operand2; // this 指向包含对象
}
}
calculator.add()
calculator.result // 2
一般方法通过点号进行属性访问,但使用方括号的属性访问表达式也可以实现方法调用:
o["m"](x, y) // o.m(x, y) 的另一种写法
除了箭头函数,嵌套函数不会继承包含函数的 this
值,如果嵌套函数被当作方法调用,this
值就是调用它的对象,如果被当作函数来调用,this
值要么是全局对象,要么是 undefined
。
let o = {
m: function() {
let self = this;
this === o // true
f();
function f() {
this === o // false: this 是全局对象或undefined
self === o // true
}
}
}
上面代码演示了一种常见技巧:把外层函数的 this
值赋值给另一个变量,并在嵌套内部函数内部使用该变量而非 this
。
2.2.1 方法调用链
前提:如果方法返回对象,基于该方法的返回值还可以调用其它方法。
说明
方法调用链是一种编程风格,即对象的方法统一返回 this
,则可以连续调用该对象的方法。
new Square().x(100).y(100).size(50).outline('red').fill('blue').draw()
2.3 构造函数调用
- 如果函数或方法调用前加上
new
关键字,就是构造函数调用。 - 构造函数调用时会先创造一个空对象,这个对象继承构造函数
prototype
属性指定的对象,构造函数中的this
指向新创建的对象。 - 构造函数不需要
return
,默认会返回新创建的对象,如果用return
显式返回了一个对象,则该对象替换新创建的对象被返回,如果return
非对象值,则仍返回新创建的对象。
2.4 间接调用
- JavaScript 函数有两个方法
call()
和apply()
,通过它们可以间接调用函数。 call()
和apply()
允许指定调用时的this
值,意味着可以将任何函数作为任何对象的方法来调用。
2.5 隐式调用
- 如果对象有获取方法或设置方法,则查询或设置其属性值可能会调用这些方法。
- 当对象在字符串上下文中使用时(比如当拼接对象与字符串时),会调用对象的
toString()
方法。类似地,当对象用于数值上下文时,则会调用它的valueOf()
方法。 - 遍历可迭代对象时,会涉及一系列方法调用。
- 标签模板字面量是一种伪装的函数调用。
- 代理对象。
3. 函数实参与形参
3.1 可选形参与默认值
- 当调用函数时传入的实参少于声明的形参,额外的形参会获得默认值,通常是
undefined
。 - 在设置有可选参数的函数时,一定要把可选参数放在参数列表的最后,这样调用时才能省略。
- ES6 之后,可以直接在参数列表中给参数指定默认值。
- 形参默认值可以是常量,也可以是变量或函数调用计算默认值。
// 不传 a 则创建个新数组并返回
function getPropertyNames(o, a = []) {
for (let property in o) a.push(property);
return a;
}
3.2 剩余形参与可变长度实参列表
剩余形参的作用:可以在调用时传入比形参多任意数量的实参。
function max(first = -Infinity, ...rest) {
let maxValue = first;
for (let n of rest) {
if (n > maxValue) {
maxValue = n;
}
}
return maxValue;
}
max(1, 10, 100, 2, 3, 1000, 4, 5, 6) // 1000
- 剩余形参前面有三个点
...
,且必须在参数列表的最后。传入的实参首先赋值给非剩余形参,然后剩余的实参保存到一个数组中赋值给剩余形参。 - 在函数体内,剩余形参的值永远是数组。
3.3 Arguments 对象
Arguments
对象是一个类数组对象,它保存了传给函数的所有实参值。- 严格模式下,
arguments
会被当作保留字。
function max(x) {
let maxValue = -Infinity;
for (let i = 0; i < arguments.length; i++) {
if (arguments[i] > maxValue) {
maxValue = arguments[i];
}
}
return maxValue;
}
max(1, 10, 100) // 100
3.4 函数调用中使用拓展操作符
在函数调用时,可以使用展开运算符 ...
展开可迭代对象。
let numbers = [5, 2, 10];
Math.min(...numbers) // 2
3.5 把函数实参解构为形参
如果函数形参名包含在方括号中,那么调用时传入的数组实参会被解构赋值为单独的命名形参:
function vectorAdd([x1, y1], [x2, y2]) {
return [x1 + x2, y1 + y2];
}
vectorAdd([1, 2], [3, 4]) // [4, 6]
类似的,如果函数需要一个对象实参,也可以把传入的对象解构赋值给形参:
function vectorMultiply({x, y}, scalar) {
return { x: x * scalar, y: t * scalar }
}
vectorMultiply({ x: 1, y: 2 }, 2) // { x: 2, y: 4 }
在解构赋值中可以为形参设置默认值:
function vectorMultiply({x, y, z = 0}, scalar) {
return { x: x * scalar, y: t * scalar, z: z * scalar }
}
vectorMultiply({ x: 1, y: 2 }, 2) // { x: 2, y: 4, z: 0 }
同正常数组和对象解构一样,形参的解构也可以使用剩余参数:
// 需注意解构中剩余语法与形参中剩余参数的区别
function f([x, y, ...records], ...rest) {
return [x + y, ...rest, ...records]; // 这里是拓展操作符
}
f([1, 2, 3, 4], 5, 6) // [3, 5, 6, 3, 4]
// 对象解构使用剩余语法,ES2018开始支持
function vectorMultiply({x, y, z = 0, ...props}, scalar) {
return { x: x * scalar, y: t * scalar, z: z * scalar, ...props }
}
vectorMultiply({ x: 1, y: 2, w: -1 }, 2) // { x: 2, y: 4, z: 0, w: -1 }
3.6 参数类型
JavaScript 会自动进行类型转换,但最好在一开始就判断参数类型,避免因类型不对而报错,减少不必要的运算。
function sum(a) {
let total = 0;
for (let ele of a) {
if (typeof ele !== 'number') {
throw new TypeError('element must be number')
}
total += ele;
}
return total;
}
sum([1, 2, 3]) // 6
sum(['a', 1, 2]) // TypeError
4. 函数作为值
- 函数可以作为值赋值给其它变量或传递给函数调用。
- 函数甚至可以没有名字,匿名函数也可以调用(立即调用表达式):
((x) => x * x)(3); // 9
let a = [x => x * x, 20];
a[0](a[1]); // 400
4.1 自定义函数属性
如果一个函数需要一个静态变量,且每次调用时都能访问到该变量,通常可以将该变量定义为函数自身的一个属性:
uniqueInteger.counter = 0;
function uniqueInteger() {
return uniqueInteger.counter++;
}
uniqueInteger(); // 0
uniqueInteger(); // 1
5. 函数作为命名空间
函数体内声明的变量在函数外部不可见。可以把函数作为临时的命名空间,保证其中定义的变量不会污染全局命名空间。
6. 闭包
- 函数对象与作用域组合起来解析函数变量的机制叫闭包,更简单点说就是能够访问其它函数内部变量的函数。
- 闭包由两部分组成:一个是函数本身,一个是函数创建时的词法环境,这个环境包括了闭包创建时所能访问的所有局部变量。
- 闭包的作用是读取函数内部的变量,同时让这些变量的值始终保存在内存中,不会在外部函数调用后自动清除。
// 利用闭包改写4.1的函数,防止自定义属性被恶意修改
let uniqueInteger = (function() {
let counter = 0;
return function() {
return counter++;
}
})();
uniqueInteger(); // 0
uniqueInteger(); // 1
function counter() {
let n = 0;
return {
count: function() { return n++; },
reset: function() { n = 0; }
}
}
let c = counter(), d = counter(); // 每次调用都会创建新的作用域,互相独立
c.count() // 0
d.count() // 0
c.reset() // reset 和 count 共享状态
c.count() // 0
d.count() // 1
7. 函数属性、方法和构造函数
7.1 length
只读属性,表示参数列表中的形参个数,不计算剩余形参。
7.2 name
只读属性,即函数定义时使用的名字,如果是未命名的函数,返回第一次创建时赋值的变量名。
7.3 prototype
除了箭头函数外,所有函数都有 prototype
属性。
7.4 call() 和 apply()
call()
和apply()
允许一个对象间接调用一个函数,就仿佛该函数是该对象的一个方法。call()
和apply()
第一个参数是要调用这个函数的对象,在函数体内变成this
的值。- 如果对箭头函数调用这两个方法,则传入的第一个参数会被省略。
- 对于
call()
方法,除了第一个参数,后续的参数都会传给被调用的函数,apply()
方法与call()
类似,只不过传给函数的参数需要以数组的形式提供:
f.call(o, 1, 2);
f.apply(o, [1, 2]);
// 通过apply将数组传给需要多个参数的函数
let bigger = Math.max.apply(Math, [1, 2, 3])
7.5 bind()
bind()
方法的主要目的是把函数绑定到对象身上,即确定函数内部this
值。如果在函数f
上调用bind()
并传入对象o
,会返回一个新函数。该新函数内部this
就指向o
。
function f(y) { return this.x + y }
let o = { x: 1 }
let g = f.bind(o);
g(2); // 3
let p = { x: 10, g }
p.g(2) // 3, g 仍然绑定在 o 上
- 传递给新函数的所有参数都会传给原始函数。
bind()
也可以执行“部分应用”,即在第一个参数之后传给bind()
的参数也会随this
值一起被绑定,这种技术有时候也称为柯里化:
let sum = (x, y) => x + y;
let succ = sum.bind(null, 1); // 把第一个参数绑定为1
succ(2) // 3
function f(y, z) {
return this.x + y + z;
}
let g = f.bind({ x: 1}, 2);
g(3) // 6
7.6 toString()
函数也有 toString()
方法,ECMAScript 规范要求该方法返回一个符合函数声明的字符串。
7.7 Function 构造函数
Function
构造函数可以用来创建新函数,通过构造函数创建的是匿名函数:
const f = new Function('x', 'y', 'return x + y;')
Function
构造函数可以接收任意多个字符串参数,最后一个参数是函数体文本,前面的参数都是形参名。如果函数没有参数,可以只给构造函数传一个参数作为函数体。
注意
Function
构造函数允许在运行时动态创建和编译函数。- 每次调用时都会创建一个新的函数对象。
- 创建的函数不使用词法作用域,始终编译为同顶级函数一样。
8. 函数式编程
8.1 高阶函数
高阶函数就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数。
function not(f) {
return function(...args) {
let result = f.apply(this, args);
return !result;
}
}
const even = x => x % 2 === 0;
const odd = not(even); // 确定数值是不是偶数的新函数
[1, 1, 3, 5, 5].every(odd) // true
8.2 函数的部分应用
- 通过
bind()
方法可以实现函数的部分应用,即将一个函数绑定到一个对象上,并指定部分参数,返回一个新函数。 bind()
方法在左侧应用参数,也可以通过高阶函数实现左侧或右侧的部分应用:
// 传给这个函数的参数会部分应用到左边
function partialLeft(f, ...outArgs) {
return function (...innerArgs) {
let args = [...outArgs, ...innerArgs]
return f.apply(this, args);
}
}
// 传给这个函数的参数会部分应用到右边
function partialRight(f, ...outArgs) {
return function (...innerArgs) {
let args = [...innerArgs, ...outArgs]
return f.apply(this, args);
}
}
8.3 函数记忆
在函数式编程中,如果函数拥有缓存数据,则称为函数记忆,如:
function memoize(f) {
// 缓存
const cache = new Map();
return function(...args) {
let key = args.length + args.join('+');
if (cache.has(key)) {
return cache.get(key);
} else {
let result = f.apply(this, args);
cache.set(key, result);
return result;
}
}
}
说明
memoize
函数创建了一个 map
对象作为缓存,并赋值给一个局部变量,在闭包中成为被返回函数的私有变量。