js设计模式行为型

一,策略模式-(各策略平行无影响)

策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

策略模式由两部分构成:一部分是封装不同策略的策略组,另一部分是 Context。通过组合和委托来让 Context 拥有执行策略的能力,从而实现可复用、可扩展和可维护,并且避免大量复制粘贴的工作。

直接这样讲可能不大好理解,用案例讲就会简单很多,在JavaScript中,策略模式通常用于以下应用场景:

  1. 表单验证:在表单提交之前,可能需要进行一系列的验证,例如检查是否所有字段都已填写,检查输入的数据格式是否正确等。这些验证规则可以视为不同的策略,根据实际需求,选择合适的策略进行验证。
  2. 支付系统:在支付系统中,可能支持多种支付方式,比如支付宝、微信支付、银联支付等。这些支付方式可以视为不同的策略,根据用户选择的支付方式,选择合适的支付策略。

1.1,表单验证

在表单验证中,我们常常需要多个if-else来对手机号、邮箱、密码之类的进行校验.校验方法啥的基本都一致:

js 复制代码
let userName='猪八戒',email='example@example',phone='12345622341';//表单中输入的
const nameReg = /^[\u4e00-\u9fa5]{2,10}$/
const phoneReg = /^1\d{10}$/
const emailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/
if(!nameReg.test(userName)){
  console.log('姓名请限定在2-10字符之间');
  return
}
if(!emailReg.test(email)){
  console.log('请输入有效的邮箱');
  return
}
if(!phoneReg.test(phone)){
  console.log('请输入有效手机号');
  return
}

经由策略模式改造,将不同的校验类目给予不同的策略,然后根据实际需要,选择合适的策略惊醒处理,这里把策略放在对象中,利用对象的key值不同,选择不同的策略:

js 复制代码
const validates={
    /* 姓名校验策略 由2-10位汉字组成 */
    validateUsername(str,msg) {
      const reg = /^[\u4e00-\u9fa5]{2,10}$/
      return reg.test(str)?'':msg
    },
    /* 手机号校验 由以1开头的11位数字组成  */
    validateMobile(str,msg) {
      const reg = /^1\d{10}$/
      return reg.test(str)?'':msg
    },
    /* 邮箱校验策略 */
    validateEmail(str,msg) {
      const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/
      return reg.test(str)?'':msg
    }
}
​
/* 生成表格自定义校验函数 */
const formValidateGene = (key,value,msg) =>validates[key](value,msg)
​
var nameResult = formValidateGene('validateUsername','猪八戒','姓名请限定在2-10字符之间');
var phoneResult = formValidateGene('validateMobile','12345622341','请输入有效手机号');
var mailResult = formValidateGene('validateEmail','example@example','请输入有效的邮箱');
var errorMsg = nameResult||phoneResult|| mailResult;
if(errorMsg){
    console.log(errorMsg);
    return false;
}

2.1,支付系统

接下来看支付系统的策略模式,先定义好不同支付渠道对应的策略,然后利用委托执行对应的策略:

js 复制代码
// 首先我们定义一个策略接口  
interface PaymentStrategy {  
    pay(amount: number): void;  
}  
  
// 我们定义两个具体的策略  
class Alipay implements PaymentStrategy {  
    pay(amount: number) {  
        console.log(`通过支付宝支付了 ${amount} 元`);  
    }  
}  
  
class WechatPay implements PaymentStrategy {  
    pay(amount: number) {  
        console.log(`通过微信支付了 ${amount} 元`);  
    }  
}  
  
// 环境角色类,它会根据具体的策略变化  
class PaymentContext {  
    private strategy: PaymentStrategy;  
    constructor(strategy: PaymentStrategy) {  
        this.strategy = strategy;  
    }  
    executePayment(amount: number) {  
        return this.strategy.pay(amount);  
    }  
}  
  
// 客户端代码  
let context = new PaymentContext(new Alipay());  
context.executePayment(100);  // 输出:通过支付宝支付了 100 元  
  
context = new PaymentContext(new WechatPay());  
context.executePayment(100);  // 输出:通过微信支付了 100 元

策略模式的优势在于它可以让你的代码更加模块化,更容易扩展和维护。然而,如果我们需要处理的策略数量很大,或者策略间的差异很小时,使用策略模式可能会让代码变得过于复杂。

因此,使用策略模式的时候,你需要考虑清楚是否真的需要它。如果你的代码中包含了大量的 if/elseswitch 语句,并且这些语句都在处理相同类型的操作,那么策略模式可能就是你的救星。但如果你只是在处理一两种简单的情况,那么策略模式可能就没有那么必要了。

