ES6 速通(下)

本文涉及的产品
可视分析地图(DataV-Atlas),3 个项目,100M 存储空间
数据可视化DataV,5个大屏 1个月
简介: ES6 速通(下)

ES6 速通(中)

23. Module的语法

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

  1. // ES6模块
  2. import { stat, exists, readFile } from 'fs';

下面是import()的一些适用场合。

(1)按需加载。

import()可以在需要的时候,再加载某个模块。

  1. button.addEventListener('click', event => {
  2. import('./dialogBox.js')
  3. .then(dialogBox => {
`dialogBox.open();`
  1. })
  2. .catch(error => {
`/* Error handling */`
  1. })
  2. });

上面代码中,import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。

(2)条件加载

import()可以放在if代码块,根据不同的情况,加载不同的模块。

  1. if (condition) {
  2. import('moduleA').then(...);
  3. } else {
  4. import('moduleB').then(...);
  5. }

上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。

(3)动态的模块路径

import()允许模块路径动态生成。

  1. import(f())
  2. .then(...);

上面代码中,根据函数f的返回结果,加载不同的模块。

注意点

import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

  1. import('./myModule.js')
  2. .then(({export1, export2}) => {
  3. // ...·
  4. });

上面代码中,export1export2都是myModule.js的输出接口,可以解构获得。

如果模块有default输出接口,可以用参数直接获得。

  1. import('./myModule.js')
  2. .then(myModule => {
  3. console.log(myModule.default);
  4. });

上面的代码也可以使用具名输入的形式。

  1. import('./myModule.js')
  2. .then(({default: theDefault}) => {
  3. console.log(theDefault);
  4. });

如果想同时加载多个模块,可以采用下面的写法。

  1. Promise.all([
  2. import('./module1.js'),
  3. import('./module2.js'),
  4. import('./module3.js'),
  5. ])
  6. .then(([module1, module2, module3]) => {
  7. ···
  8. });

import()也可以用在 async 函数之中。

  1. async function main() {
  2. const myModule = await import('./myModule.js');
  3. const {export1, export2} = await import('./myModule.js');
  4. const [module1, module2, module3] =
`await Promise.all([`
`import('./module1.js'),`
`import('./module2.js'),`
`import('./module3.js'),`
`]);`
  1. }
  2. main();
严格模式:

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。

其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

export:

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

  1. // profile.js
  2. export var firstName = 'Michael';
  3. export var lastName = 'Jackson';
  4. export var year = 1958;

上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。

export的写法,除了像上面这样,还有另外一种。

  1. // profile.js
  2. var firstName = 'Michael';
  3. var lastName = 'Jackson';
  4. var year = 1958;
  5. export { firstName, lastName, year };
  6. function v1() { ... }
  7. function v2() { ... }
  8. export {
  9. v1 as streamV1,
  10. v2 as streamV2,
  11. v2 as streamLatestVersion
  12. };
  13. // 写法一
  14. export var m = 1;
  15. // 写法二
  16. var m = 1;
  17. export {m};
  18. // 写法三
  19. var n = 1;
  20. export {n as m};
  21. // 报错
  22. function f() {}
  23. export f;
  24. // 正确
  25. export function f() {};
  26. // 正确
  27. function f() {}
  28. export {f};

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

  1. function foo() {
  2. export default 'bar' // SyntaxError
  3. }
  4. foo()
    上面代码中,export语句放在函数之中,结果报错。
  5. // main.js
  6. import { firstName, lastName, year } from './profile.js';
  7. function setName(element) {
  8. element.textContent = firstName + ' ' + lastName;
  9. }

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

  1. import { lastName as surname } from './profile.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

  1. import {a} from './xxx.js'
  2. a = {}; // Syntax Error : 'a' is read-only;

上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

  1. import {a} from './xxx.js'
  2. a.foo = 'hello'; // 合法操作

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

  1. // 报错
  2. import { 'f' + 'oo' } from 'my_module';
  3. // 报错
  4. let module = 'my_module';
  5. import { foo } from module;
  6. // 报错
  7. if (x === 1) {
  8. import { foo } from 'module1';
  9. } else {
  10. import { foo } from 'module2';
  11. }
  12. import * as circle from './circle';
  13. console.log('圆面积:' + circle.area(4));
  14. console.log('圆周长:' + circle.circumference(14));

注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。

  1. import * as circle from './circle';
  2. // 下面两行都是不允许的
  3. circle.foo = 'hello';
  4. circle.area = function () {};

