什么?ES6 中还有 Tail Calls!

栏目: JavaScript · 发布时间: 6年前

内容简介:先吐槽一件事,最近把原先的 TOP 域名更换到 CN 域名,并且用有人要问了,都 9102 年,ES10 都出来了,怎么还在讲 ES6,非也!本文针对 ES6 几个不为人知、和重要的特性做讲解,精彩的在后面!ES6 除了固有的函数作用域,还引入了

先吐槽一件事,最近把原先的 TOP 域名更换到 CN 域名,并且用 Gatsby 重建个人站点,之前是用采用 HTTPS 部署的方式绕过阿里云的域名备案系统。更换 CN 域名后,这招不管用了,:sob::sob: 域名必须要备案了,等待幕布邮寄中……

有人要问了,都 9102 年,ES10 都出来了,怎么还在讲 ES6,非也!本文针对 ES6 几个不为人知、和重要的特性做讲解,精彩的在后面!

基础篇

Let + Const

ES6 除了固有的函数作用域,还引入了 块级作用域({})

function f() {
  {
    let x; // ①
    {
      // 包含在当前块中,与 ① 中的 x 分属不同作用域
      const x = "sneaky";
      // 错误,const 定义的变量不可以重新赋值,如果 const 定义了一个对象,那么对象的属性是可以修改的
      x = "foo";
    }
    // let 定义的变量可以重新赋值
    x = "bar";
    // 错误,x 在 ① 块中已被定义
    let x = "inner";
  }
}
复制代码

默认、剩余、展开参数(Default + Rest + Spread)

function f(x, y=12) {
  // y 等于 12 如果不传递 (或者传递 undefined)
  return x + y;
}
f(3); // 15
复制代码
function f(x, ...y) {
  // y 是一个数组
  return x * y.length;
}
f(3, "hello", true); // 6
复制代码
function f(x, y, z) {
  return x + y + z;
}
// 将数组的每一项作为参数传递
f(...[1,2,3]); // 6
复制代码

解构(Destructuring)

var [a, ,b] = [1,2,3];
a === 1; // true
b === 3; // true

var { op: a, lhs: { op: b }, rhs: c } = getASTNode()

// var {op: op, lhs: lhs, rhs: rhs} = getASTNode()
var {op, lhs, rhs} = getASTNode() 

// 参数解构
function g({name: x}) {
  console.log(x);
}
g({name: 5})

var [a] = [];
a === undefined; // true

var [a = 1] = [];
a === 1; // true

// 解构 + 默认参数
function r({x, y, w = 10, h = 10}) {
  return x + y + w + h;
}
r({x:1, y:2}) === 23 // true
复制代码

箭头函数(Arrows and Lexical This)

// 除了支持返回语句,还可以将表达式作为返回主体
const foo = () => ({ name: 'es6' }); 
const bar = (num) => (num++, num ** 2);

foo();  // 返回一个对象 { name: 'es6' }
bar(3); // 执行多个表达式,并返回最后一个表达式的值 16
复制代码

JS 中 this 的指向问题一直都是面试高频考点,不少人在实战中也掉入坑中,总结起来就是一句话:“ this 永远指向调用它的那个对象 ”,而箭头函数则改写了这一规则,就是

箭头函数共享当前代码上下文的 this

什么意思呢?可以理解为

  • 箭头函数不会创建自己的 this 它只会从自己的作用域链的上一层继承 this ,如果上一层还是箭头函数,则继续向上查找,直至全局作用域,在浏览器环境下即 window
  • 函数具有作用域链,对象则不具有

因此,在下面的代码中,传递给 setInterval 的函数内的 thissayHello 函数中的 this 一致:

const bob = {
  name: 'Bob',
  sayHello() {
    setTimeout(() => {
      console.log(`hello, I am ${this.name}`);
    }, 1000);
  }
};
const hello = bob.sayHello;

bob.sayHello();
// hello, I am Bob 
// 作为对象的方法调用,sayHello的this指向bob 
hello();
// hello, I am undefined 
// 作为普通函数调用,相当于window.hello(),this指向全局对象
hello.call({name:'Mike'});
// hello, I am Mike 
// call,apply调用,第一个参数为this指向的对象
复制代码
language = 'Python';
const obj = {
  language: 'TS',
  speak() {
    language = 'GO';
    return function() {
      return () => {
        console.log(`I speak ${this.language}`);
      };
    };
  }
};

