关于javascript:前端装逼技巧-108-式三-冇得感情的API调用工程师

24次阅读

共计 22555 个字符,预计需要花费 57 分钟才能阅读完成。

系列文章公布汇总:

  • 前端装逼技巧 108 式(一)—— 打工人
  • 前端装逼技巧 108 式(二)—— 不讲武德
  • 前端装逼技巧 108 式(三)—— 冇得感情的 API 调用工程师
  • 浏览器是如何工作的:Chrome V8 让你更懂 JavaScript

文章格调所限,援用材料局部,将在对应大节开端标出。

第三十七式:茫然一顾眼前亮,糊涂宛如在梦中 —— "123​4".length === 5?这一刻,我感触到了眼睛的背离和羞辱

  • 复制 以下代码到浏览器控制台:
console.log('123​4'.length === 5); // true

  哈哈,是不是有种被眼睛背离的感觉?其实这就是所谓的 零宽空格(Zero Width Space,简称“ZWSP”),零宽度字符是不可见的非打印字符,它用于打断长英文单词或长阿拉伯数字,以便于换行显示,否则长英文单词和长阿拉伯数字会越过盒模型的边界,常见于富文本编辑器,用于格局隔断。

  • 探索一下下面代码的玄机:
const common = '1234';
const special = '123​4';
console.log(common.length); // 4
console.log(special.length); // 5
console.log(encodeURIComponent(common)); // 1234
console.log(encodeURIComponent(special)); // 123%E2%80%8B4
// 把下面两头特殊字符局部进行解码
console.log(decodeURIComponent('%E2%80%8B')); //(空)const otherSpecial = '123\u200b4'; // 或者 "123\u{200b}4"
console.log(otherSpecial); // 1234
console.log(otherSpecial.length, common === special, special === otherSpecial); // 5 false true
  • 在 HTML 中应用零宽度空格(在 HTML 中,零宽度空格与 <wbr> 等效):
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <!-- &#8203; 和 <wbr /> 是零宽空格在 html 中的两种示意 -->
    <div>abc&#8203;def</div>
    <div>abc<wbr />def</div>
  </body>
</html>

ESLint 有一条禁止不规则的空白 (no-irregular-whitespace)的规定,避免代码外面误拷贝了一些诸如零宽空格类的空格,免得造成一些误导。

拓展:咱们常常在 html 中应用的 &nbsp; 全称是 No-Break SPace,即不间断空格,当 HTML 有多个间断的 一般空格 时,浏览器在渲染时只会渲染一个空格,而应用这个不间断空格,能够禁止浏览器合并空格。罕用于富文本编辑器之中,当咱们在富文本编辑器间断敲下多个空格时,最初输入的内容便会带有很多不间断空格。

参考资料:常见空格一览 – 李银城 | 什么是零宽度空格 | 维基百科 - 空格

第三十八式:如何禁止网页复制粘贴

  对于禁止网页复制粘贴,兴许你并不生疏。一些网页是间接禁止复制粘贴;一些网页,则是要求登陆后才可复制粘贴;还有一些网站,复制粘贴时会带上网站的相干起源标识信息。

  • 如何禁止网页复制粘贴
const html = document.querySelector('html');
html.oncopy = () => {alert('牛逼你复制我呀');
  return false;
};
html.onpaste = () => false;
  • 在复制时做些别的操作,比方跳转登陆页面
const html = document.querySelector('html');
html.oncopy = (e) => {console.log(e);
  // 比方指向百度或者登陆页
  // window.location.href='http://www.baidu.com';
};
html.onpaste = (e) => {console.log(e);
};
  • 如何应用 js 设置 / 获取剪贴板内容
// 设置剪切板内容
document.addEventListener('copy', () => {
  const clipboardData =
    event.clipboardData || event.originalEvent?.clipboardData;
  clipboardData?.setData('text/plain', '不论复制什么,都是我!');
  event.preventDefault();});

// 获取剪切板的内容
document.addEventListener('paste', () => {
  const clipboardData =
    event.clipboardData || event.originalEvent?.clipboardData;
  const text = clipboardData?.getData('text');
  console.log(text);
  event.preventDefault();});
  • 有什么用

    • 对于注册输出明码等须要输出两次雷同内容的场景,应该是须要禁止粘贴的,这时候就能够禁止对应输入框的复制粘贴动作。
    • 登陆能力复制。很多网站上的页面内容是不容许复制的,这样能够避免用户或者程序歹意的去抓取页面数据。

参考资料:Clipboard API and events | Document.execCommand()

第三十九式:function.length指代什么?—— 意识柯里化和 JS 函数重载

  在函数式编程里,有几个比拟重要的概念:函数的合成、柯里化和函子。其中柯里化(Currying),是指把承受多个参数的函数变换成承受一个繁多参数(最后函数的第一个参数)的函数,并且返回承受余下的参数而且返回后果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,然而它是 Moses Schnfinkel 和 Gottlob Frege 创造的。

  lodash 实现了_.curry 函数,_.curry函数接管一个函数作为参数,返回新的柯里化(curry)函数。调用新的柯里化函数时,当传递的参数个数小于柯里化函数要求的参数时,返回一个接管残余参数的函数,当传递的参数达到柯里化函数要求时,返回后果。那么,_.curry函数是如何判断传递的参数是否达到要求的呢?咱们无妨先看看上面的例子:

function func(a, b, c) {console.log(func.length, arguments.length);
}
func(1); // 3  1
  • 看看 MDN 的解释:

    • length 是函数对象的一个属性值,指该函数有多少个 必须要传入的参数 ,那些 已定义了默认值的参数不算在内,比方 function(x = 0)的 length 是 0。即形参的数量仅包含第一个具备默认值之前的参数个数。
    • 与之比照的是,arguments.length 是函数被调用时理论传参的个数。
  • 实现 lodash curry 化函数
// 模仿实现 lodash 中的 curry 办法
function curry(func) {return function curriedFn(...args) {
    // 判断实参和形参的个数
    if (args.length < func.length) {return function () {return curriedFn(...args.concat(Array.from(arguments)));
      };
    }
    return func(...args);
  };
}

function getSum(a, b, c) {return a + b + c;}

const curried = curry(getSum);

console.log(curried(1, 2, 3));
console.log(curried(1)(2, 3));
console.log(curried(1, 2)(3));
  • JS 函数重载

  函数重载,就是函数名称一样,然而容许有不同输出,依据输出的不同,调用不同的函数,返回不同的后果。JS 里默认是没有函数重载的,然而有了 Function.length 属性和 arguments.length,咱们便可简略的通过if…else 或者 switch 来实现 JS 函数重载了。

function overLoading() {
  // 依据 arguments.length,对不同的值进行不同的操作
  switch (arguments.length) {
    case 0 /* 操作 1 的代码写在这里 */:
      break;
    case 1 /* 操作 2 的代码写在这里 */:
      break;
    case 2: /* 操作 3 的代码写在这里 */
  }
}

  更高级的函数重载,请参考 jQuery 之父 John Resig 的 JavaScript Method Overloading,这篇文章里,作者奇妙地利用闭包,实现了 JS 函数的重载。

参考资料:浅谈 JavaScript 函数重载 | JavaScript Method Overloading |【译】JavaScript 函数重载 – Fundebug | Function.length | 函数式编程入门教程 – 阮一峰

第四十式:["1","7","11"].map(parseInt)为什么会返回[1,NaN,3]?

  • map 返回 3 个参数,item,index,Array,所以 [1,7,11].map(console.log) 打印:

  • parseInt 承受两个参数:string,radix,其中 radix 默认为 10;
  • 那么,每次调用 parseInt,相当于:parseInt(item,index,Array),map 传递的第三个参数 Array 会被疏忽。index 为 0 时,parseInt(1,0),radix 取默认值 10;parseInt(7,1)中,7 在 1 进制中不存在,所以返回”NaN“;parseInt(11,2),2 进制中 11 刚好是十进制中的 3。