export default:

  1. // modules.js
  2. function add(x, y) {
  3. return x * y;
  4. }
  5. export {add as default};
  6. // 等同于
  7. // export default add;
  8. // app.js
  9. import { default as foo } from 'modules';
  10. // 等同于
  11. // import foo from 'modules';

有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例。

  1. import _ from 'lodash';

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

  1. import _, { each, forEach } from 'lodash';

export 与 import 的复合写法

  1. export { foo, bar } from 'my_module';
  2. // 可以简单理解为
  3. import { foo, bar } from 'my_module';
  4. export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

  1. // 接口改名
  2. export { foo as myFoo } from 'my_module';
  3. // 整体输出
  4. export * from 'my_module';
  5. // 默认接口
  6. export { default } from 'foo';

具名接口改为默认接口的写法如下。

  1. export { es6 as default } from './someModule';
  2. // 等同于
  3. import { es6 } from './someModule';
  4. export default es6;

模块的继承:

  1. // circleplus.js
  2. export * from 'circle';
  3. export var e = 2.71828182846;
  4. export default function(x) {
  5. return Math.exp(x);
  6. }

上面代码中的export *,表示再输出circle模块的所有属性和方法。注意,export *命令会忽略circle模块的default方法。然后,上面代码又输出了自定义的e变量和默认方法。

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。

  1. // constants/db.js
  2. export const db = {
  3. url: 'http://my.couchdbserver.local:5984',
  4. admin_username: 'admin',
  5. admin_password: 'admin password'
  6. };
  7. // constants/user.js
  8. export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

24. Module 的加载实现

HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。

  1. <!-- 页面内嵌的脚本 -->
  2. <script type="application/javascript">
  3. // module code
  4. </script>
  5. <!-- 外部脚本 -->
  6. <script type="application/javascript" src="path/to/myModule.js">
  7. </script>

上面代码中,由于浏览器脚本的默认语言是 JavaScript,因此type="application/javascript"可以省略。

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。

如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。

  1. <script src="path/to/myModule.js" defer></script>
  2. <script src="path/to/myModule.js" async></script>

deferasync的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

加载规则

浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。

  1. <script type="module" src="./foo.js"></script>

上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。

浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

  1. <script type="module">
  2. import $ from "./jquery/src/jquery.js";
  3. $('#message').text('Hi from jQuery!');
  4. </script>

对于外部的模块脚本(上例是foo.js),有几点需要注意。

  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
  • 同一个模块如果加载多次,将只执行一次。

下面是一个示例模块。

  1. import utils from 'https://example.com/js/utils.js';
  2. const x = 1;
  3. console.log(x === window.x); //false
  4. console.log(this === undefined); // true

利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。

  1. const isNotModuleScript = this !== undefined;

讨论 Node.js 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。

它们有两个重大差异。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。

  1. // lib.js
  2. var counter = 3;
  3. function incCounter() {
  4. counter++;
  5. }
  6. module.exports = {
  7. counter: counter,
  8. incCounter: incCounter,
  9. };

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。

  1. // main.js
  2. var mod = require('./lib');
  3. console.log(mod.counter); // 3
  4. mod.incCounter();
  5. console.log(mod.counter); // 3

上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

Node.js 加载

Node.js 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"

如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module

  1. {
  2. "type": "module"
  3. }

一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。

  1. # 解释成 ES6 模块
  2. $ node my-app.js

如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。

总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。

注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import

25. 编程风格

尽量不要使用var,而是使用let和const,在let和const之间优选使用const

静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。

使用数组成员对变量赋值时,优先使用解构赋值。

单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。

对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。

使用扩展运算符(…)拷贝数组。

使用 Array.from 方法,将类似数组的对象转为数组。

立即执行函数可以写成箭头函数的形式。

那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。

注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。

  1. let map = new Map(arr);
  2. for (let key of map.keys()) {
  3. console.log(key);
  4. }
  5. for (let value of map.values()) {
  6. console.log(value);
  7. }
  8. for (let item of map.entries()) {
  9. console.log(item[0], item[1]);
  10. }

总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。

使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。

首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require

使用export取代module.exports

如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export defaultexport default与普通的export不要同时使用。

不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。

  1. // bad
  2. import * as myObject from './importModule';
  3. // good
  4. import myObject from './importModule';

如果模块默认输出一个函数,函数名的首字母应该小写。

如果模块默认输出一个对象,对象名的首字母应该大写。

语法规则和代码风格的检查工具

ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。

首先,安装 ESLint。

  1. $ npm i -g eslint

然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。

  1. $ npm i -g eslint-config-airbnb
  2. $ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react

