JS基础知识学习总结(二)

126 阅读9分钟

函数式编程

函数式编程的特点

  1. 函数是一等公民
  2. 只用表达式,不用语句
  3. 没有副作用
  4. 引用透明(函数运行只靠参数)

纯函数

对于相同的输入,得到的一定是相同的输出,而且没有任何可以观察的副作用。

function Sum(a, b){
 return a + b
}

高阶函数

函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
常用的高阶函数

  • forEach
  • map
  • filter
  • every

函数柯里化

当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后不变),然后返回一个新的函数去接收处理剩余的参数,返回结果。

function checkByRegExp(regExp,str){
    return regExp.test(str)
}
=>
function checkByRegExp(regExp){
    return function(str){
        return regExp.test(str)
    }
}

某种意义上来讲,柯里化是一种对参数的“缓存”,是一种非常高效的编写函数的方法。

函数组合

为了解决函数嵌套的问题,避免写出洋葱式代码。

const reverseArr = x => x.reverse()
const first = x => x[0]
const compose = (f,g)=>(x=>f(g(x)))
const getLastValue = compose(first,reverseArr)
getLastValue([1,2,3,4,5])

函数组合需要满足结合律

//结合律 let f = compose (f,g,h)
compose(compose(f,g),h) === compose(f,compose(g,h)) // true

模块化

CommonJS 规范

通过上面的介绍,我们可以简单地了解到:CommonJS 是随着 JS 在服务端的发展而发展起来的,Node.js 中的模块系统就是参照 CommonJS 规范实现的。

CommonJS 规范的具体内容如下:

  • 一个文件就是一个模块;
  • module 对象代表模块自身,module 中有两个属性,require 和 export;
  • 使用 require(path) 方法引入外模模块,其中 path 可以是相对路径也可以是绝对路径;
  • 使用 export 对象作为唯一出口导出模块。

比如,我们在 index.js 中需要引入 a.js 文件中导出的模块 a,其代码如下:

// a.js
export.a = 'a';

// index.js
const moduleA = require('./a.js');
console.log(moduleA);  // {a: 'a'}

AMD

CommonJS 最开始是在服务端中使用,在浏览器环境中同样需要模块化规范。那么可不可以直接将 CommonJS 用于浏览器环境呢?

答案是否定的,因为 CommonJS 的设计思想是同步加载模块。在服务端,所有的模块都储存在本地硬盘,所以模块之间就算是同步加载的那么等待的时间也只是读取硬盘的时间,对于性能上的影响不大。

但是在浏览器环境就不一样了,浏览器环境下加载模块需要先将模块通过网络传输下载到本地,如果使用同步的方式加载模块的话,加载的时间就取决于所使用的设备、传输协议以及当时的网速。如果一个大型项目,所有的模块依赖都是同步的话,就会对性能产生有很大的影响。所以,在浏览器环境中,模块的加载只能是异步的。

AMD(Asynchronous Module Definition) 是 requireJS 在推广过程对模块定义的规范。使用异步加载而非同步加载的方式,模块在加载的时候就不会影响后面代码的执行。如果后面的代码依赖前面加载的内容,可以将其放在回调函数中,AMD 相比于 CommonJS更适合浏览器环境

AMD 规范的具体内容如下:

// ts声明
/**
 * @param {string} id 模块名称
 * @param {string[]} dependencies 模块所依赖模块的数组
 * @param {function} factory 模块初始化要执行的函数或对象
 * @return {any} 模块导出的接口
 */
function define(id?, dependencies?, factory): any

// 设置模块名称为 alpha,使用 require,exports,beta 为依赖的模块
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
   exports.verb = function() {
       return beta.verb();
       //Or:
       return require("beta").verb();
   }
});

define()是一个用来定义模块的全局函数,其中的 alpha 是模块名,["require", "exports", "beta"] 是模块的依赖。模块名和依赖都是可以省略的,在模块名省略的时候,会采用文件名作为模块名。比如:

// id 和 dependencies 被省略的情况。此时,id 就是文件名。
define(function (require, exports, module) {
	const a = require('a')
});

AMD 还有一个重要的设计思想就是依赖前置,AMD 会通过动态创建 script 标签的方式来异步加载模块,加载完成后立即执行该模块,只有当所有的依赖都加载并执行完之后才执行本模块,也就是说依赖的模块不管有没有使用,都会在运行时全量加载并执行(后面介绍的 CMD 则是先将文件缓存下来,在使用的时候才执行)。