二,状态模式-(会和实例对象主体进行交互)

scss 复制代码
状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

我们平时开发时本质上就是对应用程序的各种状态进行切换并作出相应处理.最直接的解决方案是将这些所有可能发生的情况全都考虑到,然后使用if... ellse语句来做状态判断来进行不同情况的处理.但是对复杂状态的判断就显得代码逻辑特别的乱.随着增加新的状态或者修改一个状态,if else或switch case语句就要相应的的增多或者修改,程序的可读性、扩展性就会变得很弱。

于是可以使用状态模式来优化代码,它长得和策略模式很像,但它们的适用场景略有区别:策略模式是在外部定义了一个行为,并由外部发起一次性的行为替换,而状态模式在内部定义了多个行为,并由内部原因持续地发生行为替换。

举个参考文章中修言大佬的咖啡机例子,如果是我写的话,学习了上文的策略模式,我最多写出如下的代码,将咖啡机的处理逻辑根据状态的不同映射不同行为,公共行为抽离复用,让单个方法只完成最小颗粒的事情:

js 复制代码
const stateToProcessor = {
  american() {
    console.log('我只吐黑咖啡');    
  },
  latte() {
    this.american();
    console.log('加点奶');  
  },
  vanillaLatte() {
    this.latte();
    console.log('再加香草糖浆');
  },
  mocha() {
    this.latte();
    console.log('再加巧克力');
  }
}
​
class CoffeeMaker {
  constructor() {
    /**
    这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
  **/
    // 初始化状态,没有切换任何咖啡模式
    this.state = 'init';
  }
​
  // 关注咖啡机状态切换函数
  changeState(state) {
    // 记录当前状态
    this.state = state;
    // 若状态不存在,则返回
    if(!stateToProcessor[state]) {
      return ;
    }
    stateToProcessor[state]();
  }
}
​
const mk = new CoffeeMaker();
mk.changeState('latte');

如文中所说,这样写看似没问题,却忽略了在stateToProcessor中读取咖啡机当前状态的渠道.

stateToProcessor 里的工序函数,感知不到咖啡机的内部状况。

策略模式和状态模式确实是相似的,它们都封装行为、都通过委托来实现行为分发。 但策略模式中的行为函数不依赖调用主体,互相平行。而状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。于是工序函数需要能够读取调用主体.

js 复制代码
class CoffeeMaker {
  constructor() {
    /**
    这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
  **/
    // 初始化状态,没有切换任何咖啡模式
    this.state = 'init';
    // 初始化牛奶的存储量
    this.leftMilk = '500ml';
  }
  //映射对象-通过状态来映射
  stateToProcessor = {
    that: this,//在const mk = new CoffeeMaker();的时候,this指向实例对象mk,也就是这个that存储着mk实例对象
    american() {
      // 尝试在行为函数里拿到咖啡机实例的信息并输出
      console.log("===",this)//这个this是stateToProcessor这个对象调用,指向这个对象
      console.log("==+++=",this.that),//stateToProcessor这个对象中的that存储着咖啡机主体,于是可以通过它来访问this.that.leftMilk
      console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
      console.log('我只吐黑咖啡');
    },
    latte() {
      this.american()
      console.log('加点奶');
    },
    vanillaLatte() {
      this.latte();
      console.log('再加香草糖浆');
    },
    mocha() {
      this.latte();
      console.log('再加巧克力');
    }
  }
​
  // 关注咖啡机状态切换函数
  changeState(state) {
    this.state = state;
    if (!this.stateToProcessor[state]) {
      return;
    }
    this.stateToProcessor[state]();
  }
}
​
const mk = new CoffeeMaker();
mk.changeState('latte');

这里主要是利用了把状态-行为映射对象作为主体类对应实例的一个属性添加进去.

在执行时是这样的:

js 复制代码
1,const mk = new CoffeeMaker();//CoffeeMaker中的this指向实例对象mk
2,mk.changeState('latte');//mk对象调用changeState方法
3,this.stateToProcessor['latte']();//这个this就是mk对象,this.stateToProcessor又作为对象,调用它的latte方法,所以latte中的this指向this.stateToProcessor对象,该对象的that存储着mk实例对象.

因此可以通过this.that访问到主体类对应实例对象mk.

三,观察者模式

观察者模式在前端开发中有着广泛的应用。这种模式用于建立对象之间的一对多依赖关系,当一个对象的状态发生变化时,它会通知所有依赖项。这种模式能够有效地解耦对象之间的关系,提高代码的可维护性和复用性。