最后,在项目的根目录下新建一个.eslintrc文件,配置 ESLint。

  1. {
  2. "extends": "eslint-config-airbnb"
  3. }

26. 读懂规格

  1. Let O be ToObject(this value).
  2. ReturnIfAbrupt(O).
  3. Let len be ToLength(Get(O, "length")).
  4. ReturnIfAbrupt(len).
  5. If IsCallable(callbackfn) is false, throw a TypeError exception.
  6. If thisArg was supplied, let T be thisArg; else let T be undefined.
  7. Let A be ArraySpeciesCreate(O, len).
  8. ReturnIfAbrupt(A).
  9. Let k be 0.
  10. Repeat, whilek<len
  1. Let Pk be ToString(k).
  2. Let kPresent be HasProperty(O, Pk).
  3. ReturnIfAbrupt(kPresent).
  4. IfkPresentistrue, then
  1. Let kValue be Get(O, Pk).
  2. ReturnIfAbrupt(kValue).
  3. Let mappedValue be Call(callbackfn, T, «kValue, k, O»).
  4. ReturnIfAbrupt(mappedValue).
  5. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue).
  6. ReturnIfAbrupt(status).
  1. Increase k by 1.
  1. Return A.

翻译如下。

  1. 得到当前数组的this对象
  2. 如果报错就返回
  3. 求出当前数组的length属性
  4. 如果报错就返回
  5. 如果 map 方法的参数callbackfn不可执行,就报错
  6. 如果 map 方法的参数之中,指定了this,就让T等于该参数,否则Tundefined
  7. 生成一个新的数组A,跟当前数组的length属性保持一致
  8. 如果报错就返回
  9. 设定k等于 0
  10. 只要k小于当前数组的length属性,就重复下面步骤
  1. 设定Pk等于ToString(k),即将K转为字符串
  2. 设定kPresent等于HasProperty(O, Pk),即求当前数组有没有指定属性
  3. 如果报错就返回
  4. 如果kPresent等于true,则进行下面步骤
  1. 设定kValue等于Get(O, Pk),取出当前数组的指定属性
  2. 如果报错就返回
  3. 设定mappedValue等于Call(callbackfn, T, «kValue, k, O»),即执行回调函数
  4. 如果报错就返回
  5. 设定status等于CreateDataPropertyOrThrow (A, Pk, mappedValue),即将回调函数的值放入A数组的指定位置
  6. 如果报错就返回
  1. k增加 1
  1. 返回A

仔细查看上面的算法,可以发现,当处理一个全是空位的数组时,前面步骤都没有问题。进入第 10 步中第 2 步时,kPresent会报错,因为空位对应的属性名,对于数组来说是不存在的,因此就会返回,不会进行后面的步骤。

27. 异步遍历器

将异步操作包装成 Thunk 函数或者 Promise 对象,即next()方法返回值的value属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而done属性则还是同步产生的。

  1. function idMaker() {
  2. let index = 0;
  3. return {
`next: function() {`
`return {`
`value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)),`
`done: false`
`};`
`}`
  1. };
  2. }
  3. const it = idMaker();
  4. it.next().value.then(o => console.log(o)) // 1
  5. it.next().value.then(o => console.log(o)) // 2
  6. it.next().value.then(o => console.log(o)) // 3
  7. // ...

上面代码中,value属性的返回值是一个 Promise 对象,用来放置异步操作。但是这样写很麻烦,不太符合直觉,语义也比较绕。

asyncIterator是一个异步遍历器,调用next方法以后,返回一个 Promise 对象。因此,可以使用then方法指定,这个 Promise 对象的状态变为resolve以后的回调函数。回调函数的参数,则是一个具有valuedone两个属性的对象,这个跟同步遍历器是一样的。

我们知道,一个对象的同步遍历器的接口,部署在Symbol.iterator属性上面。同样地,对象的异步遍历器接口,部署在Symbol.asyncIterator属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator属性有值,就表示应该对它进行异步遍历。

  1. const asyncIterable = createAsyncIterable(['a', 'b']);
  2. const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  3. asyncIterator
  4. .next()
  5. .then(iterResult1 => {
  6. console.log(iterResult1); // { value: 'a', done: false }
  7. return asyncIterator.next();
  8. })
  9. .then(iterResult2 => {
  10. console.log(iterResult2); // { value: 'b', done: false }
  11. return asyncIterator.next();
  12. })
  13. .then(iterResult3 => {
  14. console.log(iterResult3); // { value: undefined, done: true }
  15. });
  16. async function f() {
  17. const asyncIterable = createAsyncIterable(['a', 'b']);
  18. const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  19. console.log(await asyncIterator.next());
  20. // { value: 'a', done: false }
  21. console.log(await asyncIterator.next());
  22. // { value: 'b', done: false }
  23. console.log(await asyncIterator.next());
  24. // { value: undefined, done: true }
  25. }

