Skip to content

AHABHGK

ES6

FE Tutorial4 min read

Table of Contents

原则:

  1. 尽量不讲 APIs,APIs 看看有个印象就行,用到再查

  2. 尽量扩展,所以涉及很多现在不需要的知识,会提到很多以后可能用到的库,看看就好(希望以后对于库能关注其实现原理)

大部分都是语法糖,但是语法糖很重要,因为语法糖可以提升 DX(更开心的写代码)

语法糖:旨在使内容更易阅读,但不引入任何新内容的语法

一堆扩展

let 和 const 命令

暂时性死区

temporal dead zone,简称 TDZ

1if (true) {
2 // TDZ开始
3 tmp = 'abc'; // ReferenceError
4 console.log(tmp); // ReferenceError
5
6 let tmp; // TDZ结束
7 console.log(tmp); // undefined
8
9 tmp = 123;
10 console.log(tmp); // 123
11}

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

块级作用域

for 的圆括号也有作用域,大括号产生的作用域是圆括号产生的作用域的子作用域

1for (let i = 0; i < 3; i++) {
2 let i = 1 // ok
3 console.log(i)
4}
1for (int i = 0; i < 3; i++) {
2 int i = 1 // error
3 cout << i << endl;
4}

{} 产生作用域

1let i = 0
2if (true) let i = 1 // error
3
4if (true) {
5 let i = 1 // ok
6}
7
8{
9 let i = 1 // ok
10}

const

const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了

globalThis

ES2020

polyfill

变量的结构赋值

只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值

默认值

1let [a = 1, b = 2] = []
2
3function move({x = 0, y = 0} = {}) { // 常用
4 return [x, y];
5}
6move({x: 3, y: 8}); // [3, 8]
7move({x: 3}); // [3, 0]
8move({}); // [0, 0]
9move(); // [0, 0]
10
11function move({x, y} = { x: 0, y: 0 }) {
12 return [x, y];
13}
14move({x: 3, y: 8}); // [3, 8]
15move({x: 3}); // [3, undefined]
16move({}); // [undefined, undefined]
17move(); // [0, 0]

数值的扩展

parseInt parseFloat

全局方法 parseInt 和 parseFloat,移植到Number对象上面,这样做的目的,是逐步减少全局性方法,使得语言逐步模块化

Number.EPSILON

Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了

1// 比如,误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2))
2function withinErrorMargin (left, right) {
3 return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
4}
5
60.1 + 0.2 === 0.3 // false
7withinErrorMargin(0.1 + 0.2, 0.3) // true
8
91.1 + 1.3 === 2.4 // false
10withinErrorMargin(1.1 + 1.3, 2.4) // true

BigInt

JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity。ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示

11234 // 普通整数
21234n // BigInt
3
4// BigInt 的运算
51n + 2n // 3n
642n === 42 // false
7typeof 123n // 'bigint'
8
9new BigInt() // TypeError
10BigInt(undefined) //TypeError
11BigInt(null) // TypeError
12BigInt('123n') // SyntaxError
13BigInt('abc') // SyntaxError
14BigInt(1.5) // RangeError
15BigInt('1.5') // SyntaxError

函数的扩展

.length

指定了默认值和 ... 后,length属性将失真

1(function(a, ...args) {}).length // 1
2(function (a, b = 1, c) {}).length // 1

作用域

() 和 {} 是同一个作用域

1var x = 1
2
3function f(x, y = x) {
4 let y = 0 // error
5 console.log(y)
6}

箭头函数

箭头函数表达式的语法比函数表达式更短,并且不绑定自己的 this,arguments,super 或 new.target。这些函数表达式最适合用于非方法函数(non-method functions),并且它们不能用作构造函数(没 prototype)

尾递归优化

尾递归优化只在严格模式下生效,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持

标准是标准,浏览器实现不一定听话

数组的扩展

扩展运算符

