标签 javascript 下的文章

最近处理一些用户录入数据时,发现有一些明明是纯数字的数据,却通过不了类型校验(无法解析成 Int/BigInt)类型,由于一直都是通过日志查看错误,觉得很疑惑,后来找到原始数据,发现因为这个『字符串』带了一个 zero width space,导致程序异常。

zero width space 在大多数文本输出的地方都是不可见的(顾名思义,没有宽度的空格),因此通过类似页面、日志排查都很难发现这个问题。

当把这个字符串手动复制粘贴到 Chrome 的控制台时,可以看到,这个 zero width space 表现成了一个·,这应该是 Chrome 为了方便调试设计的。

zero width space

而粘贴到 iTerm 终端,则可以直接看到 Unicode 值(但是当作为日志输出到终端时,是无法显示的)

zero width space in iTerm

这个字符一般来源都是用户从一些特定的平台复制而来,因此建议在类似的用户数据录入处进行一下相应处理,这里需要注意,一般语言的标准库的 trim 处理都是不会处理 zero width space 的,所以只能手动移除\u200B( 正则下)。

事实上,还有很多不可见字符,具体请参照这个回答:

https://stackoverflow.com/a/11305926/2530524

Control character in ASCII

随机数与随机字符串

早前我都使用Math.random()来生成随机数,然后转成16进制字符串来生成随机字符串,后来看到了这个回答(stackoverflow)Math.random() 确实不是一个好的选择,因此我需要更加科学的方法。

window.crypto (Web Cryptography API)

Web Cryptography API 已经是 W3C Recommendation 级别,常见浏览器的近期版本也都已经实现了。 0ivS.png 使用的时候也可以不用担心兼容性问题。

crypto.getRandomValues

crypto.getRandomValues 是我们用来实现随机字符串的主要方法,这个方法从实现和随机性的角度来说,更加高效、可靠。 10.2.1. The getRandomValues method The getRandomValues method generates cryptographically random values. It must act as follows:

  1. If array is not of an integer type (i.e., Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array or UInt8ClampedArray), throw a TypeMismatchError and terminate the algorithm.
  2. If the byteLength of array is greater than 65536, throw a QuotaExceededError and terminate the algorithm.
  3. Overwrite all elements of array with cryptographically random values of the appropriate type.
  4. Return array.
Note Do not generate keys using the getRandomValues method. Use the generateKey method instead.

使用方法就是传入一个 Int 的数组,然后返回被加密之后的数组(覆盖原始数组)。

科学的随机字符串生成方法

知道了crypto.getRandomValues的使用方法之后,我们就可以来实现一个科学的随机字符串生成方法了。 首先毫无疑问我们需要一个 UintArray,这比我们手动生成随机数的数组要简单多了。

let len = 64;
const arr = new Uint8Array(len / 2);

假设我们要生成长度为 64 的随机字符串,len 就是 64,之所以在生成 Uint8Array 时候,要将长度除以2,是因为最终我们是通过16进制字符串的形式来输出的,并且在头部补0取末尾两位,所以只需要一半长度的 Uint8Array 就可以了。 用crypto.getRandomValues()加密这个数据

window.crypto.getRandomValues(arr);

把这个 Uint8Array 转换成普通数组,并且把每一个值转换成16进制字符串(首位补0),取末尾2位,最后拼接到一起,就是最终的随机字符串了。

// dec2hex :: Integer -> String
function dec2hex (dec) {
  return ('0' + dec.toString(16)).substr(-2);
}
    
// generateId :: Integer -> String
function generateId (len = 40) {
  const arr = new Uint8Array(len / 2);
  window.crypto.getRandomValues(arr);
  return Array.from(arr, dec2hex).join('');
}

使用这个方法来生成随机字符串,更加高效、科学、可靠。 gist: Generate random string/characters in JavaScript

问题

起因是这样的,一开始在项目中写了一个原始的 Popup 组件(通过 react-redux 的 connect 方法接入 redux),后来有了一个更高级的需求,我就想要直接 extends 这个原始的 Popup 组件,然后覆写一些方法。 然后遇到了一个问题,继承之后的子组件没有父组件的自定义方法。

解疑

最初一度怀疑是否是 babel 的 decorator 插件有问题(毕竟不是浏览器天然支持的特性),但是仔细看了 decorator 插件的源码,发现并不存在这种问题。 后来在这个插件的 issues 里看到了这么一个 issue,问题并不算是同一个,但是作者提到,react-redux 的 connect 方法可能实现有一些问题(当然这个地方并不是),我就想到去看看 connect 的实现。 然后就看到了底层方法 connectAdvanced 中有这么一句代码:

export default connectAdvanced() {
    // ...

    return hoistStatics(Connect, WrappedComponent);
}

hoistStatics 是一个第三方库 hoist-non-react-statics,它干的事情也比较简单:

Copies non-react specific statics from a child component to a parent component. Similar to Object.assign, but with React static keywords blacklisted from being overridden.

简单来说,就是复制所有非 React 自有的属性,因为使用了 Object.getOwnPropertyNames()的方法,所以我们定义在 Component 上的所有方法就不会被复制到这个新的对象上了。 而在 connectAdvanced 内部,render 时实际是这样的:

render() {
  const selector = this.selector
  selector.shouldComponentUpdate = false

  if (selector.error) {
      throw selector.error
  } else {
      return createElement(WrappedComponent, this.addExtraProps(selector.props))
  }
}

通过 createElement 方法给原始的 WrappedComponent 传入额外的 props,来实现 connect 的效果。 总之,最终 connect 方法返回这个对象,其实和原始的 Component 的结构并不一样,虽然它的 WrappedComponent 属性指向了原始的 Component,但是显然在我们的场景下,并不能使用。

那怎么办呢

通常的推荐解决方式就是『高阶组件』,这样就屏蔽了 connect 的逻辑。

为什么要用 hoist-non-react-statics 这种模式

关于这个问题,作者在 这一个 issue 的回答中 也说了很多,总结来说,就是为了避免使用者可以直接的接触到 connect 之后的组件的内部方法,防止因为各种改动导致组件的 "break",这也算是为了提升健壮性。

一般来说,当我们在页面内部一个可滚动元素上使用滚动时,如果滚动距离超过它的顶部或者底部,就会触发页面滚动,但是在一些情况下,我们不希望引起页面滚动,因为这会影响用户操作。

下面是一种阻止页面滚动的方法。

var scroll = document.getElementsByClassName('scroll')[0];
function handleScroll (e) {
  if (scroll.scrollHeight > scroll.offsetHeight) { // 仅在元素处于可滚动状态下才进行阻止
    if (e.deltaY < 0 && scroll.scrollTop === 0) {
      e.preventDefault();
    }
    if (e.deltaY > 0 && (scroll.scrollTop + scroll.offsetHeight) >= scroll.scrollHeight) {
      e.preventDefault();
    }
  }
}
scroll.addEventListener('mousewheel', handleScroll);

DEMO: JSBIN