CMD

CMD(Common Module Definition) 规范是在 SeaJs 推广过程中对模块定义的规范而产生的,也是一种在浏览器环境使用的异步模块化规范。CMD 更贴近于 CommonJS Modules/1.1 和 Node Modules 规范:

  • 一个文件就是一个模块;
  • 使用 define 定义模块;
  • require 方法用获取其他模块提供的接口。

刚刚也有说到,CMD 和 AMD 最大的不同在于:CMD 推崇依赖就近、延迟执行的原则。

CMD 规范的内容很简单:

define(factory)

其中,factory 可以是一个函数、一个对象或者是字符串。factory 为对象、字符串时,表示模块的接口就是该对象、字符串。如果为函数,则该函数表示的是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。

factory 默认有三个参数:require、exports、module

define(function(require, exports, module) {
  // 模块代码
});

到这里是不是有点眼熟呢?是不是和刚刚介绍的 AMD 规范有些类似?在 AMD 中,如果省略了模块名称 id 和模块依赖 deeps,那么是不是就和 CMD 一样了呀~~

// AMD 依赖必须一开始就写好
define("alpha", ["require", "exports", "beta"], function (require, exports, module) {
  // 模块代码
  exports.verb = function() {
     return beta.verb();
   }
});

// CMD 依赖可以就近书写
define(function(require, exports, module) {
  // 模块代码
  var beta = require('./beta');
  beta.verb();
});

虽然这只是写法上的略有不同,但是这实际上是设计思想上的不同。CMD 推崇的是一个文件一个模块,所以可以直接省略模块名称,使用文件名作为模块名称。CMD 又推崇依赖就近,所以不在 define 中写依赖,而是直接在 factory 中写。

ES Modules

终于来到了 ES Modules(下面简称 ESM)的时代了,ESM 是 JavaScript 官方突出的标准化模块系统。在 ES 2015(ES6)中,直接在语言标准层面上实现了模块的功能。并且是浏览器和服务端都支持的模块化解决方案。

ESM 规范很简单:export 用来导出模块接口,import 用来引入模块接口。export 可以直接导出也可以集中导出一个对象,只是写法不一样,实质是一样的。

// arithmetic.js
// 直接导出
export const add = () => {};
export const subtract = () => {};

// 集中导出一个对象
const add = () => {};
const subtract = () => {};

export {
  add,
  subtract,
}

import 可以整体导入模块也可以导入模块中的具体某一部分:

// 整体导入模块
import * from './add';

// 导入模块的某一部分
import { add } from './arithmetic.js';

在 ES2020 中,新引入了 import() 动态加载模块,该方法返回的是一个 Promise 对象,可以支持按需加载,大大提高了模块引用的灵活性。

function foo() {
  import('./config.js')
    .then(({ default }) => {
        default();
    });
}

ESM 导入模块是在编译阶段进行静态分析确定模块的依赖关系,并将 import 导入语句提升到模块首部,生成只读引用,链接到引入模块的 export 接口,所以,ESM import 导入的是值的引用

例如:我们在 b.js 文件中定义一个变量 b 值为 0,然后在 1s 之后将其值改为 1。然后我们在 a.js 文件中引入变量 b。在引入语句之后打印变量 b 的值,然后在 2s 之后再次打印 b 的值。可以看到打印的结果证明了导出的 b 为引用值。

// a.js
import { b } from './b.js';
console.log(b);
setTimeout(() => {
  console.log(b);
}, 2000);

// b.js
export let b = 0;
setTimeout(() => {
  b = 1;
}, 1000);

// 0
// 1

正是因为 ESM import 导入的是值的引用,所以在遇到循环依赖的时候,ESM 只有在真正需要用到的时候才会去模块中取值。因为需要在编译阶段进行静态分析,所以 import 的只能是字符串,不能是表达式和变量。并且导入的是单例模式,所以在依赖循环的时候,一个模块被多次导入,但是只会执行一次。

