这里写目录标题
JavaScript
是支持
函数式编程的,在
JavaScript
中函数是
一等公民。函数可以作为
别的函数的参数、函数的返回值,赋值给变量或存储在数据结构中
1. 函数的特性
声明调用
- 声明函数,在
JavaScript
中也可以称为定义函数,这个过程是对某些功能的封装过程,声明方式有以下几种:- 函数声明:这种声明在代码执行前会被提升,可以在函数声明之前调用
function functionName() { // 函数体 }
- 函数表达式:
const sum = function add(a, b) {}
- 箭头函数:文章后面有详细讲解
const sum = () => console.log("sum")
- 匿名函数:
const greet = function() {}
Function
构造函数:不推荐使用,
const sum = new Function('a', 'b', 'console.log(a, b)')
- 函数声明:这种声明在代码执行前会被提升,可以在函数声明之前调用
- 函数声明完后里面的代码是不会执行的,函数必须调用才会执行
- 调用函数可通过函数名
()
即可:比如test() - 在函数内部是可以调用另外一个函数的
- 函数调用自己时必须有结束条件,否则会产生无限调用,造成报错
- 调用函数可通过标签字符串如
test``
调用,在react
中css-in-js
时使用了raw
属性是字符串模板字面量解析过程中生成的一个只读属性,包含模板字面量的原始字符串形式
const obj = { name: "小小", age: 18, height: 188, }; function foo(...args) { console.log(args); } foo`hello world`; foo`hello${obj.name},年龄${obj.age},height${obj.height}${obj}`;
- 调用函数可通过函数名
参数
函数调用时,按照函数定义的参数顺序,把希望在函数内部处理的数据通过参数传递到内部,进行需要的数据处理
- 形参(
parameter
):定义函数时,小括号中的参数是用来接收参数用的,在函数内部作为变量使用,可以写默认值即function greet(name = 'Guest') { console.log(name) }
- 实参(
argument
):调用函数时,小括号中的参数,是用来把数据传递到函数内部用的 - 剩余参数和
arguments
在文章后面有详细讲解
参数默认值
- 传参为
undefined
时会使用默认值,参数的默认值我们通常会将其放到最后 - 默认值会改变函数的
length
的个数,默认值以及后面的参数都不计算在length
之内了 - 默认值也可以和解构一起来使用
function foo1(x, y = 20) {
console.log(x, y);
console.log(arguments.length);
}
// 和解构一起用
function foo2({ name, age } = { name: "小小", age: 18 }) {
console.log(name, age);
}
function foo3({ name = "小小", age = 20 } = {}) {
console.log(name, age);
}
foo1(10); // 10 20 这时参数arguments.length = 1
foo1(20, 40); // 20 40
foo1(30, 0); // 30 0
foo1(40, null); // 40 null
foo1(50, undefined); // 50 20
foo2(); // 小小 18
foo3(); // 小小 20
返回值
函数可以通过 return
语句返回一个值。
- 可以返回任意类型的值,如原始类型、对象、数组、函数、
undefined
、this
以及Promise
。 - 当函数执行到
return
语句时,会立即停止执行,并返回指定的值 - 如果函数中没有使用
return
语句或者return
后面没有跟任何值,那么函数默认返回undefined
属性方法
JavaScript
中函数也是一个对象,那么就可以有属性和方法 :
- 属性
name
:一个函数的名称可以通过name
来访问,如果函数是匿名的,则返回空字符串 - 属性
length
:属性length
用于返回函数参数的个数,个数是不包括剩余参数的 - 属性
prototype
:每个函数都有一个prototype
属性,这是用于创建对象实例时提供的原型对象,具体学习这篇文章: - 方法
toString()
:返回函数的字符串表示 - 方法
bind()/call()/apply()
:这些方法可以用来调用函数、绑定this
值和设置参数,具体在文章后面学习
arguments
arguments
是一个类数组对象,包含传递给函数的所有参数
- 具有
length
属性也可以通过索引来访问每个参数 - 但它不是一个真正的数组(没有数组方法,如
forEach
、map
、filter
等) - 不可在箭头函数中使用,在严格模式下也不可用,推荐使用
rest
参数代替。
arguments
转Array
:以便使用数组的一些特性
- 方式一:遍历
arguments
,添加到一个新数组中 - 方式二:调用数组
slice
函数的call/apply
方法 - 方式三:
Array.from
和[...arguments]
function foo(name) {
console.log(arguments, arguments[1]);
var arrArguments1 = [];
for (var i = 0; i < arguments.length; i++) {
arrArguments1.push(arguments[i]);
}
var arrArguments2 = [...arguments];
// Array.from() An iterable object to convert to an array.
var arrArguments3 = Array.from(arguments);
// [].slice() 这时slice函数中 this指向[]并进行截取
// 当我们使用apply调用slice函数时this指向了传的arguments,也就对arguments截取并返回
var arrArguments4 = [].slice.apply(arguments);
console.log(arrArguments1, arrArguments2, arrArguments3, arrArguments4);
}
foo("nihao", 18);
剩余参数(rest
)
ES6
中引用了rest parameter
,可以将不定数量的参数放入到一个数组中:
- 剩余参数也可以在箭头函数中使用
- 剩余参数可以用于将所有传入的参数收集到一个数组中
- 剩余参数可以与其他参数一起使用,但它必须是参数列表中的最后一个参数
function exampleFunction(...args) {
// args 是一个包含所有传入参数的数组
console.log(args);
}
exampleFunction(1, 2, 3); // 输出: [1, 2, 3]
function greet(greeting, ...names) {
return `${greeting}, ${names.join(' and ')}!`;
}
console.log(greet('Hello', 'Alice', 'Bob')); // 输出: Hello, Alice and Bob!
var multiply = (...args) => args.reduce((acc, num) => acc * num, 1);
console.log(multiply(2, 3, 4)); // 输出: 24
作用域
作用域(Scope
)表示一些标识符的作用有效范围
- 在函数内部的变量,被称之为局部变量;在函数外部的变量,被称之为外部变量
- 函数的作用域表示在函数内部定义的变量,只有在函数内部可以被访问到
- 优先访问自己函数中的变量,没有找到时,在外部中访问
- 关于作用域、作用域链、变量提升、AO、VO、GO和函数执行过程等具体可以学习这篇文章:https://juejin.cn/post/7393522719080857639#heading-14
原型
具体学习这篇文章:https://blog.youkuaiyun.com/qq_45730399/article/details/141104727?spm=1001.2014.3001.5501
this
指向
具体学习这篇文章:https://blog.youkuaiyun.com/qq_45730399/article/details/140995147?spm=1001.2014.3001.5501
执行过程
具体学习这篇文章:https://blog.youkuaiyun.com/qq_45730399/article/details/141055268?spm=1001.2014.3001.5501
2. 特殊的函数
with
函数
with
语句 扩展一个语句的作用域链,不建议使用with
语句,因为它可能是混淆错误和兼容性问题的根源
var obj = {
name: "你好",
age: 18,
};
with (obj) {
console.log(name, age); // 你好 18
}
eval
函数
内建函数 eval
允许执行一个代码字符串
eval
是一个特殊的函数,它可以将传入的字符串当做JavaScript
代码来运行eval
会将最后一句执行语句的结果,作为返回值
var str = "var msg = 'hello';console.log(msg)";
eval(str);
console.log(msg); // hello
不建议在开发中使用eval
:
eval
代码的可读性非常的差(代码的可读性是高质量代码的重要原则)eval
是一个字符串,那么有可能在执行的过程中被刻意篡改,会造成被攻击的风险eval
的执行必须经过JavaScript
解释器,不能被JavaScript
引擎优化;
apply/call/bind函数
在 JavaScript
中,apply
、call
和 bind
是 Function
原型对象的三个方法,用于改变函数 this
的值。它们的主要区别在于传递参数的方式以及是否立即调用函数
- 区别:
apply:functionName.apply(thisArg, [argsArray])
会立即调用函数,第一个参数也是用来指定this
的值,后面是参数作为数组传递call:functionName.call(thisArg, arg1, arg2, ...)
会立即调用函数,第一个参数也是用来指定this
的值,后面是参数单独传递bind:const boundFunction = functionName.bind(thisArg, arg1, arg2, ...)
- 它不会立即调用函数,而是返回一个新函数
- 这个新函数的
this
值被永久绑定到bind
的第一个参数,新函数的this
值不能再被重新绑定 bind
返回的新函数可以用作构造函数,其this
会被设置为新创建的对象- 后续参数单独传入将作为新函数的参数传入,新函数的
length
属性(参数个数)会去掉绑定的参数个数function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); } const boundGreet = greet.bind(null, 'Hello'); console.log(greet.length); // 输出: 2 console.log(boundGreet.length); // 输出: 1
- 手写:
封装实现:Function.prototype.myFn = function (thisArg) { console.log(this); // 当前调用的函数 // 如果thisArg为null或undefined,设置为window(或globalThis) thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg); thisArg.fn = this; // 将当前函数赋值给thisArg对象的fn属性 }; Function.prototype.myApply = function (thisArg, argsArray) { console.log(this) // foo函数 this.myFn(thisArg); // 使用argsArray数组调用函数 const result = thisArg.fn(...argsArray); delete thisArg.fn; // 删除临时添加的fn属性 return result; // 返回结果 }; Function.prototype.myCall = function (thisArg, ...args) { this.myFn(thisArg); // 使用展开运算符将参数传递给函数 const result = thisArg.fn(...args); delete thisArg.fn; // 删除临时添加的fn属性 return result; // 返回结果 }; Function.prototype.myBind = function (thisArg, ...args) { // 这里用到上层的this,使用箭头函数 return (...newArgs) => { // 调用myCall,将参数合并 return this.myCall(thisArg, ...args, ...newArgs); }; }; // 测试 function foo(a, b) { console.log(this.name, a, b); } const obj = { name: 'Alice' }; foo.myApply(obj, [1, 2]); // 输出: Alice 1 2 foo.myCall(obj, 1, 2); // 输出: Alice 1 2 const boundFoo = foo.myBind(obj, 1); boundFoo(2); // 输出: Alice 1 2
纯函数
纯函数(Pure Function
)是函数式编程中的一个重要概念。纯函数具有以下两个主要特性:
- 确定的输入一定会产生确定的输出:纯函数对于相同的输入参数,总是返回相同的结果
- 函数在执行过程中不能产生副作用:纯函数不依赖于外部状态或变量,也不修改外部状态或变量,仅依赖于输入参数,并且不影响函数外部的任何状态
- 比如
slice
就是一个纯函数,不会修改数组本身,而splice
函数不是一个纯函数
作用和优势:
- 安心的编写: 在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改
- 安心的使用: 在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出
箭头函数
是ES6
之后增加的一种编写函数的方法,它比函数表达式要更加简洁
- 不会绑定
this
、arguments
和super
参数,没有原型prototype
属性 - 不能作为构造函数来使用(不能和
new
一起来使用,会抛出错误) - 编写:
()
函数的参数;{}
函数的执行体,
如:var add = (num1, num2) =>{ console.log(num1 + num2) }
编写优化:
- 优化一:只有一个参数时
()
可以省略 - 优化二:函数执行体中只有一行代码时可以省略
{}
,省略{}
时不能写return
,但函数内部会将这行代码的返回值作为整个函数的返回值 - 优化三:函数执行体只返回一个对象并省略
{}
时, 那么需要给这个对象加上()
var add = num => console.log(num);
var add1 = (num1, num2) => num1 + num2;
var add2 = () => ({ name: "add3", num1: 88, num2: 99 });
add(10); // 10
console.log(add(), add1(2, 3), add2());
// undefined 5 {name: 'add3', num1: 88, num2: 99}
立即执行函数 IIFE
一个函数定义完后被立即执行,专业名字:Immediately-Invoked Function Expression
(IIFE
立即调用函数表达式)立即执行函数必须是一个表达式,表达式是任何可以进行计算并产生一个值的代码单元
- 格式如下:当
()
包裹函数时,它会默认将函数作为表达式去解析,而不是函数声明(function(){ console.log('函数无需调用会立即执行') })()
- 其他写法:
+function bar() { console.log("bar"); }(); (function bar() { console.log("bar"); }());
- 用处:会创建一个独立的执行上下文环境,可以避免外界访问或修改内部的变量,也避免了对内部变量的修改,比如下面例子:
var btnEl = document.querySelector(".btn"); /* 1. 使用var声明时 i 会被提升值为undefined 2. 再执行for循环,i = 0, 3. 判断 0 < btnEl.children.length,得出0小于4 4. 执行循环体的代码btnEl.children[0].onclick = 函数 5. i++,i = 1,再重复执行for循环 当循环执行完时 i = 4,这时你点击按钮会找 i, 在函数作用域没找到会向外层即全局查找,找到 i=4 所以不管你点击第几个按钮都是4,那么怎么解决呐? */ for (var i = 0; i < btnEl.children.length; i++) { // 事件处理函数是闭包,创建时会捕获其外部作用域(全局作用域) btnEl.children[i].onclick = function () { console.log(`第${i + 1}个按钮被点击了`); }; } /* 使用立即执行函数解决: 当执行到循环体代码时,立即执行函数会立即调用创建FEC, 形成自己的作用域,并定义传入的参数ii=0和事件, 当点击时执行事件函数,取到立即执行函数中的ii,不会取到全局的4 */ for (var i = 0; i < btnEl.children.length; i++) { (function (ii) { // 事件处理函数是闭包,创建时会捕获其外部作用域(立即执行函数的函数作用域) btnEl.children[ii].onclick = function () { console.log(`第${ii + 1}个按钮被点击了`); }; })(i); }
高阶函数
高阶函数必须至少满足两个条件之一:
- 接受一个或多个函数作为输入参数
- 会输出一个函数
function foo(fn){
fn()
}
function bar(){
console.log("我是bar函数被调用")
}
foo(bar)
foo
这种函数我们可以称之为高阶函数
递归函数
递归是一种重要的编程思想,它将一个复杂的任务,转化成可以重复执行的相同任务
案例:实现一个自己的幂函数pow()
function myPow(n, m) {
// console.log(Math.pow(2, 3)); // 8
// var sum = 1
// for (var i = 0; i < m; i++) {
// sum *= n;
// }
// console.log(sum) // 8
return m === 1 ? n : n * myPow(n, m - 1);
}
console.log(myPow(2, 3)); // 8
组合函数
组合(Compose
)函数是在JavaScript
开发过程中一种对函数的使用技巧和模式,基本思想是将多个函数结合起来使其依次执行,将一个函数的输出作为下一个函数的输入
例如现在有三个函数f1, f2, f3
,现在希望输入一个x
依次调用这三个函数来得到y
,过程大概是这样:y = f1(f2(f3(x)))
,我们要做的就是实现一个函数可以传入多个回调函数做参数,内部自动执行f1(f2(f3(x)))
,并且返回一个函数,让其调用时再传入x
,实现组合函数如下:
function compose(...fns) {
if (!fns.length) return;
for (var i = 0; i < fns.length; i++) {
if (typeof fns[i] !== "function") {
throw new Error(`index position ${i + 1} must be function`);
}
}
return (...args) => {
/* // 方式一
if (fns.length === 1) {
return fns[0](...args);
} else {
return fns[0](
compose.apply(this, fns.slice(1, fns.length)).apply(this, args)
);
}
*/
/* // 方式二
return fns.length === 1
? fns[0](...args)
: fns[0](
compose.apply(this, fns.slice(1, fns.length)).apply(this, args)
);
*/
// 方式三: reduceRight从数组的右端(即从最后一个函数)开始,依次对每个函数进行处理
return fns.reduceRight((acc, fn) => [fn(...acc)], args)[0];
};
}
var addNum = (x, y) => x + y;
var multiply = (x) => x * 2;
var subtract = (x) => x - 2;
var combinedFn = compose(subtract, multiply, addNum);
console.log(combinedFn(10, 5)); // 输出 28
柯里化函数
概念
柯里化也是属于函数式编程里面一个非常重要的概念,它属于一种关于函数的高阶技术还被用于其他编程语言。
是指将一个接受多个参数的函数转换成一系列每次只接受一个参数的函数的技术,这个技术以逻辑学家Haskell Curry
命名
- 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数
- 柯里化是一种函数的转换,将一个函数从可调用的
f(a, b, c)
转换为可调用的c
- 柯里化不会调用函数。它只是对函数进行转换
简单例子:
// 未柯里化
function calcNum(x, y, z) {
return x + y * z;
}
// 进行柯里化后 f(x)(y)(z)
// function curryFn(x) {
// return function (y) {
// return function (z) {
// return x + y * z;
// };
// };
// }
// 简化代码
var curryFn = x => y => z => x + y * z;
console.log(calcNum(1, 2, 3), curryFn(1)(2)(3)); // 7 7
优势
那么为什么需要有柯里化呢?
- 函数的职责单一
- 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理
- 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果;
- 比如对上面的案例进行一个修改:传入的函数需要分别被进行如下处理:
x + 2;y * 2;z ** 2
function curryFn(x) { x = x + 2 return function (y) { y = y * 2 return function (z) { z = z ** 2 return x + y * z; }; }; }
- 函数的参数复用,如下面例子:
makeAdder
函数要求我们传入一个num
(并且如果我们需要的话,可以在这里对num
进行一些修改)- 在之后使用返回的函数时,我们不需要再继续传入
num
了// 未柯里化 function calcAdd(num1, num2) { return num1 + num2; } console.log(calcAdd(5, 3), calcAdd(5, 6), calcAdd(5, 10)); // 8 11 15 // 上面函数调用时我们可以看到有重复的部分,这时如果柯里化之后 function curryCalcAdd(num1) { return (num2) => num1 + num2; } var curryCalcAdd5 = curryCalcAdd(5); console.log(curryCalcAdd5(3), curryCalcAdd5(6), curryCalcAdd5(10)); // 8 11 15
自动柯里化
实现一个自动柯里化工具函数,将一个或多个普通函数转换成柯里化函数
function curry(fn) {
// 返回一个内部的柯里化函数
return function curried(...args) {
// 检查传入的参数数量是否大于或等于原始函数的参数数量
if (args.length >= fn.length) {
// 如果参数足够,调用原始函数
return fn.apply(this, args);
} else {
// 如果参数不足,返回一个新的函数,继续收集参数
return function(...newArgs) {
// 递归调用 curried 函数,合并当前的参数和新传入的参数
return curried.apply(this, args.concat(newArgs));
};
}
};
}
// 原始的多参数函数
function add(x, y, z) {
return x + y + z;
}
// 创建柯里化版本的 add 函数
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出 6
console.log(curriedAdd(1, 2)(3)); // 输出 6
console.log(curriedAdd(1)(2, 3)); // 输出 6
console.log(curriedAdd(1, 2, 3)); // 输出 6
类和构造函数
具体学习这篇文章:https://blog.youkuaiyun.com/qq_45730399/article/details/141143522?spm=1001.2014.3001.5501