学习js入门之ES6(新增的函数和语法)

ES6的新语法和功能:

一、新的原始类型和变量声明
1,symbol
在ES6之前,我们知道

JavaScript支持8种数据类型:Object,String,Boolean,Number,Null,Undefined、Array、Function。现在,ES6新增了一种原始数据类型:symbol,表示独一无二的值,即每个symbol类型的值都不相同。这让我想起了另一个特殊的值:NaN,想一想,他们是不是有一点类似呢!

var sy = Symbol(‘test’);
var sy1 = Symbol(‘test’);
console.log(tepeof sy); //‘symbol’
sy == sy1; //false
var sy2 = new Symbol(‘test’); //error : Symbol is not a constructor
创建symbol数据类型的值时,需要给Symbol函数传递一个字符串,并且有一点特殊的是:不能使用new关键字调用它。另外,每个symbol类型值都是独一无二的,即使传递的是相同的字符串。

2,let和const

ES6新增了两个声明变量的关键字:let和const。

他们声明的变量仅在let和const关键字所在的代码块内起作用,即在使用let和const的那一对大括号{}内起作用,也称块级作用域(ES6之前只有函数作用域和全局作用域)。

let和const声明变量不会在预编译过程中有提升行为(在全局声明也不会变成window的属性),且同一变量不能重复声明。所以要使用这类变量,只能在let和const关键字之后使用它们。

let和const关键字还有一个特性:“暂时性死区”,即在使用了该关键字的块级作用域中,其内部使用let和const关键字声明的变量与外部作用域中的变量相互隔绝,互不影响。即使是同名变量。

var a = 1;
{
console.log(a); //error Cannot access ‘a’ before initialization
let a = 0;
console.log(a); //0
}
console.log(a); //1
 const用来声明一个常量,声明时必须赋值,且一旦声明就不能改变。

其实说const变量不能更改是不准确的,请看下面的例子:

const obj = {
name:‘ren’,
age:12
};
obj = {}; //error
obj.sex = male;
consol.log(obj); //{name:‘ren’,age:12;sex:‘male’}
const声明的如果是一个原始值,那么上面的说法是准确的,如果const声明的是一个引用值,那么更准确的说法应该是一个不能被重新赋值的变量。

3,解构赋值

解构赋值是对赋值运算符的扩展。它是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。

let [a,b,c] = [1,2,3];
console.log(a,b,c); //1,2,3

let [a,b,c] = [1,3];
console.log(a,b,c); //1,undefined,3

let [a,b] = [1,2,3];
console.log(a,b);//1,3

let [a,…b] = [1,2,3]; //…是剩余运算符,表示赋值运算符右边除第一个值外剩余的都赋值给b
console.log(a,b);//1,[2,3]
事实上所有可枚举(iterable)的对象都可以使用解构赋值,例如数组,字符串对象,以及ES6新增的Map和Set类型。

let arr = ‘hello’;
let [a,b,c,d,e] = arr;
console.log(a,b,c,d,e); //‘h’,‘e’,‘l’,‘l’,‘o’
对象的解构赋值和数组类似,不过左边的变量名需要使用对象的属性名,并且用大括号{}而非中括号[]:

二  新的对象和方法
1,Map和Set

Map对象用于保存键值对,任何值JavaScript支持的值都可以作为一个键或者一个值。这听起来和对象差不多啊?其实它们还是有区别的:

a) object的键只能是字符串或ES6的symbol值,而Map可以是任何值。

b) Map对象有一个size属性,存储了键值对的个数,而object对象没有类似属性

let myMap = new Map([[‘name’,‘ren’],[‘age’,12]]);
console.log(myMap); //{‘name’=>‘ren’,‘age’=>12}
myMap.set(‘sex’,‘male’);
console.log(myMap); //{‘name’=>‘ren’,‘age’=>12,‘sex’=>‘male’}
myMap.get(‘name’); //‘ren’
myMap.has(‘age’); //true
myMap.delete(‘age’); //true
myMap.has(‘age’); //false
myMap.get(‘age’); //undefined

