6. 函数与函数式编程
1 函数
1.1 arguments 对象
arguments 是一个对应于 传递给函数的参数 的 类数组(array-like) + 可迭代对象。
- array-like 意味着它一个对象,不是数组:
- 拥有数组的一些特性:length,index 下标访问;
- 没有数组的一些方法:forEach、map 等遍历方法,但是可以用
[].call(arguments)
调用。
- iterable 可迭代意味着它拥有一个
Symbol.iterator
属性:- 可以调用
Symbol.iterator
实现迭代。
- 可以调用
- arguments 有三个参数
- length:传递给函数的参数数量
- callee:指向参数所属的当前执行的函数
- 返回一个新的 Array迭代器 对象,可遍历所有参数。
注:
- 箭头函数没有 arguments;
- arguments 已经逐渐被遗弃,如果需要拿到所有参数,使用扩展运算符
...args
。
Arguments
的特性:
function foo(x, y, z) {
console.log(arguments); // arguments: [10, 20, 30, length: 3, callee: ƒ, Symbol(Symbol.iterator): ƒ, [[Prototype]]: Object]
// arguments 的三个属性
console.log(arguments.length); // 3
console.log(arguments.callee); // 函数本身 ƒ foo(x, y, z) { ... }
console.log(arguments[Symbol.iterator]); // 迭代器 f
// 类数组对象
console.log(arguments[0]); // 10
// 可迭代对象
for (const val of arguments)
console.log(val); // 10,20,30
// 手动迭代
const iterator = arguments[Symbol.iterator]();
while (true) {
const elem = iterator.next();
console.log(elem);
// {value: 10, done: false}
// {value: 20, done: false}
// {value: 30, done: false}
// {value: undefined, done: true}
if (elem.done) break;
}
}
foo(10, 20, 30);
1.1.1 arguments
转换为数组
// 方法一:遍历+push
const arr1 = [];
for (const val of arguments) arr1.push(val);
// 方法二:所有Array可返回新数组的API:
// contact() 将传入的数组或者元素与原数组合并,组成一个新的数组并返回
// slice() 连接两个或多个数组
// join() 将数组中的所有元素连接成一个字符串
// indexOf() 用于查找元素在数组中第一次出现时的索引,如果没有,则返回-1
// lastIndexOf() 用于查找元素在数组中最后一次出现时的索引,如果没有,则返回-1
// includes() ES7 判断当前数组是否包含某个指定的值
const arr2 = Array.prototype.slice.call(arguments);
const arr3 = [].slice.call(argumtnts);
// 方式三:扩展运算符、Array api
const arr4 = [...arguments];
const arr5 = Array.from(arguments);
1.1.2 对比:类数组对象、可迭代对象
类数组和可迭代在遍历功能上非常相似,都可以方便的方式内部元素,但是二者仍然有明显的区别:
iterable
可迭代对象:实现了Symbol.iterator
的对象;array-like
类数组对象:具有数字索引,并且有length
属性;
1.1.3 附:Array.from
用法
Array.from()
可以把定义的类数组对象转化为一个真正的数组;Array.from()
在 leetcode 的背包问题 / dp 问题中,常常需要声明一个二维数组:
const dp = Array.from(new Array(nums.length), () => new Array(2).fill(0));
- 第一个参数,声明一个固定长度的数组;
- 第二个参数,对数组中每个成员都执行一遍回调函数;
返回的 dp 就是一个二维数组,每个成员都是一个长度为 2,赋值为 0 的子数组。
1.2 其他待补充
JavaScript 函数有两个不同的内部方法:[[Call]] 和 [[Construct]]。当通过 new 关键字调用函数时,执行的是 [[Construct]] 函数,它负责创建一个通常被称作实例的新对象,然后再执行函数体,讲 this 绑定到实例上:如果不通过 new 关键字调用函数,则执行 [[Call]] 函数,从而直接执行代码中的函数体。
具有 [[Construct]] 方法的函数被统称为构造函数。
—— 第三章《函数》P54
元属性(Metaproperty)new.target
为了解决判断函数是否通过 new 关键字调用的问题,ECMAScript6 引入了 new.target 这个元属性。
元属性是指非对象的属性,其可以提供非对象目标的补充信息(例如 new)。当调用函数的 [[Construct]] 方法时,new.target 被赋值为 new 操作符的目标,通常是新创建对象实例,也就是函数体内 this 的构造函数;如果调用 [[Call]] 方法,则 new.target 的值为 undefined。
有了这个元属性,可以通过检查 new.target 是否被定义过,从而安全低检测一个函数是否是通过 new 关键字调用的,就像这样:
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name;
} else {
throw new Error("必须通过 new 关键字来调用 Person");
}
}
let person = new Person("Moxy"); //
let person2 = Person.call(person, "Moxy") // 报错,没有用 new,new.target值为 undefined
—— 第三章《函数》P55
2. 函数式编程
2.1 纯函数
JavaScript 符合函数式编程的范式,函数式编程中有一个非常重要的概念叫 纯函数。
- 确定的入参(输入),一定会返回确定的结果(输出)。
- 同样的输入内容,不论执行多少次,返回的结果必须都是一定的。
- 函数在执行过程中,不能产生副作用
- 不能有事件绑定,比如绑定触发事件监听等等;
- 不能创建/修改,当前函数作用域以外的任何参数、变量,尤其注意不能直接修改入参。
- 不能修改有外部存储,产生新的 I/O 输入/输出操作。
在 react 中的应用:
- react 中的 函数组件 必须是一个纯函数;
- redux 中的 reducer 必须是一个纯函数;
在 js 中的应用:
- 不会原地修改数组的 API,就是纯函数(splice 等,上文 argument 有总结)。
- slice:截取数组时,不会对原数组进行任何操作,而是生成一个新的数组;
- splice:截取数组时,会返回截断的数组,也会对原数组进行修改;
纯函数的意义:
提高项目的解耦:
- 自己编写的组件 / 函数 / API 不会对项目中其他部分造成影响。程序员只需要关心自己的业务逻辑,而不用关心传入的内容是如何获得的,或者传入的内容是否会依赖其他的外部变量。
2.2 Currying 柯里化
currying:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数。
- 把一个接收多个参数的函数改造,变成接受一个单一参数的新函数。
- 这个新函数只接受第一个参数,然后新函数也会返回一个函数。
- 返回的这个函数接受余下的参数,然后旧函数的逻辑在这里集中完成。
// 未柯里化
function add1(x, y, z) {
return x + y + z;
}
// 柯里化1
function add2(x) {
return function(y) {
return function(z) {
return x + y + z;
}
}
}
// 柯里化2
const add3 = x => y => z => {
return x + y + z;
}
// test
add1(10, 20, 30);
add2(10)(20)(30);
add3(10)(20)(30);
柯里化的意义1:单一职责原则(SRP single responsibility principle)
- 让每一个函数处理的问题尽可能单一,而不是将一大堆的处理过程交给一个函数来处理。
- 类似于异步编程的思想,解决完问题一,再解决问题二,接着解决问题三。让问题一步步处理。
柯里化的意义2:复用参数逻辑
- 形成一个树状结构,最外层函数是根节点,往下可以展开为多个节点。
function add(x, y, z) {
x = x + 2; // 这个逻辑可能要10行
y = y * 2; // 50行
z = z * z; // 80行
return x + y + z;
}
add(10, 20, 30);
// add 函数中包含了3个自逻辑,currying 把职责单一
function sum(x) {
x = x + 2;
return function(y) {
y = y * 2;
return function(z) {
z = z * z;
return x + y + z
}
}
}
// 柯里化用法1:职责单一
sum(10)(20)(30);
// 柯里化用法2:复用,最后一步计算可以直接复用
const square = sum(10)(20);
square(3);
square(10);
square(20);
例如,日志打印:
// log 来日志打印
function log(date, type, message) {
console.log(`[${date.getHours()}:${date.getMinutes()}][${type}]: [${message}]`)
}
// test,每次使用 log 都需要重复输入非常多内容,用柯里化优化:
log(new Date(), "DEBUG", "查找到轮播图的bug")
log(new Date(), "DEBUG", "查询菜单的bug")
log(new Date(), "DEBUG", "查询数据的bug")
// 柯里化的优化
var log = date => type => message => {
console.log(`[${date.getHours()}:${date.getMinutes()}][${type}]: [${message}]`)
}
// 如果我现在打印的都是当前时间
var nowLog = log(new Date())
nowLog("DEBUG")("查找到轮播图的bug")
nowLog("FETURE")("新增了添加用户的功能")
// 如果都是bug问题:
var nowAndDebugLog = log(new Date())("DEBUG")
nowAndDebugLog("查找到轮播图的bug-1")
nowAndDebugLog("查找到轮播图的bug-2")
nowAndDebugLog("查找到轮播图的bug-3")
nowAndDebugLog("查找到轮播图的bug-4")
// 如果都是新特新问题:
var nowAndFetureLog = log(new Date())("FETURE")
nowAndFetureLog("添加新功能~")
- 项目中用 log 打印日志,用柯里化实现了不同模块的输出:
log: 规则引擎-index-func1-10;
log: 规则引擎-index-func2-10;
log: 规则引擎-新增规则-foo1-10;
....
面试题:currying 实现
把一个普通函数转化为 curry 柯里化函数。
function currying(fn) {
// 递归 curried,接受所有参数
function curried(...args) {
// base case
if (fn.length <= args.length) return fn.call(this, ...args);
// 递归
return function(...args2) {
return curried.call(this, ...args, ...args2);
}
}
return curried;
}
// 详细注释
function currying(fn){
// 返回已柯里化函数
function curried(...args){
// args.length 传入参数的个数
// fn.length 函数形参的个数
// 判断当前已经接受到的参数个数,是否和函数本身需要的参数一致了
if (args.length >= fn.length) {
return fn.call(this, ...args); // 防止外部调用fn时绑定过this,用.call调用,不丢失this指向
}
// 参数不够,返回一个新函数接受剩余参数
return function(...otherArgs) {
// 递归调用curried,并把之前已经接收的参数添加
// 这里要return,因为并不知道当前参数是否足够,如果不够还会返回一个函数接受剩余的参数
return curried.call(this, ...args, ...otherArgs);
}
}
return curried;
}
// test
function add(x, y, z) {
return x + y + z;
}
// 假如 curryAdd 最多有3个参数,但传递方式可以有如下情况:
const curryAdd = currying(add);
curryAdd(10, 20, 30);
curryAdd(10, 20)(30);
curryAdd(10)(20)(30);
3. 组合函数
把两个逻辑相连的函数组合起来,按顺连接。
function double(num) {
return num * 2;
}
function square(num) {
return num ** 2;
}
// 分开调用,可以简化为一个函数:
const result = square(double(19));
// 定义组合函数
function composeFn(fn1, fn2) {
return function(count) {
return fn1(fn2(count));
}
}
// 组合调用:
const newFn = composeFn(double, square);
const result = newFn(19);
如果实现通用组合函数,也就是多个函数组合为一个函数:
function compose(...fns) {
const len = fns.length; // 统计共有几个函数需要组合
for (let i = 0; i < len; i++) {
if (typeof fns[i] !== 'function') {
throw new TypeError("Expected arguments are function");
}
}
// 递归执行所有fns
return function (...args) {
let index = 0;
// 如果没有传入任何函数,则直接返回参数当作结果。
let result = len ? fns[index].call(this, ...args) : args;
while (++index < len) {
result = fns[index].call(this, result);
}
return result;
}
}
// 测试:
function add(num1, num2) {
return num1 + num2;
}
function double(num) {
return num * 2;
}
function square(num) {
return num ** 2;
}
const newFn = compose(add, double, square);
const result = newFn(1, 2); // 36
4. with 和 eval
4.1 with
with语句 扩展一个语句的作用域链。
不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源。
var obj = {
name: "hello world",
age: 22
}
with(obj) {
console.log(name); // hello world
console.log(age); // 22
}
4.2 eval
是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行。
问题:
eval 代码的可读性非常的差(代码的可读性是高质量代码的重要原则);
eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;
eval 的执行必须经过 JS 解释器,不能被 JS 引擎优化;
var evalString = `var message = "hello world"; console.log(message)`
eval(evalString);
console.log(message);
5. Strict Mode
严格模式,ES5 提出的标准。在严格模式中,有以下限制:
- 严格模式通过 抛出错误 来消除一些原有的 静默错误(silent fail); 原本错误语法,被认为也是可以正常被解析的,严格模式下会抛出错误;
- 左查询时,无法创建全局变量:
a = "123"
;
- 左查询时,无法创建全局变量:
- 严格模式让 JS 引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
- eval 不再为上层引用变量;
- with 不允许使用;
- this 绑定不会默认转化为包装对象;
- 严格模式禁用了在 ECMAScript 未来版本中可能会定义的一些语法;
严格模式在全局/ 函数作用域中开启:
"use strict"