1fn.apple(null, arr)
2fn(...arr)

任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组

1Number.prototype[Symbol.iterator] = function* () {
2 let i = 0
3 let num = this.valueOf()
4 while (i < num) {
5 yield i++
6 }
7}
8console.log([...5]) // [0, 1, 2, 3, 4]
9
10let arrayLike = {
11 '0': 'a',
12 '1': 'b',
13 '2': 'c',
14 length: 3,
15}
16// TypeError: Cannot spread non-iterable object.
17let arr = [...arrayLike]
1const go = function* () {
2 yield 1
3 yield 2
4 yield 3
5}
6[...go()] // [1, 2, 3]

from

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)

所谓类似数组的对象,本质特征只有一点,即必须有 length 属性,会根据 length 创建数组长度,'0', '1', '2' 这样的最为数组的下标加入数组,其他的舍去,length 多的话为 empty,少的话就舍去

1let arrayLike = {
2 '0': 'a',
3 '1': 'b',
4 '2': 'c',
5 length: 3
6}
7// ES5的写法
8var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
9// ES6的写法
10let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
1const toArray = (() =>
2 Array.from ? Array.from : obj => [].slice.call(obj)
3)();

of flatMap

方便 functor monad

简单理解就是 functor 是有实现 map 方法的对象,monad 是有实现 flatMap 方法的对象,比如 Promise 是个 functor 也是 monad,then 相当于 map 也相当于 flatMap

1const { of } = Array
2of(1).flatMap(e => [e * 2]) // of(2) 就有 fp 那味了

对象的扩展

对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面

对象的新增方法

__proto__ 写入 es6 规范的附录,要求实现,但双下划线表示内部 API,仍不推荐直接使用

Set 和 Map 数据结构

Set

集合

Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化

1Number.prototype[Symbol.iterator] = function* () {
2 let i = 0
3 let num = this.valueOf()
4 while (i < num) {
5 yield i++
6 }
7}
8new Set(5) // Set(5) {1, 2, 3, 4, 5}

Set 有 [Symbol.iterator] 接口,可以 Array.from 转化成数组

keys(),values(),entries() 返回遍历器对象

1Set.prototype[Symbol.iterator] === Set.prototype.values
2// true 默认遍历器生成函数就是它的values方法
3
4let set = new Set(['red', 'green', 'blue']);
5
6for (let x of set.values()) { // for (let x of set)
7 console.log(x);
8}
9// red
10// green
11// blue

Map

Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适

不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map

1const items = [
2 ['name', '张三'],
3 ['title', 'Author']
4];
5
6const map = new Map();
7
8const set = new Set([
9 ['foo', 1],
10 ['bar', 2]
11]);
12const m1 = new Map(set);
13m1.get('foo') // 1

keys(),values(),entries() 返回遍历器对象

1map[Symbol.iterator] === map.entries
2// true 默认遍历器接口(Symbol.iterator属性),就是entries方法
3[...map.entries()]
4// [[1,'one'], [2, 'two'], [3, 'three']]
5
6[...map]
7// [[1,'one'], [2, 'two'], [3, 'three']]

WeakSet、WeakMap

WeakMap 是类似于 Map 的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问它们,便会将它们与其关联值一同删除。

WeakSet 是类似于 Set 的集合,它仅存储对象,并且一旦通过其他方式无法访问它们,便会将其删除。

它们都不支持引用所有键或其计数的方法和属性。仅允许单个操作。

WeakMap 和 WeakSet 被用作“主要”对象存储之外的“辅助”数据结构。一旦将对象从主存储器中删除,如果该对象仅被用作 WeakMap 或 WeakSet 的键,那么它将被自动清除

用例:

WeakSet 记录谁访问过我们的网站:

1let visitedSet = new WeakSet();
2
3let john = { name: "John" };
4let pete = { name: "Pete" };
5let mary = { name: "Mary" };
6
7visitedSet.add(john); // John 访问了我们
8visitedSet.add(pete); // 然后是 Pete
9visitedSet.add(john); // John 再次访问
10
11// visitedSet 现在有两个用户了
12
13// 检查 John 是否来访过?
14alert(visitedSet.has(john)); // true
15
16// 检查 Mary 是否来访过?
17alert(visitedSet.has(mary)); // false
18
19john = null;
20
21// visitedSet 将被自动清理

WeakMap 保存 DOM 节点相关状态信息:

1let myWeakmap = new WeakMap();
2
3myWeakmap.set(
4 document.getElementById('logo'),
5 {timesClicked: 0} // 相关信息,一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险
6)
7
8document.getElementById('logo').addEventListener('click', function() {
9 let logoData = myWeakmap.get(document.getElementById('logo'));
10 logoData.timesClicked++;
11}, false);

WeakMap 做私有属性:

TypeScript 3.8 的 # 私有属性就是通过 WeakMap 保证编译后代码的兼容性

1const privateData = new WeakMap();
2
3class Person {
4 constructor(name, age) {
5 privateData.set(this, { name: name, age: age });
6 }
7
8 getName() {
9 return privateData.get(this).name;
10 }
11
12 getAge() {
13 return privateData.get(this).age;
14 }
15}
16
17export default Person

迭代器(Iterator)和生成器(Generator)

Iterator 和 for...of 循环

遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

只要有 next 方法,并且 next 方法返回对象包含 value 和 done 就是 Iterator(鸭子模型),return 和 throw 方法可选

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子

1let arr = ['a', 'b', 'c']
2
3// 直接拿数组的 iterator 接口
4let iter = arr[Symbol.iterator]()
5iter.next() // { value: 'a', done: false }
6iter.next() // { value: 'b', done: false }
7iter.next() // { value: 'c', done: false }
8iter.next() // { value: undefined, done: true }
9
10// for of 自动消费 iterator 接口
11for (const value of arr) {
12 console.log(value)
13}

原生具备 Iterator 接口的数据结构如下:

  • Array

  • Map

  • Set

  • String

  • TypedArray

  • 函数的 arguments 对象

  • NodeList 对象

Object 没有,所以 Object 不能用 for of 遍历 for in 与 for of 区别:for in 根据对象的可枚举的属性,拿到属性(字符串)进行遍历,数组也是个对象,所以数组可以用 for in 遍历,同时 for in 会遍历出 prototype 上的属性,所以对象使用 for in 时可能有意想不到的东西遍历出来,而且某些情况下 for in 遍历是任意顺序的,更推荐 Object.keys Object.values Object.entries 进行遍历。for of 通过 iterator 接口进行遍历,没有 iterator 接口的对象可以通过添加实现

调用 Iterator 接口的场合:

  • 数组Set 结构进行解构赋值

  • ... 扩展运算符

  • yield* 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口

  • 接受数组作为参数的场合:for...of、Array.from()、Map()、Set()、WeakMap()、WeakSet()、Promise.all()、Promise.race()

例子:单向链表

1function Node(value) {
2 this.value = value
3 this.next = null
4}
5Node.prototype[Symbol.iterator] = function () {
6 let current = this
7 return {
8 next() {
9 if (current) {
10 const value = current.value
11 current = current.next
12 return { done: false, value }
13 } else {
14 return { done: true, value: undefined }
15 }
16 }
17 }
18}
19
20let head = new Node(1)
21head.next = new Node(2)
22head.next.next = new Node(3)
23for (const i of head) console.log(i)

Generator 部署 Iterator 接口更方便:

1let obj = {
2 * [Symbol.iterator]() {
3 yield 'hello';
4 yield 'world';
5 }
6}
7for (let x of obj) {
8 console.log(x);
9}
10// "hello"
11// "world"

Generator 函数的语法

Generator 返回一个 Iterator(有 next、return、throw 三个方法)