obj.speak()()(); // 做个小测试,会打印什么呢?
复制代码

箭头函数还有以下特点

  • 由于箭头函数没有自己的 this 指针,通过 callapply 调用,第一个参数会被忽略
  • 不绑定 Arguments 对象,其引用上一层作用域链的 Arguments 对象
  • 不能用作构造器,和 new 一起用会抛出错误。
  • 没有 prototype 属性。

现在你应该明白为何 React 中的函数写法都为箭头函数,就是为了绑定 this

Symbols

ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,它的功能类似于一种标识唯一性的 ID

// 每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false
const s1 = Symbol('macOS');
const s2 = Symbol('macOS');

// Symbol.for 机制有点类似于单例模式
const s3 = Symbol.for('windows'); // 注册一个全局 Symbol
const s4 = Symbol.for('windows'); // 已存在相同名称的 Symbol,返回全局 Symbol

s1 === s2; // false
s3 === s4; // true

复制代码
let key = Symbol('key');

function MyClass(privateData) {
  // 注意,Symbol值作为对象属性名时,不能用点运算符
  this[key] = privateData;
}

MyClass.prototype = {
  doStuff() {
    console.log(this[key]);
  }
};

// Symbol的一些特性必须要浏览器的原生实现,不可被 transpiled 或 polyfilled
typeof key // symbol

let c = new MyClass('hello');
c.key; // undefined
c[key]; // hello

复制代码

应用场景

  • 更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。

在实际应用中,我们经常会需要使用 Object.keys() 或者 for...in 来枚举对象的属性名,那在这方面, Symbol 类型的 key 表现的会有什么不同之处呢?来看以下示例代码:

let obj = {
   [Symbol('name')]: '一斤代码',
   age: 18,
   title: 'Engineer'
}

Object.keys(obj)   // ['age', 'title']

for (let p in obj) {
   console.log(p)   // 分别会输出:'age' 和 'title'
}

Object.getOwnPropertyNames(obj)   // ['age', 'title']
复制代码

也正因为这样一个特性,当使用 JSON.stringify() 将对象转换成 JSON 字符串的时候, Symbol 属性也会被排除在输出内容之外:

JSON.stringify(obj)  // {"age":18,"title":"Engineer"}
复制代码

由上代码可知, Symbol 类型的 key 是不能通过 Object.keys() 或者 for...in 来枚举的,所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol 来定义。

  • 消除魔术字符串
const TYPE_AUDIO = 'AUDIO'
const TYPE_VIDEO = 'VIDEO'
const TYPE_IMAGE = 'IMAGE'

function handleFileResource(resource) {
  switch(resource.type) {
    case TYPE_AUDIO:
      playAudio(resource)
      break
    case TYPE_VIDEO:
      playVideo(resource)
      break
    case TYPE_IMAGE:
      previewImage(resource)
      break
    default:
      throw new Error('Unknown type of resource')
  }
}
复制代码

上面的代码中那样,我们需要为常量赋一个唯一的值(比如这里的 'AUDIO' ), 'AUDIO' 就是一个魔术字符串,它本身没意义,只是为了保证常量唯一的关系。常量一多,就变得十分臃肿且难以理解

现在有了 Symbol ,我们大可不必这么麻烦了:

// 保证了三个常量的值是唯一的
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()
复制代码

增强的对象字面量(Enhanced Object Literals)

const obj = {
  // 允许设置原型
  __proto__: theProtoObj,
  // 允许覆盖属性
  ['__proto__']: somethingElse,
  // 属性简写,等于 ‘handler: handler’
  handler,
  // 计算 (动态) 属性名
  ['prop_' + (() => 42)()]: 42
};
obj.prop_42 // 42
obj.__proto__ // somethingElse
复制代码

__proto__ 需要原生支持,它在之前的 ECMAScript 版本中被移除,但大多数浏览器都实现了这一特性,包括 Node 环境