Map构造函数接收一个二维数组来创建一个Map对象。数组元素的第0位表示Map对象的key,第1位表示Map对象的value。

Map对象使用set方法来新增数据,set方法接收两个参数,第一个表示key,第二个表示value。使用get方法获取数据,参数是对象的key。

Map对象使用delete方法来删除数据,接收一个参数,表示需要被删除的key。

Map对象使用has方法检测是否已经具有某个属性,返回boolean值。

Set对象和Map对象类似,但它是用来存储一组唯一值的,而不是键值对。类似数组,但它的每个元素都是唯一的。

let mySet = new Set([1,2,3]);
console.log(mySet); //{1,2,3}
mySet.add(4);
console.log(mySet); //{1,2,3,4}
mySet.delete(1); //true
mySet.has(1); //false
利用Set对象唯一性的特点,可以轻松实现数组的去重:

let arr = [1,1,2,3,4,4];
let mySet = new Set(arr);
let newArr = Array.from(mySet);
console.log(newArr); //[1,2,3,4]
2,对象新特性

创建对象的字面量方式可以更加简洁。直接使用变量名作为属性,函数体作为方法,最终变量值变成属性值,函数名变成方法名。

let name = ‘ren’;
let age = 12;
let myself = {
name,
age,
say(){
console.log(this.name);
}
};
console.log(myself); //{name:‘ren’,age:12,say:fn}
myself.say(); //‘ren’
对象的拓展运算符(…)三点。用于拷贝目标对象所有可遍历的属性到当前对象。

let obj = {name:‘ren’,age:12};
let person = {…obj};
console.log(person);//{name:‘ren’,age:12}
obj == person;//false
let another = {sex:‘male’};
let someone = {…person,…another};//合并对象
console.log(someone);//{name:‘ren’,age:12,sex:‘male’}
ES6对象新增了两个方法,assign和is。

assign用于浅拷贝源对象可枚举属性到目标对象。

let source = {a:{ b: 1},b: 2};
let target = {c: 3};
Object.assign(target, source);
console.log(target); //{c: 3, a: {b:1}, b: 2}
source.a.b = 2;
console.log(target.a.b); //2
如果有同名属性,那么目标对象的属性值会被源对象的属性值覆盖。所以数组的表现就有一点特别了:

Object.assign([1,2,3],[11,22,33,44]);//[11,22,33,44]
数组的index就是属性名,当使用assign方法时,从第0位开始,目标数组的值便开始被源数组的值覆盖了。

is方法和(===)功能基本类似,用于判断两个值是否绝对相等。

Object.is(1,1);//true
Object.is(1,true);//false
Object.is([],[]);//false
Object.is(+0,-0);//false
Object.is(NaN,NaN);//true
他们仅有的两点区别是,is方法可以区分+0还是-0,还有就是它认为NaN是相等的。

3,字符串新方法

includes()判断字符串是否包含参数字符串,返回boolean值。如果想要知道参数字符串出现的位置,还是需要indexOf或lastIndexOf方法。

startsWith()/endsWith(),判断字符串是否以参数字符串开头或结尾。返回boolean值。这两个方法可以有第二个参数,一个数字,表示开始查找的位置。

let str = ‘blue,red,orange,white’;
str.includes(‘blue’);//true
str.startsWith(‘blue’);//true
str.endsWith(‘blue’);//false
repeat()方法按指定次数返回一个新的字符串。如果次数是大于0的小数则向下取整,0到-1之间的小数则向上取整,其他负数将抛出错误。

console.log(‘hello’.repeat(2));//‘hellohello’
console.log(‘hello’.repeat(1.9));//‘hello’
console.log(‘hello’.repeat(-0.9));//‘’
console.log(‘hello’.repeat(-1.9));//error
padStart()/padEnd(),用参数字符串按给定长度从前面或后面补全字符串,返回新字符串。