面代码中,next方法用await处理以后,就不必使用then方法了。整个流程已经很接近同步处理了。

注意,异步遍历器的next方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve以后再调用。这种情况下,next方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的next方法放在Promise.all方法里面。

  1. const asyncIterable = createAsyncIterable(['a', 'b']);
  2. const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  3. const [{value: v1}, {value: v2}] = await Promise.all([
  4. asyncIterator.next(), asyncIterator.next()
  5. ]);
  6. console.log(v1, v2); // a b

另一种用法是一次性调用所有的next方法,然后await最后一步操作。

  1. async function runner() {
  2. const writer = openFile('someFile.txt');
  3. writer.next('hello');
  4. writer.next('world');
  5. await writer.return();
  6. }
  7. runner();

createAsyncIterable()返回一个拥有异步遍历器接口的对象,for...of循环自动调用这个对象的异步遍历器的next方法,会得到一个 Promise 对象。await用来处理这个 Promise 对象,一旦resolve,就把得到的值(x)传入for...of的循环体。

for await...of循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。

  1. let body = '';
  2. async function f() {
  3. for await(const data of req) body += data;
  4. const parsed = JSON.parse(body);
  5. console.log('got', parsed);
  6. }

上面代码中,req是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用for await...of循环以后,代码会非常简洁。

如果next方法返回的 Promise 对象被rejectfor await...of就会报错,要用try...catch捕捉。

  1. async function () {
  2. try {
`for await (const x of createRejectingIterable()) {`
`console.log(x);`
`}`
  1. } catch (e) {
`console.error(e);`
  1. }
  2. }

注意,for await...of循环也可以用于同步遍历器。

  1. (async function () {
  2. for await (const x of ['a', 'b']) {
`console.log(x);`
  1. }
  2. })();
  3. // a
  4. // b

异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。

  1. // 同步 Generator 函数
  2. function* map(iterable, func) {
  3. const iter = iterable[Symbol.iterator]();
  4. while (true) {
`const {value, done} = iter.next();`
`if (done) break;`
`yield func(value);`
  1. }
  2. }
  3. // 异步 Generator 函数
  4. async function* map(iterable, func) {
  5. const iter = iterable[Symbol.asyncIterator]();
  6. while (true) {
`const {value, done} = await iter.next();`
`if (done) break;`
`yield func(value);`
  1. }
  2. }

yield*语句也可以跟一个异步遍历器。

  1. async function* gen1() {
  2. yield 'a';
  3. yield 'b';
  4. return 2;
  5. }
  6. async function* gen2() {
  7. // result 最终会等于 2
  8. const result = yield* gen1();
  9. }

28. ArrayBuffer

二进制数组由三类对象组成。

(1)ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。

(2)TypedArray视图:共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。

(3)DataView视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。

简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。

TypedArray视图支持的数据类型一共有 9 种(DataView视图支持除Uint8C以外的其他 8 种)。

数据类型 字节长度 含义 对应的 C 语言类型
Int8 1 8 位带符号整数 signed char
Uint8 1 8 位不带符号整数 unsigned char
Uint8C 1 8 位不带符号整数(自动过滤溢出) unsigned char
Int16 2 16 位带符号整数 short
Uint16 2 16 位不带符号整数 unsigned short
Int32 4 32 位带符号整数 int
Uint32 4 32 位不带符号的整数 unsigned int
Float32 4 32 位浮点数 float
Float64 8 64 位浮点数 double

ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。

ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。

  1. const buf = new ArrayBuffer(32);

上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。

为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。

  1. const buf = new ArrayBuffer(32);
  2. const dataView = new DataView(buf);
  3. dataView.getUint8(0) // 0

上面代码对一段 32 字节的内存,建立DataView视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0,因为原始内存的ArrayBuffer对象,默认所有位都是 0。

另一种TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。

  1. const buffer = new ArrayBuffer(12);
  2. const x1 = new Int32Array(buffer);
  3. x1[0] = 1;
  4. const x2 = new Uint8Array(buffer);
  5. x2[0] = 2;
  6. x1[0] // 2

TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。

  1. const typedArray = new Uint8Array([0,1,2]);
  2. typedArray.length // 3
  3. typedArray[0] = 5;
  4. typedArray // [5, 1, 2]

ArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。

  1. const buffer = new ArrayBuffer(32);
  2. buffer.byteLength
  3. // 32

ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。

  1. const buffer = new ArrayBuffer(8);
  2. const newBuffer = buffer.slice(0, 3);

ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。

  1. const buffer = new ArrayBuffer(8);
  2. ArrayBuffer.isView(buffer) // false
  3. const v = new Int32Array(buffer);
  4. ArrayBuffer.isView(v) // true

普通数组的操作方法和属性,对 TypedArray 数组完全适用。

  • TypedArray.prototype.copyWithin(target, start[, end = this.length])
  • TypedArray.prototype.entries()
  • TypedArray.prototype.every(callbackfn, thisArg?)
  • TypedArray.prototype.fill(value, start=0, end=this.length)
  • TypedArray.prototype.filter(callbackfn, thisArg?)
  • TypedArray.prototype.find(predicate, thisArg?)
  • TypedArray.prototype.findIndex(predicate, thisArg?)
  • TypedArray.prototype.forEach(callbackfn, thisArg?)
  • TypedArray.prototype.indexOf(searchElement, fromIndex=0)
  • TypedArray.prototype.join(separator)
  • TypedArray.prototype.keys()
  • TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
  • TypedArray.prototype.map(callbackfn, thisArg?)
  • TypedArray.prototype.reduce(callbackfn, initialValue?)
  • TypedArray.prototype.reduceRight(callbackfn, initialValue?)
  • TypedArray.prototype.reverse()
  • TypedArray.prototype.slice(start=0, end=this.length)
  • TypedArray.prototype.some(callbackfn, thisArg?)
  • TypedArray.prototype.sort(comparefn)
  • TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
  • TypedArray.prototype.toString()
  • TypedArray.prototype.values()

复合视图:

由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。

  1. const buffer = new ArrayBuffer(24);
  2. const idView = new Uint32Array(buffer, 0, 1);
  3. const usernameView = new Uint8Array(buffer, 4, 16);
  4. const amountDueView = new Float32Array(buffer, 20, 1);

DataView视图本身也是构造函数,接受一个ArrayBuffer对象作为参数,生成视图。

  1. new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);

下面是一个例子。

  1. const buffer = new ArrayBuffer(24);
  2. const dv = new DataView(buffer);

DataView实例有以下属性,含义与TypedArray实例的同名方法相同。

  • DataView.prototype.buffer:返回对应的 ArrayBuffer 对象
  • DataView.prototype.byteLength:返回占据的内存字节长度
  • DataView.prototype.byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始

DataView实例提供 8 个方法读取内存。

  • getInt8:读取 1 个字节,返回一个 8 位整数。
  • getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
  • getInt16:读取 2 个字节,返回一个 16 位整数。
  • getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
  • getInt32:读取 4 个字节,返回一个 32 位整数。
  • getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
  • getFloat32:读取 4 个字节,返回一个 32 位浮点数。
  • getFloat64:读取 8 个字节,返回一个 64 位浮点数。

arraybuffer的应用:

传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为textXMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob

  1. let xhr = new XMLHttpRequest();
  2. xhr.open('GET', someUrl);
  3. xhr.responseType = 'arraybuffer';
  4. xhr.onload = function () {
  5. let arrayBuffer = xhr.response;
  6. // ···
  7. };
  8. xhr.send();

网页Canvas元素输出的二进制像素数据,就是 TypedArray 数组。

  1. const canvas = document.getElementById('myCanvas');
  2. const ctx = canvas.getContext('2d');
  3. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  4. const uint8ClampedArray = imageData.data;

ES2017 引入SharedArrayBuffer,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer的 API 与ArrayBuffer一模一样,唯一的区别是后者无法共享数据。

  1. // 主线程
  2. // 新建 1KB 共享内存
  3. const sharedBuffer = new SharedArrayBuffer(1024);
  4. // 主线程将共享内存的地址发送出去
  5. w.postMessage(sharedBuffer);
  6. // 在共享内存上建立视图,供写入数据
  7. const sharedArray = new Int32Array(sharedBuffer);

上面代码中,postMessage方法的参数是SharedArrayBuffer对象。

Worker 线程从事件的data属性上面取到数据。

  1. // Worker 线程
  2. onmessage = function (ev) {
  3. // 主线程共享的数据,就是 1KB 的共享内存
  4. const sharedBuffer = ev.data;
  5. // 在共享内存上建立视图,方便读写
  6. const sharedArray = new Int32Array(sharedBuffer);
  7. // ...
  8. };

共享内存也可以在 Worker 线程创建,发给主线程。