观察者模式: 定义了对象之间 一对多 的依赖关系,它只有两个角色,分别是观察的目标对象 Subject 和观察者对象 Observer,当一个 目标对象 的状态发生改变时,所有依赖于它的观察者对象都会收到通知。

举个例子,我们经常使用的promise,就是利用的promise实现的.

回顾一下,promise出现的最大意义是啥?是做到了异步操作和结果的分离,避免了回调地狱.

现在我们来实现下最简单的一个promise:

js 复制代码
// 先定义三个常量表示状态
var PENDING = 'pending';//等待中
var FULFILLED = 'fulfilled';//执行完成
var REJECTED = 'rejected';//拒绝执行
​
function MyPromise(fn) {
    this.status = PENDING; // 初始状态为pending
    this.value = null; // 初始化value
    this.reason = null; // 初始化reason
    // 构造函数里面添加两个数组存储成功和失败的回调
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    //同步执行这个函数
    try {
     fn(resolve, reject);
    } catch (error) {
     reject(error);
    }
​
    // 存一下this,以便resolve和reject里面访问
    var that = this;
    // resolve方法参数是value
    function resolve(value) {
        if(that.status === PENDING) {
          that.status = FULFILLED;
          that.value = value;
          // resolve里面将所有成功的回调拿出来执行
          that.onFulfilledCallbacks.forEach(callback => {
              callback(that.value);
          });
        }
    }
     
    // reject方法参数是reason
    function reject(reason) {
      if(that.status === PENDING) {
            that.status = REJECTED;
            that.reason = reason;
            that.onRejectedCallbacks.forEach(callback => {
                callback(that.reason);
            })
        }
    }
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if(this.status === FULFILLED) {
           onFulfilled(this.value)//将结果传入回调函数
    }
       
    if(this.status === REJECTED) {
       onRejected(this.reason);
    }
    if(this.status === PENDING) {
       this.onFulfilledCallbacks.push(onFulfilled);
       this.onRejectedCallbacks.push(onRejected);
    }
}
​
exports.MyPromise=MyPromise

实际使用:

js 复制代码
var MyPromise = require('./myPromise.js').MyPromise;
const p1 = new MyPromise((resolve, reject) => {
    console.log('create a promise');
    setTimeout(()=>{
        console.log("异步")
        resolve('成功了');
    },2000)
    
  })
console.log("直接打印",p1)
p1.then((value)=>{
   console.log(value)
})

在这里,promise的状态status一旦发生改变(调用了resolve或者reject),就会对应地通知所有的依赖项(onFulfilledCallbacks数组或onRejectedCallbacks数组),从而执行其中收集好的回调函数.

大体路径如下:

也就是说,一个promise实例对象就是一个观察对象subject,它的状态(pending、fulfilled、rejected),一旦状态发生了变更(resolve或reject来变更状态),观察者对象 Observer就会收到通知(回调函数收集器中的回调函数执行).

而resolve/reject的调用时机是异步操作完成后,于是就保证了回调函数在异步操作结束后执行,做到了异步操作和结果的分离.

从这个可以看到,观察者模式主要是做到两点:

scss 复制代码
1,观察者监听状态(status)、收集依赖项(then的回调函数收集进数组)
2,观察者发现状态发生变更,通知依赖项做对应反应(执行回调函数收集器中的回调函数)

四,发布订阅模式

很多地方都说,观察者模式就是发布订阅模式,因为他两真的很像.但是像上文promise中,被观察者直接通知依赖项的,就是观察者模式.而发布订阅模式则是在中间多了个平台,状态发生了变化,就发布到平台上,订阅者订阅的是这个平台的信息,状态发生了变化,平台就会通知订阅者.

也就是发布者不直接接触订阅者,而是通过第三方通知订阅者.

例如,过年或者节假日火车票买不到票时,购票软件会有一个候补下单的功能,用户可以向购票软件后台发出一个订阅请求:如果有人退票或某些原因增加座位后,软件后台会帮助用户下单并及时通知用户成功购买该车次车票。

发布订阅模式: 订阅者向事件调度中心(PubSub)注册(subscribe)监听,当事件调度中心(PubSub)发布通知时(publish),订阅者的监听事件将会被触发。

举个例子:在我们前端的数据驱动视图mvvm框架中,如果数据发生了变化,视图需要自动更新,就可以利用这个观察者模式.