let arr = ‘hell’;
console.log(arr.padEnd(5,‘o’)); //‘hello’
console.log(arr.padEnd(6,‘o’)); //‘helloo’
console.log(arr.padEnd(6)); //'hell ',如果没有指定将用空格代替
console.log(arr.padStart(5,‘o’)); //‘ohell’
另外,如果字符串加上补全的字符串超出了给定的长度,那么,超出的部分将被截去。

4,数组的新方法

of()是ES6新增的用于创建数组的方法。of把传入的参数当做数组元素,形成新的数组。

let arr = Array.of(1,‘2’,[3],{});
console.log(arr); //[1,‘2’,[3],{}]
from()方法可以将可迭代对象转换为新的数组。函数可接受3个参数:第一个表示将被转换的可迭代对象,第二个是回调函数,将对每个数组元素应用该回调函数,然后返回新的值到新数组,第三个是回到函数内this的指向。后两个参数是可选的。

let obj = {
double(n) {
return n * 2;
}
}
let arr = [1, 2, 3];
console.log(Array.from(arr, function (n){
return this.double(n);
}, obj)); // [2, 4, 6]

find()和findIndex(),查找数组中符合条件的元素值或索引,方法不会修改原数组。

接受一个回调函数作为参数,函数可以接受四个参数,分别是当前遍历到的元素,当前遍历到的索引,数组本身以及函数内this的指向。方法会把回调函数作用于每一个遍历到的元素,如果遍历到某一个元素可以使回调函数返回true,那么find方法会立即返回该元素,findIndex方法会返回该元素的索引。并终止继续遍历。

如果有多个符合条件的,也将只返回第一个。如果遍历完整个数组也无法是回调函数返回true,那么find方法将返回undefined,findIndex方法将返回-1。

let arr = [1,2,3,4,5];
console.log(arr.find((ele) => {
return ele === 1;
}));//1
console.log(arr.findIndex((ele) => {
return ele > 4;
})); //4
fill()/copyWithin(),替换数组中部分元素,会修改原数组。

let arr = [1,2,3,4,5];
console.log(arr.fill(0,0,3));//[0,0,0,4,5]
//参数1表示目标值,参数2,3表示替换的始末位置,左闭右开区间。
console.log(arr.copyWithin(0,2,4));//[0,4,0,4,5]
//参数1表示修改的起始位置,参数2,3表示用来替换的数据的始末位置,左闭右开区间。
fill()用指定的值替换,copyWithin()使用数组中原有的某一部分值替换。

includes()用于检测数组是否包含某个值,可以指定开始位置。

let arr = [1,2,3,4,5];
console.log(arr.includes(2));//true
console.log(arr.includes(1,1));//false
三、函数
1,参数默认值

ES6首次添加了参数默认值。我们再也不用在函数内部编写容错代码了

function add(a=1,b=2){
return a + b;
}
add();//3
add(2);//4
add(3,4);//7
和参数默认值一起,ES6还带来了不定参。它的功能和使用arguments差不多。

function add(…num){
return num.reduce(function(result,value){
return result + value;
});
}
add(1,2,3,4);//10
下面介绍的箭头函数没有arguments属性,如果箭头函数内要实现不定参,上述方式就是一个不错的选择了。

2,箭头函数

箭头函数实现了一种更加简洁的书写方式,并且也解决了关键字声明方式的一些麻烦事儿。箭头函数内部没有arguments,也没有prototype属性,所以不能用new关键字调用箭头函数。

箭头函数的书写方式:参数 => 函数体。

let add = (a,b) => {
return a+b;
}
let print = () => {
console.log(‘hi’);
}
let fn = a => a * a;
//当只有一个参数时,括号可以省略,函数体只有单行return语句时,大括号也可以省略,强烈建议不要省略它们,是真的难以阅读
当函数需要直接返回对象时,你必须使用小括号把对象包裹起来。否则将抛出错误。

const fn = () =>{name:‘ren’,age:12};
// SyntaxError


