Skip to content

1.生成器与迭代器

前置知识:ES6 的 Generator 生成器函数

ES6 的 Generator 是一种特殊类型的函数,它具有暂停和恢复执行的能力。它使用 function* 声明语法定义,并使用 yield 关键字来控制函数的执行流程。

以下是 Generator 函数的一些关键概念和特性:

  1. 声明和执行:Generator 函数使用 function* 声明语法进行定义。与普通函数不同的是,在调用 Generator 函数时,并不会立即执行函数体,而是返回一个称为 Generator 对象的迭代器。

    注意:生成器对象 = 迭代器

    我们可以在生成器对象上用for...of循环以及扩展运算符,因为这个对象是一个可迭代的对象,类似于数组,而不像一般的对象一样不可迭代!

  2. 暂停和恢复:Generator 函数的执行过程可以通过 yield 关键字来暂停和恢复。**当 yield 被执行时,函数会暂停,并将一个值返回给调用者。**在下一次调用 next() 方法时,函数会从上次暂停的地方继续执行。

  3. 迭代器接口:Generator 对象实现了迭代器接口,可以使用 next() 方法逐步遍历函数体中的每个 yield 表达式,并获取函数的返回值。

  4. 控制流程:在 Generator 函数内部,可以使用控制流程的结构,如条件语句(if-else)、循环语句(for、while)、异常处理等。这使得可以根据特定的条件决定是否暂停函数的执行或跳转到特定的代码块。

  5. 双向通信:**除了从 Generator 函数中获取值外,还可以通过 next() 方法向 Generator 函数内部发送值。**这种双向通信可以实现更灵活的控制流程和数据传递。

Generator 函数的主要优点是它提供了一种简洁而强大的方式来处理异步操作、迭代算法和状态机等复杂的控制流程。它能够将复杂的问题分解成多个步骤,并以可读性高的方式表达。

下面是一个简单的示例,展示了如何定义和使用 Generator 函数:

js
function* test() {
  console.log("111111");
  var input1 = yield "111-输出"; //注意:input1 是我们收到外面传的值,而 yield 后面的值 "111-输出" 是我们给外面的值!
  console.log("22222", input1); //22222 aaaa
  var input2 = yield "222-输出";
  console.log("333333", input2); //333333 bbbb
  var input3 = yield "333-输出";
  console.log("444444", input3); //444444 ccccc
}

var kerwintest = test(); //第一次运行生成器,会创建迭代器,其他什么都不干
console.log(kerwintest); //Object [Generator] {}

//后面每当 调用next方法时 就会把next 的参数先传给当前所处的 yield,然后从当前位置开始向下寻找直到(再次)遇到 yield,就运行 yield 后面的函数,并返回yield的结果并在当前位置停下来 ——> 注意接收next参数的时机
var res1 = kerwintest.next();
console.log(res1); //返回一个对象{value:"111-输出", done:false}
var res2 = kerwintest.next("aaaa"); //可以传值,里面可以收到值
console.log(res2); //{value:"222-输出", done:false}
var res3 = kerwintest.next("bbbb");
console.log(res3); //{value:"333-输出", done:false}
var res4 = kerwintest.next("ccccc");
console.log(res4); //{value:undefined, done:true} //代表生成器没有内容了

在上述示例中,myGenerator() 是一个 Generator 函数,通过 yield 关键字定义了三个步骤。通过调用 next() 方法,我们可以逐步遍历 Generator 函数并获取每个 yield 表达式的值。

总的来说,ES6 的 Generator 函数是一种强大的功能,它提供了一种处理复杂控制流程和状态机的方式,并且在异步编程中具有重要的应用价值。

注意:ES7 中的 async 和 await 实际上就是对于Generator的一种升级!是它的语法糖!

——> 对于异步请求的处理来说,一般就是解决链式的 ajax(后一个请求需要前一个请求的返回值做参数)

一个最简单的不合理的链式异步请求示例:

js
function* test1() {
  setTimeout(() => {
    console.log("11111-success");
    kerwintest1.next(); //在第一个成功的回调执行第二个
  }, 1000);

  yield; //遇到 yield 暂停
  setTimeout(() => {
    console.log("222222-success");

    kerwintest1.next(); //在第二个成功的回调执行第三个
  }, 1000);
  yield;
  setTimeout(() => {
    console.log("3333-success");
  }, 1000);
  yield;
}

var kerwintest1 = test1();

kerwintest1.next();

缺点:返回值不好维护把控,内部调用外部变量:耦合性大,可读性可维护性不好

可执行生成器:

自动化地进行 ajax 链式调用,最终得到一个返回值

js
function getData1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("data1");
    }, 1000);
  });
}
function getData2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("data2");
    }, 1000);
  });
}
function getData3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("data3");
    }, 1000);
  });
}

//生成器函数
function* gen() {
  var f1 = yield getData1(); //运行getData1返回 res 给外面,f1 在下一次 next 的时候通过参数传进来
  console.log(f1);
  var f2 = yield getData2(f1); //把上面接收到的f1 用在这里作为getData2请求的参数
  console.log(f2);
  var f3 = yield getData3(f2); //把接收到的f2 用在这里作为getData3请求的参数
  console.log(f3);
}