SharedArrayBufferArrayBuffer一样,本身是无法读写的,必须在上面建立视图,然后通过视图读写。

  1. // 分配 10 万个 32 位整数占据的内存空间
  2. const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
  3. // 建立 32 位整数视图
  4. const ia = new Int32Array(sab); // ia.length == 100000
  5. // 新建一个质数生成器
  6. const primes = new PrimeGenerator();
  7. // 将 10 万个质数,写入这段内存空间
  8. for ( let i=0 ; i < ia.length ; i++ )
  9. ia[i] = primes.next();
  10. // 向 Worker 线程发送这段共享内存
  11. w.postMessage(ia);

Worker 线程收到数据后的处理如下。

  1. // Worker 线程
  2. let ia;
  3. onmessage = function (ev) {
  4. ia = ev.data;
  5. console.log(ia.length); // 100000
  6. console.log(ia[37]); // 输出 163,因为这是第38个质数
  7. };

多线程共享内存,最大的问题就是如何防止两个线程同时修改某个地址,或者说,当一个线程修改共享内存以后,必须有一个机制让其他线程同步。SharedArrayBuffer API 提供Atomics对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步。

共享内存上面的某些运算是不能被打断的,即不能在运算过程中,让其他线程改写内存上面的值。Atomics 对象提供了一些运算方法,防止数据被改写。

  1. Atomics.add(sharedArray, index, value)

Atomics.add用于将value加到sharedArray[index],返回sharedArray[index]旧的值。

  1. Atomics.sub(sharedArray, index, value)

Atomics.sub用于将valuesharedArray[index]减去,返回sharedArray[index]旧的值。

  1. Atomics.and(sharedArray, index, value)

Atomics.and用于将valuesharedArray[index]进行位运算and,放入sharedArray[index],并返回旧的值。

  1. Atomics.or(sharedArray, index, value)

Atomics.or用于将valuesharedArray[index]进行位运算or,放入sharedArray[index],并返回旧的值。

  1. Atomics.xor(sharedArray, index, value)

Atomic.xor用于将vaulesharedArray[index]进行位运算xor,放入sharedArray[index],并返回旧的值。

(5)其他方法

Atomics对象还有以下方法。

  • Atomics.compareExchange(sharedArray, index, oldval, newval):如果sharedArray[index]等于oldval,就写入newval,返回oldval
  • Atomics.isLockFree(size):返回一个布尔值,表示Atomics对象是否可以处理某个size的内存锁定。如果返回false,应用程序就需要自己来实现锁定。

Atomics.compareExchange的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。

29. 最新提案

do 表达式

  1. // 等同于 <表达式>
  2. do { <表达式>; }
  3. // 等同于 <语句>
  4. do { <语句> }

do表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。

  1. let x = do {
  2. if (foo()) { f() }
  3. else if (bar()) { g() }
  4. else { h() }
  5. };

开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。现在有一个提案,为 import 命令添加了一个元属性import.meta,返回当前模块的元信息。

import.meta只能在模块内部使用,如果在模块外部使用会报错。

这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说,import.meta至少会有下面两个属性。

(1)import.meta.url

import.meta.url返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是https://foo.com/main.jsimport.meta.url就返回这个路径。如果模块里面还有一个数据文件data.txt,那么就可以用下面的代码,获取这个数据文件的路径。

  1. new URL('data.txt', import.meta.url)

注意,Node.js 环境中,import.meta.url返回的总是本地路径,即是file:URL协议的字符串,比如file:///home/user/foo.js

(2)import.meta.scriptElement

import.meta.scriptElement是浏览器特有的元属性,返回加载模块的那个<script>元素,相当于document.currentScript属性。

  1. // HTML 代码为
  2. // <script type="module" src="my-module.js" data-foo="abc"></script>
  3. // my-module.js 内部执行下面的代码
  4. import.meta.scriptElement.dataset.foo
  5. // "abc"

函数的部分执行有一些特别注意的地方。

(1)函数的部分执行是基于原函数的。如果原函数发生变化,部分执行生成的新函数也会立即反映这种变化。

(2)如果预先提供的那个值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值。

(3)如果新函数的参数多于占位符的数量,那么多余的参数将被忽略。

(4)...只会被采集一次,如果函数的部分执行使用了多个...,那么每个...的值都将相同。

JavaScript 的管道是一个运算符,写作|>。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。

  1. x |> f
  2. // 等同于
  3. f(x)

数值分割:

  1. 123_00 === 12_300 // true
  2. 12345_00 === 123_4500 // true
  3. 12345_00 === 1_234_500 // true