运行环境加载方式运行机制特点
CommonJS服务器同步运行时第一次加载后会将结果缓存,再次加载会读取缓存的结构。
AMD浏览器异步运行时依赖前置,不管模块是否有用到,都会全量加载。
CMD浏览器异步运行时依赖就近,延迟加载
ESM浏览器/服务端异步编译时静态化,在编译时就确定模块之间的依赖关系,输入和输出。
分类:
  • es6: import / export
  • commonjs: require / module.exports / exports
  • amd: require / defined

require与import的区别

  • require支持 动态导入,import不支持,正在提案 (babel 下可支持)
  • require是 同步 导入,import属于 异步 导入
  • require是 值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化

前端错误监控

错误监控包含的内容是:

  • 前端错误的分类
  • 每种错误的捕获方式
  • 上报错误的基本原理

面试时,可能有两种问法:

  • 如何监测 js 错误?(开门见山的方式)
  • 如何保证产品质量?(其实问的也是错误监控)

前端错误的分类

包括两种:

  • 即时运行错误(代码错误)
  • 资源加载错误

每种错误的捕获方式

即时运行错误的捕获方式

方式1try ... catch

这种方式要部署在代码中。

方式2: window.onerror函数。这个函数是全局的。

	window.onerror = function(msg, url, row, col, error) { ... }

参数解释:

  • msg为异常基本信息
  • source为发生异常Javascript文件的url
  • row为发生错误的行号

方式二中的window.onerror是属于DOM0的写法,我们也可以用DOM2的写法:window.addEventListener("error", fn);也可以。

问题延伸1:

window.onerror默认无法捕获跨域js运行错误。捕获出来的信息如下:(基本属于无效信息)

比如说,我们的代码想引入B网站的b.js文件,怎么捕获它的异常呢?

解决办法:在方法二的基础之上,做如下操作:

  1. b.js文件里,加入如下 response header,表示允许跨域:(或者世界给静态资源b.js加这个 response header)
	Access-Control-Allow-Origin: *
  1. 引入第三方的文件b.js时,在<script>标签中增加crossorigin属性;

问题延伸2:

只靠方式二中的window.onerror是不够的,因为我们无法获取文件名是什么,不知道哪里出了错误。解决办法:把堆栈信息作为msg打印出来,堆栈里很详细。

资源加载错误的捕获方式

上面的window.onerror只能捕获即时运行错误,无法捕获资源加载错误。原理是:资源加载错误,并不会向上冒泡,object.onerror捕获后就会终止(不会冒泡给window),所以window.onerror并不能捕获资源加载错误。

  • 方式1object.onerrorimg标签、script标签等节点都可以添加onerror事件,用来捕获资源加载的错误。
  • 方式2:performance.getEntries。可以获取所有已加载资源的加载时长,通过这种方式,可以间接的拿到没有加载的资源错误。

举例:

浏览器打开一个网站,在Console控制台下,输入:

	performance.getEntries().forEach(function(item){console.log(item.name)})

或者输入:

	performance.getEntries().forEach(item=>{console.log(item.name)})

上面这个api,返回的是数组,既然是数组,就可以用forEach遍历。打印出来的资源就是已经成功加载的资源。;

再入document.getElementsByTagName('img'),就会显示出所有需要加载的的img集合。

于是,document.getElementsByTagName('img')获取的资源数组减去通过performance.getEntries()获取的资源数组,剩下的就是没有成功加载的,这种方式可以间接捕获到资源加载错误。

这种方式非常有用,一定要记住。

**方式3;**Error事件捕获。

源加载错误,虽然会阻止冒泡,但是不会阻止捕获。我们可以在捕获阶段绑定error事件。例如:

**总结:**如果我们能回答出后面的两种方式,面试官对我们的印象会大大增加。既可以体现出我们对错误监控的了解,还可以体现出我们对事件模型的掌握。

错误上报的两种方式

  • 方式一:采用Ajax通信的方式上报(此方式虽然可以上报错误,但是我们并不采用这种方式)
  • **方式二:**利用Image对象上报(推荐。网站的监控体系都是采用的这种方式)

方式二的实现方式如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script>
	//通过Image对象进行错误上报
    (new Image()).src = 'http://blog.com/myPath?badjs=msg';   // myPath表示上报的路径(我要上报到哪里去)。后面的内容是自己加的参数。
</script>
</body>
</html>

打开浏览器,效果如下:

上图中,红色那一栏表明,我的请求已经发出去了。点进去看看:

这种方式,不需要借助第三方的库,一行代码即可搞定。