//封装的链式 ajax 运行入口
function run(fn) {
  var g = fn(); //创建迭代器
  //构造一个递归函数执行 next 方法
  function next(data) {
    var result = g.next(data); //请求返回的res
    if (result.done) { //如果执行完了,就直接返回结果
      return result.value;
    }
    result.value.then((res) => { //如果没有执行完,那么把 res 给到 next 继续向下执行
      next(res); //递归调用自己
    });
  }
  next();
}

var res = run(gen);

其实就类似于 async 和 await 的效果了,我们用async 和 await的时候只是不用写 run 这个封装方法。

注意:

Async/Await 不是基于 Generator 函数实现的,它们是两种独立的语言特性。在底层实现上,Async/Await 基于 Promise,而 Generator 函数基于迭代器(Iterator)和生成器(Generator)协议。

2.装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改类的行为。

通俗的讲,装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。

常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器

装饰器的写法:普通装饰器(无法传参)、装饰器工厂(可传参)

对修饰器的实验支持是一项将在将来版本中更改的功能。

普通类装饰器定义

typescript
// 定义一个function,作为装饰器
// 里面的params参数,其实就是调用该装饰器的类
function logClass(params:any) {
    console.log(params);

    // 使用该装饰器的类,会增加一个apiUrl的属性
    params.prototype.apiUrl = 'xxxx';

    // 使用该装饰器的类,会增加一个run方法
    params.prototype.run = function(){
        console.log('run方法运行了.');
    }
}

// 使用装饰器
@logClass
class HttpClient {
    constructor() {
    }

    getData() {
    }
}

// 测试
let a:any = new HttpClient();
console.log(a.apiUrl);
a.run();

定义装饰器工厂(常用)

typescript
// 定义装饰器工厂,params即为调用时传入的参数,target即调用的类本身
function logClass(params:string) {
    
    return function(target:any) {
        target.prototype.apiUri = params;
    }
}

// 此处必须要传入参数,否则会报错
@logClass('http://xxxx')
class HttpClient {
    constructor() {
    }

    getData() {
    }
}

let a:any = new HttpClient();
console.log(a.apiUri);

类装饰器重写类的构造方法和其他方法

类装饰器表达式会在运行时当做函数被调用,类的构造函数作为唯一的参数。

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

typescript
function logClass(target:any) {
    console.log(target);

    return class extends target {
        // 在类构造方法后运行,修改apiUrl的值
        apiUrl = '我是修改后的apiUrl';

        // 重写getData方法
        getData() {
            console.log('修改后的getData方法');
            console.log(this.apiUrl);
        }
    }
}

@logClass
class HttpClient {

    public apiUrl:string | undefined;

    constructor() {
        this.apiUrl = '构造方法中的apiUrl';
    }

    getData() {
        console.log(this.apiUrl);
    }
}

let client = new HttpClient();
client.getData();

属性装饰器

属性装饰器表达式会在运行时当做函数被调用,传入下列2个参数:

1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

2、成员的名字

typescript
// 定义属性装饰器
// params:传入的值
// target:类的原型对象,即HttpClient.prototype
// attr:属性名称
function logProperty(params:any) {

    return function(target:any, attr:any) {
        console.log(params);
        console.log(target);
        console.log(attr);
        
        // 修改apiUrl属性的值
        target.attr = "https://yyyyyyy";
    }
}

class HttpClient {

    @logProperty("http://xxxxxxx")
    public apiUrl:string | undefined; //这里是实例属性!因为没有static关键字!


    constructor() {
        this.apiUrl = '构造方法中的apiUrl';
    }

    getData() {
        console.log(this.apiUrl);
    }
}

注意:类里面的构造函数的this指向?

在JavaScript中,类(class)里面的构造函数的this指向新创建的实例对象。当你使用new关键字来实例化一个类时,构造函数会被调用,并且在构造函数内部的this关键字将指向新创建的对象

构造函数的this 和 构造函数的原型对象prototype 的关系是什么样子的?

构造函数的this和构造函数的原型对象 prototype 在 JavaScript 中有不同的作用和关联:

  1. 构造函数的 this :
    • 当你通过 new 关键字调用构造函数时,构造函数内部的 this 指向新创建的实例对象。这允许你在构造函数中设置实例特有的属性和方法
    • this 在构造函数内部用于引用当前正在创建的对象。
js
function Person(name) {
  this.name = name; // 在构造函数内部,this 指向新创建的实例对象
}

const person1 = new Person("Alice");
console.log(person1.name); // 输出 "Alice"
  1. 构造函数的原型对象 prototype :
  • 构造函数的原型对象是一个普通的 JavaScript 对象,它包含了实例对象共享的方法和属性这些方法和属性可以被所有通过构造函数创建的实例对象访问。
  • 原型对象通常用于存储共享的方法,以避免在每个实例对象上重复创建相同的方法,从而节省内存。