这三个方法都可以传入参数:它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式

next return throw 和方法的参数

next 是将 yield 表达式替换成一个值

1const g = function* (x, y) {
2 let result = yield x + y;
3 return result;
4};
5
6const gen = g(1, 2);
7gen.next(); // Object {value: 3, done: false}
8
9gen.next(1); // Object {value: 1, done: true}
10// 相当于将 let result = yield x + y
11// 替换成 let result = 1;

throw 是将 yield 表达式替换成一个 throw 语句

1gen.throw(new Error('出错了')); // Uncaught Error: 出错了
2// 相当于将 let result = yield x + y
3// 替换成 let result = throw(new Error('出错了'));
4// 用于 generator 内部

return 是将 yield 表达式替换成一个 return 语句

1gen.return(2); // Object {value: 2, done: true}
2// 相当于将 let result = yield x + y
3// 替换成 let result = return 2;

yield* 表达式

如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历,就非常麻烦

yield* 就是解决这个问题:

1function* concat(iter1, iter2) {
2 yield* iter1;
3 yield* iter2;
4}
5// 等同于
6function* concat(iter1, iter2) {
7 for (var value of iter1) {
8 yield value;
9 }
10 for (var value of iter2) {
11 yield value;
12 }
13}

任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历

含义

(强烈推荐读原文)

协程

ES6 中的 Generator 是“半协程”,意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行

异步编程

后面学姐细讲

Promise

如果 then 返回一个 thenable 对象(鸭子模型)那么下一个 then 接收到的是这个 thenable 对象转化成 promise 后所 resolve 的值。如果返回是一个 promise,那下一个 then 接收到的是

1new Promise(resolve => {
2 resolve(1)
3})
4 .then((v) => ({
5 then(resolve) {
6 resolve(3)
7 }
8 }))
9 .then(console.log) // 3
10
11new Promise(resolve => {
12 resolve(1)
13})
14 .then((v) => Promise.resolve(3))
15 .then(console.log) // 3

then 相当于 flatMap and map,所以是个 monad

co

Generator 一大应用就是异步与流程控制 tj/co,Generator 函数将 JavaScript 异步编程带入了一个全新的阶段

1var gen = function * () {
2 var f1 = yield readFile('/etc/fstab'); // 管理流程时需要 yield 出 promise 或 thunk 函数
3 var f2 = yield readFile('/etc/shells');
4 console.log(f1.toString());
5 console.log(f2.toString());
6};
7co(gen);

async

Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器

async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)

async 函数的返回值是 Promise 对象,进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖

async iterator

  • function () {}

  • () => {}

  • async function () {}

  • async () => {}

  • function* () {}

  • async function* () {} 返回 Iterator,但是调用 next 后返回一个 Promise,then cb 的参数是带有 next、done 属性的对象

1async function* run() {
2 await new Promise(resolve => setTimeout(resolve, 100));
3 yield 'Hello';
4 console.log('World');
5}
6
7// `run()` returns an async iterator.
8const asyncIterator = run();
9
10// The function doesn't start running until you call `next()`
11asyncIterator.next()
12 .then(obj => console.log(obj.value)) // Prints "Hello"
13 .then(() => asyncIterator.next()) // Prints "World"

元编程与 DSL

元编程是针对程序本身的行为进行操作的编程。换句话说,它是为你程序的编程而进行的编程

DSL (domain-specific languages) 领域特定语言

元编程的一大作用就是实现 DSL,

标签模版的 DSL

模版标签其实是在字符串扩展那一节,但是元编程的一大作用就是实现 DSL,比如 Io 的 forward、Ruby 的 method_missing,但这里不是元编程实现,而是用模版字符串实现类似的功能,只是觉得两个概念比较配,就放这里了

