关于 JavaScript 的几个冷知识

586 阅读3分钟

如果你也有「冷知识」不妨发挥开源精神,一起壮大这篇文章。

冷知识不表示是解决问题最合适的办法,请大家以可读性可维护性为第一要旨。

长期维护,希望大家留言编辑。

一、JavaScript

1 for...in 可以遍历任何类型的变量

可以遍历 null undefined false number function array object class 一切 js 变量。这是其他任何遍历方式无法做到的,比如 fo-of forEach 等。

当然不是说有这个能力就要用,处于诸多考虑并不建议使用 for...in。现代 JS 有诸多方法可以替换 for-in

2 可以用 func?.() 代替 func && func()

该语法其实是 JS 的语法,Optional chaining ?.,以下用法来自 MDN

obj.val?.prop
obj.val?.[expr]
obj.arr?.[index]
obj.func?.(args)

3. 用正则表达式判断素数(非JS独有)

function isPrimeUseRegexp(number) {
  return '1'.repeat(number).match(/^1?$|^(11+?)\1+$/) === null;
}

测试:

var primes = []
for (let index = 0; index < 30; index++) {
  if (isPrimeUseRegexp(index)) { primes.push(index) }
}

console.log('Primes:', primes.join(' '))

将输出 Primes: 2 3 5 7 11 13 17 19 23 29

更多详见:A regular expression to check for prime numbers

4. 类型

利用 jsdoc 我们可以享受 TS 带来的额外好处。

4.1 JS 也支持泛型

利用 jsdoc 的 @template

// utils.js
/**
 * 返回数组任意一项,功能等同于 `Array.prototype.at`
 * @template T
 * @param {T[]} arr
 * @param {number} index
 * @returns {T}
 */
function at(arr, index) {
  return arr[index < 0 ? length - Math.abs(index) : index]
}

详见 掘金 - JS 也支持泛型?

4.2 JS 能写 TS 的 interface 且支持字段描述的那种

利用 jsdoc 的 @typedef

/**
 * @typedef {object} IFooOptions
 * @property {string} field1 描述1
 * @property {string} field2 描述2
 * @property {string} field3 描述3
 * @property {string} field3 描述4
 */

/**
 * I am foo.
 * @param {IFooOptions} opt
 * @return {Response}
 */
function foo(opt) {
  // ...
}

如果只是一次性使用,可无需先定义,直接内联到 @param 即可:

/**
 * I am foo.
 * @param {object} opt
 * @property {string} opt.field1 描述1
 * @property {string=} opt.field2 描述2
 * @property {string} [opt.field3] 描述3
 * @property {string} [opt.field3=""] 描述4
 * @return {Response}
 */
function foo(opt) {
  // ...
}

更多详见:jsdoc-supported-types

4.3 JS 能想 TS 一样定义 type 别名

// foo.js

/**
 * @typedef {string} DOMString
 */

更多详见 @typedef

效果:

image.png

4.4 复用三方包的类型

@param/type {import("foo").IBar},常见是 jest.config.js 配置类型能自动提示字段:

// jest.config.js

/** @type {import("jest").Config} */
const config = {
  ...
};

module.exports = config;

以下案例 createStyle 时参数将自动提示所有 css 属性的 key 以及 value,非常强大。

/**
 * @param {import("react").CSSProperties} params 
 */
function createStyle(params) {
  return Object.keys(params)
    .map((key) => {
      const rule = params[key];

      return toKebabCase(key) + ': ' + rule
    }, '')
    .join('; ')
}

// style => "font-size: 0.7rem; color: brown;"
const style = createStyle({
  fontSize: '0.7rem',
  color: 'brown',
})

4.5 Type Predict

先看一段代码

function isString(val) {
  return typeof val === 'string'
}

以下使用会报错因为 foo 并不存在 slice 方法。

let foo;

if (isString(val)) {
  foo.slice() 
}

更好的写法是利用 TS 的 Type Predicate,即下方的 val is string

/**
 * @type {(val: any) => val is string}
 */
const isString = (val) => typeof val === 'string'

5. 扩展参数符会给不具备原型链的对象增加原型链

const empty = Object.create(null)

empty.__proto__ === undefined

const clone = { ...empty }

clone.__proto__ === Object.prototype

二、CSS

1. 如何选择属性不等于某字符串的元素

我们知道如何写选择属性等于或包含某字符串的选择器。但是不等于如何写呢?

  • 等于 tag[attr="foo"]
  • 包含 tag[attr*="foo"]

答案:tag:not(tag[attr="foo"])

示例:获取所有在当前窗口打开的链接:a:not([target="_blank"])

阅读更多 Can't find a "not equal" css attribute selector

三、React

1 React.memo 入参可以是 class Component

React.memo 也能对 class Component 做缓存优化,效果相当于 class component 继承 React.PureComponent。

Yeah we should probably document that it works with classes. It's intended to work with any valid component (including more exotic ones like lazy() result or forwardRef() result). But documenting this risks obscuring the primary use case (memoized function components).

React 作者 @Dan Abramov

以下来自 issue Is it intended that React.memo() also works with class components? #13937

const Test = React.memo(class extends React.Component {
  render() {
    return <h1>TEST</h1>
  }
})

有几个问题?

1 为何官方文档不说明?

这是有意为之,因为『React.memo 主要场景就是为了对函数式组件做优化』若同时记录能够对类组件也能优化会分散文档阅读者对 React.memo 主要使用场景的注意力。

2 那是不是意味着 React.PureComponent 即将过时?

『现在不会立马过时,PureComponent 目前对 class component 做的优化其实比 React.memo 更多,但也是有可能在未来废弃掉』。

『PureComponent 的优化对象是 classes,而 React.memo 是 pure + function』。

2 useMemo 可以和 React.memo 一样阻止组件重复渲染

如果组件是 inline 的,此时可以使用 useMemo 来阻止其重复渲染,已实验有效。

以下二者功能等价:

// components/Test
const Test = () => {
  console.log('Test class Component render');

  return <h1>TEST</h1>;
};

export React.memo(Test)


// App.jsx
import { Test } from './components/Test';

function App() {
  return <div>
    <Test />
  </div>
}

VS

// components/Test
export const Test = () => {
  console.log('Test class Component render');

  return <h1>TEST</h1>;
};


// App.jsx
import { Test } from './components/Test';

function App() {
  return <div>
    { React.useMemo(() => <Test />, []); }
  </div>
}

完整测试代码:

function UseMemoVsReactMemo() {
  let [count, setCount] = useState(0);
  
  const handleAdd = () => {
    setCount(count + 1);
  };
  
  const Child1 = () => {
    console.log("子组件1 被重新渲染");
    return <p>子组件一</p>;
  };
  
  const Child2 = (props) => {
    return (
      <div>
        <p>子组件二</p>
        <p>count的值为:{props.count}</p>
      </div>
    );
  };
  
  return (
    <div>
      {
        useMemo(() => {
          return <Child1 />
        }, [])
      }
      
      <Child2 count={count} />
      <button onClick={handleAdd}>增加</button>
    </div>
  );
}

点击按钮仅触发 Child2 渲染,Child1 不会渲染,说明 useMemo 也能达到 React.memo 的优化效果。

参考

  1. Optional chaining (?.)
  2. Is it intended that React.memo() also works with class components? #13937
  3. 面试官求你别再问我hook了