erlang 复制代码
利用一个观察者对象监听数据变化,一但数据发生了变化,就通知对应的dom做出视图的更新.

接下来我们来实现一下一个简易版的vue,限于篇幅仅实现@方法和{{}}绑定数据.

4.0,vue的基本使用

我们常用vue的写法如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>{{counter}}</p>
        <div class="text" @click="add">点击</div>
            </div>
            <script src="./index.js"></script>
            <script>
                const app = new FakeVue({
                    el: "#app",
                    data: {
                        counter: 1
                    },
                    methods:{
                        add(){
                            this.counter++
                        }
                    }
                });
        </script>
</body>
</html>

4.1,新建vue类

js 复制代码
class FakeVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.$methods = options.methods;
    observe(this.$data);//将数据响应式化
    this.proxy();//将data中的对象,让实例化对象最外层也能访问和修改,也就是再做一层代理,后续this.$vm[exp]才能直接访问,而不是this.$vm.data[key]
    new Compile(this.$options.el, this);//将data中的数据首次渲染到dom上,并且收集订阅者
  }
  proxy() {
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(v) {
          this.$data[key] = v;
        }
      });
    });
  }
}
​
// 对象响应式处理
function observe(target) {
  // 若target是一个对象,则遍历它
  if(target && typeof target === 'object') {
    Object.keys(target).forEach((key)=> {
        // defineReactive方法会给目标属性装上"监听器"
        defineReactive(target, key, target[key])
    })
  }
}
​
// 对象响应式原理
function defineReactive(obj, key, value) {
  // 解决递归嵌套问题
  observe(value);
  const dep = new Dep();//在数据变化的时候进行订阅并执行对应的更新函数重新渲染。一个data[key]就实例化一个Dep,这行代码后续会讲到,这里先不看
  Object.defineProperty(obj, key, {
    get() {
      console.log("get", value);
      Dep.target && dep.addDep(Dep.target);//发现Dep类上临时存的watch,则添加到dep数组中,这里同样后续讲
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        value = newValue;
        dep.notify()//值发生变更了,则调用dep的notify,这个dep只是本函数中的,所以它的deps应该只有一个data[key]的watcher
      }
    }
  });
}

第一步,当然是做数据劫持,把data中的数据递归地响应式化.这样一来,当访问data中的数据时,就能先执行get方法,修改data中的数据时,就能先执行set中的方法.这样我们就能捕获到data中数据的变更.

第二步,执行new Compile(this.$options.el, this)主要是为了遍历dom节点,完成页面的初始化:把data中的数据渲染到页面上.

4.2,完成compile初次渲染页面

这个compile主要的作用就是遍历dom节点,然后将data中的数据渲染到页面上:

js 复制代码
​
class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      this.compile(this.$el);
    }
  }
  compile(dom) {
    dom.childNodes.forEach(node => {
      if (this.isElement(node)) {//元素节点,有v-或者没有
        this.compileElement(node);
      } else if (this.isInter(node)) {//html中数据双向{{}}
        this.compileText(node);
      }
      //有子节点则遍历
      if (node.childNodes) {
        this.compile(node);
      }
    });
  }
  compileElement(node) {
    const nodeAttrs = node.attributes;//伪数组
    Array.from(nodeAttrs).forEach(attr => {//转化为真数组
      const attrName = attr.name;//属性名称
      const exp = attr.value;//属性的值
      //是@事件绑定的
      if(this.isEvent(attrName)){
        const directName = attrName.substring(1)
        this.compileEvent(node,this.$vm, exp, directName);
      }
    });
  }
​
  update(node, exp, dir) {
    const fn = this[dir + "Updater"];//拼接出v-value需要执行的函数
    fn && fn(node, this.$vm[exp]);//执行对应的函数,传入的参数是当前元素和app.data中对应的数据,这一步是初始化渲染使用
    //初次渲染时,还需要在Dep收集器中收集对应的订阅者watcher
    new Watcher(this.$vm, exp, newValue => {
      fn && fn(node, newValue);//因为这里传入的是对应的node,所以每个watcher是会准确更新对应绑定数据的node,而不是整个页面重绘
    });
  }
  compileEvent(node,vm, exp, dir){
    let eventType
    if(dir.indexOf(':')!==-1){
      eventType= dir.split(':')[1];
    }else{
      eventType=dir;
    }
    var cb = vm.$methods && vm.$methods[exp]; //取得绑定的事件方法
    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(vm), false); //冒泡机制绑定事件
    }
  }
  //v-text的处理
  textUpdater(node, value) {
    node.textContent = value;
  }
  //{{}}的处理
  compileText(node) {
    this.update(node, RegExp.$1, "text");
  }
  //判断是不是元素节点
  isElement(node) {
    return node.nodeType === 1;
  }
  //判断是不是{{}}
  isInter(node) {
    return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent);
  }
  // 判断是不是@开头的方法
  isEvent(attr) {
    return attr.startsWith('@')
  }
}