数值分隔符有几个使用注意点。

  • 不能在数值的最前面(leading)或最后面(trailing)。
  • 不能两个或两个以上的分隔符连在一起。
  • 小数点的前后不能有分隔符。
  • 科学计数法里面,表示指数的eE前后不能有分隔符。

Math.sign()用来判断一个值的正负,但是如果参数是-0,它会返回-0

  1. Math.sign(-0) // -0
  2. Math.signbit(2) //false
  3. Math.signbit(-2) //true
  4. Math.signbit(0) //false
  5. Math.signbit(-0) //true
双冒号运算符

箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(callapplybind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代callapplybind调用。

函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

  1. foo::bar;
  2. // 等同于
  3. bar.bind(foo);
  4. foo::bar(...arguments);
  5. // 等同于
  6. bar.apply(foo, arguments);
  7. const hasOwnProperty = Object.prototype.hasOwnProperty;
  8. function hasOwn(obj, key) {
  9. return obj::hasOwnProperty(key);
  10. }

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

  1. var method = obj::obj.foo;
  2. // 等同于
  3. var method = ::obj.foo;
  4. let log = ::console.log;
  5. // 等同于
  6. var log = console.log.bind(console);

如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。

  1. import { map, takeWhile, forEach } from "iterlib";
  2. getPlayers()
  3. ::map(x => x.character())
  4. ::takeWhile(x => x.strength > 100)
  5. ::forEach(x => console.log(x));

30. Decorator

装饰器可以用来装饰整个类。

  1. function testable(isTestable) {
  2. return function(target) {
`target.isTestable = isTestable;`
  1. }
  2. }
  3. @testable(true)
  4. class MyTestableClass {}
  5. MyTestableClass.isTestable // true
  6. @testable(false)
  7. class MyClass {}
  8. MyClass.isTestable // false

上面代码中,@testable就是一个装饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestabletestable函数的参数targetMyTestableClass类本身。

装饰器不仅可以装饰类,还可以装饰类的属性。

  1. class Person {
  2. @readonly
  3. name() { return `${this.first} ${this.last}` }
  4. }

上面代码中,装饰器readonly用来装饰“类”的name方法。

装饰器函数readonly一共可以接受三个参数。

  1. function readonly(target, name, descriptor){
  2. // descriptor对象原来的值如下
  3. // {
  4. // value: specifiedFunction,
  5. // enumerable: false,
  6. // configurable: true,
  7. // writable: true
  8. // };
  9. descriptor.writable = false;
  10. return descriptor;
  11. }
  12. readonly(Person.prototype, 'name', descriptor);
  13. // 类似于
  14. Object.defineProperty(Person.prototype, 'name', descriptor);

装饰器第一个参数是类的原型对象,上例是Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);第二个参数是所要装饰的属性名,第三个参数是该属性的描述对象。

另外,上面代码说明,装饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。

装饰器只适用于类和类的方法,并不适用于函数

core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。

autobind装饰器使得方法中的this对象,绑定原始对象。

readonly装饰器使得属性或方法不可写。

override装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。

deprecatedeprecated装饰器在控制台显示一条警告,表示该方法将废除。

suppressWarnings装饰器抑制deprecated装饰器导致的console.warn()调用。但是,异步代码发出的调用除外。

在装饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。

方法一:

  1. const Foo = {
  2. foo() { console.log('foo') }
  3. };
  4. class MyClass {}
  5. Object.assign(MyClass.prototype, Foo);
  6. let obj = new MyClass();
  7. obj.foo() // 'foo'

方法二:

部署一个通用脚本mixins.js,将 Mixin 写成一个装饰器。

  1. export function mixins(...list) {
  2. return function (target) {
`Object.assign(target.prototype, ...list);`
  1. };
  2. }

然后,就可以使用上面这个装饰器,为类“混入”各种方法。

  1. import { mixins } from './mixins';
  2. const Foo = {
  3. foo() { console.log('foo') }
  4. };
  5. @mixins(Foo)
  6. class MyClass {}
  7. let obj = new MyClass();
  8. obj.foo() // "foo"

Trait 也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。

下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits装饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。

  1. import { traits } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') }
  7. };
  8. @traits(TFoo, TBar)
  9. class MyClass { }
  10. let obj = new MyClass();
  11. obj.foo() // foo
  12. obj.bar() // bar

31. 参考链接

官方文件
综合介绍
let 和 const
解构赋值
字符串
正则
数值
数组
函数
对象
Symbol
Set 和 Map
Proxy 和 Reflect
Promise 对象
Iterator
Generator
异步操作和 Async 函数
Class
Decorator
Module
二进制数组
SIMD
工具