Map + Set + WeakMap + WeakSet

Set

Set 是 ES6 中新增的数据结构,它允许创建唯一值的集合。集合中的值可以是简单的基本类型(如字符串或数值),但更复杂的对象类型(如对象或数组)也可以,亦或是一个新的 Set

let animals = new Set();

animals.add(':pig:');
animals.add(':panda_face:');
animals.add(':turtle:');
animals.add('  ');
console.log(animals.size); // 4
animals.add(':panda_face:');
console.log(animals.size); // 4

console.log(animals.has(':pig:')); // true
animals.delete(':pig:');
console.log(animals.has(':pig:')); // false

animals.forEach(animal => {
  console.log(`Hey ${animal}!`);
});

// Hey :panda_face:!
// Hey :turtle:!
// Hey   !

animals.clear();
console.log(animals.size); // 0
复制代码

我们还可以传入一个数组来初始化集合

let myAnimals = new Set([':pig:', ':turtle:', ':pig:', ':pig:']);

myAnimals.add([':koala:', ':sheep:']);
myAnimals.add({ name: 'Rud', type: ':turtle:' });
console.log(myAnimals.size); // 4

// Set 内置了遍历器,可以调用 forEach, for…of
myAnimals.forEach(animal => {
  console.log(animal);
});

// :pig:
// :turtle:
// [":koala:", ":sheep:"]
// Object { name: "Rud", type: ":turtle:" }
复制代码

Map

与普通对象( Object )不同, Map 的键名( Key )可以是任何类型,不再局限于字符串( String ),包括但不限于 objectsfunctions

let things = new Map();

const myFunc = () => ':pizza:';

things.set(':car:', 'Car');
things.set(':house:', 'House');
things.set(':airplane:', 'Airplane');
things.set(myFunc, ':smile: Key is a function!');

things.size; // 4

things.has(':car:'); // true

things.has(myFunc) // true
things.has(() => ':pizza:'); // false
things.get(myFunc); // ':smile: Key is a function!'

things.delete(':airplane:');
things.has(':airplane:'); // false

things.clear();
things.size; // 0

// 链式设置键值对
things.set(':wrench:', 'Wrench')
      .set(':guitar:', 'Guitar')
      .set('  ', 'Joystick');

const myMap = new Map();

// 甚至键名可以是另一个 Map
things.set(myMap, 'Oh gosh!');
things.size; // 4
things.get(myMap); // 'Oh gosh!'

复制代码

可以通过传入包含两个元素的数组来初始化 Map

const funArray = [
  ['  ', 'Champagne'],
  [':lollipop:', 'Lollipop'],
  [':confetti_ball:', 'Confetti'],
];

let funMap = new Map(funArray);
funMap.get('  '); // Champagne
复制代码

WeakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。它最重要的特性是 WeakMap 保持了对键名所引用的对象的弱引用

我们可以通过 Node 来证明一下这个问题:

// 允许手动执行垃圾回收机制
node --expose-gc

global.gc();
// 返回 Nodejs 的内存占用情况,单位是 bytes
process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4M

let map = new Map();
let key = new Array(5 * 1024 * 1024); // new Array 当为 Obj
map.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46751472 注意这里大约是 44.6M

// 所以当你设置 key = null 时,只是去掉了 key 对 Obj 的强引用
// 并没有去除 arr 对 Obj 的强引用,所以 Obj 还是不会被回收掉
key = null;
global.gc();
process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M

// 这句话其实是无用的,因为 key 已经是 null 了
map.delete(key);
global.gc();
process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M
复制代码
node --expose-gc

global.gc();
process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4M

const wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M

// 当我们设置 key = null 的时候,就只有 wm 对所引用对象的弱引用
// 下次垃圾回收机制执行的时候,该引用对象就会被回收掉。
key = null;
global.gc();
process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M
复制代码

应用场景

传统使用 jQuery 的时候,我们会通过 $.data() 方法在 DOM 对象上储存相关信息(就比如在删除按钮元素上储存帖子的 ID 信息), jQuery 内部会使用一个对象管理 DOM 和对应的数据,当你将 DOM 元素删除, DOM 对象置为空的时候,相关联的数据并不会被删除,你必须手动执行 $.removeData() 方法才能删除掉相关联的数据, WeakMap 就可以简化这一操作:

let wm = new WeakMap(), element = document.querySelector(".element");
wm.set(element, "data");

let value = wm.get(elemet);
console.log(value); // data

element.parentNode.removeChild(element);
element = null;
复制代码

WeakSet

特性与 WeakMap 相似

遍历器(Iterators + For..Of)

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

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • ES6 创造了一种新的遍历命令 for...of 循环, Iterator 接口主要供 for...of 消费。

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”( iterable

let fibonacci = {
  [Symbol.iterator]() {
    let pre = 0, cur = 1;
    return {
      next() {
        [pre, cur] = [cur, pre + cur]; // 数组解构
        return { done: false, value: cur }
      }
    }
  }
}

for (var n of fibonacci) {
  // 当n超过1000时停止
  if (n > 1000)
    break;
  console.log(n);
}
复制代码

上面代码中,对象 fibonacci 是可遍历的( iterable ),因为具有 Symbol.iterator 属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 valuedone 两个属性

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

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

for...in 和 for..of 的差别

  • for...in 遍历键名(Key)并转化为字符串,for...of 遍历键值(Value)
  • for...in 语句以任意顺序遍历一个对象自有的、继承的、可枚举的、非 Symbol 的属性
  • for...in 更适合遍历对象,for...of 更适合遍历数组。
for (let i in [1, 2, 3]) {
  console.log(typeof i); // string 数组下标被转化字符串
  console.log(i); // '1', '2', '3'
}

var triangle = { a: 1, b: 2, c: 3 };

function ColoredTriangle() {
  this.color = 'red';
}

ColoredTriangle.prototype = triangle;

var obj = new ColoredTriangle();

for (var prop in obj) {
  if (obj.hasOwnProperty(prop)) { // 如果去了 hasOwnProperty() 这个约束条件会怎么样?
    console.log(`obj.${prop} = ${obj[prop]}`); // obj.color = red
  }
}
复制代码

新增 API(Math + Number + String + Object APIs)

我们先来看看新增的 Number.EPSILON ,不少人都是懵逼的状态,WTF?

先来看看的 JS 世界中的一道送命题

0.1 + 0.2 // 结果0.30000000000000004 而不是0.3
复制代码

事出必有因,这是因为 JS 的数值采用了 IEEE 754 标准,而且 JS 是弱类型语言,所以数字都是以64位双精度浮点数据类型储存。也就是说,JS 语言底层根本没有整数,所有数字都是小数!当我们以为在用整数进行计算时,都会被转换为小数

而浮点数都是以多位二进制的方式进行存储的

十进制的0.1用二进制表示为:0.0 0011 0011 0011 0011…,循环部分是0011

十进制0.2用二进制表示为:0.0011 0011 0011 0011…,循环部分是0011

由于存储空间有限,最后计算机会舍弃后面的数值,所以我们最后就只能得到一个近似值

JS中采用的 IEEE 754 的双精度标准也是一样的道理在存储空间有限的情况下,当出现这种无法整除的小数的时候就会取一个近似值,在 JS 中如果这个近似值足够近似,那么 JS 就会认为他就是那个值。

console.log(0.1000000000000001) 
// 0.1000000000000001 (中间14个0,不会被近似处理,输出本身)
console.log(0.10000000000000001) 
// 0.1 (中间15个0,js会认为两个值足够近似,所以输出0.1)
复制代码

那么这个近似的界限如何判断呢?

ES6的 Number.EPSILON 就是一个界限,它表示 1 与大于 1 的最小浮点数之间的差。

对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方

Number.EPSILON === Math.pow(2, -52)
// true
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"
复制代码

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

0.1 + 0.2 - 0.3
// 5.551115123125783e-17

5.551115123125783e-17.toFixed(20)
// '0.00000000000000005551'
复制代码
0.00000000000000005551 < 0.00000000000000022204 // true
复制代码

显然, 0.30000000000000004 不存在误差,不会被近似处理

我们可以通过以下手段来达到我们想要的效果

function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}

0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true
复制代码

其他一些新增的 API