1function SaferHTML(templateData) {
2 let s = templateData[0];
3 for (let i = 1; i < arguments.length; i++) {
4 let arg = String(arguments[i]);
5
6 // Escape special characters in the substitution.
7 s += arg.replace(/&/g, "&amp;")
8 .replace(/</g, "&lt;")
9 .replace(/>/g, "&gt;");
10
11 // Don't escape special characters in the template.
12 s += templateData[i];
13 }
14 return s;
15}
16let sender = '<script>alert("abc")</script>'; // 恶意代码
17let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
18
19message
20// <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>

style-component(CSS-in-JSX)

lit-html、民间 jsx(HTMl-in-JS)

GLSL、C-in-JS、Lisp-in-JS …… 只要你敢写,要什么有什么

使字符串写 DSL 更方便,原来的字符串也可以写,但解析字符串就很麻烦,比如:精读《手写 JSON Parser》

JS 原本就有别的方法,比如通过函数的 React JSX,Vue,Angular,通过数组的表单验证的方案,甚至 JQuery 也是一种

Symbal

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因

表示独一无二的值

1const a = {
2 b: 'lal',
3 [Symbal('hah')]() {
4 console.log('hah')
5 }
6}
7
8Object.getOwnPropertyNames(a) // ['b']
9Object.getOwnPropertySymbols(a) // [Symbol('hah')]
10Reflect.ownKeys(a) // ['b', Symbal('hah')]

比如某状态管理工具:

我们这样使用:

1// 定义初始状态
2let initialState = 0
3
4// 定于一个 reducer
5function counterReducer(action, state = initialState) {
6 switch (action.type) {
7 case 'INC':
8 return state + 1
9 case 'DEC':
10 return state - 1
11 default:
12 return state
13 }
14}
15
16// 通过 reducer 和 initialState 获取 store
17let store = redux(counterReducer, initialState)
18
19// store.getState 用来获取当前 state
20store.getState() // 0
21// store.dispatch 通过一个带有 type 属性的对象来根据之前定义的 reducer 对 state 进行更改
22store.dispatch({ type: 'INC' })
23// 获取新的 state
24store.getState() // 1

就是这样,由于第一次的 state 要有,就需要 redux 函数内部调用一次 reducer,同时为了不对第一次的 state 做出改变(第一次的 state 是 initialState),这次 type 跟我们的可能用到的 type 都不一样,也就是说 redux 函数内部并不能知道我们可能用到的 type 是什么,必须是一个独一无二的值,这时就可以用 Symbol:

1function redux(reducer, initialState) {
2 let currentState = reducer({ type: Symbol('STATE_INIT') }, initialState)
3 return {
4 getState() {
5 return currentState
6 },
7 dispatch(action) {
8 currentState = reducer(action, currentState)
9 }
10 }
11}

内置 Symbol 值

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法

hasInstance、isConcatSpreadable、species、match、replace、search、split、iterator、toPrimitive、toStringTag、unscopables

可以通过添加或修改内置 Symbol 值 [Symbol.xxx] 来添加或修改对象一些场景的行为

比如上面出现的添加 Number.prototype[Symbol.iterator] 实现 [...5]new Set(5) 的操作

还有修改 toPrimitive 实现:让 a == 1 && a == 2 && a == 3 返回 true

1// == 两侧类型不同会有隐式类型转换,toPrimitive 就是定义对象被转为原始类型的值时调用的方法
2const a = {
3 value: [3, 2, 1],
4 [Symbol.toPrimitive]() {
5 return this.value.pop()
6 },
7}

赋予 JS 某些场景下的元编程的能力

Proxy、Reflect

Proxy 可以代理对象的某些行为,一共 13 种

Proxy 代理之后,对象原来的行为就没了,所以需要有东西存原来的行为,就是 Reflect

Reflect 可以拿到语言本身的行为,它的方法与 Proxy 一一对应,也是 13 种

getset、has、apply、construct、defineProperty、deleteProperty、ownKeys、isExtensible、preventExtensions、getOwnPropertyDescriptor、getPrototypeOf、setPrototypeOf

Proxy 的 this 指向调用它的对象,target 调用就指向 target,proxy 调用就指向 proxy

1let target = {
2 foo() {
3 console.log(this)
4 }
5}
6let handler = {}
7let proxy = new Proxy(target, handler)
8
9target.foo() // target
10proxy.foo() // proxy

IE11 不兼容

Proxies require support on the engine level and it is not possible to polyfill Proxy. GoogleChrome/proxy-polyfill 只支持 get、set、apply、construct

Decorator

Decorator 提案经过了大幅修改,现在还没有定下来,随便看看就好

装饰器是一个函数,传入类或类的方法,并对其进行修改,增强类本身的行为

比如 Mobx5 中使用装饰器和 Proxy 实现响应式数据、Nest 和 Angular 使用装饰器实现 IoC(控制反转)DI(依赖注入)

元编程

元编程是针对程序本身的行为进行操作的编程

  • Symbol:通过修改内置的 Symbol 值,重写对象的某些方法

  • Proxy:通过代理来拦截对象的行为

  • Reflect:存有对象行为信息,一般配合 Proxy 使用

ES6 增强了这些能力,而且有本质上的增强,从 Proxy 的 polyfill 只能实现部分功能就可以看出。现在 Proxy 的应用有 Vue3、Mobx 的响应式、Immer.js 使用 mutable 写法写 immutable 数据

ES6 之前也是有元编程的能力的:

  • defineProperty 在 Vue2 中实现拦截 get set

  • __defineGetter____defineSetter__ 在 Koa 中用来实现代理一些属性但,这两个不是标准尽量不要用

  • eval 和 new Function 也是,但性能不好尽量不要用,可用闭包和高阶函数替代

其他语言中比如 C++、Ruby 重载运算符,Ruby、Io 重写 method_missing 方法……

1// Proxy 通过实现 method_missing
2let handler = {
3 get: function(target, name) {
4 if (!Reflect.has(target, name)) {
5 return 'method_missing XvX you can do some hacking functions at here...'
6 }
7 return Reflect.get(target, name)
8 }
9}
10
11let { proxy, revoke } = Proxy.revocable({ a: 1 }, handler)
12proxy.b // 'method_missing XvX you can do some hacking functions at here...'
13proxy.a // 1
14
15revoke() // 撤销代理
16proxy.a // TypeError: Revoke

class?语法糖?模版?

JS 的 OOP 是基于原型的,不同于工业常用的基于类的,虽然 ES6 添加了一些 class 的东西,但本质还是原型

Io JS 没有类,只需要与对象打交道,必要时把对象复制(Object clone Object.create)一下就行,这些被复制的对象叫原型,原型语言中,每个对象都不是类的复制品,而是一个实实在在的对象

1let Vehicle = Object.create(Object.prototype) // {},等价于 let Vehicle = {}
2Vehicle.drive = 'gogogo'
3let Car = Object.create(Vehicle) // Car.__proto__ === Object.getPrototypeOf(Car) === Vehicle
4Car.drive = 'dididi'
5let aodi = Object.create(Car)
6aodi.sayAodi = function () {
7 console.log('AODI!!!')
8}
9aodi.drive // 'dididi'

我们实现的就是一个 Vehicle,Car 继承 Vehicle,aodi 是 Car 的一个实例(注意我们自定的规范:小写是实例)

再以一种常见一点的写法(“经典”组合式创建和寄生组合式继承):

1function Vehicle() {}
2Vehicle.prototype.drive = 'gogogo'
3
4function Car(...args) {
5 Vehicle.apply(this, args) // 调用父类构造函数
6}
7Car.prototype = Object.create(Vehicle.prototype) // 继承,链接原型链
8Car.prototype.constructor = Car // 修正 constructor
9
10Car.prototype.drive = 'dididi'
11
12let aodi = new Car()
13aodi.sayAodi = function () {
14 console.log('AODO!!!')
15}