js
function Person(name) {
  this.name = name;
}

// 在 Person 构造函数的原型对象上添加一个方法
Person.prototype.sayHello = function () {
  console.log(`Hello, my name is ${this.name}`);
};

const person1 = new Person("Alice");
const person2 = new Person("Bob");

person1.sayHello(); // 输出 "Hello, my name is Alice"
person2.sayHello(); // 输出 "Hello, my name is Bob"

所以,this 主要用于在构造函数内部操作实例对象的属性,而构造函数的原型对象 prototype 用于存储实例对象共享的方法和属性。这种组合允许你创建多个实例对象,这些对象共享相同的方法,同时具有各自的属性值。

方法装饰器

方法装饰器会在运行时传入下列三个参数:

1、对于静态成员来说,是类的构造函数;对于实例成员,是类的原型对象;

2、成员的名字;

3、成员的属性描述符;

typescript
// 定义方法装饰器
// params: 传入的参数
// target:如果方法是static的,target为类的构造函数;如果方法是实例化的,target为类的原型对象;
// methodName:方法名称
// describe:方法描述;describe.value就是方法本身
function logMethod(params:any) {
    return function(target:any, methodName:any, describe:any) {
        console.log(params);
        console.log(target);
        console.log(methodName);
        console.log(describe);
        
        // 修改或扩展类的属性
        target.apiUrl = 'http://xxxxxx';
        // 修改或扩展类的方法
        target.run = function() {
            console.log('run 方法运行了.');
        }
        
        // 修改方法装饰器装饰的方法:将传入的参数转换为string类型
        let preMethod = describe.value; // 保存原来的方法
        // 替换掉原来的方法
        describe.value = function(...args:any[]) {
            console.log('我是修改后的describe方法');
            args = args.map((value:any) => String(value));
            console.log(args);
            
            // 调用原来的方法
            preMethod.apply(this, args);
        }
    }
}

class HttpClient {

    public apiUrl:string | undefined;

    constructor() {
        this.apiUrl = '构造方法中的apiUrl';
    }

    @logMethod("aaa")
    getData(...args:any[]) {
        // console.log(this.apiUrl);
        console.log(args); // 此处输出的args也都变成了字符串
        console.log('我是原来的getData方法');
    }

}

let client = new HttpClient();
client.getData(1,2,'c');

方法参数装饰器

参数装饰器表达式会在运行时当做函数被调用,可以使用参数装饰器为类的原型增加一些数据元素,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象;

  2. 方法的名称;

  3. 参数在函数参数列表中的索引。

typescript
// 定义方法参数装饰器
// params为调用该装饰器传入的参数
// target:静态成员是类的构造函数,实例成员是类的原型对象;
// methodName:方法名
// index:属性的位置,从0开始
function logParam(params:any) {
    return function(target:any, methodName:any, index:any) {
        console.log(params);
        console.log(target);
        console.log(methodName);
        console.log(index);
    }
}

class HttpClient {
    public apiUrl:string | undefined;
    constructor() {
        this.apiUrl = '构造方法中的apiUrl';
    }
    // 使用方法参数装饰器
    getData(@logParam('aaa') param:any) {
    }

}

let a = new HttpClient();
a.getData('xxx');

装饰器执行顺序

从下向上、从内向外执行

typescript
// 参数加了问号变成可选参数,就可以使用@logClass()进行调用
function logClass1(params?:any) {
    return function(target:any) {
        console.log('类装饰器1: '+ params);
    }
}

function logClass2(params?:any) {
    return function(target:any) {
        console.log('类装饰器2: '+ params);
    }
}

function logAttribute(params?:any) {
    return function(target:any, attributeName:any) {
        console.log('属性装饰器: '+ params;
    }
}

function logMethod(params?:any) {
    return function(target:any, methodName:any, desc:any) {
        console.log('方法装饰器: '+ params);
    }
}

function logParams(params?:any) {
    return function(target:any, methodName:any, index:any) {
        console.log('方法参数装饰器: '+ params);
    }
}

// 一个类上可以同时加上多个装饰器
@logClass1('test1')
@logClass1('test2')
@logClass2('class2')
class HttpClient {

    @logAttribute('apiUrl')
    public apiUrl:string | undefined;

    constructor() {
        this.apiUrl = '构造方法中的apiUrl';
    }

    @logMethod('getData')
    getData(@logParams('getDataParam') param:any) {
    }

    @logMethod('setData')
    setData(@logParams('setDataParam1') param1:any, @logParams('setDataParam2') param2:any) {

    }
}

let a = new HttpClient();
a.getData('xxx');
a.setData('xxxx','yyyyy');

运行结果:

plain
属性装饰器: apiUrl
方法参数装饰器: getDataParam
方法装饰器: getData
方法参数装饰器: setDataParam2
方法参数装饰器: setDataParam1
方法装饰器: setData
类装饰器2: class2
类装饰器1: test2
类装饰器1: test1