const fn = () =>({name:‘ren’,age:12});
 箭头函数和普通函数最大的区别在于其内部this永远指向其父级AO对象的this。

普通函数在预编译环节会在AO对象上添加this属性,保存一个对象(请参照《JavaScript之深入对象(二)》)。每个普通函数在执行时都有一个特定的this对象,而箭头函数执行时并不直接拥有this属性,如果你在箭头函数中使用this,将根据函数作用域链,直接引用父级AO对象上this绑定的对象。普通函数的AO对象只有在函数执行时才产生,换言之,普通函数的this是由函数执行时的环境决定。而箭头函数的特别之处在于,当函数被定义时,就引用了其父级AO对象的this,即箭头函数的this由定义时的环境决定。

根据箭头函数的特点,不难推测:如果定义对象的方法直接使用箭头函数,那么函数内的this将直接指向window。

var age = 123;
let obj = {
age:456,
say:() => {
console.log(this.age);
}
};
obj.say(); //123
//对象是没有执行期上下文的(AO对象),定义对象的方法实际上是在全局作用域下,即window
如果你一定要在箭头函数中让this指向当前对象,其实也还是有办法的(但是没必要这么麻烦啊,直接使用普通函数不是更好吗?):

var age = 123;
let obj = {
age:456,
say:function(){
var fn = () => {
console.log(this.age);
}
return fn();
}
};
obj.say(); //456

我们来分析一下这是怎么做到的:首先,我们使用obj调用say方法时,say内创建了AO对象,并且该AO对象的this属性指向了obj(这里不明白的请回去复习一下我的《JavaScript之深入函数/对象》),然后,say内部又声明了一个箭头函数。我们说箭头函数在声明时就要强行引用父级AO的this属性,那么现在该箭头函数的父级AO是谁呢?当然就是say的AO啦,所以这里箭头函数的this直接就绑定了obj,最后箭头函数在执行时拿到的this,实际上就是say方法的AO.this,即obj本身。

上面是在对象中使用箭头函数,如果那让你难于理解,那么请看下面这种方式:在普通函数中使用箭头函数。

var obj = {name:‘ren’};
function test(){
var fn = () => {
console.log(this);
};
fn();
}
test(); //window
test.call(obj); //{name:‘ren’}
test函数在全局执行时,其this指向window,这时也产生了箭头函数的定义,于是箭头函数内的this也被指向了window,所以最终打印出window对象。

当我们手动改变test函数执行时this的指向时,箭头函数定义所绑定的this实际上也被我们修改了。所以最终打印出obj。
四、class(类)

class 作为对象的模板被引入ES6,你可以通过 class 关键字定义类。class 的本质依然是一个函数。

1,创建类

