ES6 学习教程与demo-case总结

学习序

1.前端技术

十年前说前端,一定是Flash、Html、Js、Css,这是我工作的时候,前端接触最普遍的技术,Jquery可以说是很新的技术了。

但发展到今天的前端是一个范围很广的技术领域,发展迭代之快,衍生框架之多,可以说其他语言是不能媲美的,这也说明了前端技术是一个活跃的、标准难制定的领域。

这也直接推动了前端技术栈的发展,包括JS的UI框架、模块化的MVVM框架及构建工具、JavaScript 编译器、CSS的预处理器、HTML5技术。

常用的JS相关框架:
Ext.js、Jquery、Jquery MiniUI、Jquery EasyUI (解决浏览器兼容)
React、Angular 2、 Vue(MVVM框架)
RequireJS、 SeaJS、Webpack
Babel、CoffeeScript、 TypeScript
Less、Saas

2.移动开发技术

App(Application 应用程序)一般是指手机端的软件。手机端的操作系统有iOS、Android、Windows Phone、Fuchsia(Google下一代手机操作系统)
各系统的开发语言有:
iOS --- Object-C、Swift
Android ----Java、Kotlin
Fuchsia ---- Dart
上面都可以称为原生开发,移动端技术发展也是提高生产力的过程,出现了跨平台开发的框架,一套代码多端运行,现在常见的开发框架有H5、小程序、uni-app(VUE跨平台框架)、Vue-Native、weex、React-Native、Flutter

3.学习基础的重要性

学习好这些基础是写出合理代码的重要支撑。

实际工作中可能受限于工期和领导的压力,我们在基础的学习上都是蜻蜓点水,急急忙忙的做出效果,导致代码频出bug,这点我是亲身经历过的痛。

去年由原生开发转Native的同事,es6看了一礼拜,告诉我已经学的差不多了,然后让他加入了项目组开始开发,一天的折腾后,临下班前说界面输入的文本,在传参的时候获取不到,找不到问题所在,然后加入log输入参数

我和他一起看打印出的参数,他义正言辞的说没问题,我看了知道是结构赋值的问题。让他重新看了看解构赋值,多想想以前的json取值

结果再过两天又犯了同样的错误,抱怨半天。其实还是基础没理解好,这会很影响开发效率。

废话不多说,祝君学习顺利 !


1. 环境搭建

Node.js是运行在服务端的JavaScript环境,基于Google的V8引擎,下载地址Node.js 安装包及源码下载地址为:https://nodejs.org/en/download/

Node.js 历史版本下载地址:https://nodejs.org/dist/

具体的安装教程:https://www.runoob.com/nodejs/nodejs-install-setup.html
安装完成以后,以windows为例,打开cmd窗口 执行node -v查看,输出版本号说明安装成功。
成功之后就可以使用 npm 命令来管理安装的依赖包了。也可使用 yarn

npm install -g yarn

由于某些安装包有可能安装失败的风险,做React Native的同学应该深有体会,原因当然是天朝网络的问题了,所以我们需要安使用国内源,cmd窗口执行

npm config set registry https://registry.npm.taobao.org --global
npm config set disturl https://npm.taobao.org/dist --global

或

yarn config set registry https://registry.npm.taobao.org --global
yarn config set disturl https://npm.taobao.org/dist --global