做了与之前同样的事,只不过用了构造函数,来看看 class 写法:

1class Vehicle {
2 drive() { // 由于不能直接给原型添加属性,所以用方法代替
3 console.log('gogogo')
4 }
5}
6
7class Car extends Vehicle {
8 dirve() {
9 console.log('dididi')
10 }
11}
12
13let aodi = new Car()
14aodi.sayAodi = function () { // 为了不影响其他 Car 实例,所以直接在这里添加
15 console.log('AODI!!!')
16}

原型链的本质:一个单向链表,没有的方法和属性沿着这条单向链表寻找,直到找到或遇到 null 为止

对比第一种和第二种,第二种是对于基于类的一种模拟,第一种才是基于原型语言中常用的写法,至于为什么更常用于第一种,大概是 JS 的历史问题吧(同 JS 为什么叫 JavaScript)

对比第二种和第三种,明显感觉第三种更为清晰,可读性更好,像是以一种基于类的模版写基于原型的 OOP,但同是也可以感受到少了一些灵活性,所以语法糖并不一定能提升语言的表达力,而主要是为了开发者的便利而设计的

但是 class 和 class extends 虽是“组合式创建”和“寄生组合式继承”的语法糖,但有些表现也是不同的,寄生组合式继承子类的 this 是自己的,然后调用父类构造函数在 this 上,而 class extends 中子类的 this 是父类传给子类的,所以寄生组合式不能继承原生对象,而 class extends 可以

语法

1class Vehicle {
2 isVehicle = true
3
4 constructor() { // 构造函数
5 if (new.target === Vehicle) { // new.target 指向 new 的那个类(new Car 的话就指向 Car),可以用来实现抽象类
6 throw new Error('本类不能实例化')
7 }
8 }
9
10 static isVehicle(vehicle) { // 静态方法,相当于 Car.isCar,在构造函数上,所以不能访问 this
11 return vehicle.isVehicle
12 }
13
14 drive() { // 方法,相当于 Car.prototype.drive
15 console.log('gogogo')
16 }
17}
18
19class Car extends Vehicle {
20 isCar = true // 属性,相当于 this.isCar
21 #price = 0 // 私有属性
22
23 constructor(name) {
24 super() // 相当于父类构造函数,对应 Vehicle.apple(this),但实际上是拿到父类的 this
25 this.name = name
26 }
27
28 set price(value) { // setter
29 this.#price = value
30 }
31
32 get price() { // getter
33 return this.#price
34 }
35
36 static isCar(car) {
37 return super.isVehicle(car) && car.isCar // super 只有在 static 中才相当于父类构造函数,Vehicle.isVehicle
38 }
39
40 drive() {
41 super.drive() // 这里 super 相当于父类的原型,Vehicle.prototype
42 console.log('dididi')
43 }
44}

模块化

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

ES6 的模块自动采用严格模式

常用就两种:

1// xxx.js
2const a = 1
3export default a
4
5// main.js
6import a from './xxx.js'
1// xxx.js
2export const a = 1
3
4// main.js
5import { a } from './xxx.js'

编码风格

推荐 airbnb 的编码风格规范

使用 Lint 工具:ESLint、VSCode ESLint 插件

1npm install -g eslint
2# 进入你的项目的文件夹中
3eslint --init
4# 根据你的需要选择就好

作业

  • Level 1:

ref

ES6 入门教程

TypeScript 版图解 Functor, Applicative 和 Monad

ES6 系列之 WeakMap

从类型理解 rxjs 与async generator (一)

Async Generator Functions in JavaScript

ES6 系列之 defineProperty 与 proxy

MDN Proxy

JavaScript DSL 元编程

JavaScript DSL示例

JavaScript 元编程之ES6 Proxy

immer.js 简介及源码简析

七周七语言

进阶必读:深入理解 JavaScript 原型