一、promise方法的案例
Promise对象通过new Promise()语法创建,它接受一个函数作为参数,该函数接受两个参数:resolve和reject。resolve表示异步操作成功,reject表示异步操作失败。
案例:异步加载图片
TypeScript
const loadImage = (url) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve(img);
};
img.onerror = () => {
reject(new Error(`Failed to load image from ${url}`));
};
img.src = url;
});
};
loadImage('https://example.com/image.jpg')
.then((img) => {
console.log('Image loaded successfully');
document.body.appendChild(img);
})
.catch((error) => {
console.error(error.message);
});
Promise.prototype.then()
then()
方法接受两个可选参数:onFulfilled和onRejected,分别表示成功时和失败时的回调函数。当Promise对象的状态为fulfilled时,会立即执行onFulfilled回调函数,并将操作结果作为参数传递给该函数;当Promise对象的状态为rejected时,会立即执行onRejected回调函数,并将拒绝原因(错误信息)作为参数传递给该函数。
案例:
TypeScript
const p = new Promise((resolve, reject) => {
resolve(1);
});
p.then(
(res) => {
console.log(res); // 输出:1
return 2; // 返回值会被作为下一个then的输入
},
(error) => {
console.log(error);
}
)
.then(
(res) => {
console.log(res); // 输出:2
}
);
Promise.prototype.catch()
catch()
方法是then(undefined, onRejected)
的语法糖,用于捕获Promise对象被rejected时的错误信息。
案例:
TypeScript
const p = new Promise((resolve, reject) => {
reject('error');
});
p.then((res) => {
console.log(res); // 不会执行
})
.catch((error) => {
console.log(error); // 输出:error
});
1.1.promise.all
过程:
1.创建promise数组:首先,需要创建一个包含多个promise对象的数组,每个promise对象都代表一个异步操作。
2.调用promise.all:然后调用promise.all方法,并将这个数组作为参数传递给它。promise.all会返回一个新的promise对象
3.处理结果:
如果所有的输入promiase对象都成功解析,那么promise.all返回的promise对象也会被解析,并且它的解析值是一个数组,包含了所有输入promise对象的解析值,顺序与输入数组一致。
如果任何一个输入promise对象被拒绝,那么promise.all返回的promise对象也会被拒绝,并且它的拒绝原因与第一个被拒绝的promise对象的拒绝原因相同。
4.使用.then和.catch:可以使用.then方法来处理成功的情况,使用.catch方法来处理失败的情况
例子:同时加载多张图片
TypeScript
const urls = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
'https://example.com/image3.jpg'
];
const loadImage = (url) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve(img);
};
img.onerror = () => {
reject(new Error(`Failed to load image from ${url}`));
};
img.src = url;
});
};
Promise.all(urls.map(loadImage))
.then((images) => {
console.log('All images loaded successfully');
images.forEach((img) => {
document.body.appendChild(img);
});
})
.catch((error) => {
console.error(error.message);
});
1.2.promise.race
过程:
1.创建promise数组:首先,需要创建一个包含多个promise对象的数组,每个promise对象都代表一个异步操作。
2.调用promise.race:然后调用promise.race方法,并将这个数组作为参数传递给它。promise.race会返回一个新的promise对象
3.处理结果:
promise.race只关心第一个完成的promise对象(无论是成功还是拒绝)
如果第一个完成的promise是成功解析的,那么promise.race返回的promise对象也会解析,并且它的解析值与第一个完成的promise对象的解析值相同
如果过第一个完成的promise是被拒绝的,那么promise.race返回的promise对象也会被拒绝,并且它的拒绝原因第一个被决绝的promise对象的拒绝原因相同
4.使用.then和.catch:同样,也可以使用.then方法;来处理成功的情况,使用.catch方法来处理失败的情况。
例子:同时加载多张图片,但只显示第一张加载成功的图片
TypeScript
const urls = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
'https://example.com/image3.jpg'
];
const loadImage = (url) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve(img);
};
img.onerror = () => {
reject(new Error(`Failed to load image from ${url}`));
};
img.src = url;
});
};
Promise.race(urls.map(loadImage))
.then((img) => {
console.log('First image loaded successfully');
document.body.appendChild(img);
})
.catch((error) => {
console.error(error.message);
});
TypeScript
// 创建两个异步操作的Promise,其中一个会更快完成
const slowFetch = () => new Promise((resolve) => {
setTimeout(() => resolve('Slow Data'), 2000);
});
const fastFetch = () => new Promise((resolve) => {
setTimeout(() => resolve('Fast Data'), 500);
});
// 使用 Promise.race 来等待第一个异步操作完成
Promise.race([slowFetch(), fastFetch()])
.then((data) => {
// 由于 fastFetch 更快完成,所以这里会输出 'Fast Data'
console.log(data); // 输出: 'Fast Data'
})
.catch((error) => {
// 如果没有任何Promise被拒绝,这里的catch不会被调用
console.error(error);
});
// 另一个例子,包含一个会被拒绝的Promise
const fetchWithTimeout = () => {
return Promise.race([
fetch('https://jsonplaceholder.typicode.com/posts/1') // 一个真实的fetch请求
.then(response => response.json()),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timed out')), 1000); // 1秒后拒绝
})
]);
};
// 使用 fetchWithTimeout,并处理可能的超时
fetchWithTimeout()
.then(post => {
console.log(post); // 如果fetch请求在1秒内完成,这里会输出post数据
})
.catch(error => {
console.error('Error:', error.message); // 如果fetch请求超时,这里会输出 'Error: Request timed out'
});
在这个fetchWithTimeout的例子中,我们使用了promise.race来设置一个超时机制。如果fetch请求在1秒内没有完成,那么promise.race会返回被拒绝的promise,并且我们可以在.catch中处理这个超时错误。
promise.resolve()和promise.reject()
promise.resolve()方法返回一个已经成功状态的promise对象,并将指定的值作为参数传递给它的then方法。
promise.reject()方法返回一个已经失败状态的promise对象,并将指定原因作为参数传递给它的catch方法
案例:
TypeScript
// Promise.resolve()
Promise.resolve(10)
.then((value) => {
console.log(`The value is ${value}`); // 输出:The value is 10
})
.catch((error) => {
console.error(error.message); // 不会执行
});
// Promise.reject()
Promise.reject('error')
.then((value) => {
console.log(value); // 不会执行
})
.catch((reason) => {
console.log(reason); // 输出:error
});
二、高阶语法与特性
2.1模板字符串的高级用法
本小节讲解模板字符串的嵌套、标签模板等高级用法,了解它们如何用于字符串的格式化、插值等操作。
2.1.1.模板字符串的嵌套
模板字符串允许嵌套另一个模板字符串,这在处理复杂数据结构时非常有用。例如,假设有一个包含地址信息的数组,可以使用嵌套模板字符串来生成一个HTML表格:
TypeScript
const addrs = [
{ first: 'John', last: 'Doe' },
{ first: 'Jane', last: 'Smith' }
];
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join('')}
</table>
`;
console.log(tmpl(addrs));
上述代码中,内部的模板字符串用于生成每一行的表格内容,外部的模板字符串则负责将这些行组合成一个完整的表格。
2.1.2.标签模板用于字符串的格式化、插值等操作
标签模板(Tagged Template)是一种特殊的函数,它允许对模板字符串进行自定义处理。标签模板函数的基本语法是在一个函数名后面紧跟一个模板字符串字面量。这个函数会接收两个参数:一个由模板字符串中的静态文本部分组成的数组,以及由模板字符串中的表达式求值结果组成的剩余参数列表。
1.字符串格式化
可以创建一个标签模板函数来格式化日期、数字或货币等。例如,以下是一个简单的标签模板函数,用于将数字格式化为货币形式:
TypeScript
function formatCurrency(strings, ...values) {
let result = '';
for (let i = 0; i < strings.length; i++) {
result += strings[i];
if (i < values.length) {
result += '$' + values[i].toFixed(2); // 将数字格式化为两位小数的货币形式
}
}
return result;
}
const price = 123.456;
console.log(formatCurrency`The price is ${price}`); // 输出 "The price is $123.46"
2.字符串插值:
标签模板函数也可以用于字符串插值,但与普通的模板字符串插值不同,标签模板函数可以对插入的变量或表达式进行自定义处理。例如,以下是一个将字符串转换为大写的标签模板函数:
TypeScript
function upperCase(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i - 1] || '';
return result + str + value.toUpperCase();
}, '');
}
const greeting = upperCase`Hello, ${'world'}!`; // 输出 "Hello, WORLD!"
3. 避免XSS攻击
在处理用户输入时,标签模板函数还可以用于避免跨站脚本攻击(XSS)。例如,可以对用户输入进行HTML转义
TypeScript
function escapeHTML(strings, ...values) {
let result = '';
for (let i = 0; i < strings.length; i++) {
result += strings[i];
if (i < values.length) {
// 简单的HTML转义函数
const escape = (html) => html.replace(/[&<>"'`=\/]/g, (s) => `&#${s.charCodeAt(0)};`);
result += escape(values[i]);
}
}
return result;
}
const userInput = '<script>alert("XSS!");</script>';
console.log(escapeHTML`Safe input: ${userInput}`);
// 输出 "Safe input: <script>alert("XSS!");</script>"
三、尾调用优化
3.1.尾调用优化(Tail Call Optimization)的概念和作用
尾调用优化是编译器或解释器对函数调用的一种优化手段。当函数A的最后一步是调用另一个函数B,并且A的返回值就是B的调用结果时,这个调用就被称为尾调用。如果编译器或解释器支持尾调用优化,那么在执行尾调用时,可以只保留函数B的调用记录,而删除函数A的调用记录,因为A的调用结果已经确定并且不再需要A的调用记录。这种优化可以显著减少内存使用,避免因为递归调用过深而导致的栈溢出错误。
3.2.递归函数中的尾调用优化
在递归函数中,尾调用优化尤为重要。传统的递归调用可能会导致大量的栈空间被占用,因为每次递归调用都会创建一个新的栈帧。而尾调用优化可以确保只有一个栈帧被重复使用,从而避免了栈空间的无谓消耗。
3.3.举例说明
已计算阶乘的递归函数为例,非尾递归和尾递归的实现方式如下:
非尾递归实现:
TypeScript
function factorial_recursive(n) {
if (n === 0) {
return 1;
} else {
return n * factorial_recursive(n - 1); // 这里不是尾调用,因为乘法操作在调用之后
}
}
在这个非尾递归的实现中,每次递归调用之后都有一个乘法操作,因此这不是一个尾调用。编译器无法对这个递归调用进行优化。
尾递归实现:
TypeScript
function factorial_tail_recursive(n, result = 1) {
if (n === 0) {
return result;
} else {
return factorial_tail_recursive(n - 1, n * result); // 这里是尾调用,因为调用之后没有其他操作
}
}
在这个尾递归的实现中,递归调用是函数的最后一个操作,并且这个调用的返回值就是整个函数的返回值。因此,编译器可以对这个递归调用进行优化,通过重用当前栈帧来避免增加额外的栈空间。
3.4.如何应用尾调用优化
要将一个递归函数改写为尾递归函数,通常需要引入一个辅助参数来保存中间结果。这个辅助参数在每次递归调用时都会被更新,并在递归结束时返回最终结果。
例2:计算斐波那契数列
非尾递归实现:
TypeScript
function fibonacci_non_tail_recursive(n) {
if (n <= 1) {
return n;
} else {
return fibonacci_non_tail_recursive(n - 1) + fibonacci_non_tail_recursive(n - 2);
}
}
这个非尾递归的实现导致大量的重复计算和栈空间消耗。
尾递归实现
TypeScript
function fibonacci_tail_recursive(n, a = 0, b = 1) {
if (n === 0) {
return a;
} else if (n === 1) {
return b;
} else {
return fibonacci_tail_recursive(n - 1, b, a + b);
}
}
在这个尾递归的实现中,我们引入了两个辅助参数 a
和 b
来保存斐波那契数列的前两个数。每次递归调用时,我们都会更新这两个参数,并在递归结束时返回最终结果。由于递归调用是函数的最后一个操作,并且这个调用的返回值就是整个函数的返回值,因此编译器可以对这个递归调用进行优化。
四、模块化进阶
4.1.动态导入
4.1.1.基本语法
动态导入使用import()
函数,该函数返回一个Promise
对象。这意味着你可以使用.then()
方法或async/await
语法来处理导入的模块。以下是动态导入的常见写法:
1.导入整个模块:
javascript
import('./module.js').then((module) => {
// 使用 module
});
2.导入模块的特定导出
javascript
import('./module.js').then(({ export1, export2 }) => {
// 使用 export1 和 export2
});
4.1.2.动态导入的特点与优势
按需加载:动态导入允许你在需要时才加载模块,这有助于减少初始加载时间,提高应用性能。
代码分割:结合构建工具(如Webpack),动态导入可以实现代码分割,将代码拆分成更小的块,以便更高效地加载和执行。
条件加载:你可以根据条件动态加载不同的模块,这在实现功能切换或按需加载特定功能时非常有用。
4.1.3.动态导入的应用场景
按需加载组件:在单页面应用(SPA)中,你可以使用动态导入来按需加载组件,从而减少初始加载时间和内存占用。
条件加载模块:根据用户的操作或环境变量等条件,动态加载不同的模块以实现功能切换。
优化性能:通过代码分割和按需加载,动态导入可以显著提高应用的加载速度和性能。
4.1.4示例
使用动态导入根据条件加载不同模块
javascript
// 定义一个函数,根据不同的条件导入不同的模块
function loadModule(condition) {
if (condition === 'moduleA') {
return import('./moduleA.js');
} else {
return import('./moduleB.js');
}
}
// 使用 loadModule() 函数来动态导入模块
loadModule('moduleA').then((module) => {
// 使用 moduleA 模块
}).catch((error) => {
// 处理错误
});
loadModule('moduleB').then((module) => {
// 使用 moduleB 模块
}).catch((error) => {
// 处理错误
});
在这个示例中,我们定义了一个loadModule
函数,它根据传入的条件动态加载不同的模块。然后,我们使用loadModule
函数来导入模块,并在Promise
的then
方法中使用相应的模块。
4.1.5.注意事项
兼容性:虽然大多数现代浏览器都支持动态导入,但在一些旧版浏览器中可能无法使用。因此,在开发过程中需要注意兼容性问题。
构建工具:为了充分利用动态导入的优势,通常需要结合构建工具(如Webpack)进行代码分割和优化。
错误处理:在使用动态导入时,需要妥善处理可能出现的错误,以确保应用的稳定性和用户体验。
4.2.循环依赖
循环依赖指的是两个或多个模块之间互相引用,形成一个闭环的依赖关系。这种依赖关系可能会导致一些潜在的问题,如模块加载顺序错误、未定义或未初始化的变量等。
4.2.1.循环依赖的定义与示例
循环依赖通常发生在以下情况:模块A依赖于模块B的导出,同时模块B也依赖于模块A的导出。这种依赖关系形成了一个闭环,可能导致模块加载和执行时的错误。
例如,有以下连个模块文件:
javascript
// a.js
import { b } from './b.js';
export const a = () => {
console.log('This is a.js');
b();
};
// b.js
import { a } from './a.js';
export const b = () => {
console.log('This is b.js');
a();
};
在这个例子中,a.js
和b.js
互相引用对方,形成了一个循环依赖。
4.2.2.循环依赖的问题与影响
1.加载顺序问题:在循环依赖的情况下,模块的加载顺序变得不确定。这可能导致某些模块在还未完全初始化的情况下就被其他模块引用,从而引发错误。
2.未定义或未初始化的变量:由于加载顺序的不确定性,可能会出现某些变量在引用时还未被定义或初始化的情况。
3.性能问题:循环依赖可能导致模块加载和执行的效率降低,因为模块之间的依赖关系变得更加复杂。
4.2.3.解决循环依赖的方法
1.重构代码:通过重构代码来消除循环依赖。例如,可以将一些公共的功能或数据提取到一个新的模块中,并让其他模块都依赖于这个新模块。
2.使用异步加载 :在ES6中,可以使用动态导入(import()
)来异步加载模块。这有助于在需要时才加载模块,从而避免循环依赖的问题。但需要注意的是,异步加载可能会增加代码的复杂性和加载时间。
3.依赖注入:通过依赖注入模式来管理模块之间的依赖关系。这有助于降低模块之间的耦合度,并使得依赖关系更加清晰和可控。
4.使用第三方库或工具 :一些第三方库或工具(如Webpack的splitChunks
插件)可以帮助开发者更好地管理和优化模块之间的依赖关系。
4.2.4.最佳实践与建议
1.避免循环依赖:在设计模块时,应尽量避免循环依赖的发生。通过合理的模块划分和组织,可以降低循环依赖的风险。
2.清晰的模块接口:确保每个模块都有清晰的接口和职责。这有助于其他模块更容易地理解和使用它们,并降低循环依赖的可能性。
3.使用构建工具:利用构建工具(如Webpack、Rollup等)来优化模块之间的依赖关系。这些工具可以帮助开发者更好地管理模块、减少打包文件的大小并提高加载速度。
亲爱的友友们~~~码这么多字不容易啊 给孩子点点赞叭,评论个1也成呐
当然上述内容若有遗漏或不足之处,恳请各位大佬不吝赐教,指正并帮助美化,以期更加完善。