1. 动态加载 JS 文件
在一些特殊的场景下,尤其是在一些库和框架的开发中,我们有时会动态加载并执行 JS 文件。
下面是使用 Promise 的简单封装。
function loadJS(files, done) {
// Get the head tag
const head = document. getElementsByTagName('head')[0];
Promise.all(files.map(file => {
return new Promise(resolve => {
// create script tag and add to head
const s = document.createElement('script');
s.type = "text/javascript";
s.async = true;
s.src = file;
// Listen to the load event, resolve if the loading is complete
s. addEventListener('load', (e) => resolve(), false);
head.appendChild(s);
});
})).then(done); // everything is done, execute the user's callback event
}
loadJS(["test1.js", "test2.js"], () => {
// user's callback logic
});There are two core points in the code above. One is to use Promise to process asynchronous logic, but to use script tags to load and execute js.
2. 实现模板引擎
以下示例使用很少的代码来实现动态模板渲染引擎。它不仅支持普通动态变量的替换,还支持动态 JS 语法逻辑包括 for 循环、if 判断等。
// This is a dynamic template that contains js code
var template =
'My avorite sports:' +
'<%if(this.showSports) {%>' +
'<% for(var index in this.sports) { %>' +
'<a><%this.sports[index]%></a>' +
'<%}%>' +
'<%} else {%>' +
'<p>none</p>' +
'<%}%>';
// This is the function string we're going to concatenate
const code = `with(obj) {
var r=[];
r.push("My avorite sports:");
if(this. showSports) {
for(var index in this. sports) {
r. push("<a>");
r.push(this.sports[index]);
r. push("</a>");
}
} else {
r.push("<span>none</span>");
}
return r.join("");
}`
// dynamically rendered data
const options = {
sports: ["swimming", "basketball", "football"],
showSports: true
}
// Build a feasible function and pass in parameters to change the direction of this when the function is executed
result = new Function("obj", code).apply(options, [options]);
console. log(result);
3. 使用 reduce 转换数据结构
有时候前端需要对后端传来的数据进行转换以适应前端的业务逻辑,或者转换组件的数据格式然后传给后端处理,而 reduce 就是 一个非常强大的工具。
const arr = [
{ classId: "1", name: "Jack", age: 16 },
{ classId: "1", name: "Jon", age: 15 },
{ classId: "2", name: "Jenny", age: 16 },
{ classId: "3", name: "Jim", age: 15 },
{ classId: "2", name: "Zoe", age: 16 }
];
groupArrayByKey(arr, "classId");
function groupArrayByKey(arr = [], key) {
return arr.reduce((t, v) => (!t[v[key]] && (t[v[key]] = []), t[v[key]].push(v), t), {})
}
很多复杂的逻辑如果通过 reduce 处理的话,就非常简单了。
4. 添加默认值
有时,方法需要用户传入参数。通常,我们有两种方法来处理。如果用户不传入,我们通常会给出一个默认值,或者用户必须传入一个参数,不传则抛出错误。
function double() {
return value *2
}
// If not passed, give a default value of 0
function double(value = 0) {
return value * 2
}
// The user must pass a parameter, and an error will be thrown if no parameter is passed
const required = () => {
throw new Error("This function requires one parameter.")
}
function double(value = required()) {
return value * 2
}
double(3) // 6
double() // throw Error
Listen 方法用于创建 NodeJS 原生 http 服务并监听端口,在服务的回调函数中创建上下文,然后调用用户注册的回调函数并传递生成的上下文。我们先看一下之前 createContext 和 handleRequest 的实现。
5. 该函数只执行一次
在某些情况下,我们有一些特殊的场景,某个函数只允许执行一次,或者某个绑定方法只允许执行一次。
export function once (fn) {
// Use the closure to determine whether the function has been executed
let called = false
return function () {
if (! called) {
called = true
fn. apply(this, arguments)
}
}
}
6. 实现 Curry
JavaScript 中的柯里化是将采用多个参数的函数转换为一系列仅采用一个参数的函数的过程。这样可以更灵活地使用函数,减少代码的重复,提高代码的可读性。
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
function add(x, y) {
return x + y;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)); // output 3
console.log(curriedAdd(1, 2)); // output 3
通过柯里化,我们可以将一些常用的功能模块化,比如验证、缓存等。这提高了代码的可维护性和可读性,并减少了出错的机会。
7. 实现单例模式
JavaScript 的单例模式是一种常用的设计模式。它可以确保一个类只有一个实例,并提供对该实例的全局访问点。它在 JS 中有广泛的应用场景,比如购物车、缓存对象、全局状态管理等等。
let cache;
class A {
// ...
}
function getInstance() {
if (cache) return cache;
return cache = new A();
}
const x = getInstance();
const y = getInstance();
console.log(x === y); // true
8. 实现 CommonJs 规范
CommonJS 规范的核心思想是将每个文件视为一个模块,每个模块都有自己的作用域,其中的变量、函数和对象都是私有的,外部无法访问。要访问模块中的数据,您必须导出并要求。
// id: full file name
const path = require('path');
const fs = require('fs');
function Module(id){
// Used to uniquely identify the module
this.id = id;
// Properties and methods used to export modules
this.exports = {};
}
function myRequire(filePath) {
// Directly call the static method of Module to load the file
return Module._load(filePath);
}
Module._cache = {};
Module._load = function(filePath) {
// First address the absolute path of the file through the filePath passed in by the user
// Because in CommnJS, the unique identifier of the module is the absolute path of the file
const realPath = Module._resoleveFilename(filePath);
// Cache priority, if it exists in the cache, it will directly return the exports property of the module
let cacheModule = Module._cache[realPath];
if(cacheModule) return cacheModule. exports;
// If it is loaded for the first time, a new module is required, and the parameter is the absolute path of the file
let module = new Module(realPath);
// Call the load method of the module to compile the module
module.load(realPath);
return module. exports;
}
// The node file is not discussed yet
Module._extensions = {
// Process the js file
".js": handleJS,
// process the json file
".json": handleJSON
}
function handleJSON(module) {
// If it is a json file, read it directly with fs.readFileSync,
// Then use JSON.parse to convert and return directly
const json = fs.readFileSync(module.id, 'utf-8')
module.exports = JSON.parse(json)
}
function handleJS(module) {
const js = fs. readFileSync(module. id, 'utf-8')
let fn = new Function('exports', 'myRequire', 'module', '__filename', '__dirname', js)
let exports = module. exports;
// The assembled function can be executed directly
fn.call(exports, exports, myRequire, module, module.id, path.dirname(module.id))
}
Module._resolveFilename = function (filePath) {
// Splice the absolute path, and then search it, if it exists, it will return
let absPath = path. resolve(__dirname, filePath);
let exists = fs.existsSync(absPath);
if (exists) return absPath;
// If it does not exist, try splicing .js, .json, .node in sequence
let keys = Object.keys(Module._extensions);
for (let i = 0; i < keys. length; i++) {
let currentPath = absPath + keys[i];
if (fs.existsSync(currentPath)) return currentPath;
}
};
Module.prototype.load = function(realPath) {
// Get the file extension and hand it over to the corresponding method for processing
let extname = path.extname(realPath)
Module._extensions[extname](this)
}
以上是 CommonJs 规范的简单实现。核心解决了作用域的隔离,提供了 Myrequire 方法来加载方法和属性。
9. 递归获取对象属性
如果让我选择使用最广泛的设计模式,我会选择观察者模式。如果要选我遇到过最多的算法思维,那一定是递归。递归将原问题划分为具有相同结构的结构。子问题,然后依次解决这些子问题,并结合子问题的结果,最终得到原问题的答案。
const user = {
info: {
name: "Jacky",
address: { home: "MLB", company: "AI" },
},
};
// obj is the object to get the property, path is the path, and fallback is the default value
function get(obj, path, fallback) {
const parts = path. split(".");
const key = parts. shift();
if (typeof obj[key] !== "undefined") {
return parts. length > 0 ?
get(obj[key], parts. join("."), fallback) :
obj[key];
}
// return fallback if key not found
return fallback;
}
console.log(get(user, "info.name")); // Jacky
console.log(get(user, "info.address.home")); // MLB
console.log(get(user, "info.address.company")); // AI
console.log(get(user, "info.address.abc", "fallback")); // fallback