32. Mixin

JavaScript 语言的设计是单一继承,即子类只能继承一个父类,不允许继承多个父类。这种设计保证了对象继承的层次结构是树状的,而不是复杂的网状结构

但是,这大大降低了编程的灵活性。因为实际开发中,有时不可避免,子类需要继承多个父类。举例来说,“猫”可以继承“哺乳类动物”,也可以继承“宠物”。

这里使用mixin和trait解决

33. SIMD

SIMD(发音/sim-dee/)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是 JavaScript 操作 CPU 对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是 SISD(“Single Instruction/Single Data”),即“单指令,单数据”。

SIMD 的含义是使用一个指令,完成多个数据的运算;SISD 的含义是使用一个指令,完成单个数据的运算,这是 JavaScript 的默认运算模式。显而易见,SIMD 的执行效率要高于 SISD,所以被广泛用于 3D 图形运算、物理模拟等运算量超大的项目之中。

总的来说,SIMD 是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。

34. 函数式编程

柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数(unary)。

  1. function add (a) {
  2. return function (b) {
`return a + b;`
  1. }
  2. }
  3. // 或者采用箭头函数写法
  4. const add = x => y => x + y;
  5. const f = add(1);
  6. f(1) // 2

函数合成(function composition)指的是,将多个函数合成一个函数。

  1. const compose = f => g => x => f(g(x));
  2. const f = compose (x => x * 4) (x => x + 3);
  3. f(2) // 20

参数倒置(flip)指的是改变函数前两个参数的顺序。

  1. let f = {};
  2. f.flip =
  3. fn =>
`(a, b, ...args) => fn(b, a, ...args.reverse());`
  1. var divide = (a, b) => a / b;
  2. var flip = f.flip(divide);
  3. flip(10, 5) // 0.5
  4. flip(1, 10) // 10
  5. var three = (a, b, c) => [a, b, c];
  6. var flip = f.flip(three);
  7. flip(1, 2, 3); // => [2, 1, 3]

执行边界(until)指的是函数执行到满足条件为止。

  1. let f = {};
  2. f.until = (condition, f) =>
  3. (...args) => {
`var r = f.apply(null, args);`
`return condition(r) ? r : f.until(condition, f)(r);`
  1. };
  2. let condition = x => x > 100;
  3. let inc = x => x + 1;
  4. let until = f.until(condition, inc);
  5. until(0) // 101
  6. condition = x => x === 5;
  7. until = f.until(condition, inc);
  8. until(3) // 5

Mateo Gianolio, Haskell in ES6: Part 1

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。


相关实践学习
DataV Board用户界面概览
本实验带领用户熟悉DataV Board这款可视化产品的用户界面
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
目录
相关文章
|
6月前
|
JSON 前端开发 JavaScript
ES6 速通(上)
ES6 速通(上)
39 1
|
6月前
|
JSON 前端开发 JavaScript
ES6 速通(中)
ES6 速通(中)
36 1
|
前端开发 JavaScript Java
【编程指南】ES2016到ES2023新特性解析一网打尽(二)
【编程指南】ES2016到ES2023新特性解析一网打尽(二)
149 0
|
JSON JavaScript 前端开发
【编程指南】ES2016到ES2023新特性解析一网打尽(一)
【编程指南】ES2016到ES2023新特性解析一网打尽(一)
98 0
|
缓存 JavaScript 算法
每天3分钟,重学ES6-ES12(十八) CJS
每天3分钟,重学ES6-ES12(十八) CJS
89 0
|
前端开发 JavaScript 小程序
每天3分钟,重学ES6-ES12(十七)模块化历史
每天3分钟,重学ES6-ES12(十七)模块化历史
87 0
|
前端开发 JavaScript
每天3分钟,重学ES6-ES12系列文章汇总
每天3分钟,重学ES6-ES12系列文章汇总
66 0
|
存储 监控 JavaScript
保熟的TS知识,拜托,超快超酷的好吗
这一步对于很多人来说是最简单的一步,也是最难的一步,说简单是因为这确确实实仅是入门的一步,就是一个环境配置,说难则是因为很多人无法跨出这一步,当你跨出这一步之后,你会发现后面的真的学得很快很快,现在,就让我们一起跨出这一步吧~
71 0
好客租房162-css modules在项目中的应用
好客租房162-css modules在项目中的应用
143 0
好客租房162-css modules在项目中的应用
|
前端开发
好客租房161-css modules的说明
好客租房161-css modules的说明
76 0
好客租房161-css modules的说明