对于这一步,核心在这里:

js 复制代码
  update(node, exp, dir) {
    const fn = this[dir + "Updater"];//拼接出v-value需要执行的函数
    fn && fn(node, this.$vm[exp]);//执行对应的函数,传入的参数是当前元素和app.data中对应的数据,这一步是初始化渲染使用
    //初次渲染时,还需要在Dep收集器中收集对应的订阅者watcher
    new Watcher(this.$vm, exp, newValue => {
      fn && fn(node, newValue);//因为这里传入的是对应的node,所以每个watcher是会准确更新对应绑定数据的node,而不是整个页面重绘
    });
  }

前面调用对应的fn函数完成页面的渲染,而new Watcher则是针对这个dom创建对应的订阅者,并且传入数据和对应的dom更新函数fn.

4.3,完成订阅者的收集工作

4.2中已经开始创建对应dom的订阅者,那data中的数据一旦发生了变更,又如何通知到这个订阅者,让它去执行对应的dom更新函数fn呢?

第一步,肯定是要收集dom对应的订阅者.

我们先写出订阅者类:

js 复制代码
//监听器类,负责维护每一个数据自身的信息,更新函数一执行,则把该数据的最新值传入,调用对应的处理函数,渲染数据
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;
    Dep.target = this;//在Dep上临时存储自己这个watcher
    console.log("存储进去了再次强制访问")
    this.vm[this.key];//再次触发一次get,从而将Dep类上临时存的watch,则添加到dep数组中
    Dep.target = null;//然后将临时存储的watcher清除
  }
​
  update() {
    this.updateFn.call(this.vm, this.vm[this.key]);//this就是watcher,在这里存储了每个数据的watcher信息,is.vm[this.key]就是该数据对应的值。
  }
}

然后写出收集器,用来收集所有的订阅者:

js 复制代码
//放置监听器的数组。其中的每一个deps都是一个监听器watcher,数据一旦变更,则遍历执行其中的监听器的更新函数
class Dep {
  constructor() {
    this.deps = [];
  }
  addDep(watcher) {
    this.deps.push(watcher);
  }
//由于一个key是可以多次使用,建立Dep,一个key只有一个dep但是可以有多个watcher, deps中管理多个watcher,在订阅的时候添加,并统一执行更新,做到精确更新。
  notify() {
    this.deps.forEach(watcher =>watcher.update());//由dep调用,
  }
}

订阅者类中主要是这几行代码:

js 复制代码
Dep.target = this;//在Dep上临时存储自己这个watcher
console.log("存储进去了再次强制访问")
this.vm[this.key];//再次触发一次get,从而将Dep类上临时存的watch,则添加到dep数组中

联系上文中4.1中defineReactive中get方法:

js 复制代码
Dep.target && dep.addDep(Dep.target);//发现Dep类上临时存的watch,则添加到dep数组中

这里就在第一次渲染页面时,把对应数据的订阅者放到了对应的dep数组中.

一旦data中的数据发生了变更,则会执行4.1中的set函数:dep.notify()函数,遍历执行订阅者中的update的方法更新对应的dom.

4.4,vue1.x原理总结

总的来说,vue1.x实现的原理就是:

js 复制代码
1,Object.defineProperty数据劫持data中的数据,在get时如果dep.target上有值则收集订阅者进dep,data发生变更时,则执行dep.notify触发对应dom的更新.
2,compile方法在初始化时执行,它做的事情就两个:第一个就是遍历dom,把data中的数据渲染到页面上.第二个呢?就是new watcher()为该dom创建订阅者,并暂存在Dep.target上,然后让dep.addDep(Dep.target)来存储对应的订阅者.

利用发布订阅模式来理解的话.

js 复制代码
observe:就是一个发布者,是它劫持了data,当数据发生变化时,会执行对应数据的dep.notify()
watcher:就是一个订阅者,它保存了data中的数据,以及对应的dom更新函数(在compile中遍历dom时先渲染页面,再创建watcher)
dep收集器:就是一个中间平台(在compile中遍历dom时先渲染页面,再创建watcher时收集该订阅者).发布者数据变更先触发dep.notify(),就会把dep中的deps遍历通知一遍,执行订阅者的dom更新函数

