一些实用的 JavaScript 技巧
我想记录一些我最近从 Twitter 和其他在线资源(可惜我没怎么关注)学到的精妙 JavaScript 技巧和模式。所有功劳都归功于 JavaScript 在线社区。
目录
class
是一个表达式,它所扩展的也是一个表达式this
在静态类方法中- 调用不带额外括号的 IIFE
- 调用异步 IIFE 而不使用额外的括号
- 函数参数的内联解构
- 函数参数的内联部分解构
- 使用表达式
switch
- 将非函数对象作为事件处理程序传递给
addEventListener
- 检查变量是否属于特定类型
- 检查变量是否
nullish
(即null
或undefined
) - 转换为原始类型
Symbol.toPrimitive
- 忽略承诺错误的助记方式(如适用)
- Thenable 可以与 Promise 一起使用
- 告诉哪个承诺首先得到解决
Promise.race
- “承诺”同步函数调用以推迟异常处理
Symbol.species
在扩展标准类时很有用await
可以在字符串模板中使用
class
是一个表达式,它所扩展的也是一个表达式
与 类似function funcName() { ... }
,class className { ... }
是一个表达式,可以赋值给变量或作为函数参数传递。className
此处 也可以是可选的,就像匿名函数一样。此外,基类也是一个表达式。例如,以下是可能的:
class Base1 {
whatAmI() {
return 'Base1';
}
}
class Base2 {
whatAmI() {
return 'Base2';
}
}
const createDerivedClass = base => class extends base {
whatAmI() {
return `Derived from ${super.whatAmI()}`;
}
};
const Derived1 = createDerivedClass(Base1);
// output: Derived from Base1
console.log(new Derived1().whatAmI());
const Derived2 = createDerivedClass(Base2);
// output: Derived from Base2
console.log(new Derived2().whatAmI());
这对于类继承树的动态组合(包括mixins )非常有用。我从 Justin Fagnani 的优秀著作《Mixins and Javascript: The Good, the Bad, and the Ugly》中学到了很多。
方便的是,this
在静态类中方法指的是类本身
因此,静态方法可以实现多态性,如oncreate
下面的方法:
// Base
class Base {
static create() {
const instance = new this();
this.oncreate(instance);
return instance;
}
static oncreate(instance) {
console.log(`Something of the base class ${
Base.name} has been created.`);
}
}
// Derived
class Derived extends Base {
static oncreate(instance) {
console.log(`It's a new instance of ${
Derived.name}, all right!`);
}
}
// output: Something of the base class Base has been created.
const base = Base.create();
// output: It's a new instance of Derived, all right!
const derived = Derived.create();
// output: true
console.log(derived instanceof Derived);
new this()
当我偶然发现这条推文时,我了解到了这一点。
调用 IIFE(立即调用函数表达式)而不使用额外的括号
我们可以使用void
运算符来实现这一点,其中明确指出我们想要丢弃表达式void
的结果(IIFE 本身就是):
void function debug() {
if (confirm('stop?')) debugger;
}();
我相信这比用括号包裹函数更具可读性和助记性:
(function debug() {
if (confirm('stop?')) debugger;
})();
如果我们确实需要结果:
const rgb = function getColor(color) {
return {
red: '#FF0000',
green: '#00FF00',
blue: '#0000FF'
}[color];
}(car.color);
调用async
IIFE(立即调用函数表达式)
与上述类似,我们不需要包装括号:
await async function delay() {
const start = performance.now();
await new Promise(r => setTimeout(r, 1000));
console.log(`elapsed: ${performance.now() - start}`);
}();
函数参数的内联解构
function output ({firstName, lastName}) {
console.log(firstName, lastName);
}
const person = {
firstName: 'Jane',
lastName: 'Doe'
};
output(person);
函数参数的内联部分解构
function output ({firstName, ...rest}) {
console.log(firstName, rest.lastName, rest.age);
}
const person = {
firstName: 'John',
lastName: 'Doe',
age: 33
};
output(person);
使用表达式switch
const category = function getCategory(temp) {
// the first `case` which expression is `true` wins
switch(true) {
case temp < 0: return 'freezing';
case temp < 10: return 'cold';
case temp < 24: return 'cool';
default: return 'unknown';
}
}(10);
将非函数对象作为事件处理程序传递给addEventListener
诀窍在于实现EventListener.handleEvent
:
const listener = Object.freeze({
state: { message: 'Hello' },
handleEvent: event => {
alert(`${event.type} : ${listener.state.message}`);
}
});
button.addEventListener('click', listener);
检查变量是否属于特定类型
此方法适用于原始值类型及其包装类:String
,,,。Number
Boolean
Object
s1
您能预测以下哪个控制台输出和s2
片段是常见的吗?
const s1 = 's';
console.log(s1 instanceof String);
console.log(typeof s1);
console.log(s1.constructor === String);
const s2 = new String('s');
console.log(s2 instanceof String);
console.log(typeof s2);
console.log(s2.constructor === String);
我不能,所以我制作了一个RunKit:
s1 instanceof String: false
typeof s1: string
s1.constructor === String: true
s2 instanceof String: true
typeof s2: object
s2.constructor === String: true
有趣的是,只有s1.constructor === String
和对于(原始字符串值)和(类的实例)s2.constructor === String
都是一致的。true
s1
s2
String
TypeScript 更有趣,这对于具有 C# 或 Java 背景接触 JavaScript 的人来说可能会感觉很奇怪。
因此,要检查变量是否s
代表字符串,以下内容对于原始值及其包装类类型同样有效:
const isString = s?.constructor === String;
我们还可以使其跨领域工作(iframe
或弹出):
const isString = s?.constructor.name === 'String';
有些人可能会说,我们根本不应该使用类包装器来处理原始值。确实,我们不应该。但是,我们可以选择让我们自己的代码在被第三方调用时能够正确运行,无论它传入的是原始值还是包装器类对象作为参数。
例如,下面的代码对所有三种情况都一致有效(注意 的使用valueOf
):
takeBool(false);
takeBool(Boolean(false));
takeBool(new Boolean(false));
function takeBool(b) {
if (b?.constructor !== Boolean) throw new TypeError();
console.log(b.valueOf() === false? "is false": "is true");
}
检查变量是否nullish
(即null
或undefined
)
传统上,这是通过松散相等运算符来完成的==
,例如:
if (a == null) {
// a is either null or undefined
console.log((a == null) && (a == undefined)); // both true
}
这可能可以说是松散相等运算符(与严格相等运算符相对)的唯一有意义的用途。==
===
但是,如果您想尽量避免使用==
and!=
运算符,下面是执行“空值”检查的一些替代方法:
// The nullish coalescing (??) operator returns
// its right-hand side operand when its left-hand side operand
// is null or undefined, and otherwise returns
// its left-hand side operand.
if ((a ?? null) === null) {
// a is either null or undefined
}
if (Object.is(a ?? null, null)) {
// a is either null or undefined
}
if (Object.is(a ?? undefined, undefined)) {
// a is either null or undefined
}
// all standard or properly derived custom JavaScript objects
// have standard properties like these:
// `constructor`, `valueOf`, `toString`.
// Note though the below doesn't work for exotic cases,
// e.g. where a = Object.create(null):
if (a?.constructor) {
// a is neither null nor undefined
}
if (!a?.constructor) {
// a is either null or undefined
}
if (a?.valueOf === undefined) {
// a is either null or undefined
}
可选链式运算符的优点在于,undefined
当a
为null
或 时,结果都是明确的undefined
。这允许一些像这样的奇特表达式:
class Derived extends Base {
constructor(numericArg) {
// make sure the argument we pass to the base class'
// constructor is either a Number or DEFAULT_VALUE
super(function() {
switch (numericArg?.constructor) {
case undefined: return DEFAULT_VALUE;
case Number: return numericArg.valueOf();
default: throw new TypeError();
}
}());
}
}
关于空值合并运算符的一个值得注意的事情是,在中将在为或 时a ?? DEFAULT_VALUE
进行选择(与 不同,后者在 为时进行选择)。DEFAULT_VALUE
a
null
undefined
a || DEFAULT_VALUE
DEFAULT_VALUE
a
falsy
转换为原始类型Symbol.toPrimitive
众所周知的符号Symbol.toPrimitive
定义了如何将对象转换为原始类型,如下例所示。另请注意 的用法Symbol.toStringTag
:
class Item {
#item;
constructor(item) {
if (item?.constructor !== Number) throw new TypeError();
this.#item = item.valueOf();
}
[Symbol.toPrimitive](hint) {
// hint can be "number", "string", and "default"
switch (hint) {
case 'number':
return this.#item;
case 'string':
case 'default':
return `Item: ${this.#item}`;
default:
return null;
}
}
get [Symbol.toStringTag]() {
return this.constructor.name;
}
}
const item = new Item(42);
console.log(Number(item));
console.log(String(item));
console.log(item.toString());
console.log(item);
/* Output:
42
Item: 42
[object Item]
Item {}
*/
忽略承诺错误的助记方式(如适用)
await promise.catch(e => void e);
它的字面意思是:“取消该错误”,并且它是 ESLint 友好的。我发现它变得越来越有用,可以避免Node v15+ 中未处理的 Promise 拒绝所带来的潜在问题。例如:
// • we may want to start workflow1 before workflow2
const promise1 = workflow1();
const promise2 = workflow2();
// • and we may need workflow2 results first
// • if it fails, we don't care about the results of workflow1
// • therefore, we want to prevent
// unwanted unhandled rejection for promise1
promise1.catch(e => void e);
// • observe workflow2 results first
await promise2;
// • if the above didn't throw, now observe workflow1 results
await promise1;
Thenable 可以与 Promise 一起使用
我之前写过一篇关于 thenable 的博客。简而言之,下面是如何创建一个jQuery.Deferred
可以等待的类似 thenable 的对象:
function createDeferred() {
let resolve, reject;
const promise = new Promise((...args) =>
[resolve, reject] = args);
return Object.freeze({
resolve,
reject,
then: (...args) => promise.then(...args)
});
}
const deferred = createDeferred();
// resolve the deferred in 2s
setTimeout(deferred.resolve, 2000);
await deferred;
告诉哪个承诺首先得到解决Promise.race
有时我们需要知道哪个 Promise 首先被解决或被拒绝,从而赢得竞争Promise.race
,就像在 .NET 中一样Task.WhenAny
。链接我的SO 答案:
/**
* When any promise is resolved or rejected,
* returns that promise as the result.
* @param {Iterable.<Promise>} iterablePromises An iterable of promises.
* @return {{winner: Promise}} The winner promise.
*/
async function whenAny(iterablePromises) {
let winner;
await Promise.race(function* getRacers() {
for (const p of iterablePromises) {
if (!p?.then) throw new TypeError();
const settle = () => winner = winner ?? p;
yield p.then(settle, settle);
}
}());
// return the winner promise as an object property,
// to prevent automatic promise "unwrapping"
return { winner };
}
“承诺”同步函数调用以推迟异常处理
function ensureEven(a) {
if (a % 2 !== 0) throw new Error('Uneven!');
return a;
}
// • this throws:
const n = ensureEven(1);
// • this doesn't throw:
const promise = Promise.resolve().then(() => ensureEven(1));
// • until it is awaited
const n = await promise;
// • alternatively:
const promise = Promise(r => r(ensureEven(1)));
希望我们很快就能做到:
const promise = Promise.try(() => ensureEven(1));
在此之前,我们还可以使用像这样的polyfill 。
Symbol.species
在扩展标准类时很有用
这个众所周知的符号Symbol.species
对我来说绝对是鲜为人知的。MDN将其描述为“指定函数值属性的符号,构造函数使用该属性来创建派生对象”。
实际上,JavaScript 有时需要创建一个新的对象实例,即在不进行克隆的情况下重新生成一个对象。例如,Array.prototype.map
在进行任何映射之前创建一个新的数组实例:
class UltraArray extends Array {}
const a = new UltraArray(1, 2, 3);
const a2 = a.map(n => n/2);
console.log(a2 instanceof UltraArray); // true
以这种方式思考这种类型的对象复制可能会很有吸引力:
const a2 = new a.constructor();
但实际上,做法有点不同,更像是这样:
const constructor = a.constructor[Symbol.species] ?? a.constructor;
const a2 = new constructor();
因此,如果我们想map
将基类Array
用于新的映射实例,当在map
我们的自定义类的对象上调用时UltraArray
,我们可以这样做:
class UltraArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new UltraArray(1, 2, 3);
const a2 = a.map(n => n/2);
console.log(a2 instanceof UltraArray); // false
console.log(a2.constructor.name); // Array
这个看似不太实用的功能什么时候还会变得重要呢?我的答案是:从标准类派生并扩展Promise
它,添加诸如 、 等功能DeferredPromise
。AbortablePromise
这可能需要一篇单独的博客文章来阐述,我计划很快发布。
我希望这些建议对你有帮助
我计划持续更新这篇文章,因为我发现了更多有趣的 JavaScript 代码。如果对这些更新感兴趣,可以关注我的 Twitter 。
文章来源:https://dev.to/noseratio/a-few-handy-javascript-tricks-an9