一些实用的 JavaScript 技巧

2025-05-25

一些实用的 JavaScript 技巧

我想记录一些我最近从 Twitter 和其他在线资源(可惜我没怎么关注)学到的精妙 JavaScript 技巧和模式。所有功劳都归功于 JavaScript 在线社区。

目录

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());
Enter fullscreen mode Exit fullscreen mode

这对于类继承树的动态组合(包括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);
Enter fullscreen mode Exit fullscreen mode

new this()当我偶然发现这条推文时,我了解到了这一点


调用 IIFE(立即调用函数表达式)而不使用额外的括号

我们可以使用void运算符来实现这一点,其中明确指出我们想要丢弃表达式void的结果(IIFE 本身就是):

void function debug() {
  if (confirm('stop?')) debugger;
}(); 
Enter fullscreen mode Exit fullscreen mode

我相信这比用括号包裹函数更具可读性和助记性:

(function debug() {
  if (confirm('stop?')) debugger;
})();
Enter fullscreen mode Exit fullscreen mode

如果我们确实需要结果:

const rgb = function getColor(color) {
  return { 
    red: '#FF0000',
    green: '#00FF00',
    blue: '#0000FF'
  }[color];
}(car.color); 
Enter fullscreen mode Exit fullscreen mode


调用asyncIIFE(立即调用函数表达式)

与上述类似,我们不需要包装括号:

await async function delay() {
  const start = performance.now();
  await new Promise(r => setTimeout(r, 1000));
  console.log(`elapsed: ${performance.now() - start}`);
}();
Enter fullscreen mode Exit fullscreen mode


函数参数的内联解构

function output ({firstName, lastName}) {
  console.log(firstName, lastName);
}

const person = {
  firstName: 'Jane',
  lastName: 'Doe'
};

output(person);
Enter fullscreen mode Exit fullscreen mode


函数参数的内联部分解构

function output ({firstName, ...rest}) {
  console.log(firstName, rest.lastName, rest.age);
}

const person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 33
};

output(person);
Enter fullscreen mode Exit fullscreen mode


使用表达式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);
Enter fullscreen mode Exit fullscreen mode


将非函数对象作为事件处理程序传递给addEventListener

诀窍在于实现EventListener.handleEvent

const listener = Object.freeze({
  state: { message: 'Hello' },
  handleEvent: event => {
    alert(`${event.type} : ${listener.state.message}`);
  }
});

button.addEventListener('click', listener); 
Enter fullscreen mode Exit fullscreen mode


检查变量是否属于特定类型

此方法适用于原始值类型及其包装类:String,,,NumberBooleanObject

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);
Enter fullscreen mode Exit fullscreen mode

我不能,所以我制作了一个RunKit

s1 instanceof String: false
typeof s1: string
s1.constructor === String: true
s2 instanceof String: true
typeof s2: object
s2.constructor === String: true
Enter fullscreen mode Exit fullscreen mode

有趣的是,只有s1.constructor === String对于(原始字符串值)和(的实例s2.constructor === String都是一致的。trues1s2String

TypeScript 更有趣,这对于具有 C# 或 Java 背景接触 JavaScript 的人来说可能会感觉很奇怪。

因此,要检查变量是否s代表字符串,以下内容对于原始值及其包装类类型同样有效:

const isString = s?.constructor === String;
Enter fullscreen mode Exit fullscreen mode

我们还可以使其跨领域工作(iframe或弹出):

const isString = s?.constructor.name === 'String';
Enter fullscreen mode Exit fullscreen mode

有些人可能会说,我们根本不应该使用类包装器来处理原始值。确实,我们不应该。但是,我们可以选择让我们自己的代码在被第三方调用时能够正确运行,无论它传入的是原始值还是包装器类对象作为参数。

例如,下面的代码对所有三种情况都一致有效(注意 的使用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");
}
Enter fullscreen mode Exit fullscreen mode


检查变量是否nullish(即nullundefined

传统上,这是通过松散相等运算符来完成的==,例如:

if (a == null) {
  // a is either null or undefined
  console.log((a == null) && (a == undefined)); // both true 
}
Enter fullscreen mode Exit fullscreen mode

这可能可以说是松散相等运算符(与严格相等运算符相对)的唯一有意义的用途。=====

但是,如果您想尽量避免使用==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
}
Enter fullscreen mode Exit fullscreen mode

可选链式运算符的优点在于,undefinedanull或 时,结果都是明确的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();
      }
    }());
  }
}
Enter fullscreen mode Exit fullscreen mode

关于空值合并运算符的一个值得注意的事情是,在中将或 时a ?? DEFAULT_VALUE进行选择(与 不同,后者在 为时进行选择)。DEFAULT_VALUEanullundefineda || DEFAULT_VALUEDEFAULT_VALUEafalsy


转换为原始类型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 {}
*/
Enter fullscreen mode Exit fullscreen mode


忽略承诺错误的助记方式(如适用)

await promise.catch(e => void e); 
Enter fullscreen mode Exit fullscreen mode

它的字面意思是:“取消该错误”,并且它是 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;
Enter fullscreen mode Exit fullscreen mode


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;
Enter fullscreen mode Exit fullscreen mode


告诉哪个承诺首先得到解决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 }; 
}
Enter fullscreen mode Exit fullscreen mode


“承诺”同步函数调用以推迟异常处理

致谢:tc39-proposal-promise-try

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)));
Enter fullscreen mode Exit fullscreen mode

希望我们很快就能做到:

const promise = Promise.try(() => ensureEven(1));
Enter fullscreen mode Exit fullscreen mode

在此之前,我们还可以使用像这样的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
Enter fullscreen mode Exit fullscreen mode

以这种方式思考这种类型的对象复制可能会很有吸引力:

const a2 = new a.constructor();
Enter fullscreen mode Exit fullscreen mode

但实际上,做法有点不同,更像是这样:

const constructor = a.constructor[Symbol.species] ?? a.constructor;
const a2 = new constructor();
Enter fullscreen mode Exit fullscreen mode

因此,如果我们想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
Enter fullscreen mode Exit fullscreen mode

这个看似不太实用的功能什么时候还会变得重要呢?我的答案是:从标准类派生并扩展Promise它,添加诸如 、 等功能DeferredPromiseAbortablePromise可能需要一篇单独的博客文章来阐述,我计划很快发布。

我希望这些建议对你有帮助

我计划持续更新这篇文章,因为我发现了更多有趣的 JavaScript 代码。如果对这些更新感兴趣,可以关注我的 Twitter 。

文章来源:https://dev.to/noseratio/a-few-handy-javascript-tricks-an9
PREV
无需服务器即可在窗口之间共享状态
NEXT
现代 Web 应用程序的文件夹结构