Number.isInteger(Infinity) // false
Number.isNaN("NaN") // false

Math.sign(-5) // 判断一个数到底是正数、负数、还是零 -1
Math.hypot(3, 4) // 返回所有参数的平方和的平方根 5
Math.imul(-2, -2) // 返回两个数以 32 位带符号整数形式相乘的结果 4

"abcde".includes("cd") // true
"abc".repeat(3) // "abcabcabc"

Array.from(document.querySelectorAll("*")) // 返回一个真正的数组
Array.of(1, 2, 3) // [1,2,3]
[0, 0, 0].fill(7, 1) // [0,7,7]
[1,2,3].findIndex(x => x == 2) // 1
["a", "b", "c"].entries() // [0, "a"], [1,"b"], [2,"c"]
["a", "b", "c"].keys() // 0, 1, 2
["a", "b", "c"].values() // "a", "b", "c"

Object.assign(Point, { origin: new Point(0,0) }) // 合并对象
复制代码

二进制和八进制字面量(Binary and Octal Literals)

0b111 === 7 // true  二进制
0o111 === 73 // true  八进制
0x111 === 273 // true  十六进制
复制代码

进阶篇

尾递归(Tail Calls)

假设现在要实现一个阶乘函数,即 5!= 120 ,我们很容易想到递归实现

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}
复制代码

但递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误( stack overflow )。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

何为调用记录,在示例代码中,由于最后一步返回了一个表达式,内存会保留 n 这个变量的信息和 factorial(n - 1) 调用下一次函数的位置,形成一层层的调用栈

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身,返回函数本身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。尾递归优化如下

function factorial(n, acc = 1) {
    "use strict";
    if (n <= 1) return acc;
    return factorial(n - 1, n * acc);
}

factorial(100000)
复制代码

由此可见,"尾调用优化"对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 也是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署"尾调用优化"。这就是说,在 ES6 中,只要使用尾递归,就不会发生栈溢出,相对节省内存。

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。

反射(Reflect)

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。 Reflect 对象的设计目的有这样几个。

  • 将Object对象的一些明显属于语言内部的方法(比如 Object.defineProperty ),放到 Reflect 对象上
  • 修改某些 Object 方法的返回结果,让其变得更合理。比如, Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false
var O = {a: 1};
Reflect.defineProperty(O, 'b', {value: 2});
O[Symbol('c')] = 3;

Reflect.ownKeys(O); // ['a', 'b', Symbol(c)]
Reflect.getOwnPropertyDescriptor(O, 'b'); 
// { value: 2, writable: false, enumerable: false, configurable: false }
function C(a, b){
  this.c = a + b;
}
var instance = Reflect.construct(C, [20, 22]);
instance.c; // 42
复制代码

获取属性名的方法有很多,以上面的代码为例子,它们的区别如下

方法 结果 解释
Object.getOwnPropertyNames(O) [ 'a', 'b' ] 获取除 Symbol 外的所有属性
Object.getOwnPropertySymbols(O) [ Symbol(c) ] 只获取 Symbol 属性
OReflect.ownKeys(O) [ 'a', 'b', Symbol(c) ] 获取所有属性
for...in a 获取除 Symbol 外的可枚举属性

代理(Proxy)

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。 Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

// 代理一个对象
var target = {};
var handler = {
  get: function (receiver, name) {
    return `Hello, ${name}!`;
  }
};

var p = new Proxy(target, handler);
p.world; // "Hello, world!"
复制代码
// 代理一个函数
var target = function () { return "I am the target"; };
var handler = {
  apply: function (receiver, ...args) {
    return "I am the proxy";
  }
};

var p = new Proxy(target, handler);
p(); // "I am the proxy"
复制代码
// 代理会将所有应用到它的操作转发到这个对象上
let target = {};
let p = new Proxy(target, {});

p.a = 37; 
target.a; // 37 操作转发到目标
复制代码
// 如何实现 a == 1 && a == 2 && a == 3,利用Proxy的get劫持
const a = new Proxy(
  {},
  {
    val: 1,
    get() {
      return () => this.val++;
    }
  }
);
a == 1 && a == 2 && a == 3; // true
复制代码