好多人推荐使用 yarn,原因是npm的版本管理有个安装不一致的问题,比如 ~1.0.3 指的是 1.0.X的最新版,^1.0.3是指1.X.X的最新版,很容易导致一个月前项目跑起来好好的,来一个新同事,项目跑起来有问题,我相信入坑多年的都有这种经历。yarn的出现也解决了这个痛点(

其实npm 已经用package-lock.json解决了这个问题。用哪个看你喜好
这是常用的操作命令

npm yarn
npm install yarn
npm install react --save yarn add react
npm uninstall react --save yarn remove react
npm install react --save-dev yarn add react --dev
npm update --save yarn upgrade

2. let 与 const

代码块有效

  var PI = "a";
  {
     console.log(PI) // 报错
     let a = 0;
     var b = 1;
 }
 console.log(b)

let 只能声明一次 var 可以声明多次

 let a = 1;
 let a = 2;   //  报错
 var b = 3;
 var b = 4;
 console.log(b)

const 是声明常量,相当于java中final声明

const PI = "3.1415926";
PI = "dadada" // 报错
console.log(PI)

3. 解构赋值

  • 数组模型的解构(Array)
    情景1:
    let [a, b, c] = [1, 2, 3];
    // a = 1
    // b = 2
    // c = 3
    
    情景2:
    let [a, [\[b], c]] = [1, [\[2], 3]];
    // a = 1
    // b = 2
    // c = 3
    
    情景3:
    let [a, ...b] = [1, 2, 3]
    //a = 1
    //b = [2, 3]
    
    情景4:
    let [a, b, c] = [1, 2, 3];
    // a = 1
    // b = 2
    // c = 3
    
  • 对象模型的解构(Object)
    情景1:
    let { football, basketball } = { football: 'CSL', basketball: 'NBA' }
    // football = 'CSL'
    // basketball = 'NBA'
    
    情景2:
    const {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40};
    // a = 10
    // b = 20
    // rest = {c: 30, d: 40}
    

4. Symbol

这是一种新的基础数据类型,和Object、 Number 、 String 、 Boolean同级

  • 基本使用

    let sy = Symbol("zk");
    console.log(sy);    // Symbol(zk)
    typeof(sy);     // "symbol"
    let sy1 = Symbol("zk");     // 相同参数 Symbol() 返回的值不相等
    sy === sy1;     // false
    
  • 适用场景1
    使用symbol作为对象属性名(key)

    const param3 = Symbol()
    let obj = {
       param1: 100, 
       "param2": "egg",
       [param3]:"我是params33"
    }
    obj["param1"]   // 100
    obj["param2"]  // 'egg'
    const param4 = Symbol()
    obj[param4] = "我是param444"
    

    Symbol可同样用于对象属性的定义和访问:

    let obj = {
        [Symbol('name')]: '老师',
        age: 43,
        dept: '软件学院'
    }
    Object.keys(obj)    // ['age', 'title'] 
    
  • 适用场景2
    注册和获取全局Symbol,Symbol.for() 类似单例模式

    let instance = Symbol("Instance");
    let instance1 = Symbol.for("Instance");
    instance === instance1;      // false
     
    let instance2 = Symbol.for("Instance");
    instance1 === instance2;     // true
    

5. Map 与 Set

Map 的键值可以是任意值,开发通用是String 字符串

var myMap = new Map();
var keyString = "a string"; 
 
myMap.set(keyString, "和键'a string'关联的值");
  
myMap.get(keyString);    // "和键'a string'关联的值"
myMap.get("a string");   // "和键'a string'关联的值"
  • Map 迭代
    for...of

    var myMap = new Map();
    myMap.set(0, "zero");
    myMap.set(1, "one");
    
    // 将会显示两个 log。 一个是 "0 = zero" 另一个是 "1 = one"
    for (var [key, value] of myMap) {
    console.log(key + " = " + value);
    }
    
    // 将会显示两个log。 一个是 "0" 另一个是 "1"
    for (var key of myMap.keys()) {
    console.log(key);
    }
    /* 这个 keys 方法返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键。 */
    
    // 将会显示两个log。 一个是 "zero" 另一个是 "one"
    for (var value of myMap.values()) {
    console.log(value);
    }
    /* 这个 values 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值。 */
    

    forEach()

    var myMap = new Map();
    myMap.set(0, "zero");
    myMap.set(1, "one");
    
    // 将会显示两个 logs。 一个是 "0 = zero" 另一个是 "1 = one"
    myMap.forEach(function(value, key) {
        console.log(key + " = " + value);
    }, myMap)
    
  • Map 与 Array的转换

    var kvArray = \[["key1", "value1"], ["key2", "value2"]];
    
    // Map 构造函数可以将一个 二维 键值对数组转换成一个 Map 对象
    var newMap = new Map(kvArray);
    
    // 使用 Array.from 函数可以将一个 Map 对象转换成一个二维键值对数组
    var outArray = Array.from(newMap);
    
  • Set
    Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。

    +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复;
    undefined 与 undefined 是恒等的,所以不重复;

      let mySet = new Set();
     
      mySet.add(1); // Set(1) {1}
      mySet.add(5); // Set(2) {1, 5}
      mySet.add(5); // Set(2) {1, 5} 这里体现了值的唯一性
      mySet.add("some text");     // Set(3) {1, 5, "some text"} 这里体现了类型的多样性
     
      var o = {a: 1, b: 2}; 
      mySet.add(o);
      mySet.add({a: 1, b: 2}); 
      // Object即使一样,所以mySet的结构是 Set(5) {1, 5, "some text", {…}, {…}} 
    
  • Set 与 Array

    // Array 转 Set
    var mySet = new Set(["value1", "value2", "value3"]);
    // 用...操作符,将 Set 转 Array
    var myArray = [...mySet];
    
    // String 转 Set
    var mySet = new Set('hello');  // Set(4) {"h", "e", "l", "o"}
    

    数组去重的使用

    var mySet = new Set([1, 2, 3, 4, 4]);
    [...mySet]; // [1, 2, 3, 4]
    

6. 字符串

ES6 之前判断字符串是否包含子串,用 indexOf 方法,ES6 新增了子串的识别方法。

includes():返回布尔值,判断是否找到参数字符串。
startsWith():返回布尔值,判断参数字符串是否在原字符串的头部。
endsWith():返回布尔值,判断参数字符串是否在原字符串的尾部。

let string = "apple,banana,orange";
string.includes("banana");   // true
string.startsWith("apple");   // true
string.endsWith("apple");    // false
string.startsWith("banana",6) // true

这三个方法只返回布尔值,如果需要知道子串的位置,还是得用 indexOf 和 lastIndexOf 。

  • 模板字符串
    模板字符串相当于加强版的字符串,用反引号 `,除了作为普通字符串,还可以用来定义多行字符串,还可以在字符串中加入变量和表达式。

    let name = 'zk';
    let age = 18;
    let info =\`我的名字是 \${name},我今年 \${age+1} 岁了\`;
    

7. 对象

ES6允许对象的属性直接写变量,这时候属性名是变量名,属性值是变量值。

const age = 12;
const name = "Amy";
const person = {age, name};
//等同于
const person = {"age": age, "name": name}
  • 方法名也可以简写

    const person = {
    sayHi(){
        console.log("Hi");
    }
    }
    person.sayHi();  //"Hi"
    //等同于
    const person = {
        sayHi:function(){
            console.log("Hi");
        }
    }
    person.sayHi();//"Hi"
    
  • 拓展运算符(...)用于取出参数对象所有可遍历属性然后拷贝到当前对象

    let person = {name: "Amy", age: 15};
    let someone = { ...person };
    someone;  //{name: "Amy", age: 15}
    

    如果是相同的属性,后加入的属性覆盖前面的

    let person = {name: "Amy", age: 15};
    let someone = {name: "Mike", age: 17, ...person};
    someone;  //{name: "Amy", age: 15}
    

    let person = {name: "Amy", age: 15};
    let someone = {...person,name: "Mike", age: 17};
    someone;  //{name: "Mike", age: 17}
    
  • Object.assign()方法使用
    assign的方法定义是 Object.assign(target, source1,source2, ···)
    用于将源对象的所有可枚举属性复制到target对象中。

    let sourceObj = { a: { b: 1}};
    let targetObj = {c: 3};
    Object.assign(targetObj, sourceObj);
    
    targetObj.a.b = 2;
    sourceObj.a.b;  // 2
    // 这里体现的是assign的浅拷贝
    

8. 数组

  • 1. 数组数据的基本使用

    const arr = ["人民币","美元"] // 初始化数组
    arr.push("欧元","英镑") 
    arr//  ["人民币","美元","欧元","英镑"]
    arr[2] = "港元" ;
    arr[4] = "卢布" ;
    arr//  ["人民币","美元","港元","英镑",'卢布']
    
  • 2. 转换可迭代对象

    转换 map

    let map = new Map();
    map.set('key0', 'value0');
    map.set('key1', 'value1');
    console.log(Array.from(map));// \[['key0', 'value0'],['key1',
    // 'value1']]
    

    转换 set

    let arr = [1, 2, 3];
    let set = new Set(arr);
    console.log(Array.from(set)); // [1, 2, 3]
    

    转换字符串

    let str = 'abc';
    console.log(Array.from(str)); // ["a", "b", "c"]
    
  • 3. 数组元素的增加与删除

    array.push(e1, e2, ...eN) 将一个或多个元素添加到数组的末尾,并返回新数组的长度。

    const array = [1, 2, 3];
    const length = array.push(4, 5);
    

    array.unshift(e1, e2, ...eN) 将一个或多个元素添加到数组的开头,并返回新数组的长度。

    const array = [1, 2, 3];
    const length = array.unshift(4, 5);
    

    array.pop() 从数组中删除最后一个元素,并返回最后一个元素的值,数组为空时返回undefined。

    const array = [1, 2, 3];
    const poped = array.pop();  
    // array: [1, 2]; poped: 3
    

    array.shift() 删除数组的第一个元素,并返回第一个元素,原数组的第一个元素被删除。数组为空时返回undefined。

    const array = [1, 2, 3];
    const shifted = array.shift();  
    // array: [2, 3]; shifted: 1
    

    array.splice(start[, deleteCount, item1, item2, ...]) 从数组中添加/删除元素,返回值是由被删除的元素组成的一个新的数组,如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。

    const array = [1, 2, 3, 4, 5];
    const deleted = array.splice(2, 0, 6);*// 在索引为2的位置插入6*
    *// array 变为 [1, 2, 6, 3, 4, 5]; deleted为[]*
    
  • 4. 数组的截取与合并

      1. 数组的截取 - array.slice(start, end) 方法,start (必填), end(选填)
        slice()通过索引位置,从数组中返回start下标开始,直到end下标结束(不包括)的新数组,该方法不会修改原数组,只是返回一个新的子数组。
        end参数如果不填写,默认到数组最后
        // 获取仅包含最后一个元素的子数组
        let array = [1,2,3,4,5];
        array.slice(-1); // [5]
        
        // 获取不包含最后一个元素的子数组
        let array2 = [1,2,3,4,5];
        array2.slice(0, -1); // [1,2,3,4]
        

        注意:该方法并不会修改数组,而是返回一个子数组,如果想删除数组中的一段元素,应该使用方法 array.splice()。

      1. 数组的合并 - array.concat([item1[, item2[, . . . [,itemN]]]])方法
        conact()是将多个数组(也可以是字符串,或者是数组和字符串的混合)连接为一个数组,返回连接好的新的数组。
      const array = [1,2].concat(['a', 'b'], ['name']);
      // [1, 2, "a", "b", "name"]
      
  • 5. 数组元素的排序

      1. array.sort()方法
        按照数值大小进行排序-升序
      [1, 8, 5].sort((a, b) => {
          return a-b; // 从小到大排序
      });
      // [1, 5, 8]
      
      按照数值大小进行排序-降序
      [1, 8, 5].sort((a, b) => {
          return b-a; // 从大到小排序
      });
      // [8, 5, 1]
      
      1. array.reverse()方法
        reverse() 方法将数组中元素的位置颠倒
      let arr = [1,2,3,4,5]
      console.log(arr.reverse())    // [5,4,3,2,1]
      console.log(arr)    // [5,4,3,2,1]
      
  • 6. 数组的遍历与迭代

      1. array.filter(callback, thisArg)方法使用指定的函数测试所有元素,并创建一个包含所有通过测试的元素的新数组。
    // callback定义如下,三个参数: element:当前元素值;index:当前元素下标; array:当前数组
    
    function callback(element, index, array) {
        // callback函数必须返回true或者false,返回true保留该元素,false则不保留。
        return true || false;
    }
    
    const filtered = [1, 2, 3].filter(element => element > 1);
    // filtered: [2, 3];
    
      1. array.map(callback[, thisArg])方法返回一个由原数组中的每个元素调用callback函数后的返回值组成的新数组。
    let a = [1, 2, 3, 4, 5];
    let bbb = a.map((item) => {
        const key = `key${item}`
        const object = {}
        object[key] =item
        return object
    });
    console.log(bbb);  
    
      1. array.forEach(callbak)为数组的每个元素执行对应的方法。
    let a = [1, 2, 3, 4, 5];
    
    let b = [];
    // item:当前元素值;index:当前元素下标; array:当前数组
    a.forEach((item,index,array) => {
        b.push(item + 1);
    });
    console.log(b); // [2,3,4,5,6]
    
      1. array.reduce(callback[, initialValue])方法返回针对数组每项调用callback函数后产生的累积值。
    const total = [0, 1, 2, 3].reduce((sum,currentValue, currentIndex, array) => {
     return sum + currentValue;
    }, 0);
    // total is 6
    
    const flattened = [[0, 1], [2, 3], [4, 5]].reduce((a, b) => {
    return a.concat(b);
    }, []);
    // flattened is [0, 1, 2, 3, 4, 5]
    

9. 函数

  • 默认参数

    function fn(name,age=17){
        console.log(name+","+age);
    }
    fn("Amy",18);   // Amy,18
    fn("Amy","");   // Amy,
    fn("Amy");      // Amy,17
    

    只有在未传递参数,或者参数为 undefined 时,才会使用默认参数,null 值被认为是有效的值传递。

    fn("Amy",null); // Amy,null
    
  • 不定参数或可变参数

      function f(...values){
          console.log(values.length);
      }
      f(1,2);      //2
      f(1,2,3,4);  //4
    
  • 箭头函数
    基本语法是: 参数 => 函数体

    var f = v => v;
      //等价于
      var f = function(a){
      return a;
    }
    f(100); //100
    

    如果箭头函数返回的对象,必须使用 () 括起来

    var f = (id,name) => ({id, name});
    f(6,2);  // {id: 6, name: 2}
    

    箭头函数体中的 this 对象,是定义函数时的对象,而不是使用函数时的对象。

    function fn(){
    setTimeout(()=>{
        // 定义时,this 绑定的是 fn 中的 this 对象
        console.log(this.a);
      },0)
    }
    
    fna = ()=>{
        console.log("我是fna 函数");
    }
    
    var a = 20;
    // fn 的 this 对象为 {a: 20}
    fn.call({a: 18});  // 18
    

    上面代码中的call是替换this的对象,大白话就是可以使用箭头函数外层的变量,包括方法,实际的应用场景中 ...展开运算符可以替代这个方法。
    箭头函数常用的一个场景是:回调函数需要使用外层的this,改变变量值,或使用方法**

10. Class 类

class (类)作为对象的模板被引入,可以通过 class 关键字定义类。

class 的本质是 function。

它可以看作一个语法糖,使用起来更像面向对象编程的语法。

  • 类声明
class Example {
    constructor(a) {
        this.a = a;
    }
}

Example 只能声明一次,名字不能重复声明

  • 实例化对象
class Example {
    constructor(a, b) {
        this.a = a;
        this.b = b;
        console.log('Example');
    }
    sum() {
        return this.a + this.b;
    }
}

let exam1 = new Example(2, 1);
let exam2 = new Example(3, 1);

console.log(exam1.sum(),exam2.sum()); // 3, 4

exam1.__proto__.sub = function() {
    return this.a - this.b;
}

console.log(exam1.sub()); // 1
console.log(exam2.sub()); // 2

使用实例的proto属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。

  • decorator
    decorator 是一个函数,用来修改类的行为,在代码编译时产生作用。
    @testable
    export class Example2 {}
    
    // target就是class本身的实例
    function testable(target) {
        target.isTestable = true;
    }
    
    
    // 多参数修饰
    function makeSure(isOk,isCancle) {
        return function(target) {
            target.isOk=isOk;
            target.prototype.isCancle = isCancle
        }
    }
    
    @makeSure(true,false)
    export class Example3 {}
    
    上面是类修饰,以下是方法修饰,定义修饰函数有3个参数:target(类的原型对象)、name(修饰的属性名)、descriptor(该属性的描述对象)。
    class Math {
      @log
      add(a, b) {
        return a + b;
      }
    }
    
    function log(target, name, descriptor) {
      var oldValue = descriptor.value;
      descriptor.value = function() {
        console.log(`Calling ${name} with`,target, descriptor,arguments);
        return oldValue.apply(this, arguments);
      };
      return descriptor;
    }
    
    const math = new Math();
    
    // passed parameters should get logged now
    math.add(2, 4);
    
    修饰器执行顺序:由外向内进入,由内向外执行。
    class Example {
      @logMethod(1)
      @logMethod(2)
      sum(a, b){
        return a + b;
      }
    }
    function logMethod(id) {
        console.log('先执行 logMethod'+id);
        return (target, name, desctiptor) => console.log('后执行 logMethod '+id);
    }
    
  • 构造函数与继承
class Example{
    constructor(a, b) {
        this.a = a; // 实例化时调用 set 方法
        this.b = b;
    }
    get a(){
        console.log('getter');
        return this.a;
    }
    set a(a){
        console.log('setter');
        this._a = a; // 正确定义
        <!-- this.a = a; // 自身递归调用 -->
    }
}

extends 通过 extends 实现类的继承。getter和setter 必须同时出现

class Father1 {
    constructor(){}
    // 或者都放在子类中
    get a() {
        return this._a;
    }
    set a(a) {
        this._a = a;
    }
}
class Child1 extends Father1 {
    constructor(){
        super(); // 必须放在第一行,后面是相应的代码
        this.child = {name:"child"}
    }
}
let test1 = new Child1();
test1.a = 2;
console.log(test1.a); // 2

11. 模块的导入导出

ES6 的模块化分为导出(export) @与导入(import)两个模块。模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等。

  • export 与 import

    //   demo10.js
    
    let myName = "Tom";
    let myAge = 20;
    let myfn = function(){
        return "我的名字叫" + myName + "! 我今年" + myAge + "岁了"
    }
    let MyClass =  class MyClass {
        static a = "hello word!";
    }
    export { myName, myAge, myfn, MyClass }
    

    使用的时候有几种导入方式

    import { myName, myAge, myfn, MyClass } from "./demo10.js";
    console.log(myfn());// 我的名字是 Tom! 我今年 20 岁了.
    console.log(myAge);// 20
    console.log(myName);// Tom
    console.log(MyClass.a );// hello world!
    
    或
    import * as all from "./demo10.js";
    console.log(all.myfn());// 我的名字是 Tom! 我今年 20 岁了.
    console.log(all.myAge);// 20
    console.log(all.myName);// Tom
    console.log(all.MyClass.a );// hello world!
    

    注意点:import 是静态执行,所以不能使用表达式和变量。

  • export default 命令

    • 在一个文件或模块中,export、import 可以有多个,export default 仅有一个。
    • export default 中的 default 是对应的导出接口变量。
    • 通过 export 方式导出,在导入时要加{ },export default 则不需要。
    • export default 向外暴露的成员,可以使用任意变量来接收。
    // export-default.js
    
    export default function () {
        console.log('foo');
    }
    

    上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

    其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

    // import-default.js
    
    import customName from './export-default';
    customName(); // 'foo'
    

    export default命令的本质是将后面的值,赋给default变量

12. Promise 对象

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

  • Promise 状态
    状态的特点
    Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已拒绝或已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
    Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型或已完成)。

  • then 方法
    then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。
    我们首先创建一个Promise实例

    const promise = new Promise(function(resolve, reject) {
        // ... 业务代码
    
        if (/* 异步操作成功 */){
            resolve(value);
        } else {
            reject(error);
        }
    });
    

    Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

    Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。定义方式是这样 then(function1,function2)

    promise.then(function(value) {
        // 这是定义的 resolve
    }, function(error) {
        // 这是定义的 rejected
    });
    

    下面我们用js中的延迟函数setTimeout模拟异步操作

    function timeout(ms) {
        return new Promise((resolve, reject) => {
            setTimeout(resolve, ms, '我延迟了两秒');
        });
    }
    
    timeout(2000).then((value) => {
        console.log(value);
    });
    

    如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面代码:

    const p1 = new Promise(function (resolve, reject) {
        setTimeout(() => reject(new Error('fail')), 3000)
    })
    
    const p2 = new Promise(function (resolve, reject) {
        setTimeout(() => resolve(p1), 1000)
    })
    
    p2.then(result => console.log(result))
    .catch(error => console.log(error))
    

    上述代码中p2的resolve方法的参数是p1的Promise的实例,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态,p1的状态是resolved或者rejected时,p2的回调函数执行
     

  • Promise 的常规用法
    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});
    这是最常用的方式, 我们通过一个例子来输出看一下执行的顺序

    const promise = new Promise((resolve, reject)=> {
        console.log('我第一个执行')
        console.log(' 这里是写逻辑代码的地方1')// 这里是逻辑代码
        resolve({key:"我是返回的数据1"});
    });
    
    promise.then(result => {
        console.log(result)
        return {key:"我是then之后,返回的数据2"}
    }).then(result => {
        console.log(' 这里是写逻辑代码的地方2')// 这里是逻辑代码
        console.log(result)
    })
    .catch(error => {
        console.log(error)
    })
    
  • Promise.all
    Promise.all方法接受一个数组作为参数,Promise.all([p1, p2, p3])。
    p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

    const p1 = new Promise((resolve, reject) => {
        resolve('hello1');
    })
    .then(result => result)
    .catch(e => e);
    
    const p2 = new Promise((resolve, reject) => {
        <!-- throw new Error('报错了'); -->
        resolve('hello2');
    })
    .then(result => result)
    .catch(e => e);
    
    Promise.all([p1, p2])
    .then(result => console.log(result))
    .catch(e => console.log(e));
    

13. Generator 函数

Generator 函数可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。

其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。

  • 基本使用

我们来定义一个示例函数

function* test(){
    console.log("one");
    yield '1';
    console.log("two");
    yield '2'; 
    console.log("three");
    return '3';
}

let funResult = test()
f.next();
// one
// {value: "1", done: false}
 
f.next();
// two
// {value: "2", done: false}
 
f.next();
// three
// {value: "3", done: true}
 
f.next();
// {value: undefined, done: true}

这段代码的执行顺序:
第一次调用 next 方法时,从 Generator 函数的头部开始执行,先是打印了 one ,执行到 yield 就停下来,并将yield 后边表达式的值 '1',作为返回对象的 value 属性值,此时函数还没有执行完, 返回对象的 done 属性值是 false。

第二次调用 next 方法时,同上步 。

第三次调用 next 方法时,先是打印了 three ,然后执行了函数的返回操作,并将 return 后面的表达式的值,作为返回对象的 value 属性值,此时函数已经结束,多以 done 属性值为true 。

第四次调用 next 方法时, 此时函数已经执行完了,所以返回 value 属性值是 undefined ,done 属性值是 true 。如果执行第三步时,没有 return 语句的话,就直接返回 {value: undefined, done: true}。

  • for...of循环

for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。

function print(){
    console.log('执行了打印')
    return "我才是打印"
}
function* sendParameter(){
    console.log("start");
    var x = yield print();
    console.log("one:" + x);
    var y = yield '继续打印';
    console.log("two:" + y);
    console.log("final:" + (x + y));
}

 // 使用 for... of 相当于无参数自动执行该方法
var sendp1 = sendParameter();
for (let v of sendp1) { // v 是迭代对象 也就是yield 后面的数字或字符串或function的返回值
    console.log('for...of',v);
}


<!-- 执行的打印结果为:
start
执行了打印
for...of 我才是打印
one:undefined
for...of 继续打印
two:undefined
final:NaN    // undefined 参与了加法计算,所以值成了NaN 
-->
  • next()传参
function getData(){
    console.log('从网络get获取数据')
    setTimeout(() =>{sendp2.next({data:"我是get返回的数据"})}, 1000)
}
function postData(){
    console.log('从网络post获取数据')
    setTimeout(() =>{sendp2.next({data:"我是post返回的数据"})}, 1000)
}
function* requestDataFromServer(){
    console.log("start");
    var x = yield getData();
    console.log("get到的数据:",x);
    var y = yield postData();
    console.log("post到的数据:",y);
}

var sendp2 = requestDataFromServer();
sendp2.next();


<!--打印结果为:
start
从网络get获取数据
get到的数据: { data: '我是get返回的数据' }
从网络post获取数据
post到的数据: { data: '我是post返回的数据' }
-->
  • 打断Generator函数

结束Generator函数有两种方式,return 和 throw

return 方法返回给定值:

  • 方法提供参数时,返回该参数;
  • 不提供参数时,返回 undefined

throw 是通用关键字,不单纯用在Generator函数,也可以放在普通的方法里做打断或者验证的功能

用法如下:

function* foo(){
    try{
        yield 1;
        yield 2;
        yield 3;
    }catch (e) {
        console.log('catch inner', e);
    }
}
var f = foo();
f.next();
//返回的值是 {value: 1, done: false}

f.return("foo");
//返回的值 {value: "foo", done: true} , done为true,说明迭代完成

try {
  f.throw('a');
  f.throw('b');
} catch (e) {
  console.log('catch outside', e);
}

<!-- 
执行结果为:
catch outside a
 -->

实际是怎么样应用的,考虑以下这样的场景:
打开淘宝登录后,我们看到的了什么信息?

  1. 我的个人信息
  2. 我的订单信息
  3. 我关注的商家自动为我推荐的商品

也就是说我们在登录的时候需要获取到这三种信息,这三种信息被淘宝封装成了三个获取接口,我们以 url1,url2,url3分别代表三个接口地址
登录方式通过Token的方式,方法命名为 login()
获取三个信息的接口分别为 getUserInfo(),getOrderInfo(),getGoods()

function getToken(token){
    console.log('获取Token')
     setTimeout(() =>{loginVal.next("token122222222")}, 500)
}
function getUserInfo(token){
    console.log(`使用${token}获取用户信息`)
    setTimeout(() =>{loginVal.next({userName:'coding'})}, 500)
}

function getOrderInfo(){
    console.log(`使用${token}获取订单信息`)
    setTimeout(() =>{loginVal.next({orderName:'玩具枪'})}, 500)
}

function getGoods(){
    console.log(`使用${token}获取商品信息`)
    setTimeout(() =>{loginVal.next({goodsName:'婴儿玩具'})}, 500)
}

//为了保证登录后信息的完整性,需要将三种数据都获取到,我们会这样写

function* login(){
    try {
        const token = yield getToken();
        console.log(token);
        const userInfo = yield getUserInfo(token);
        const orderInfo = yield getOrderInfo(token);
        const goodsInfo = yield getGoods(token);
        console.log(userInfo,orderInfo,goodsInfo);

    } catch (e) {
        // 处理这个过程遇到的错误
    }
}

var loginVal;
// 点击按钮时执行的方法
function submitClick(){ 
    loginVal = login();
    loginVal.next();
}

14. async 函数

async 是 ES7 才有的与异步操作有关的关键字,和 Promise , Generator 有很大关联的。

  • 基本使用
// 这是一个没有参数的例子
async function helloAsync(){
    return "helloAsync";
  }
  
console.log(helloAsync())  // Promise {<resolved>: "helloAsync"}
 
helloAsync().then(v=>{
   console.log(v);         // helloAsync
})
  • await

await 操作符用于等待一个 Promise 对象, 它只能在异步函数 async function 内部使用。
下面是一个实现红绿灯变换的例子

let time = 8
let intervalId
let newColor ='red 红灯'

function changeColor(lightColor) {
    return new Promise(resolve => {
      if(intervalId)
        clearInterval(intervalId)
      intervalId = setInterval(() => {
          console.log('倒计时:'+time+'秒')
          time -= 1
          if(time<1&&lightColor){
            if(lightColor.includes('red'))
              newColor = "green 绿灯"
            else
              newColor = "red 红灯"
            resolve(newColor);
          }
      }, 1000);
    });
}

async function changeLightAsync() {
    var x = await changeColor(newColor);
    console.log(x); 
}

changeLightAsync()

setInterval(() => {
    time = 8
    changeLightAsync();
},9000)

15. Proxy 与 Reflect

  • Proxy

一个 Proxy 对象由两个部分组成: target 、 handler 。

var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。
其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
看一个简单的拦截例子:

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 666;
  }
});

proxy.gender // 666
proxy.name // 666
proxy.age // 666

当然y一个拦截器函数可以有多个拦截操作

let target = {name:'张三',age:18}
var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },

  apply: function(target, thisBinding, args) {
      console.log('执行了apply',thisBinding)
    return args[0];
  },

  construct: function(target, args) {
    return {value: args[1]};
  }
};

target = function(x, y) {
  return x + y;
}

var fproxy = new Proxy(target, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true

下面是所有的拦截操作,一共13项:

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。

  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。

  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。

  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。

  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。

  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。

  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。

  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。

  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。

  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。

  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

上面的基本操作我们可以暂时有一个印象,我们基本上代理的都是对象的get,set,deleteProperty,construct方法等
下面我们看一些适用的场景:

  • Form表单验证做成插件;
function createValidator(target, validator) {  
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            if (target.hasOwnProperty(key)) {
                let validator = this._validator[key];
                if (!!validator(value)) {
                    return Reflect.set(target, key, value, proxy);
                } else {
                    throw Error(`Cannot set ${key} to ${value}. Invalid.`);
                }
            } else {
                throw Error(`${key} is not a valid property`)
            }
        }
    });
}

const personValidators = {  
    name(val) {
        return typeof val === 'string';
    },
    age(val) {
        return typeof age === 'number' && age > 18;
    }
}
class Person {  
    constructor(name, age) {
        this.name = name;
        this.age = age;
        return createValidator(this, personValidators);
    }
}

const bill = new Person('Bill', 25);

// 以下操作都会报错,我们在赋值时可以快速的用try...catch 捕捉错误做出响应
bill.name = 0;  
bill.age = 'Bill';  
bill.age = 15;  
  • 私有属性
    JavaScript 或其他语言中,大家会约定俗成地在变量名之前添加下划线 _ 来表明这是一个私有属性(并不是真正的私有),但我们无法保证真的没人会去访问或修改它。
let api = {  
    _apiCase1: 'http://182.190.5.88', // 实例1
    _apiCase2: 'http://182.190.5.89', // 实例2

    /* 测试数据时使用 this._apiCase1 或者 this._apiCase2 */

    getUsers: function(){}, 
    getUser: function(userId){}, 
    setUser: function(userId, config){}
};


// 我们定义私有变量的初衷是为了不修改,但是下面依然可以执行
api._apiCase1 = 'http://182.190.5.90';

很显然,约定俗成是没有束缚力的,使用 ES6 Proxy 我们就可以实现真实的私有变量了

const privateKes = ['_apiCase1','_apiCase2'];
api = new Proxy(api, {  
    get(target, key, proxy) {
        if(privateKes.indexOf(key) > -1) {
            throw Error(`${key} 不存在.`);
        }
        return Reflect.get(target, key, proxy);
    },
    set(target, key, value, proxy) {
        if(privateKes.indexOf(key) > -1) {
            throw Error(`无法为 ${key} 赋值,因为它是私有变量`);
        }
        return Reflect.get(target, key, value, proxy);
    }
});

// 以下操作都会抛出错误
console.log(api._apiCase1);
api._apiCase2 = '987654321'; 
  • 预警过时方法和拦截删除操作
let dataStore = {  
    noDelete: 1235,
    oldMethod: function() {/*...*/ },
    doNotChange: "tried and true"
};

const NODELETE = ['noDelete'];  
const NOCHANGE = ['doNotChange'];
const DEPRECATED = ['oldMethod'];  

dataStore = new Proxy(dataStore, {  
    set(target, key, value, proxy) {
        if (NOCHANGE.includes(key)) {
            throw Error(`Error! ${key} 是只读属性.`);
        }
        return Reflect.set(target, key, value, proxy);
    },
    deleteProperty(target, key) {
        if (NODELETE.includes(key)) {
            throw Error(`Error! ${key} 是不可删除属性`);
        }
        return Reflect.deleteProperty(target, key);

    },
    get(target, key, proxy) {
        if (DEPRECATED.includes(key)) {
            console.warn(`Warning! ${key} 已过时,请谨慎使用.`);
        }
        var val = target[key];

        return typeof val === 'function' ?
            function(...args) {
                Reflect.apply(target[key], target, args);
            } :
            val;
    }
});

// 这三个都会报错
dataStore.doNotChange = "foo";  
delete dataStore.noDelete;  
dataStore.oldMethod();

  • Proxy.revocable()

Proxy.revocable 返回一个可取消的Proxy实例

let target2 = {};
let handler2 = {};

let {proxy:newProxy, revoke} = Proxy.revocable(target2, handler2);

newProxy.foo = 123;
newProxy.foo // 123

revoke();
newProxy.foo // Cannot perform 'get' on a proxy that has been revoked

Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
实际开发中有这样的需求:
比如 登录时候需要判断输入密码次数,如果输入错误密码超过5次,密码输入框变为不可用


  • Reflect

了操作对象而提供的新 API*
Reflect对象其实就是为了取代Object对象。取代原因有一下几点:

  1. Object对象的一些内部方法放在了Reflect上面,比如:Object.defineProperty。主要是优化了语言内部的方法。

  2. 修改Object方法的返回,例如:Object.definePropery(obj,name,desc)无法定义属性时报错,而Reflect.definedProperty(obj,name,desc)则会返回false。

  3. 让Object变成函数的行为,以前的:name in obj和delete obj[name],可以让Reflect.has(name)和Reflect.deleteProperty(obj,name)替代。

  4. Reflect方法和Proxy方法一一对应。主要就是为了实现本体和代理的接口一致性,方便用户通过代理操作本体。

下面我们写一个比较复杂的例子,用Proxy和Reflect实现观察者模式

// 定义一个订阅者集合
const queuedObservers = new Set();
// 添加订阅者
const observe = fn => queuedObservers.add(fn);
// 给对象添加代理对象,代理的set方法中进行遍历订阅者列表
const observable = obj => new Proxy(obj,{set});

function set(target,key,value,receiver){
        const result = Reflect.set(target,key,value,receiver);
        //遍历订阅者集合,依次触发订阅者方法
        queuedObservers.forEach(fn => fn());
        //返回订阅者
        return result;
}

//使用Proxy实现观察者模式[发布-订阅者模式]
const person = observable({name:'张三',age:30});

function print(){
    console.log(`${person.name},${person.age}`);
}
function anotherPrint(){
    console.log(`你想的很对`)
}

//订阅者集合里面加入print订阅者
observe(print);
observe(anotherPrint)

person.name = 'miya'

Reflect对象一共有 13 个静态方法 与 Proxy 的方法一一对应
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,737评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,103评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,710评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,909评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,794评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,557评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,939评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,572评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,852评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,871评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,692评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,490评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,939评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,114评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,409评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,971评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,176评论 2 339