五,迭代器模式

5.1 可迭代对象和迭代器的区别

Iterator是ES6提出来的迭代器。因为ES6开始数据结构新增了Set和Map,再加上已有的Array和Object,此外用户还可以自行组合它们来形成组合数据结构,复杂的数据结构导致循环遍历难度加大,为简化和统一循环方式,ES6就给出了迭代器(Iterator)这个接口来提供统一访问机制for..of或者...展开符。

它为各种不同的数据结构提供统一的访问机制。提供了一种按序访问集合内各个元素的方法,让我们可以方便地遍历集合内所有的元素。

所有满足迭代器协议的对象,都是可迭代对象。这里需要区分可迭代对象和迭代器的概念。

可迭代对象 迭代器对象
可使用方法 for...of...展开符 next()方法
来源 array,str,set,map等具备[Symbol.iterator]方法,且该方法返回一个迭代器的对象 具备next方法,能够指针移动实现遍历可迭代对象的对象
举例 array,str,set,map等 array,str,set,map等的[Symbol.iterator]方法的返回

迭代器协议:

vbnet 复制代码
具备[Symbol.iterator]方法,且该方法会返回一个迭代器对象,该对象的特征是具备next方法,能够进行迭代。

5.2 迭代器对象的工作原理

迭代器对象的工作原理如下:

lua 复制代码
创建一个指针对象,指向当前数据结构的起始位置
第一次调用next方法时,指针指向数据结构的第一个成员
接下来调用next方法,指针后移,直到指向最后一个成员

于是我们可以创建一个迭代器对象的生成函数,将传入的数组转化为迭代器对象,以便使用next来遍历.

js 复制代码
//迭代器生成函数
function myIterator(arr) {
    let index = 0
    return {
        next: function() {
            let done = ( index >= arr.length );
            let value = ! done ? arr[index++] : undefined;
            return { value, done };
        }
    }
}
let myIter = myIterator([1,2,3]);
console.log(myIter.next());//{ value: 1, done: false }
console.log(myIter.next());//{ value: 2, done: false } 
console.log(myIter.next());//{ value: 3, done: false }  
console.log(myIter.next());//{ value: undefined, done: true }

ES6中原生的可迭代对象迭有Array、Set、Map和String,for..of能够遍历它们是因为它们原型对象上具有Symbol.iterator属性,该属性指向该数据结构的默认迭代器方法,当使用for...of..迭代可迭代对象时,js引擎就会调用其Symbol.iterator方法,从而返回相应的默认迭代器。然后执行完其中的next方法。 举例:

js 复制代码
var arr = [1, 2, 3, 4, 5];     //数组是一个迭代器
// 使用for..of时,,js引擎就会调用其`Symbol.iterator`方法,从而返回相应数据的默认迭代器
for(var v of arr){
    console.log(v); // 12345
}

那么既然它的原型对象上有Symbol.iterator方法,且返回的是对应的默认迭代器,我们就可以利用它生成对应的迭代器,然后使用next()方法访问值:

js 复制代码
var arr = [1, 2, 3, 4, 5];     //数组是一个迭代器
//arr[Symbol.iterator]()会返回arr的默认迭代器,于是就可以使用next
var it = arr[Symbol.iterator]();
console.log(it.next())//{ value: 1, done: false }
console.log(it.next())//{ value: 2, done: false }
console.log(it.next())//{ value: 3, done: false }
console.log(...it)//4 5,只打印出剩余的没有被迭代的,所以...应该也是利用的迭代器的next
console.log(it.next())//{ value: undefined, done: true }//剩余的两个值被...迭代过了,于是这里就结束了

5.3 手写一个可迭代对象

也就是说,一个数据结构只要有Symbol.iterator方法且Symbol.iterator方法返回具备next方法,就可以认为它是是可迭代的(iterable)对象

也就是说,需要满足两个条件:

vbnet 复制代码
1,该对象具备Symbol.iterator方法
2,Symbol.iterator方法返回一个对象,该对象具备next方法(迭代器)。
js 复制代码
// 实现一个可迭代对象
const myIterable = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;
    return {
      next() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    }
  }
};
​
// 遍历可迭代对象
for (const item of myIterable) {
  console.log(item);
  // 输出 1 2 3
}
​
// 通过迭代器对象遍历可迭代对象
const iterator = myIterable[Symbol.iterator]();
console.log(iterator.next()); // {done: false, value: 1}
console.log(iterator.next()); // {done: false, value: 2}
console.log(iterator.next()); // {done: false, value: 3}
console.log(iterator.next()); // {done: true, value: undefined}

