函数
函数基础
声明函数
// 1. 自定义函数 (命名函数)
function fn() {}
// 2. 函数表达式 (匿名函数)
const fun = function () {};
const fun = () => {};
// 3. 利用 new Function('参数1', '参数2', '函数体')
const f = new Function("a", "b", "console.log(a + b)");
调用函数
// 1.调用普通函数 fn(); fn.call() fn.apply()
// 2.对象的方法,Object.key,value 是函数,则被调用了
// 3.构造函数 new
// 4.绑定事件函数,执行事件即可 (回调函数)
// 5.定时器函数 (回调函数)
// 6.立即执行函数,自动调用 ()(),前一个()里面写一个函数;后一个()表示立即执行这个函数,可传参
void (function () {
console.log("...");
})();
- 如果实参个数大于形参个数,会取形参的个数;如果实参个数小于形参个数,缺少的实参会被当作
undefined
return
只能返回一个值,返回最后一个值;没有return
时返回undefined
- 访问没有括号
()
的函数将返回函数定义 - arguments 对象的使用
- 不确定有多少参数传递时,用
arguments
对象获取,存取了传递的所有实参 - 以伪数组形式展示:具有
length
属性;按照索引的方式存储;没有真正数组的一些方法; arguments
无需指出参数名就可访问arguments
身上有一个属性callee
,存储的是这个函数本身
- 不确定有多少参数传递时,用
// args 可以换成其它名字,如 demo,它属于是剩余参数
// arguments 不能换
function fn(...args) {
console.log("🚀🚀🚀 args: ", args); // [ 'a', 'b' ]
console.log("🚀🚀🚀 arguments: ", arguments); // [Arguments] { '0': 'a', '1': 'b' }
}
fn("a", "b");
// 剩余参数
function sum(first, ...args) {
console.log(first); // 10
console.log(args); // [20, 30]
}
sum(10, 20, 30);
this 指向:函数的不同调用方式决定了 this 的指向不同
- 对象的方法 this 指向的是对象;
- 构造函数和原型对象的 this 都指向创建的实例对象;
- 绑定事件函数 this 指向的是函数的调用者 btn 这个按钮对象;
- 定时器函数 this 指向的也是 window;
- 立即执行函数 this 还是指向 window;
- 普通函数 this 指向 window。
改变 this 指向
1️⃣ call()
const p = {}; // 一个对象
function fn(a, b) {} // 一个函数
// 1.call()
fn.call(p, a, b); // 把 this 指向对象 p;实参依次传入
function greet(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Alice" };
greet.call(person, "Hello", "!"); // Hello, Alice!
2️⃣ apply()
fn.apply(p, [a, b]); // 把 this 指向对象 p;实参以数组或伪数组形式传入
function greet(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Alice" };
greet.apply(person, ["Hello", "!"]); // Hello, Alice!
// 利用 apply 借助于数学内置对象的方法求数组最大值
const max = Math.max.apply(Math, arr);
3️⃣ bind()
// 该方法不会调用原来的函数,返回的是原函数改变 this 之后产生的新函数
const fn2 = fn.bind(p, 1, 2);
// 可用在 setTimeout 里面,绑定回调函数,改变其 this 指向
箭头函数
箭头函数是用来简化函数定义语法;对于箭头函数,this
关键字始终表示定义箭头函数的对象
// 普通函数
const fun = function () {};
// 箭头函数
const fn = () => {};
// 如果函数体中只有一句代码,并且代码的执行结果就是函数的返回值,函数体大括号可以省略
const sum = (n1, n2) => n1 + n2;
// 如果形参只有一个,形参外侧的小括号也是可以省略
const fn = (v) => alert(v);
const age = 100;
const obj = {
age: 20,
say: () => {
alert(this.age);
},
};
obj.say(); // 输出 100(对象不产生作用域,箭头函数 say 实际上被定义在 window 中)
闭包
高阶函数定义:对其他函数进行操作的函数,它接收函数作为参数或者将函数作为返回值输出;如回调函数,它就是将 callback
作为参数,在高阶函数的函数体内最后一行执行。
闭包
有权访问另一个函数作用域中变量的函数
function fn() {
const num = 10;
function fun() {
console.log(num); // fun 函数访问了外部函数 fn 作用域内的变量
}
fun();
}
fn(); // fn() 为闭包函数;被访问的变量所在的函数是闭包函数
闭包延伸了变量的作用范围,可以用以下方法从外部作用域访问内部的局部变量
function fn() {
const num = 10;
function fun() {
console.log(num);
}
return fun; // 返回一个函数 !!!
}
const f = fn(); // 调用 f 就可以拿到 num 的值,相当于从函数外部获取到函数内部的局部变量
// 可简写
function fn() {
const num = 10;
return () => console.log(num); // 直接返回一个匿名函数或箭头函数
}
// 使用 fn()() 即可打印出 num 的值,从外部访问函数作用域的变量
闭包应用案例
数据私有化:createCounter 返回了一个包含闭包的对象,这些闭包可以访问 count 变量,但 count 变量本身无法从外部直接访问。
function createCounter() {
let count = 0;
return {
increment: () => {
count++;
return count;
},
decrement: () => {
count--;
return count;
},
};
}
const counter = createCounter();
console.log(counter.increment()); // 输出:1
console.log(counter.increment()); // 输出:2
console.log(counter.decrement()); // 输出:1
循环注册点击事件(点击事件是异步任务)
以下代码点击任意 li
只会输出 4 。原因如下: 点击事件是异步任务,只有点击了才会执行函数;但 for 循环是同步任务,它会立即执行,然后就给 4 个 li
注册了点击事件函数,这时 i
的值已经变为 4,所以点击任意 li
,执行其点击事件函数,输出的都是 4。
// 点击 li 输出其索引号(错误写法 var)
const lis = document.querySelector(".nav").querySelectorAll("li"); // 4 个 li
for (var i = 0; i < lis.length; i++) {
// 循环注册点击事件
lis[i].onclick = function () {
console.log(i); // 输出索引号
};
}
// 正确写法1:let
const lis = document.querySelector(".nav").querySelectorAll("li"); // 4 个 li
for (let i = 0; i < lis.length; i++) {
// 循环注册点击事件
lis[i].onclick = function () {
console.log(i); // 输出索引号
};
}
// 正确写法2:动态添加属性
for (var i = 0; i < lis.length; i++) {
lis[i].index = i; // 保存 li 的索引号
lis[i].onclick = function () {
console.log(this.index); // 输出索引号
};
}
// 正确写法3:闭包
for (var i = 0; i < lis.length; i++) {
// 利用 for 循环创建了 4 个立即执行函数
// 立即执行函数也称为小闭包,因为立即执行函数里面的任何一个函数都可以使用它的 i 这个变量
(function (i) {
// i 表示接收的参数
lis[i].onclick = function () {
console.log(i); // 使用了立即执行函数的 i 这个变量(闭包)
};
})(i); // i 表示传入的参数
}
定时器中的闭包(定时器是异步任务)
// 写法1:闭包
for (let i = 0; i < lis.length; i++) {
(function (i) {
setTimeout(function () {
console.log(lis[i].innerHTML);
}, 3000); // 3 秒之后一次全部打印
})(i);
}
// 写法2:非闭包(一次输出 0,1,2,3,4)
const output = function (i) {
setTimeout(function () {
console.log(i);
}, 1000);
};
for (let i = 0; i < 5; i++) {
output(i);
}
// Promise 写法(隔 1 秒依次输出 0,1,2,3,4)
const tasks = []; // 存放异步操作的 Promise
const output = (i) =>
new Promise((resolve) => {
setTimeout(() => {
console.log(i);
}, 1000 * i);
});
for (let i = 0; i < 5; i++) {
tasks.push(output(i)); // 生成全部的异步操作
}
Promise.all(tasks); // 执行所有的Promise,间隔 1 秒输出 0,1,2,3,4
// async await(ES7 新增)
const sleep = (timeoutMS) =>
new Promise((resolve) => {
// 设置一个异步操作
setTimeout(resolve, timeoutMS); // 指定时间之后调用 resolve
});
(async () => {
// 声明即执行的 async 异步函数
for (let i = 0; i < 5; i++) {
await sleep(1000);
console.log(i);
}
})();
闭包的优缺点
优点:跨作用域访问变量;让这些变量的值始终保存在内存中; 缺点:内存消耗大,容易产生内存泄漏(要将不使用的局部变量删除)
闭包使用场景
- 闭包的使用场景包含两点:创建私有变量和延长变量的生命周期
- 只要将函数作为返回值,就有闭包;只要使用了回调函数,都有闭包的应用
- 定时器,事件监听,ajax 请求,防抖节流等
递归函数
一个函数内部自己调用自己,这个函数就是递归函数
let num = 1;
function fn() {
console.log("我要打印 6 句话");
if (num == 6) {
return; // 递归里面必须加退出条件
}
num++;
fn(); // 调用自身
}
fn();
// 递归求阶乘
function fn(n) {
if (n == 1) {
return 1;
}
return n * fn(n - 1);
}
// 递归求 fb 数列
function fb(n) {
if (n == 1 || n == 2) {
return 1;
}
return fb(n - 1) + fb(n - 2);
}
赋值和深浅拷贝
深拷贝和浅拷贝是只针对 Object 和 Array 这样的引用数据类型的。
赋值和浅拷贝的区别:赋值是赋的对象在栈中的地址,不是堆中的数据,不会创建新的对象,也就是说两个变量指向同一个存储空间;而浅拷贝会创建一个新的对象,新对象有着原始对象属性的一份精确拷贝,如果属性是基本类型,就拷贝值,如果是引用类型,就拷贝内存地址。
const obj = {
id: 1,
name: "Andy",
msg: { age: 18 },
};
const shallow = {};
// 浅拷贝 1.for 循环拷贝
for (const k in obj) {
shallow[k] = obj[k]; // 对于 obj 中的 msg(复杂数据类型)只会拷贝其地址
}
// 浅拷贝 2.Object.assign()
Object.assign(shallow, obj); // 把 obj 浅拷贝给 shallow,实际上是合并对象
// 浅拷贝 3.arr.concat()
const arr2 = arr.concat(); // 里面啥都不写,就是浅拷贝
// 浅拷贝 4.arr.slice()
const arr2 = arr.slice(); // 里面啥都不写,就是浅拷贝
// 浅拷贝 5.对象解构
const arr2 = { ...arr };
// 深拷贝 JSON.parse(JSON.stringify())
const arr2 = JSON.parse(JSON.stringify(arr)); // 无法处理函数
// 深拷贝 函数库 lodash 里面的 cloneDeep() 方法
递归实现深拷贝
// 方法1
const deepClone = (obj) => {
// 1. 判断是否是对象或者数组 (递归出口)
if (obj === null || typeof obj !== "object") {
return obj;
}
// 2. 创建一个新的对象或数组
const clone = Array.isArray(obj) ? [] : {};
// 5. 设置对象的原型
Object.setPrototypeOf(clone, Object.getPrototypeOf(obj));
// 3. 遍历对象或数组的每一个属性
/**
* for (const key of Object.keys(obj)) 只遍历对象自身的可枚举属性(不包括继承的属性)
* for (let key in obj) 遍历对象的所有可枚举属性,包括对象原型链上的可枚举属性
* 根据实际需求选择不同的遍历方式
*/
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key]); // 递归拷贝属性
}
// 4. 返回新的对象或数组
return clone;
};
// 方法2 (防止循环引用)
const deepCloneHash = (obj, hash = new WeakMap()) => {
// 使用 WeakMap,当 map 中引用的对象被销毁时,会自动垃圾回收
// 1. 判断是否是对象或者数组 (递归出口)
if (obj === null || typeof obj !== "object") {
return obj;
}
// 2. 检查该对象是否已经存在于哈希表中,如果存在,直接从哈希表中取出结果
if (hash.has(obj)) {
return hash.get(obj);
}
// 3. 创建一个新的对象或数组
const clone = Array.isArray(obj) ? [] : {};
// 4. 将新创建的对象添加到哈希表中
hash.set(obj, clone);
// 5. 遍历对象或数组的每一个属性
for (const key of Object.keys(obj)) {
clone[key] = deepCloneHash(obj[key], hash); // 递归拷贝属性
}
// 6. 返回新的对象或数组
return clone;
};
函数柯里化
把接收多个参数的函数转化成多个只接收一个参数的函数;把函数的返回值设为一个新的函数,实现参数复用
- 柯里化可以将函数的一部分参数“预先传入”,形成一个新的函数,实现参数复用
- 通过柯里化,可以延迟传参和执行,等到条件满足时再执行函数
- 柯里化让函数调用链更灵活、可组合、可读性更强
// before
function myInfo(info, name, age) {
return `${info}:${name}${age}`;
}
const myInfo = myInfo("个人信息", "evan", "19"); // 个人信息:ljc19
// after curring
function myInfoCurry(info) {
return (name) => {
return (age) => {
return `${info}:${name}${age}`;
};
};
}
const myInfo = myInfoCurry("个人信息")("evan")("19"); // 个人信息:ljc19
// better example
const isGreaterThan = (min) => (value) => value > min;
const isAdult = isGreaterThan(18);
console.log(isAdult(20)); // true
柯里化函数
// 将传入的函数柯里化
function curry(fn) {
const arity = fn.length; // 获取原始函数的参数个数
return function curried(...args) {
if (args.length >= arity) {
return fn(...args); // 参数够了,直接调用原始函数
} else {
return (...nextArgs) => curried(...args, ...nextArgs); // 参数不够,继续返回一个函数收集参数
}
}
}
function add(a, b, c) {
return a + b + c;
}
// 使用示例
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
严格柯里化
一次只接收一个参数,返回一个新函数,直到所有参数都传入后,才调用原始函数
function curry(fn) {
const arity = fn.length; // 参数数量
function curried(arg, ...rest) {
// 提取第一个参数,剩余参数作为 rest
if (rest.length > 0) {
throw new Error("只能一次传入一个参数"); // 如果剩余参数大于 0,则抛出错误
}
const collectedArgs = [arg]; // 收集第一个参数
// 递归收集参数
function next(arg) {
collectedArgs.push(arg); // 收集每次的第一个参数
if (collectedArgs.length === arity) {
return fn(...collectedArgs); // 参数够了,调用原始函数
}
return next; // 参数不够,继续返回一个函数收集参数
}
// 参数数量为 1,直接调用原始函数
if (arity === 1) {
return fn(arg);
}
// 参数数量大于一,使用 next 函数收集参数
return next;
}
return curried;
}
// 非常粗略的标注
// declare function curry(fn: Function): Function;
// 详细标注 A 是参数类型,R 是返回值类型
declare function curry<A extends any[], R>(fn: (...args: A) => R): Function;
// 现在问题在于这个返回值 Function 的类型,有三种情况
// 1. 传入无参函数,返回类型为 () => R
// 2. 传入单参函数,返回类型为 (...args: A) => R
// 3. 传入多参函数,返回类型为 (x: 第一个参数的类型) => 新的函数
// 现在定义一个类型函数来生成这个 Function 的类型
type Curried<A extends any[], R> = A extends [] ? () => R : A extends [infer P] ? (x: P) => R : A extends [infer F, ...infer Rest] ? (x: F) => Curried<Rest, R> : never;
declare function curry<A extends any[], R>(fn: (...args: A) => R): Curried<A, R>;
function add(a: number, b: number, c: number): number {
// 参数数量为 3,返回一个数字
return a + b + c;
}
const curriedAdd = curry(add);
const result = curriedAdd(1)(2)(3);
扁平数据结构和树解构转换
const items = [
{ id: 1, name: "Item 1", pid: 0 },
{ id: 2, name: "Item 2", pid: 1 },
{ id: 3, name: "Item 3", pid: 1 },
{ id: 4, name: "Item 4", pid: 3 },
{ id: 5, name: "Item 5", pid: 4 },
];
const tree = {
id: 1,
name: "Item 1",
pid: 0,
children: [
{
id: 2,
name: "Item 2",
pid: 1,
children: [],
},
{
id: 3,
name: "Item 3",
pid: 1,
children: [
{
id: 4,
name: "Item 4",
pid: 3,
children: [
{
id: 5,
name: "Item 5",
pid: 4,
children: [],
},
],
},
],
},
],
};
1️⃣ 数组转对象
const arrayToTree = (arr) => {
// 1. 初始化根节点和查找表
let result = {};
const lookup = {};
// 2. 创建查找表,每个元素以其 id 为键存储在 lookup 中,并初始化 children 属性为空数组
arr.forEach((item) => {
lookup[item.id] = { ...item, children: [] };
});
// 3. 构建树结构
arr.forEach((item) => {
// 获取当前元素在查找表中的引用,也就是一个树节点
const treeItem = lookup[item.id];
if (item.pid === null || item.pid === 0) {
// 1. 如果当前元素是根节点,直接将其赋值给根节点
result = treeItem;
} else {
// 2. 如果当前元素有父节点,将其添加到父节点的 children 数组中
const parent = lookup[item.pid];
if (parent) {
parent.children.push(treeItem);
}
}
});
// 4. 返回根节点数组,这个数组包含所有的树结构
return result;
};
// 使用 JSON.stringify 打印数据 (否则在控制台看到的是 Object)
console.log(JSON.stringify(arrayToTree(items), null, 2));
2️⃣ 对象转数组
const treeToArray = (obj) => {
// 初始化结果数组
const result = [];
const traverse = (node) => {
// 移除 children 属性
const { children, ...item } = node;
result.push(item);
// 递归遍历 children
if (children?.length) {
children.forEach((child) => traverse(child));
}
};
traverse(obj);
return result;
};