函数 Function
函数基础
声明函数
// 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
扁平数据结构和树解构转换
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;
}