能够使用for...of遍历和...扩展符展开。当使用这两种方法时,js引擎其实是调用这个可迭代对象的[Symbol.iterator]方法,从而得到一个迭代器,然后执行这个迭代器的next方法,从而取到其中的值。

但是如果想要使用next方法手动遍历,就需要const myIterable2=createIterator(obj)[Symbol.iterator](),手动执行这个可迭代对象的[Symbol.iterator]方法,从而得到迭代器(具备next方法的对象)。

5.4 将不可迭代的object处理成可迭代

object类型之所以不能迭代,就是因为它的原型对象上没有[Symbol.iterator]属性,想想看,一个对象的属性间并没有严格的顺序要求,自然不要求能迭代。

js 复制代码
// 自定义一个可迭代对象
function createIterator(obj){
    return {    // 返回一个迭代器对象
        //模仿原生迭代器添加Symbol.iterator方法
        [Symbol.iterator]: function () { 
            const keys=Object.keys(obj)
            let i=0
            return {
                next:function () {  //迭代器对象一定有next()方法
                    const done = ( i >= keys.length );
                    const key=keys[i]
                    const value = !done ? obj[key] : undefined;
                    i++
                    return {    //next()方法返回结果对象
                        value: value,
                        done: done
                    }
                }
            }
         }
    }
}
const obj={
    name:'名字',
    age:18
​
}
const myIterable=createIterator(obj)
// 使用 for-of 循环遍历可迭代对象
for(var v of myIterable){
    console.log(v)
    //名字
    //18
}
console.log([...myIterable])//['名字',18]
  
const myIterable2=createIterator(obj)[Symbol.iterator]()
console.log(myIterable2.next())//{ value: '名字', done: false }
console.log(myIterable2.next())//{ value: 18, done: false }  
console.log(myIterable2.next())//{ value: undefined, done: true 

5.5 生成器Generator

它存在的最大作用就是帮助我们快速生成可迭代对象/生成器

JavaScript 中的生成器(Generator)是一种特殊的函数,可以用来定义在运行时生成一系列值的迭代器。Generator 函数以 function* 开头,内部使用 yield 关键字来指定生成器迭代过程中产生的每个值,每次遇到 yield 关键字时暂停函数的执行,可以通过调用迭代器上的 next 方法来恢复函数的执行。

5.5.1 创建一个生成器

js 复制代码
function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}
​
const iter = myGenerator(); // 创建可迭代对象
for(var v of iter){
  console.log(v)//1,2,3
}
const iter2 = myGenerator(); // 创建迭代器对象
console.log(iter2.next()); // { value: 1, done: false }
console.log(iter2.next()); // { value: 2, done: false }
console.log(iter2.next()); // { value: 3, done: false }
console.log(iter2.next()); // { value: undefined, done: true }
​
console.log(iter === iter[Symbol.iterator]()); // true,对象本身是可迭代对象

生成器函数返回的,既是一个可迭代对象(可以使用for...of),又是一个迭代器(可以直接使用next())

lua 复制代码
每次yield就会生成一个断点。每次next就执行到这个断点过。

5.5.2,既是可迭代对象,又是迭代器

类似于这样,Symbol.iterator是返回自身。自身上既有Symbol.iterator又有next,从而可以返回既是可迭代对象,又是迭代器。

js 复制代码
function myGenerator(arr) {
  let i=0
  return {
    next(){
      const done =  i >= arr.length;
      const value = !done ? arr[i++] : undefined;
      return {    //next()方法返回结果对象
          value: value,
          done: done
      }
    },
    [Symbol.iterator]() { 
      return this
    }
  }
}
​
const iter = myGenerator([1,2,3,4,5,6]); // 创建迭代器
for(var v of iter){
  console.log(v)//1,2,3,4,5,6
}
console.log(iter === iter[Symbol.iterator]()); // true,对象本身是可迭代对象
const iter2 = myGenerator([1,2,3,4,5,6,7]); // 创建迭代器
console.log(iter2.next()); // { value: 1, done: false }
console.log(iter2.next()); // { value: 2, done: false }
console.log(iter2.next()); // { value: 3, done: false }
console.log(iter2.next()); // { value: 4, done: false }

生成器生成的对象就有这样的特征。

5.5.3,next传递参数