由于 ES5 的限制,Proxy 不能被 transpiled or polyfilled,自己亲自入的坑,由于在项目中使用了 Mobx5.x,其内部是用 Proxy 写的,结果 IE11 不支持 ES6,只得回退版本 Mobx 到 4.x

生成器(Generators)

Generator 函数是 ES6 提供的一种异步编程解决方案。 Generator 函数有多种理解角度。语法上,首先可以把它理解成, Generator 函数是一个状态机,封装了多个内部状态。

形式上, Generator 函数是一个普通函数,但是有两个特征。一是, function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态

function* helloWorldGenerator() {
  yield 'hello'; // yield使Generator函数暂停了执行,并将结果返回给调用者
  yield 'world'; // 当下一次调用时,从它中断的地方恢复执行
  return 'ending';
}

var hw = helloWorldGenerator();
a = hw.next(); // { value: 'hello', done: false }
b = hw.next(); // { value: 'world', done: false }
c = hw.next(); // { value: 'ending', done: true }
复制代码

可以利用这种暂停执行的特性,来实现惰性求值

向Generator传递数据

function* sayFullName() {
  const firstName = yield;
  const secondName = yield;
  console.log(firstName + ' ' + secondName);
}
let fullName = sayFullName();
fullName.next(); 
// 第一次调用,代码暂停在 const firstName = yield,因为没有通过 yield 发送任何值,因此 next 将返回 undefined
fullName.next('Handsome');
// 第二次调用,传入了值 Handsome,yield 被 Handsome 替代,因此 firstName 的值变为 Handsome,代码执行恢复
// 直到再次遇到 const secondName = yield 暂停执行
fullName.next('Jack');
// 第三次调用,传入了值 Jack,yield 被 Jack 替代,因此 secondName 的值变为 Jack,代码执行恢复
// 打印 Handsome Jack
复制代码

使用Generator处理异步调用

let generator;
let getDataOne = () => {
  setTimeout(() => {
    generator.next('dummy data one');
  }, 1000);
};
let getDataTwo = () => {
  setTimeout(() => {
    generator.next('dummy data one');
  }, 1000);
};

function* main() {
  let dataOne = yield getDataOne();
  let dataTwo = yield getDataTwo();
  console.log(dataOne, dataTwo);
}
generator = main();
generator.next();
// 执行 getDataOne(),然后 yield 暂停
// 直至一秒后 generator.next('dummy data one') 恢复代码执行,并赋值 dataOne
console.log('i am previous print');
// i am previous print
// dummy data one dummy data one
复制代码

Promises

Promises 是一个异步编程的解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

所谓 Promise ,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说, Promise 是一个对象,从它可以获取异步操作的消息。 Promise 提供统一的 API ,各种异步操作都可以用同样的方法进行处理。

什么?ES6 中还有 Tail Calls!
function timeout(duration = 0) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, duration);
    })
}

var p = timeout(1000).then(() => {
    return timeout(2000);
}).then(() => {
    throw new Error("hmm");
}).catch(err => {
    return Promise.all([timeout(100), timeout(200)]);
})
复制代码

这里强调几点

  • 不要剥夺函数 return 的能力,很多人写 Promise ,照样有大量嵌套,掉进 Promise 地狱,要记得及时 return ,避免嵌套
  • 当需要多个请求全部结束时,才更新数据,可以用 Promise.all(fetch1,fetch2)
  • 当需要从多个请求中,接受最先返回数据的那个请求,可以用 Promise.race(fetch1,fetch2)

结尾

ES6 是 ECMAScript 一个非常重要的版本,我们必须深入理解,不仅能提高我们书写代码的能力,还能增强业务能力

附上一张我之前精心整理的思维导图

什么?ES6 中还有 Tail Calls!

本文参考资料


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Usability for the Web

Usability for the Web

Tom Brinck、Darren Gergle、Scott D. Wood / Morgan Kaufmann / 2001-10-15 / USD 65.95

Every stage in the design of a new web site is an opportunity to meet or miss deadlines and budgetary goals. Every stage is an opportunity to boost or undercut the site's usability. Thi......一起来看看 《Usability for the Web》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具