Node.js
迅速成为构建 Web
应用和系统软件的标准,得益于它能够在后端使用 JavaScript
。流行框架如 Express
和工具如 Webpack
进一步推动了它的广泛应用。尽管有 Deno
和 Bun
等竞争者,Node
依然是领先的服务器端 JavaScript
平台。
JavaScript
的多范式特性支持多种编程风格,但也带来了作用域和对象变异等风险。缺乏尾调用优化使大规模递归迭代变得危险,而 Node
的单线程架构要求使用异步代码以提高效率。尽管面临挑战,遵循 JavaScript
的关键概念和最佳实践可以帮助 Node.js
开发者编写可扩展和高效的代码。
-
JavaScript
闭包JavaScript
中的闭包是一个内部函数,它可以访问其外部函数的作用域,即使外部函数已经返回控制权。闭包使得内部函数的变量变得私有。函数式编程的流行使得闭包成为Node
开发者工具包中的一个重要部分。以下是JavaScript
中闭包的一个简单示例:const count = (function () { let _counter = 0; return function () { return ++_counter; } })(); count(); count(); count(); // 3
变量
count
被赋值为外部函数。外部函数只运行一次,设置计数器为零并返回一个内部函数。_counter
变量只能通过内部函数访问,这使得它表现得像一个私有变量。这里的例子是一个高阶函数(或元函数,它是一个接受或返回另一个函数的函数)。闭包在许多其他应用中也会出现。每当你在一个函数内部定义一个函数,并且内部函数既拥有自己的作用域又能访问父作用域时,就会发生闭包——也就是说,内部函数可以“看到”外部变量,但反之则不行。
-
JavaScript
原型每个
JavaScript
函数都有一个原型属性,用于附加属性和方法。这个属性是不可枚举的,允许开发者将方法或成员函数附加到其对象上。JavaScript
仅通过原型属性支持继承。在继承对象的情况下,原型属性指向对象的父级。将方法附加到函数的常见方法是使用原型,如下所示:function Rectangle(x, y) { this.length = x; this.breadth = y; } Rectangle.prototype.getDimensions = function () { return { length: this.length, breadth: this.breadth }; }; Rectangle.prototype.setDimensions = function (len, bred) { this.length = len; this.breadth = bred; };
尽管现代
JavaScript
具有相当复杂的类支持,但它在底层仍然使用原型系统。这是该语言灵活性的主要来源。 -
使用哈希命名定义私有属性
在过去,使用下划线作为前缀来表示变量应该是私有的。然而,这仅仅是一种建议,并不是平台强制执行的限制。现代
JavaScript
提供了哈希私有成员和方法用于类:class ClassWidthPrivate { #privateField; // 私有字段 #privateMethod() { // 私有方法 return "private method"; } }
-
通过闭包定义私有属性
另一种常见的解决
JavaScript
原型系统缺乏私有属性的方法是使用闭包。虽然现代JavaScript
允许通过哈希前缀定义私有属性,但这并不适用于原型系统。因此,理解这个技巧在代码中很重要。通过使用闭包定义私有属性可以模拟私有变量。需要访问私有属性的成员函数应定义在对象本身上。以下是使用闭包创建私有属性的语法:
function Rectangle(_length, _breadth) { this.getDimensions = function () { return { length: _length, breadth: _breadth }; }; this.setDimensions = function (len, bred) { _length = len; _breadth = bred; }; }
-
JavaScript
模块曾几何时,
JavaScript
没有模块系统,开发者们想出了一个巧妙的技巧(称为模块模式)来实现某种可行的方案。随着JavaScript
的发展,它产生了两个模块系统:CommonJS
包含语法和ES6 require
语法。Node
传统上使用CommonJS
,而浏览器则使用ES6
。不过,最近几年的Node
版本也支持ES6
。现在的趋势是使用ES6
模块,总有一天我们会在整个JavaScript
中使用一种模块语法。ES6
看起来像这样(我们导出一个默认模块,然后导入它):// 导出模块 export default function main() { console.log('This is file1'); } // 导入模块 import main from './file1';
你仍然会看到
CommonJS
,有时还需要用它来导入模块。下面是使用CommonJS
导出然后导入默认模块的过程:// 导出模块 function main() { console.log('This is file1'); } module.exports = main; // 导入模块 const main = require('./file1');
-
错误处理
无论你使用什么语言或环境,错误处理都是必要且不可避免的,
Node.js
也不例外。处理错误有三种基本方法:try/catch
块、抛出新错误以及on()
处理器。使用
try/catch
块是捕获错误的可靠方法,当出现问题时,它们可以有效地捕获错误:try { someRiskyOperation(); } catch (error) { console.error('发生了严重错误:', error); }
在这种情况下,我们使用
console.error
将错误记录到控制台。你也可以选择抛出错误,将其传递给下一个处理程序。请注意,这会中断代码的执行流程;也就是说,当前的执行停止,堆栈中的下一个错误处理程序接管控制:try { someRiskyOperation(); } catch (error) { throw new Error('发生了严重错误', error); }
现代
JavaScript
为其错误对象提供了许多有用的属性,包括Error.stack
,用于查看堆栈跟踪。在上述示例中,我们使用构造函数参数设置了Error.message
属性和Error.cause
。在异步代码块中,你还会遇到错误,这里你可以使用
.then()
处理正常结果。在这种情况下,可以根据Promise
返回错误的方式使用on('error')
处理程序或onerror
事件。有时,API
会将错误对象作为第二个返回值与正常值一起返回。(如果在异步调用上使用await
,可以将其包裹在try/catch
中来处理任何错误。)以下是处理异步错误的简单示例:someAsyncOperation().then(result => { // 一切正常 }).catch(error => { // 发生了错误 console.error('发生了严重错误:', error); });
无论如何,千万不要忽略错误!我不会在这里展示那种代码,因为可能有人会复制粘贴它。基本上,如果你捕获了一个错误却不做任何处理,程序会在没有任何明显错误提示的情况下悄然继续运行。逻辑会被破坏,你将陷入困惑,直到找到那个什么都没做的
catch
块。(注意,提供一个没有catch
的finally {}
块会导致错误被忽略。) -
JavaScript
柯里化柯里化是一种使函数更灵活的方法。使用柯里化函数,你可以传递所有期望的参数并获得结果,或者只传递部分参数并返回一个等待其余参数的函数。以下是一个简单的柯里化示例:
const myFirstCurry = function(word) { return function(user) { return [word, ", ", user].join(""); } } const HelloUser = myFirstCurry("Hello"); HelloUser("InfoWorld"); // 输出:"Hello, InfoWorld"
原始的柯里化函数可以通过将每个参数依次放在一对括号中直接调用:
myFirstCurry("Hey, how are you?")("InfoWorld") // 输出:"Hey, how are you?, InfoWorld"
这是一种有趣的技术,可以让你创建函数工厂,外部函数允许你部分配置内部函数。例如,你可以这样使用上述的柯里化函数:
const greeter = myFirstCurry("Namaste"); greeter("InfoWorld"); // 输出:"Namaste, InfoWorld"
在实际使用中,这个想法可以帮助你创建许多根据特定参数变化的函数。
-
JavaScript
的apply
、call
和bind
方法虽然我们并不每天都使用它们,但理解
call
、apply
和bind
方法的功能是很重要的。在这里,我们处理的是一些相当灵活的语言特性。这些方法的核心作用是允许你指定this
关键字的解析对象。在这三个函数中,第一个参数始终是你想要传递给函数的
this
值或context
。在这三者中,
call
是最简单的。它的作用与调用一个函数并指定其上下文是一样的。以下是一个示例:const user = { name: "Info World", whatIsYourName: function () { console.log(this.name); } } user.whatIsYourName(); // 输出:"Info World" const user2 = { name: "Hack Info" } user.whatIsYourName.call(user2); // 输出:"Hack Info"
请注意,
apply
与call
几乎是相同的。唯一的区别在于,apply
接受一个数组作为参数,而不是单独传递。数组在JavaScript
中更易于操作,从而为函数的使用打开了更多的可能性。以下是一个使用apply
和call
的示例:const user = { greet: "Hello!", greetUser: function (name) { console.log(this.greet + " " + name); } } const greet1 = { greet: "Hi!" } user.greetUser("Mary"); // 输出:"Hello! Mary" user.greetUser.call(greet1, "John"); // 输出:"Hi! John" user.greetUser.apply(greet1, ["John"]); // 输出:"Hi! John"
bind
方法允许你在不调用函数的情况下传递参数。它返回一个新函数,参数会被绑定在任何后续参数之前。以下是一个示例:const user = { greet: "Hello!", greetUser: function (name) { console.log(this.greet + " " + name); } } const greetMary = user.greetUser.bind({ greet: "Mary" }); const greetJohn = user.greetUser.bind({ greet: "John" }); greetMary("InfoWorld"); // 输出:"Mary InfoWorld" greetJohn("InfoWorld"); // 输出:"John InfoWorld"
-
JavaScript
记忆化记忆化是一种优化技术,通过存储耗时操作的结果来加速函数执行,当相同的输入再次出现时返回缓存的结果。
JavaScript
对象像关联数组一样,使得在JavaScript
中实现记忆化变得简单。以下是如何将递归阶乘函数转换为记忆化阶乘函数的示例:function memoizeFunction(func) { const cache = new Map(); return function (...args) { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = func.apply(this, args); cache.set(key, result); return result; }; } const fibonacci = memoizeFunction(function (n) { if (n < 2) return n; return fibonacci(n - 1) + fibonacci(n - 2); });
-
JavaScript IIFE
立即调用函数表达式(
IIFE
)是指在创建后立即执行的函数。它与任何事件或异步执行没有关联。你可以这样定义一个IIFE
:(function() { // 你的代码在这里执行 })();
第一个括号对
function(){...}
的包围将括号内的代码转换为一个表达式。第二个括号调用这个表达式所得到的函数。IIFE
也可以被描述为自执行的匿名函数。它最常见的用途是限制通过var
声明的变量的作用域,或封装context
以避免命名冲突。在某些情况下,你需要使用
await
调用一个函数,但又不在async
函数块内。这种情况有时出现在你想要直接执行的文件中,同时又希望它能作为模块被导入。你可以像下面这样将这样的函数调用包装在一个IIFE
块中:(async function() { await callAsyncFunction(); })();
-
有用的参数特性
虽然
JavaScript
不支持方法重载(因为它能够处理任意数量的参数),但它确实提供了几种强大的处理参数的功能。首先,你可以为函数或方法定义默认值:function greet(name = 'Guest') { console.log(`Hello, ${name}!`); } greet(); // 输出:Hello, Guest! greet('John'); // 输出:Hello, John!
你还可以一次性接受和处理所有参数,以便处理传入的任意数量的参数。这使用了扩展运算符,将所有参数收集到一个数组中:
function sum(...numbers) { return numbers.reduce((total, num) => total + num, 0); } console.log(sum(1, 2, 3, 4, 5)); // 输出:15 console.log(sum(2, 3)); // 输出:5
如果你真的需要处理不同的参数配置,可以随时进行检查:
function findStudent(firstName, lastName) { if (typeof firstName === 'string' && typeof lastName === 'string') { // 根据名字和姓氏查找学生 } else if (typeof firstName === 'string') { // 根据名字查找学生 } else { // 查找所有学生 } } findStudent('Alice', 'Johnson'); findStudent('Bob'); findStudent();
结论
随着你对 Node.js
的深入,你会发现几乎每个问题都有多种解决方案。正确的方法并不总是显而易见。了解可用的多种选项将对你大有帮助。
这篇文章讨论的 11 个 JavaScript
概念是每位 Node
开发者应掌握的基础,但这只是冰山一角。JavaScript
是一门强大而复杂的语言。你使用得越多,就越会意识到它的广阔,以及你可以用它实现的各种可能性。