class Ex { //关键字声明方式
constructor(name){
this.name = name;
this.say = () => {
console.log(this.name);
}
}
methods(){
console.log('hello ’ + this.name);
}
static a = 123;
static m = () => {
console.log(this.a);
};
}
//let ex = class{} 字面量方式
var example = new Ex(‘ren’);
example.say(); //‘ren’
Ex.m(); //123
example.methods(); //‘hello ren’

constructor是创建类必须的方法,当使用new调用类创建实例时,将自动执行该方法,该方法和构造函数类似,默认返回this对象。实例的方法和属性都定义在constructor内部。相当于构造函数的this方式
类保留了prototype属性,类中的方法不需要使用function关键字,并且方法之间不需要逗号隔开。类中定义的方法实际上还是保存在类的prototype属性上。
使用static关键字定义类的静态属性和方法。类中不能定义共有属性,要想定义实例的共有属性还是需要使用prototype属性:Ex.prototype.属性名 = 属性值。

创建实例依然使用new关键字。

2、类的继承

类的继承通过extends关键字实现。
class Person {
constructor (name,age){
this.name = name;
this.age = age;
}
say(){
console.log(this.name + ‘:’ + this.age);
}
}
class Student extends Person{
constructor (name,age,sex){
super(name,age);
this.sex = sex;
}
}
var student = new Student(‘ren’,12,‘male’);
student.name; //‘ren’
student.sex; //‘male’
student.say(); //‘ren:12’

子类继承自父类,不会隐式的创建自己的this对象,而是通过super()引用父类的this。这个过程和在子构造函数内使用父构造函数call(this)很像,但他们有本质的区别。另外,ES6规定,super()必须在子类的this之前执行。所以一般我们把super()放在子类constructor方法的第一行,这样准没错!

五、模块导入和导出

1,导入

ES6使用关键字 import 导入模块(文件),有两种常用的方式:

import ‘模块名称’ from ‘路径’;
import ‘路径’;
通过 import…from 的方式引入模块,模块名称实际上相当于定义一个变量,用来接收即将导入的模块。

路径可以有很多方式,既可以是绝对路径,也可以是相对路径,甚至只是一个简单的模块名称,更甚至连文件后缀都不需要。当你使用该命令时,系统会自动从配置文件中指定的路径中去寻找并加载正确的文件。

import Vue from “vue”;
//完整路劲其实是 “…/node_modules/vue/dist/vue.js”;
通过 import… 的方式一般用来引入样式文件或预处理文件,因为他们不需要用变量来接收。

2,导出

ES6 通过 export 和export default 导出模块。导出的含义是向外暴露、输出,在一个文件中通过 import 导入另一个文件,通过变量即可以接收到导出的数据了。一般情况下,JS文件可以向外输出变量、函数和对象。

let name = ‘ren’,age = 12;
export {name,age};
//注意:变量需要用大括号包裹,然后才能向外输出
如果仅需向外暴露一个变量:

export var name = ‘ren’;

使用 export 向外输出函数的用法和变量相同,这里不再举例。

总结:使用 export 向外输出成员时,可以同时输出多个,并且必须用‘{}’大括号包裹,在其他地方使用 import 导入时,接收成员的变量名必须和这里输出的名称一致,同时,可以根据实际情况,仅接收实际需要的的成员(接收的时候也要用大括号包裹)。

如果希望通过 export 向外暴露成员,并且在导入的时候自定义接收名称,那么你可以使用 as 关键字重命名。

let name = ‘ren’, age = 12;
export {name, age};

import {name as myName, age as myAge} from ‘url’;
与 export 相比,export default 有以下几点不同:首先,在同一个模块中,export default 只允许向外暴露一次成员;然后,这种方式可以使用任意的名称接收,不像 export 那样有严格的要求;最后,export 和 export default 可以在同一模块中同时存在。

let person = {name:‘ren’};
let age = 12;
let address = ‘cd’;
export default person;
export {age};
export {address};

import man,{age,address} from ‘url’
小技巧:通常 import 无法直接被浏览器识别,即如果在HTML文档中引入的 JS 文件直接使用了 import 关键字,浏览器会报错。要想直接在被HTML文档引用的 JS 文件中使用 import,需要给该

六  异步机制

ES6新增了两种实现异步的新机制,Promise和Generator。

Promise
什么是Promise
Promise 是异步编程的一种解决方案,比传统的解决方案“回调函数和事件”更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

Promise的特点
Promise有两个特点:

对象的状态不受外界影响。
Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。

这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Promise的优点
Promise将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise的缺点
无法取消Promise,一旦新建它就会立即执行,无法中途取消。

如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。

当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Promise的用法
Promise对象是一个构造函数,用来生成Promise实例:

var promise = new Promise(function(resolve, reject) {
// … some code
if (/* 异步操作成功 */){
resolve(value);
} else { reject(error); }
}
);
promise可以接then操作,then操作可以接两个function参数,第一个function的参数就是构建Promise的时候resolve的value,第二个function的参数就是构建Promise的reject的error。

promise.then(function(value) {
// success
}, function(error) {
// failure }
);
我们看一个具体的例子:

function timeout(ms){
return new Promise(((resolve, reject) => {
setTimeout(resolve,ms,‘done’);
}))
}

timeout(100).then(value => console.log(value));
Promise中调用了一个setTimeout方法,并会定时触发resolve方法,并传入参数done。

最后程序输出done。

Promise的执行顺序
Promise一经创建就会立马执行。但是Promise.then中的方法,则会等到一个调用周期过后再次调用,我们看下面的例子:

let promise = new Promise(((resolve, reject) => {
console.log(‘Step1’);
resolve();
}));

promise.then(() => {
console.log(‘Step3’);
});

console.log(‘Step2’);

输出:
Step1
Step2
Step3
Promise.prototype.then()
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法.

getJSON(“/users.json”).then(function(json){
return json.name;
}).then(function(name){
console.log(name);
});
上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数

Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。

getJSON(“/users.json”).then(function(json){
return json.name;
}).catch(function(error){
console.log(error);
});
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获

getJSON(“/users.json”).then(function(json){
return json.name;
}).then(function(name){
console.log(name);
}).catch(function(error){
//处理前面所有产生的错误
console.log(error);
});
Promise.all()
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例

var p = Promise.all([p1,p2,p3]);
只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.race()
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例

var p = Promise.race([p1,p2,p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数.

Promise.resolve()
Promise.resolve()将现有对象转为Promise对象.

Promise.resolve(‘js’);
//等价于
new Promise(resolve => resolve(‘js’));
那么什么样的对象能够转化成为Promise对象呢?

参数是一个Promise实例
参数是一个thenable对象
参数不是具有then方法的对象,或根本就不是对象
不带有任何参数
Promise.reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

var p = Promise.reject(‘error’);
//等价于
var p = new Promise((resolve,reject) => reject(‘error’));
Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致

done()
Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误

asyncFunc().then(f1).catch(f2).then(f3).done();
finally()
finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行.

server.listen(1000).then(function(){
//do something
}.finally(server.stop);
Generator
什么是Generator
Generator 函数是 ES6 提供的一种异步编程解决方案

从语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态

执行 Generator 函数会返回一个遍历器对象.

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

举个例子:

function * helloWorldGenerator(){
yield ‘hello’;
yield ‘world’;
return ‘ending’;
}

var gen = helloWorldGenerator();
输出结果:

console.log(gen.next());
console.log(gen.next());
console.log(gen.next());

{ value: ‘hello’, done: false }
{ value: ‘world’, done: false }
{ value: ‘ending’, done: true }
yield
遍历器对象的next方法的运行逻辑如下:

(1)遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。

(3)如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

注意,yield句本身没有返回值,或者说总是返回undefined。

next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。

function * f() {
for( let i =0; true; i++){
let reset = yield i;
if(reset){
i = -1;
}
}
}

let g = f();
console.log(g.next());
console.log(g.next());
console.log(g.next(true));
输出结果:

{ value: 0, done: false }
{ value: 1, done: false }
{ value: 0, done: false }
可以看到最后的一步,我们使用next传入的true替代了i的值,最后导致i= -1 + 1 = 0.

我们再看一个例子:

function * f2(x){
var y = 2 * ( yield ( x + 1));
var z = yield (y / 3);
return (x + y + z);
}

var r1= f2(5);
console.log(r1.next());
console.log(r1.next());
console.log(r1.next());

var r2= f2(5);
console.log(r2.next());
console.log(r2.next(12));
console.log(r2.next(13));
输出结果:

{ value: 6, done: false }
{ value: NaN, done: false }
{ value: NaN, done: true }

{ value: 6, done: false }
{ value: 8, done: false }
{ value: 42, done: true }
如果next不传值的话,yield本身是没有返回值的,所以我们会得到NaN。

但是如果next传入特定的值,则该值会替换该yield,成为真正的返回值。

yield *
如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的

function * a1(){
yield ‘a’;
yield ‘b’;
}

function * b1(){
yield ‘x’;
a1();
yield ‘y’;
}

for(let v of b1()){
console.log(v);
}
输出结果:

x
y
可以看到,在b1中调用a1是没有效果的。

将上面的例子修改一下:

function * a1(){
yield ‘a’;
yield ‘b’;
}

function * b1(){
yield ‘x’;
yield * a1();
yield ‘y’;
}

for(let v of b1()){
console.log(v);
}
输出结果:

x
a
b
y
异步操作的同步化表达
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

我们看一个怎么通过Generator来获取一个Ajax的结果。

function * ajaxCall(){
let result = yield request(“http://www.flydean.com”);
let resp = JSON.parse(result);
console.log(resp.value);
}

function request(url){
makeAjaxCall(url, function(response){
it.next(response);
});
}

var it = ajaxCall();
it.next();
我们使用一个yield来获取异步执行的结果。但是我们如何将这个yield传给result变量呢?要记住yield本身是没有返回值的。

我们需要调用generator的next方法,将异步执行的结果传进去。这就是我们在request方法中做的事情。

Generator 的异步应用
什么是异步应用呢?

所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。

相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。

ES6诞生以前,异步编程的方法,大概有下面四种。
回调函数
事件监听
发布/订阅
Promise 对象

回调函数
fs.readFile(fileA, ‘utf-8’, function(error,data){
fs.readFile(fileB, ‘utf-8’, function(error,data){
}
})
如果依次读取两个以上的文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"(callback hell)。

Promise
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。

let readFile = require(‘fs-readfile-promise’);
readFile(fileA).then(function(){
return readFile(fileB);
}).then(function(data){
console.log(data);
})
Thunk函数和异步函数自动执行
在讲Thunk函数之前,我们讲一下函数的调用有两种方式,一种是传值调用,一种是传名调用。

“传值调用”(call by value),即在进入函数体之前,就计算x + 5的值(等于6),再将这个值传入函数f。C语言就采用这种策略。

“传名调用”(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。

编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

举个例子:

function f(m){
return m * 2;
}

f(x + 5);
上面的代码等于:

var thunk = function () {
return x + 5;
}
function f(thunk){
return thunk() * 2;
}
在 JavaScript 语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

怎么解释呢?

比如nodejs中的:

fs.readFile(filename,[encoding],[callback(err,data)])
readFile接收3个参数,其中encoding是可选的。我们就以两个参数为例。

一般来说,我们这样调用:

fs.readFile(fileA,callback);
那么有没有办法将其改写成为单个参数的function的级联调用呢?

var Thunk = function (fn){
return function (…args){
return functon (callback){
return fn.call(this,…args, callback);
}
}
}

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
可以看到上面的Thunk将两个参数的函数改写成为了单个参数函数的级联方式。或者说Thunk是接收一个callback并执行方法的函数。

这样改写有什么用呢?Thunk函数现在可以用于 Generator 函数的自动流程管理。

之前在讲Generator的时候,如果Generator中有多个yield的异步方法,那么我们需要在next方法中传入这些异步方法的执行结果。

手动传入异步执行结果当然是可以的。但是有没有自动执行的办法呢?

let fs = require(‘fs’);
let thunkify = require(‘thunkify’);
let readFileThunk = thunkify(fs.readFile);

let gen = function * (){
let r1 = yield readFileThunk(‘/tmp/file1’);
console.log(r1.toString());

let r2 = yield readFileThunk('/tmp/file2');
console.log(r2.toString());

}

let g = gen();

function run(fn){
let gen = fn();

function next (err, data){
    let result = gen.next(data);
    if(result.done) return;
    result.value(next);
}
next();

}

run(g);
gen.next返回的是一个对象,对象的value就是Thunk函数,我们向Thunk函数再次传入next callback,从而出发下一次的yield操作。

有了这个执行器,执行Generator函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入run函数即可。当然,前提是每一个异步操作,都要是Thunk函数,也就是说,跟在yield命令后面的必须是Thunk函数。

总结
Promise和Generator是ES6中引入的非常中要的语法,
koa框架就是Generator的一种具体的实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值