yield 表达式本身没有返回值,或者说总是返回 undefined 。 next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。 【注意这里是上一个】,也就是说,next在第一个yield处停住,第二次的next入参才是这第一个yield的返回值。

也就是说第一个next带入参没意义,因为它没有上一个断点yield。

js 复制代码
function* foo(x) {
    let y = x * (yield)
    return y
    console.log("111")//后续的代码不会被执行
    yield 2
}
const it = foo(6)
it.next()//执行到yield过,x*这段语句还未执行
let res = it.next(7)//next传入7作为上一个yield的返回,代码接着执行,计算后赋值给y,然后return结束
console.log(res) // { value: 42, done: true }
it.next(8)

return 会强制生成器进入关闭状态,提供给 return 方法的值,就是终止迭代器对象的值 ,也就是说此时返回的对象状态为true,值为传入的值。

5.5.4,return方法提前终止生成器

和上文中一样,return方法也会强制生成器进入关闭状态,提供给 return 方法的值,就是终止迭代器对象的值 ,也就是说此时返回的对象状态为true,值为传入的值。

js 复制代码
function* foo() {
    console.log("11")
    yield 1
    console.log("22")
    yield 2
    console.log("33")
    yield 3
    console.log("44")
    yield 4
}
const it = foo()
console.log("next",it.next())//next { value: 1, done: false }
console.log("return",it.return("test"))//return { value: 'test', done: true }

5.5.5,throw抛出错误终止生成器

throw() 方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。

js 复制代码
function* foo() {
    console.log("11")
    yield 1
    console.log("22")
    yield 2
    console.log("33")
    yield 3
    console.log("44")
    yield 4
}
const it = foo()
console.log("next",it.next())//next { value: 1, done: false }
console.log("return",it.throw(new Error('出错了!')))//Error: 出错了!

简单的理解就是,next()、throw()、return()方法,都是把yield以及后面换成传入的参数。

5.5.6,yield* 表达式委托迭代

yield* 允许我们在 Generator 函数中调用另一个 Generator 函数或可迭代对象。

Generator 函数执行到一个 yield* 表达式时,它会暂停执行,并且将执行权转移到另一个 Generator 函数或可迭代对象中。直到这个函数或对象迭代结束后,执行权才会返回到原 Generator 函数中。

这也叫委托迭代。通过这样的方式,能将多个生成器连接在一起。

js 复制代码
function * anotherGenerator(i) {
    yield i + 1;
    yield i + 2;
    yield i + 3;
}
​
function * generator(i) {
    yield* anotherGenerator(i);
    yield "最后一个"
}
var gen = generator(1);
console.log(gen.next().value)//2
console.log(gen.next().value)//3
console.log(gen.next().value)//4
console.log(gen.next().value)//最后一个
for (let value of generator(2)) {
    console.log(value); // 输出 3,4,5,'最后一个'
}

六,参考文章:

JavaScript 设计模式核心原理与应用实践 - 修言 - 掘金小册 (juejin.cn)

vue1.x原理解析 - 掘金 (juejin.cn)

从promise到await - 掘金 (juejin.cn)

七,系列文章

本文是我整理的js基础文章中的一篇,下面是已经完成的文章:

js从编译到执行过程 - 掘金 (juejin.cn)

从异步到promise - 掘金 (juejin.cn)

从promise到await - 掘金 (juejin.cn)

浅谈异步编程中错误的捕获 - 掘金 (juejin.cn)

作用域和作用域链 - 掘金 (juejin.cn)

原型链和原型对象 - 掘金 (juejin.cn)

this的指向原理浅谈 - 掘金 (juejin.cn)

js的函数传参之值传递 - 掘金 (juejin.cn)

js的事件循环机制 - 掘金 (juejin.cn)

从作用域链和内存角度重新理解闭包 - 掘金 (juejin.cn)

js的垃圾回收机制 - 掘金 (juejin.cn)

js的模块化 - 掘金 (juejin.cn)

js设计模式-创建型 - 掘金 (juejin.cn)

js设计模式-结构型 - 掘金 (juejin.cn)

相关推荐
时清云27 分钟前
【算法】合并两个有序链表
前端·算法·面试
小爱丨同学35 分钟前
宏队列和微队列
前端·javascript
沉登c1 小时前
Javascript客户端时间与服务器时间
服务器·javascript
持久的棒棒君1 小时前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_857297911 小时前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋2 小时前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者4 小时前
React 19 新特性详解
前端
小程xy4 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6324 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6324 小时前
WebGL编程指南之进入三维世界
前端·webgl