参考:[JS 中为啥 [‘1’, ‘7’, ’11’].map(parseInt) 返回 [1, NaN, 3]](https://mp.weixin.qq.com/s/h-…

第四十一式:iframe 间数据传递,postMessage 能够是你的抉择

  平时开发中,兴许咱们会遇到须要在非同源站点、iframe 间传递数据的状况,这个时候,咱们能够应用 postMessage 实现数据的传递。
  window.postMessage() 办法能够平安地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具备雷同的协定(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 Document.domain 设置为雷同的值) 时,这两个脚本能力互相通信(即同源)。window.postMessage() 办法提供了一种受控机制来躲避此限度,只有正确的应用,这种办法就很平安。

// 页面 1 触发事件,发送数据
top.postMessage(data, '*');
// window  以后所在 iframe
// parent  上一层 iframe
// top     最外层 iframe

// 页面 2 监听 message 事件
useEffect(() => {const listener = (ev) => {console.log(ev, ev.data);
  };
  window.addEventListener('message', listener);
  return () => {window.removeEventListener('message', listener);
  };
}, []);

留神:

  • postMessage第二个参数 targetOrigin 用来指定哪些窗口能接管到音讯事件,其值能够是字符串 ”*”(示意无限度)或者一个 URI。
  • 如果你明确的晓得音讯应该发送到哪个窗口,那么请始终提供一个有确切值的 targetOrigin,而不是 ”*”。
  • 不提供确切的指标将导致数据泄露到任何对数据感兴趣的歹意站点。
  • 参考资料:window.postMessage

第四十二式:薛定谔的 X —— 乏味的let x = x

  薛定谔的猫(英文名称:Erwin Schrödinger’s Cat)是奥地利驰名物理学家薛定谔提出的一个思维试验,是指将一只猫关在装有大量镭和氰化物的密闭容器里。镭的衰变存在几率,如果镭产生衰变,会触发机关打碎装有氰化物的瓶子,猫就会死;如果镭不产生衰变,猫就存活。依据量子力学实践,因为放射性的镭处于衰变和没有衰变两种状态的叠加,猫就理当处于死猫和活猫的叠加状态。这只既死又活的猫就是所谓的“薛定谔猫”。

  JS 引入 let 和 const 之后,也呈现了一种乏味的景象:

<!-- 能够拷贝上面的代码,放的一个 html 文件中,而后应用浏览器关上,查看控制台 -->
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      let x = x;
    </script>
    <script>
      x = 2;
      console.log(x);
    </script>
  </body>
</html>

  下面的代码里,咱们在第一个 script 里引入写了let x = x;,就导致在其余 script 下都无奈在全局作用域下应用 x 变量了(无论是对 x 进行赋值、取值,还是申明,都不行)。也就是说当初 x 处于一种“既被定义了,又没被定义”的中间状态。

  这个问题阐明:如果 let x 的初始化过程失败了,那么:

  • x 变量就将永远处于 created 状态。
  • 你无奈再次对 x 进行初始化(初始化只有一次机会,而那次机会你失败了)。
  • 因为 x 无奈被初始化,所以 x 永远处在临时死区(也就是盗梦空间里的 limbo)!
  • 有人会感觉 JS 坑,怎么能呈现这种状况;其实问题不大,因为此时代码曾经报错了,前面的代码想执行也没机会。

参考资料:JS 变量封禁大法:薛定谔的 X

第四十三式:聊聊前端错误处理

一个 React-dnd 引出的前端错误处理

  年初的时候,笔者曾做过一个前端错误处理的笔记,事件是这样的:

  我的项目中某菜单定义的页面因有拖拽的需要,就引入了 React DnD 来实现这一工作;随着业务的更新迭代,局部列表页面又引入了自定义列的性能,能够通过拖动来对列进行排序,前面就发现在某些页面上,试图关上自定义列的弹窗时,页面就解体白屏了,控制台会透出谬误:'Cannot have two HTML5 backends at the same time.'。在排查问题的时候,查看源码发现:

// ...
value: function setup() {if (this.window === undefined) {return;}
  if (this.window.__isReactDndBackendSetUp) {throw new Error('Cannot have two HTML5 backends at the same time.');
  }
  this.window.__isReactDndBackendSetUp = true;
  this.addEventListeners(this.window);
}
// ...

  也就是说,react-dnd-html5-backend在创立新的实例前会通过 window.__isReactDndBackendSetUp 的全局变量来判断是否曾经存在一个可拖拽组件,如果有的话,就间接报错,而因为我的项目里对应组件没有相应的错误处理逻辑,抛出的 Error 异样层层上传到 root,始终没有被捕捉和解决,最终导致页面解体。其实在当是的业务场景下,这个问题比拟好解决,因为菜单定义页面没有自定义列的需要,而其余页面自定义列又是通过弹窗展现的,所以不要忘了给自定义列弹窗设置 destroyOnClose 属性(敞开销毁)即可。为了防止我的项目中因为一些谬误导致系统白屏,在我的项目中,咱们应该正当应用错误处理。

前端错误处理的办法

1、Error Boundaries

  如何使一个 React 组件变成一个“Error Boundaries”呢?只须要在组件中定义个新的生命周期函数——componentDidCatch(error, info):

error: 这是一个曾经被抛出的谬误;info: 这是一个 componentStack key。这个属性有对于抛出谬误的组件堆栈信息。

// ErrorBoundary 实现
class ErrorBoundary extends React.Component {state = { hasError: false};

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({hasError: true});
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

ErrorBoundary 应用:

// ErrorBoundary 应用
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Erro Boundaries 实质上也是一个组件,通过减少了新的生命周期函数 componentDidCatch 使其变成了一个新的组件,这个非凡组件能够捕捉其子组件树中的 js 错误信息,输入错误信息或者在报错条件下,显示默认谬误页。留神一个 Error Boundaries 只能捕捉其子组件中的 js 谬误,而不能捕捉其组件自身的谬误和非子组件中的 js 谬误。

  然而 Error Boundaries 也不是万能的,上面咱们来看哪些状况下不能通过 Error Boundaries 来 catch{}谬误:

  • 组件的外部的事件处理函数,因为 Error Boundaries 解决的仅仅是 Render 中的谬误,而 Hander Event 并不产生在 Render 过程中。
  • 异步函数中的异样 Error Boundaries 不能 catch,比方 setTimeout 或者 setInterval ,requestAnimationFrame 等函数中的异样。
  • 服务器端的 rendering
  • 产生在 Error Boundaries 组件自身的谬误

2、componentDidCatch()生命周期函数:

  componentDidCatch 是一个新的生命周期函数,当组件有了这个生命周期函数,就成为了一个 Error Boundaries。

3、try/catch 模块

  Error Boundaries 仅仅抛出了子组件的错误信息,并且不能抛出组件中的事件处理函数中的异样。(因为 Error Boundaries 仅仅能保障正确的 render,而事件处理函数并不会产生在 render 过程中),咱们须要用 try/catch 来处理事件处理函数中的异样。

try/catch 只能捕捉到同步的运行时谬误,对语法和异步谬误却无能为力,捕捉不到。

4、window.onerror

  当 JS 运行时谬误产生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()。

在理论的应用过程中,onerror 次要是来捕捉意料之外的谬误,而 try-catch 则是用来在可预感状况下监控特定的谬误,两者联合应用更加高效。

/**
 * @param {String}  message    错误信息
 * @param {String}  source    出错文件
 * @param {Number}  lineno    行号
 * @param {Number}  colno    列号
 * @param {Object}  error  Error 对象(对象)*/
window.onerror = function (message, source, lineno, colno, error) {console.log('捕捉到异样:', { message, source, lineno, colno, error});
  // window.onerror 函数只有在返回 true 的时候,异样才不会向上抛出,否则即便是晓得异样的产生控制台还是会显示 Uncaught Error: xxxxx。//  return true;
};

5、window.addEventListener

  次要用于动态资源加载异样捕捉。

6、Promise Catch

7、unhandledrejection:

  当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;这可能产生在 window 下,但也可能产生在 Worker 中。unhandledrejection 继承自 PromiseRejectionEvent,而 PromiseRejectionEvent 又继承自 Event。因而 unhandledrejection 含有 PromiseRejectionEvent 和 Event 的属性和办法。

总结

  前端组件 / 我的项目中,须要有适当的谬误处理过程,否则呈现谬误,层层上传,没有进行捕捉,就会导致页面挂掉。

第四十四式:不做工具人 —— 应用 nodejs 依据配置主动生成文件

  笔者在工作中有一个需要是搭建一个 BFF 层我的项目,实现对每一个接口的权限管制和转发到后端底层接口。因为 BFF 层接口逻辑较少,70% 状况下都只是实现一个转发,所以每个文件类似度较高,但因为每个 API 需独自管制权限,所以 API 文件又必须存在,所以应用 nodejs 编写 API 自动化生成脚本,防止进行大量的手动创立文件和复制批改的操作,示例如下:

  • 编写主动生成文件的脚本:
// auto.js
const fs = require('fs');
const path = require('path');
const config = require('./apiConfig'); // json 配置文件,格局见上面正文内容
// config 的格局如下:// [
//     {
//         filename: 'querySupplierInfoForPage.js',
//         url: '/supplier/rest/v1/supplier/querySupplierInfoForPage',
//         comment: '分页查问供应商档案 - 主信息',
//     },
// ]

// 验证数量是否统一
// 也能够在次做一些其余的验证
function verify() {
  console.log(
    config.length,
    fs.readdirSync(path.join(__dirname, '/server/api')).length
  );
}

// 生成文件
function writeFileAuto(filePath, item) {
  fs.writeFileSync(
    filePath,
    `/**
* ${item.comment}
*/
const {Controller, Joi} = require('ukoa');

module.exports = class ${item.filename.split('.')[0]} extends Controller {init() {
        this.schema = {Params: Joi.object().default({}).notes('参数'),
            Action: Joi.string().required().notes('Action')
        };
    }

    // 执行函数体
    async main() {const { http_supply_chain} = this.ctx.galaxy;
        const [data] = await http_supply_chain("${item.url}", this.params.Params, {throw: true});
        return this.ok = data.obj;
    }
};
`
  );
}

function exec() {config.forEach((item) => {var filePath = path.join(__dirname, '/server/api/', item.filename);
    fs.exists(filePath, function (exists) {if (exists) {
        // 已存在的文件就不要反复生成了,因为兴许你曾经对已存在的文件做了非凡逻辑解决
        //(毕竟只有 70% 左右的 API 是纯转发,还有 30% 左右有本人的解决逻辑)console.log(` 文件 ${item.filename}已存在 `);
      } else {console.log(` 创立文件:${item.filename}`);
        writeFileAuto(filePath, item);
      }
    });
  });
}

exec();
  • 执行脚本,生成文件:node auto.js
// querySupplierInfoForPage.js
/**
 * 分页查问供应商档案 - 主信息
 */
const {Controller, Joi} = require('ukoa');

module.exports = class querySupplierInfoForPage extends (Controller) {init() {
    this.schema = {Params: Joi.object().default({}).notes('参数'),
      Action: Joi.string().required().notes('Action'),
    };
  }

  // 执行函数体
  async main() {const { http_supply_chain} = this.ctx.galaxy;
    const [data,] = await http_supply_chain(
      '/supplier/rest/v1/supplier/querySupplierInfoForPage',
      this.params.Params,
      {throw: true}
    );
    return (this.ok = data.obj);
  }
};

  此处只是抛砖引玉,联合具体业务场景,兴许你会为 nodejs 脚本找到更多更好的用法,为前端赋能。

第四十五式:明明元素存在,我的 document.getElementsByTagName('video') 却获取不到?

  • 应用 Chrome 浏览器在线看视频的时候,有些网站不反对倍速播放;有的网站只反对 1.5 和 2 倍速,然而本人更喜爱 1.75 倍;又或者有些网站须要会员能力倍速播放(比方某盘),个别咱们能够通过装置相应的浏览器插件解决,如果不违心装置插件,也能够应用相似 document.getElementsByTagName('video')[0].playbackRate = 1.75(1.75 倍速)的形式实现倍速播放,这个办法在大部分网站上是无效的(当然,如果晓得 video 标签的 id 或者 class,通过 id 和 class 来获取元素会更便捷一点),经测试,playbackRate 反对的最大倍速 Chrome 下是 16。同时,给 playbackRate 设置一个小于 1 的值,比方 0.3,能够模拟出 相似鬼片的音效
  • 然而在某盘,这种办法却生效了,因为我没有方法获取到 video 元素,审查元素如下:

  审查元素时,咱们发现了 #shadow-root (closed) 和 videojs 的存在。兴许你还记得,在第六式中咱们曾简略探讨过 Web Components,其中介绍到attachShadow() 办法能够开启 Shadow DOM(这部分 DOM 默认与内部 DOM 隔离,外部任何代码都无奈影响内部),暗藏自定义元素的外部实现,咱们内部也没法获取到相应元素,如下图所以(点击图片跳转 Web Components 示例代码):

  是以,咱们能够正当推断,某盘的网页视频播放也应用了相似 Element.attachShadow()办法进行了元素暗藏,所以咱们无奈通过 document.getElementsByTagName('video') 获取到 video 元素。通过浏览 videojs 文档发现,能够通过相应 API 实现自定义倍速播放:

videojs.getPlayers('video-player').html5player.tech_.setPlaybackRate(1.666);

参考资料:百度网盘视频倍速播放办法 | videojs 文档 | Element.attachShadow() | 深刻了解 Shadow DOM v1

第四十六式:SQL 也能够 if else?—— 不常写 SQL 的我神奇的常识减少了

  在刷 leetcode 的时候遇到一个 SQL 题目 627. 变更性别,题目要求:

给定一个  salary  表,有 m = 男性 和 f = 女性 的值。替换所有的 f 和 m 值(例如,将所有 f 值更改为 m,反之亦然)。要求只应用一个更新(Update)语句,并且没有两头的长期表。留神,您必只能写一个 Update 语句,请不要编写任何 Select 语句。

  UPDATE salary
    SET
      sex = CASE sex
          WHEN 'm' THEN 'f'
          ELSE 'm'
        END;

参考资料:SQL 之 CASE WHEN 用法详解

第四十七式:庭院深深深几许,杨柳堆烟,帘幕无重数 —— 如何实现深拷贝?

  深拷贝,在前端面试里仿佛是一个永恒的话题了,最简略的办法是 JSON.stringify() 以及JSON.parse(),然而这种办法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,不能够拷贝 undefined,function,RegExp 等类型。还有其余一些包含扩大运算符、object.asign、递归拷贝、lodash 库等的实现,网上有很多相干材料和实现,这里不是咱们探讨的重点。这次咱们来探讨一个新的实现 —— MessageChannel。咱们间接看代码:

// 创立一个 obj 对象,这个对象中有 undefined 和 循环援用
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
  f: undefined,
};
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;

// 深拷贝办法封装
function deepCopy(obj) {return new Promise((resolve) => {const { port1, port2} = new MessageChannel();
    port1.postMessage(obj);
    port2.onmessage = (e) => resolve(e.data);
  });
}

// 调用
deepCopy(obj).then((copy) => {
  // 请记住 `MessageChannel` 是异步的这个前提!let copyObj = copy;
  console.log(copyObj, obj);
  console.log(copyObj == obj);
});

  咱们发现 MessageChannelpostMessage传递的数据也是深拷贝的,这和 web workerpostMessage一样。而且还能够拷贝 undefined 和循环援用的对象。简略说,MessageChannel创立了一个通信的管道,这个管道有两个端口,每个端口都能够通过 postMessage 发送数据,而一个端口只有绑定了 onmessage 回调办法,就能够接管从另一个端口传过来的数据。

须要阐明的一点是:MessageChannel在拷贝有函数的对象时,还是会报错。

参考资料:MessageChannel | MessageChannel 是什么,怎么应用?

第四十八式:换了电脑,如何应用 VSCode 保留插件配置?

  兴许每一个冇得感情的 API 调用工程师在应用 VSCode 进行开发时,都有本人的插件、个性化配置以及代码片段等,应用 VSCode 不必登陆,不必注册账号,的确很不便,但这同时也带来一个问题:如果你有多台电脑,比方家里一个、公司一个,都会用来开发;又或者,你到职入职了新的公司。此时,咱们就须要从头再次配置一遍 VSCode,包含插件、配置、代码片段,如此重复,兴许真的会解体。其实 VSCode 提供了 setting sync 插件,很不便咱们同步插件配置。具体应用如下:

  • 在 VSCode 中搜寻 Settings Sync 并进行装置;
  • 装置后,摁下 Ctrl(mac 为 command)+ Shift + P 关上控制面板,搜寻 Sync,抉择 Sync: Update/Upload Settings 能够上传你的配置,抉择 Sync: Download Settings 会下载近程配置;
  • 如果你之前没有应用过 Settings Sync,在上传配置的时候,会让你在 Github 上创立一个受权码,容许 IDE 在你的 gist 中创立资源;下载近程配置,你能够间接将 gist 的 id 填入。
  • 下载后期待装置,而后重启即可。

  如此以来,咱们就能够在多台设施间同步配置了。

参考资料:Settings Sync | VSCode 保留插件配置并应用 gist 治理代码片段

第四十九式:避免对象被篡改,能够试试 Object.seal 和 Object.freeze

  有时候你可能怕你的对象被误改了,所以须要把它爱护起来。

  • Object.seal 避免新增和删除属性

  通常,一个对象是可扩大的(能够增加新的属性)。Object.seal()办法关闭一个对象会让这个对象变的 不能增加新属性 ,且所有 已有属性会变的不可配置 。属性不可配置的成果就是属性变的 不可删除,以及一个数据属性不能被从新定义成为拜访器属性,或者反之。以后属性的值只有原来是可写的就能够扭转。尝试删除一个密封对象的属性或者将某个密封对象的属性从数据属性转换成拜访器属性,后果会静默失败或抛出 TypeError。

数据属性 蕴含一个数据值的地位,在这个地位能够读取和写入值。拜访器属性 不蕴含数据值。它蕴含一对 getter 和 setter 函数。当读取拜访器属性时,会调用 getter 函数并返回有效值;当写入拜访器属性时,会调用 setter 函数并传入新值,setter 函数负责解决数据。

const person = {name: 'jack',};
Object.seal(person);
delete person.name;
console.log(person); // {name: "jack"}
  • Object.freeze 解冻对象
      Object.freeze() 办法能够解冻一个对象。一个被解冻的对象再也 不能被批改 ;解冻了一个对象则 不能向这个对象增加新的属性,不能删除已有属性,不能批改该对象已有属性的可枚举性、可配置性、可写性,以及不能批改已有属性的值 。此外,解冻一个对象后该对象的 原型也不能被批改。freeze() 返回和传入的参数雷同的对象。
const obj = {prop: 42,};
Object.freeze(obj);
obj.prop = 33;
// Throws an error in strict mode
console.log(obj.prop);
// expected output: 42

Tips:Object.freeze 浅解冻 ,即只解冻一层,要使对象不可变,须要递归解冻每个类型为对象的属性( 深解冻 )。应用Object.freeze() 解冻的对象中的现有属性值是不可变的。用 Object.seal() 密封的对象能够扭转其现有属性值。同时能够应用 Object.isFrozen、Object.isSealed、Object.isExtensible 判断以后对象的状态。

  • Object.defineProperty 解冻单个属性:设置 enumable/writable 为 false,那么这个属性将不可遍历和写
  • 参考资料:JS 高级技巧 | javascript 的数据属性和拜访器属性 | Object.freeze() | Object.seal() | 深入浅出 Object.defineProperty()

第五十式:不随机的随机数 —— 咱们都晓得 Math.random 是伪随机的,那如何失去密码学平安的随机数

  在 JavaScript 中产生随机数的形式是调用 Math.random,这个函数返回 [0, 1) 之间的数字,咱们通过对 Math.random 的包装解决,能够失去咱们想要的各种随机值。

  • 怎么实现一个随机数发生器
// from stackoverflow
// 上面的实现还是很随机的
let seed = 1;
function random() {let x = Math.sin(seed++) * 10000;
  return x - Math.floor(x);
}

  随机数发生器函数须要一个种子 seed,每次调用 random 函数的时候种子都会发生变化。因为 random() 是一个没有输出的函数,不论执行多少次,其运行后果都是一样的,所以须要有一个一直变动的入参,这个入参就叫种子,每运行一次种子就会产生一次变动。所以咱们能够借助以上思路实现本人的随机数发生器(或者有些场合,咱们不用管他是不是真的是随机的,再或者就是要让他不随机呢)。

  • 为什么说 Math.random 是不平安的呢?

  V8 源码显示 Math.random 种子的可能个数为 2 ^ 64,随机算法绝对简略,只是保障尽可能的随机散布。咱们晓得扑克牌有 52 张,总共有 52! = 2 ^ 226 种组合,如果随机种子只有 2 ^ 64 种可能,那么可能会有大量的组合无奈呈现。

  从 V8 里 Math.random 的实现逻辑来看,每次会一次性产生 128 个随机数,并放到 cache 外面,供后续应用,当 128 个应用完了再从新生成一批随机数。所以 Math.random 的随机数具备可预测性,这种由算法生成的随机数也叫伪随机数。只有种子确定,随机算法也确定,便能晓得下一个随机数是什么。具体可参考随机数的故事。

  • Crypto.getRandomValues()

  Crypto.getRandomValues() 办法让你能够获取 合乎密码学要求 的平安的随机值。传入参数的数组被随机值填充(在 加密意义上的随机 )。window.crypto.getRandomValue 的实现在 Safari,Chrome 和 Opera 浏览器上是应用带有 1024 位种子的 ARC4 流明码。

var array = new Uint32Array(10);
window.crypto.getRandomValues(array);

console.log('Your lucky numbers:');
for (var i = 0; i < array.length; i++) {console.log(array[i]);
}

参考资料:随机数的故事 | Crypto.getRandomValues() | 如何应用 window.crypto.getRandomValues 在 JavaScript 中调用扑克牌?

第五十一式:forEach 只是对 for 循环的简略封装?你了解的 forEach 可能并不正确

  咱们先看看上面这个 forEach 的实现:

Array.prototype.forEachCustom = function (fn, context) {context = context || arguments[1];
  if (typeof fn !== 'function') {throw new TypeError(fn + 'is not a function');
  }

  for (let i = 0; i < this.length; i++) {fn.call(context, this[i], i, this);
  }
};

  咱们发现,下面的代码实现其实只是对 for 循环的简略封装,看起来仿佛没有什么问题,因为很多时候,forEach 办法是被用来代替 for 循环来实现数组遍历的。其实不然,咱们再看看上面的测试代码:

//  示例 1
const items = ['','item2','item3', , undefined, null, 0];
items.forEach((item) => {console.log(item); //  顺次打印:'',item2,item3,undefined,null,0
});
items.forEachCustom((item) => {console.log(item); // 顺次打印:'',item2,item3,undefined,undefined,null,0
});
// 示例 2
let arr = new Array(8);
arr.forEach((item) => {console.log(item); //  无打印输出
});
arr[1] = 9;
arr[5] = 3;
arr.forEach((item) => {console.log(item); //  打印输出:9 3
});
arr.forEachCustom((item) => {console.log(item); // 打印输出:undefined 9 undefined*3  3 undefined*2
});

  咱们发现,forEachCustom 和原生的 forEach 在下面测试代码的执行后果并不相同。对于各个新个性的实现,其实咱们都能够在 ECMA 文档中找到答案:

  咱们能够发现,真正执行遍历操作的是第 8 条,通过一个 while 循环来实现,循环的终止条件是后面获取到的数组的长度(也就是说前期扭转数组长度不会影响遍历次数),while 循环里,会先把以后遍历项的下标转为字符串,通过 HasProperty 办法判断数组对象中是否有下标对应的已初始化的项,有的话,获取对应的值,执行回调,没有的话,不会执行回调函数,而是间接遍历下一项

  如此看来,forEach 不对未初始化的值进行任何操作(稠密数组),所以才会呈现示例 1 和示例 2 中自定义办法打印出的值和值的数量上均有差异的景象。那么,咱们只需对后面的实现稍加革新,即可实现一个本人的 forEach 办法:

Array.prototype.forEachCustom = function (fn, context) {context = context || arguments[1];
  if (typeof fn !== 'function') {throw new TypeError(fn + 'is not a function');
  }

  let len = this.length;
  let k = 0;
  while (k < len) {
    // 上面是两种实现思路,ECMA 文档应用的是 HasProperty,在此,应用 in 应该比 hasOwnProperty 更确切
    // if (this.hasOwnProperty(k)) {//   fn.call(context, this[k], k, this);
    // };
    if (k in this) {fn.call(context, this[k], k, this);
    }
    k++;
  }
};

  再次运行示例 1 和示例 2 的测试用列,发现输入和原生 forEach 统一。

  通过文档,咱们还发现,在迭代前 while 循环的次数就曾经定了,且执行了 while 循环,不代表就肯定会执行回调函数,咱们尝试在迭代时批改数组:

// 示例 3
var words = ['one', 'two', 'three', 'four'];
words.forEach(function (word) {console.log(word); // one,two,four(在迭代过程中删除元素,导致 three 被跳过,因为 three 的下标曾经变成 1,而下标为 1 的曾经被遍历了过)if (word === 'two') {words.shift();
  }
});
words = ['one', 'two', 'three', 'four']; // 从新初始化数组进行 forEachCustom 测试
words.forEachCustom(function (word) {console.log(word); // one,two,four
  if (word === 'two') {words.shift();
  }
});
// 示例 4
var arr = [1, 2, 3];
arr.forEach((item) => {if (item == 2) {arr.push(4);
    arr.push(5);
  }
  console.log(item); // 1,2,3(迭代过程中在开端减少元素,并不会使迭代次数减少)});
arr = [1, 2, 3];
arr.forEachCustom((item) => {if (item == 2) {arr.push(4);
    arr.push(5);
  }
  console.log(item); // 1,2,3
});

  以上过程启发咱们,在工作中碰见和咱们预期存在差别的问题时,咱们齐全能够去 ECMA 官网文档中寻求答案。

这里能够参考笔者之前的一篇文章:JavaScript 很简略?那你了解的 forEach 真的对吗?

第五十二式:Git 文件名大小写敏感问题,你栽过坑吗?

  笔者大概两年前刚用 Mac 开发前端时已经遇到一个坑:代码在本地运行 ok,然而发现 push 到 git,主动部署后报错了,排查了很久,最初发现有个文件名没有留神大小写,重命名了该文件,然而 git 没有辨认到这个更改,导致主动部署后找不到这个文件。解决办法如下:

  • 查看 git 的设置:git config –get core.ignorecase
  • git 默认是不辨别大小的,因而当你批改了文件名 / 文件夹的大小写后,git 并不会认为你有批改(git status 不会提醒你有批改)
  • 更改设置解决:git config core.ignorecase false

  这么以来,git 就能辨认到文件名大小写的更改了。在次倡议,平时咱们在应用 React 编写我的项目时,文件名最初放弃首字母大写。

参考:在 Git 中当更改一个文件名为首字母大写时

第五十三式:你看到的 0.1 其实并不是真的 0.1 —— 老成长谈的 0.1 + 0.2 !== 0.3,这次咱们说点不一样的

  0.1 + 0.2 !== 0.3是一个老成长谈的问题来,想必你也明确其中的本源:JS 采纳 IEEE 754 双精度版本(64 位),并且只有采纳 IEEE 754 的语言都有这样的问题。详情可查看笔者之前的一篇文章 0.1 + 0.2 != 0.3 背地的原理,本节咱们只探讨解法。

  • 既然 IEEE 754 存在精度问题,那为 什么 x=0.1 能失去 0.1

  因为在浮点数的存储中,mantissa(尾数)固定长度是 52 位,再加上省略的一位,最多能够示意的数是 2^53=9007199254740992,对应迷信计数尾数是 9.007199254740992,这也是 JS 最多能示意的精度。它的长度是 16,所以能够应用 toPrecision(16) 来做精度运算,超过的精度会主动做凑整解决。于是就有:

0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉开端的零后正好为 0.1

// 但你看到的 `0.1` 实际上并不是 `0.1`。不信你可用更高的精度试试:0.1.toPrecision(21) = 0.100000000000000005551
  • toFixed设置准确位数

  toFixed() 办法可把 Number 四舍五入为指定小数位数的数字,语法:NumberObject.toFixed(num)

// 保留两位小数
console.log((0.1 + 0.2).toFixed(2)); // 0.30
  • Number.EPSILON

  想必你还有印象,在高中数学或者大学数学分析、数值迫近中,在证实两个值相等的时候,咱们会让他们的差去迫近一个任意小的数。那么,在此天然能够想到让 0.1 + 0.2 的和减去 0.3 小于一个任意小的数,比如说咱们能够通过他们差值是否小于 0.0000000001 来判断他们是否相等。

  其实 ES6 曾经在 Number 对象下面,新增一个极小的常量 Number.EPSILON。依据规格,它示意 1 与大于 1 的最小浮点数之间的差。Number.EPSILON 实际上是 JavaScript 可能示意的最小精度。误差如果小于这个值,就能够认为曾经没有意义了,即不存在误差了。

console.log(0.1 + 0.2 - 0.3 < Number.EPSILON); // true
  • 转换成整数或者字符串再进行求和运算

  为了防止产生精度差别,咱们要把须要计算的数字乘以 10 的 n 次幂,换算成计算机可能准确辨认的整数,而后再除以 10 的 n 次幂,大部分编程语言都是这样解决精度差别的,咱们就借用过去解决一下 JS 中的浮点数精度误差。

传入 n 次幂的 n 值:

formatNum = function (f, digit) {var m = Math.pow(10, digit);
  return parseInt(f * m, 10) / m;
};
var num1 = 0.1;
var num2 = 0.2;
console.log(num1 + num2);
console.log(formatNum(num1 + num2, 1));

主动计算 n 次幂的 n 值:

/**
 * 准确加法
 */
function add(num1, num2) {const num1Digits = (num1.toString().split('.')[1] || '').length;
  const num2Digits = (num2.toString().split('.')[1] || '').length;
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
  return (num1 * baseNum + num2 * baseNum) / baseNum;
}
add(0.1,0.2); // 0.3
  • 应用类库:

  通常这种对精度要求高的计算都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算问题。前端也有几个不错的类库:

  • Math.js
  • decimal.js
  • big.js

参考资料:JavaScript 浮点数运算的精度问题 | JavaScript 浮点数陷阱及解法

第五十四式:发版揭示全靠吼 —— 如何纯前端实现页面检测更新并提醒?

  开发过程中,常常遇到页面更新、版本公布时,须要通知应用人员刷新页面的状况,甚至有些经营、测试人员感觉切换一下菜单再切回去就是更新了 web 页面资源,有的分不清一般刷新和强刷的区别,所以实现了一个页面更新检测性能,页面更新了主动提醒应用人员刷新页面。

  基本思路为:应用 webpack 配置打包编译时在 js 文件名里增加 hash,而后应用 js 向 ${window.location.origin}/index.html 发送申请,解析出 html 文件里引入的 js 文件名称 hash,比照以后 js 的 hash 与新版本的 hash 是否统一,不统一则提醒用户更新版本。

// uploadUtils.jsx
import React from 'react';
import axios from 'axios';
import {notification, Button} from 'antd';

// 弹窗是否已展现(能够改用闭包、单例模式等实现,看起来会更有逼格一点)let uploadNotificationShow = false;

// 敞开 notification
const close = () => {uploadNotificationShow = false;};

// 刷新页面
const onRefresh = (new_hash) => {close();
  // 更新 localStorage 版本号信息
  window.localStorage.setItem('XXXSystemFrontVesion', new_hash);
  // 刷新页面
  window.location.reload(true);
};

// 展现提醒弹窗
const openNotification = (new_hash) => {
  uploadNotificationShow = true;
  const btn = (<Button type='primary' size='small' onClick={() => onRefresh(new_hash)}>
      确认更新
    </Button>
  );
  // 这里不主动执行更新的起因是:// 思考到兴许此时用户正在应用零碎甚至填写一个很长的表单,那你间接刷新了页面,或者会被掐死的,哈哈
  notification.open({
    message: '版本更新提醒',
    description: '检测到零碎以后版本已更新,请刷新后应用。',
    btn,
    // duration 为 0 时,notification 不主动敞开
    duration: 0,
    onClose: close,
  });
};

// 获取 hash
export const getHash = () => {
  // 如果提醒弹窗已展现,就没必要执行接下来的查看逻辑了
  if (!uploadNotificationShow) {
    // 在 js 中申请首页地址,这样不会刷新界面,也不会跨域
    axios
      .get(`${window.location.origin}/index.html?time=${new Date().getTime()}`)
      .then((res) => {
        // 匹配 index.html 文件中引入的 js 文件是否变动(具体正则,视打包时的设置及文件门路而定)let new_hash = res.data && res.data.match(/\/static\/js\/main.(.*).js/);
        // console.log(res, new_hash);
        new_hash = new_hash ? new_hash[1] : null;
        // 查看本地版本
        let old_hash = localStorage.getItem('XXXSystemFrontVesion');
        if (!old_hash) {
          // 如果本地没有版本信息(第一次应用零碎),则间接执行一次额定的刷新逻辑
          onRefresh(new_hash);
        } else if (new_hash && new_hash != old_hash) {
          // 本地已有版本信息,然而和新版不同:版本更新,弹出提醒
          openNotification(new_hash);
        }
      });
  }
};

应用示例:

import {getHash} from './uploadUtils';

let timer = null;
componentDidMount() {getHash();
    timer = setInterval(() => {getHash();
      // 10 分钟检测一次
    }, 600000)
  }

  componentWillUnmount () {
      // 页面卸载时革除
    clearInterval(timer);
  }

  联合 Console Importer 间接在控制台面板查看:

  你也齐全能够在下面的办法上更上一层楼,build 的时候,在 index.html 同级目录下,主动生成一个 json 文件,蕴含新的文件的 hash 信息,查看版本的时候,就只需间接申请这个 json 文件进行比照了,缩小数据的冗余。

参考资料:纯前端实现页面检测更新提醒

本文首发于集体博客,欢送斧正和 star。

正文完
 0