常用的JavaScript设计模式你都知道吗?
原文:https://medium.com/better-programming...
译者:前端技术小哥
简介
我们编写代码是为了解决问题。 这些问题通常有很多相似之处,在尝试解决这些问题时,我们会注意到几种常见的模式。 这就是设计模式的用武之地。
设计模式这个术语在软件工程中用来表示软件设计中经常出现的问题的通用的、可重用的解决方案。
设计模式的基本概念从一开始就存在于软件工程行业,但它们并没有真正正式化。由Erich Gamma、Richard Helm、Ralph Johnson和John Vlisides,著名的四人帮(GoF),编写的《设计模式:可复用面对对象软件的基础》一书有助于推动软件工程中设计模式的形式化概念。现在,设计模式是软件开发的重要组成部分,并且已经存在很长时间了。
原著介绍了23种设计模式。
设计模式的好处有很多。它们是业界资深人士已经尝试和测试过的经过验证的解决方案。它们是可靠的方法,能够以广泛接受的方式解决问题,并反映出帮助定义问题的行业领先开发人员的经验和见解。模式还使我们的代码更具可重用性和可读性,同时大大加快了开发过程。
设计模式绝不是最终的解决方案。它们只是为我们提供了解决问题的方法或方案。
注意:在本文中,我们将主要从面向对象的角度以及它们在现代JavaScript中的可用性来讨论设计模式。这就是为什么许多来自GoF的经典模式不会被提及,而来自AddyOsmani的《JavaScript设计模式》中的一些现代模式将被囊括。为了便于理解,这些示例都保持简单,因此不是各自设计模式的最佳实现。
设计模式的类别
设计模式通常分为三大类。
创建型模式
顾名思义,这些模式用于处理对象创建机制。创造性设计模式基本上通过控制对象的创建过程来解决问题。
我们将详细讨论以下模式: 建造者模式、工厂模式、原型模式和单例模式。
结构型模式
这些模式与类和对象组合有关。它们帮助构造或重组一个或多个部分,而不影响整个系统。换句话说,它们帮助获得新的功能,而不改变现有的功能。
我们将详细讨论以下模式:适配器模式、复合模式、装饰模式、外观模式、享元模式和代理模式。
行为型模式
这些模式涉及改善不同对象之间的通信。
我们将详细讨论以下模式:责任链模式、命令模式、迭代器模式、中介者模式、观察者模式、状态模式、策略模式和模板方法模式。
建造者模式
这是一个基于类的创造性设计模式。构造函数是一种特殊的函数,可用于用该函数定义的方法和属性实例化新对象。
它不是经典的设计模式之一。事实上,在大多数面向对象的语言中,它更像是一种基本的语言构造,而不是模式。但是在JavaScript中,对象可以在没有任何构造函数或“类”定义的情况下动态创建。因此,我认为以这个简单模式为其他模式打下基础是很重要的。
构造函数模式是JavaScript中最常用的模式之一,用于创建给定类型的新对象。
在下面的例子中,我们定义了一个类Hero,它具有name和specialAbility等属性,以及getDetails等方法。然后,我们通过调用构造函数方法实例化一个对象IronMan,该方法使用new关键字作为参数传入相应属性的值。
// traditional Function-based syntax function Hero(name, specialAbility) { // setting property values this.name = name; this.specialAbility = specialAbility; // declaring a method on the object this.getDetails = function() { return this.name + ' can ' + this.specialAbility; }; } // ES6 Class syntax class Hero { constructor(name, specialAbility) { // setting property values this._name = name; this._specialAbility = specialAbility; // declaring a method on the object this.getDetails = function() { return `${this._name} can ${this._specialAbility}`; }; } } // creating new instances of Hero const IronMan = new Hero('Iron Man', 'fly'); console.log(IronMan.getDetails()); // Iron Man can fly
工厂模式
工厂模式是另一种基于类的创造模式。在这里,我们提供了一个泛型接口,它将对象实例化的责任委托给它的子类。
当我们需要管理或操作不同但具有许多相似特征的对象集合时,经常使用这种模式。
在下面的例子中,我们创建了一个名为BallFactory的工厂类,它有一个接受参数的方法,根据参数,它将对象实例化职责委托给相应的类。如果类型参数是"football"或"soccer"对象实例化由Football类处理,但是如果类型参数是"basketball"对象实例化则由Basketball类处理。
class BallFactory { constructor() { this.createBall = function(type) { let ball; if (type === 'football' || type === 'soccer') ball = new Football(); else if (type === 'basketball') ball = new Basketball(); ball.roll = function() { return `The ${this._type} is rolling.`; }; return ball; }; } } class Football { constructor() { this._type = 'football'; this.kick = function() { return 'You kicked the football.'; }; } } class Basketball { constructor() { this._type = 'basketball'; this.bounce = function() { return 'You bounced the basketball.'; }; } } // creating objects const factory = new BallFactory(); const myFootball = factory.createBall('football'); const myBasketball = factory.createBall('basketball'); console.log(myFootball.roll()); // The football is rolling. console.log(myBasketball.roll()); // The basketball is rolling. console.log(myFootball.kick()); // You kicked the football. console.log(myBasketball.bounce()); // You bounced the basketball.
原型模式
这是基于对象的创造型设计模式。在这里,我们使用现有对象的某种“骨架”来创建或实例化新对象。
这种模式对JavaScript特别重要和有益,因为它使用原型继承而不是经典的面向对象继承。因此,它发挥了JavaScript的优势,并具有本机支持。
在下面的例子中,我们使用一个对象car作为原型,用JavaScript的Object.create特点创建另一个对象myCar。并在新对象上定义一个额外的属性owner。
// using Object.create as was recommended by ES5 standard const car = { noOfWheels: 4, start() { return 'started'; }, stop() { return 'stopped'; }, }; // Object.create(proto[, propertiesObject]) const myCar = Object.create(car, { owner: { value: 'John' } }); console.log(myCar.__proto__ === car); // true
适配器模式
这是一种结构型模式,其中一个类的接口被转换成另一个类。这种模式允许由于接口不兼容而无法协同工作的类一起工作。
此模式通常用于为新的重构API创建包装器,以便其他现有旧API仍然可以使用它们。这通常是在新的实现或代码重构(出于性能提高等原因)导致不同的公共API时完成的,而系统的其他部分仍在使用旧的API,需要进行调整才能协同工作。
在下面的例子中,我们有一个旧的API,即类OldCalculator,和一个新的API,即类NewCalculator。类OldCalculator为加法和减法提供了一个operation方法,而NewCalculator为加法和减法提供了单独的方法。适配器类CalcAdapter将NewCalculator包裹来将操作方法添加到面向公共的API中,同时在底层使用自己的加减法。
// old interface class OldCalculator { constructor() { this.operations = function(term1, term2, operation) { switch (operation) { case 'add': return term1 + term2; case 'sub': return term1 - term2; default: return NaN; } }; } } // new interface class NewCalculator { constructor() { this.add = function(term1, term2) { return term1 + term2; }; this.sub = function(term1, term2) { return term1 - term2; }; } } // Adapter Class class CalcAdapter { constructor() { const newCalc = new NewCalculator(); this.operations = function(term1, term2, operation) { switch (operation) { case 'add': // using the new implementation under the hood return newCalc.add(term1, term2); case 'sub': return newCalc.sub(term1, term2); default: return NaN; } }; } } // usage const oldCalc = new OldCalculator(); console.log(oldCalc.operations(10, 5, 'add')); // 15 const newCalc = new NewCalculator(); console.log(newCalc.add(10, 5)); // 15 const adaptedCalc = new CalcAdapter(); console.log(adaptedCalc.operations(10, 5, 'add')); // 15;
复合模式
这是一种结构型设计模式,它将对象组合成树状结构来表示整个部分的层次结构。在此模式中,类树结构中的每个节点可以是单个对象,也可以是对象的组合集合。无论如何,每个节点都被统一处理。
将这种模式可视化是有点复杂的。思考这个问题最简单的方法是使用多层次菜单的例子。每个节点可以是一个不同的选项,也可以是菜单本身,它的子节点有多个选项。带有子组件的节点组件是复合组件,而没有子组件的节点组件是叶子组件。
在下面的例子中,我们创建了一个Component基类,它实现了所需的公共功能,并抽象了所需的其他方法。基类还有一个静态方法,它使用递归遍历由子类构成的复合树结构。然后,我们创建两个子类来扩展基类—不包含任何子类的Leaf和可以包含子类的Composite—因此具有处理添加、搜索和删除子功能的方法。这两个子类用于创建复合结构—在本例中是树。
class Component { constructor(name) { this._name = name; } getNodeName() { return this._name; } // abstract methods that need to be overridden getType() {} addChild(component) {} removeChildByName(componentName) {} removeChildByIndex(index) {} getChildByName(componentName) {} getChildByIndex(index) {} noOfChildren() {} static logTreeStructure(root) { let treeStructure = ''; function traverse(node, indent = 0) { treeStructure += `${'--'.repeat(indent)}${node.getNodeName()}\n`; indent++; for (let i = 0, length = node.noOfChildren(); i < length; i++) { traverse(node.getChildByIndex(i), indent); } } traverse(root); return treeStructure; } } class Leaf extends Component { constructor(name) { super(name); this._type = 'Leaf Node'; } getType() { return this._type; } noOfChildren() { return 0; } } class Composite extends Component { constructor(name) { super(name); this._type = 'Composite Node'; this._children = []; } getType() { return this._type; } addChild(component) { this._children = [...this._children, component]; } removeChildByName(componentName) { this._children = [...this._children].filter(component => component.getNodeName() !== componentName); } removeChildByIndex(index) { this._children = [...this._children.slice(0, index), ...this._children.slice(index + 1)]; } getChildByName(componentName) { return this._children.find(component => component.name === componentName); } getChildByIndex(index) { return this._children[index]; } noOfChildren() { return this._children.length; } } // usage const tree = new Composite('root'); tree.addChild(new Leaf('left')); const right = new Composite('right'); tree.addChild(right); right.addChild(new Leaf('right-left')); const rightMid = new Composite('right-middle'); right.addChild(rightMid); right.addChild(new Leaf('right-right')); rightMid.addChild(new Leaf('left-end')); rightMid.addChild(new Leaf('right-end')); // log console.log(Component.logTreeStructure(tree)); /* root --left --right ----right-left ----right-middle ------left-end ------right-end ----right-right */
装饰器模式
class Book { constructor(title, author, price) { this._title = title; this._author = author; this.price = price; } getDetails() { return `${this._title} by ${this._author}`; } } // decorator 1 function giftWrap(book) { book.isGiftWrapped = true; book.unwrap = function() { return `Unwrapped ${book.getDetails()}`; }; return book; } // decorator 2 function hardbindBook(book) { book.isHardbound = true; book.price += 5; return book; } // usage const alchemist = giftWrap(new Book('The Alchemist', 'Paulo Coelho', 10)); console.log(alchemist.isGiftWrapped); // true console.log(alchemist.unwrap()); // 'Unwrapped The Alchemist by Paulo Coelho' const inferno = hardbindBook(new Book('Inferno', 'Dan Brown', 15)); console.log(inferno.isHardbound); // true console.log(inferno.price); // 20
外观模式
这是一种在JavaScript库中广泛使用的结构设计模式。它用于提供一个统一的、更简单的、面向公众的接口,以便于使用,从而避免了其组成子系统或子类的复杂性。
这种模式的使用在jQuery这样的库中非常常见。
在这个例子中,我们创建了一个面向公共的API,其中包含了一个类ComplaintRegistry。它只公开客户端要使用的一种方法,即registerComplaint。它根据类型参数内部处理ProductComplaint或ServiceComplaint所需对象的实例化。它还处理所有其他复杂的功能,如生成唯一的ID、将complaints存储在内存中等等。但是,使用外观模式隐藏了所有这些复杂性。
let currentId = 0; class ComplaintRegistry { registerComplaint(customer, type, details) { const id = ComplaintRegistry._uniqueIdGenerator(); let registry; if (type === 'service') { registry = new ServiceComplaints(); } else { registry = new ProductComplaints(); } return registry.addComplaint({ id, customer, details }); } static _uniqueIdGenerator() { return ++currentId; } } class Complaints { constructor() { this.complaints = []; } addComplaint(complaint) { this.complaints.push(complaint); return this.replyMessage(complaint); } getComplaint(id) { return this.complaints.find(complaint => complaint.id === id); } replyMessage(complaint) {} } class ProductComplaints extends Complaints { constructor() { super(); if (ProductComplaints.exists) { return ProductComplaints.instance; } ProductComplaints.instance = this; ProductComplaints.exists = true; return this; } replyMessage({ id, customer, details }) { return `Complaint No. ${id} reported by ${customer} regarding ${details} have been filed with the Products Complaint Department. Replacement/Repairment of the product as per terms and conditions will be carried out soon.`; } } class ServiceComplaints extends Complaints { constructor() { super(); if (ServiceComplaints.exists) { return ServiceComplaints.instance; } ServiceComplaints.instance = this; ServiceComplaints.exists = true; return this; } replyMessage({ id, customer, details }) { return `Complaint No. ${id} reported by ${customer} regarding ${details} have been filed with the Service Complaint Department. The issue will be resolved or the purchase will be refunded as per terms and conditions.`; } } // usage const registry = new ComplaintRegistry(); const reportService = registry.registerComplaint('Martha', 'service', 'availability'); // 'Complaint No. 1 reported by Martha regarding availability have been filed with the Service Complaint Department. The issue will be resolved or the purchase will be refunded as per terms and conditions.' const reportProduct = registry.registerComplaint('Jane', 'product', 'faded color'); // 'Complaint No. 2 reported by Jane regarding faded color have been filed with the Products Complaint Department. Replacement/Repairment of the product as per terms and conditions will be carried out soon.'
享元模式
这是一种结构型设计模式,专注于通过细粒度对象进行有效的数据共享。 它用于提高效率和记忆保存目的。
此模式可用于任何类型的缓存目的。 事实上,现代浏览器使用享元模式的变体来防止两次加载相同的图像。
在下面的例子中,我们创建了一个细粒度的享元类Icecream,用于分享有关冰淇淋口味的数据,以及一个工厂级的IcecreamFactory来创建这些享元类对象。 对于内存保留,如果同一对象被实例化两次,则对象将被回收。
这是享元实现的一个简单示例。
// flyweight class class Icecream { constructor(flavour, price) { this.flavour = flavour; this.price = price; } } // factory for flyweight objects class IcecreamFactory { constructor() { this._icecreams = []; } createIcecream(flavour, price) { let icecream = this.getIcecream(flavour); if (icecream) { return icecream; } else { const newIcecream = new Icecream(flavour, price); this._icecreams.push(newIcecream); return newIcecream; } } getIcecream(flavour) { return this._icecreams.find(icecream => icecream.flavour === flavour); } } // usage const factory = new IcecreamFactory(); const chocoVanilla = factory.createIcecream('chocolate and vanilla', 15); const vanillaChoco = factory.createIcecream('chocolate and vanilla', 15); // reference to the same object console.log(chocoVanilla === vanillaChoco); // true
代理模式
这是一种结构型设计模式,其行为完全符合其名称。它充当另一个对象的代理或占位符来控制对它的访问。
它通常用于目标对象受到约束且可能无法有效地处理其所有职责的情况。在这种情况下,代理通常向客户机提供相同的接口,并添加一个间接层,以支持对目标对象的受控访问,以避免对目标对象施加过大的压力。
在处理网络请求较多的应用程序时,代理模式非常有用,可以避免不必要的或冗余的网络请求。
在下面的例子中,我们将使用两个新的ES6特性,Proxy和Reflect。代理对象用于为JavaScript对象的基本操作定义自定义行为(记住,函数和数组也是JavaScript中的对象)。它是一个可用于创建对象Proxy的构造函数方法。它接受要代理的对象target和定义必要定制的handler对象。handler对象允许定义一些陷阱函数,如get、set、has、apply等,这些函数用于将添加附加到它们的用法中的自定义行为。另一方面,Reflect是一个内置对象,它提供类似于Proxy的handler对象支持的方法作为静态方法。它不是构造函数; 其静态方法用于可拦截的JavaScript操作。
现在,我们创建一个函数,它可以看作是一个网络请求。我们将其命名为networkFetch。它接受一个URL并相应地作出响应。我们希望实现一个代理,在这个代理中,只有在缓存中没有网络响应时,我们才能从网络获得响应。否则,我们只从缓存返回一个响应。
全局变量cache将存储缓存的响应。我们创建了一个名为proxiedNetworkFetch的代理,将原始networkFetch作为target,并在对象handler中使用apply方法来代理函数调用。apply方法在对象target本身上传递。这个值作为thisArg,参数以类似数组的结构args传递给它。
我们检查传递的url参数是否在缓存中。如果它存在于缓存中,我们将从那里返回响应,而不会调用原始目标函数。如果没有,那么我们使用Reflect.apply方法用thisArg(尽管在我们的例子中它没有任何意义)来调用函数target和它传递的参数。
// Target function networkFetch(url) { return `${url} - Response from network`; } // Proxy // ES6 Proxy API = new Proxy(target, handler); const cache = []; const proxiedNetworkFetch = new Proxy(networkFetch, { apply(target, thisArg, args) { const urlParam = args[0]; if (cache.includes(urlParam)) { return `${urlParam} - Response from cache`; } else { cache.push(urlParam); return Reflect.apply(target, thisArg, args); } }, }); // usage console.log(proxiedNetworkFetch('dogPic.jpg')); // 'dogPic.jpg - Response from network' console.log(proxiedNetworkFetch('dogPic.jpg')); // 'dogPic.jpg - Response from cache'
责任链模式
这是一种提供松散耦合对象链的行为型设计模式。这些对象中的每一个都可以选择处理客户机的请求。
责任链模式的一个很好的例子是DOM中的事件冒泡机制,其中一个事件通过一系列嵌套的DOM元素传播,其中一个元素可能附加了一个“事件侦听器”来侦听该事件并对其进行操作。
在下面的例子中,我们创建了一个CumulativeSum,可以使用一个可选的initialValue进行实例化。它有一个方法add,将传递的值添加到对象的sum属性中,并返回object本身,以允许链接add方法调用。
在jQuery中也可以看到这种常见的模式,对jQuery对象的几乎任何方法调用都会返回一个jQuery对象,以便将方法调用链接在一起。
class CumulativeSum { constructor(intialValue = 0) { this.sum = intialValue; } add(value) { this.sum += value; return this; } } // usage const sum1 = new CumulativeSum(); console.log(sum1.add(10).add(2).add(50).sum); // 62 const sum2 = new CumulativeSum(10); console.log(sum2.add(10).add(20).add(5).sum); // 45
命令模式
这是一种行为型设计模式,旨在将操作或操作封装为对象。 此模式允许通过将请求操作或调用方法的对象与执行或处理实际实现的对象分离来松散耦合系统和类。
剪贴板交互API有点类似于命令模式。 如果您是Redux用户,则您已经遇到过命令模式。 使我们能够回到之前的时间节点的操作不过就是把可以跟踪以重做或撤消操作封装起来。 因此,我们实现了时间旅行式调试。
在下面的例子中,我们有一个名为SpecialMath的类,它有多个方法和一个Command类,它封装了要在其主题上执行的命令,即SpecialMath类的一个对象。Command类还跟踪所有已执行的命令,这些命令可用于扩展其功能以包括撤消和重做类型操作。
class SpecialMath { constructor(num) { this._num = num; } square() { return this._num ** 2; } cube() { return this._num ** 3; } squareRoot() { return Math.sqrt(this._num); } } class Command { constructor(subject) { this._subject = subject; this.commandsExecuted = []; } execute(command) { this.commandsExecuted.push(command); return this._subject[command](); } } // usage const x = new Command(new SpecialMath(5)); x.execute('square'); x.execute('cube'); console.log(x.commandsExecuted); // ['square', 'cube']
迭代器模式
它是一种行为型设计模式,提供了一种方法来顺序访问聚合对象的各个元素,而无需暴露其内部表示。
迭代器有一种特殊的行为,在这种行为中,我们通过调用next()一次遍历一组有序的值,直到到达末尾。在ES6中引入迭代器和生成器使得迭代器模式的实现非常简单。
下面有两个例子。首先,一个IteratorClass使用iterator规范,而另一个iteratorUsingGenerator使用生成器函数。
Symbol.iterator(Symbol-一种新的基本数据类型)用于指定对象的默认迭代器。它必须被定义为一个集合,以便能够使用for...of的循环结构。在第一个示例中,我们定义构造函数来存储一些数据集合,然后定义符号Symbol.iterator,它返回一个对象和用于迭代的next方法。
对于第二种情况,我们定义了一个生成器函数,将数据数组传递给它,并使用next和yield迭代地返回它的元素。生成器函数是一种特殊类型的函数,它充当迭代器的工厂,可以迭代地显式维护自己的内部状态和生成值。它可以暂停并恢复自己的执行周期。
中介者模式
它是一种行为型设计模式,封装了一组对象如何相互交互。它通过促进松散耦合,防止对象彼此显式引用,从而为一组对象提供了中央权限。
在下面的例子中,我们使用TrafficTower作为中介来控制Airplane对象之间的交互方式。所有Airplane对象都将自己注册到一个TrafficTower对象中,而中介类对象处理Airplane对象如何接收所有其他Airplane对象的坐标数据。
class TrafficTower { constructor() { this._airplanes = []; } register(airplane) { this._airplanes.push(airplane); airplane.register(this); } requestCoordinates(airplane) { return this._airplanes.filter(plane => airplane !== plane).map(plane => plane.coordinates); } } class Airplane { constructor(coordinates) { this.coordinates = coordinates; this.trafficTower = null; } register(trafficTower) { this.trafficTower = trafficTower; } requestCoordinates() { if (this.trafficTower) return this.trafficTower.requestCoordinates(this); return null; } } // usage const tower = new TrafficTower(); const airplanes = [new Airplane(10), new Airplane(20), new Airplane(30)]; airplanes.forEach(airplane => { tower.register(airplane); }); console.log(airplanes.map(airplane => airplane.requestCoordinates())) // [[20, 30], [10, 30], [10, 20]]
观察者模式
它是一个至关重要的行为型设计模式,定义了对象之间一对多的依赖关系,这样当一个对象(发布方)更改其状态时,所有其他依赖对象(订阅者)都会得到通知并自动更新。这也称为PubSub(发布者/订阅者)或事件分派器/侦听器模式。发布者有时称为主题,订阅者有时称为观察者。
如果您曾经使用addEventListener或jQuery的.on编写过偶数处理代码,那么您可能已经对这种模式有些熟悉了。它在反应性编程(就像RxJS)中也有影响。
在本例中,我们创建了一个简单的Subject类,该类具有从订阅者集合中添加和删除Observer类对象的方法。此外,还提供一个fire方法,用于将Subject类对象中的任何更改传播给订阅的观察者。另一方面,Observer类有自己的内部状态,以及基于订阅的Subject传播的更改更新内部状态的方法。
状态模式
是一种行为型设计模式,允许对象根据其内部状态的变化来改变其行为。状态模式类返回的对象似乎改变了它的类。它为有限的一组对象提供特定于状态的逻辑,其中每个对象类型表示特定的状态。
我们将以交通灯为例来理解这种模式。TrafficLight类根据其内部状态(Red、Yellow或Green类的对象)更改返回的对象。
策略模式
它是一种行为型设计模式,允许为特定任务封装替代算法。它定义了一系列算法,并以一种可以在运行时互换的方式封装它们,而不需要客户机的干涉或知识。
在下面的示例中,我们创建了一个Commute类,用于封装所有可能的通勤策略。然后,我们定义了三种策略,即Bus、PersonalCar和Taxi。使用此模式,我们可以在运行时将实现替换为Commute对象的travel方法。
模板方法模式
这是一种行为型设计模式,基于定义算法的框架或操作的实现,但将一些步骤推迟到子类。它允许子类在不改变算法外部结构的情况下重新定义算法的某些步骤。
在本例中,我们有一个模板类Employee,它部分实现了work方法。子类实现职责方法是为了使它作为一个整体发挥作用。然后,我们创建了两个子类Developer和Tester,它们扩展了模板类并实现了所需的方法。
总结
设计模式对软件工程至关重要,也对我们解决常见问题非常有帮助。 但这是一个非常广泛的主题,并且根本不可能在短短的一篇文章中囊括关于它们的所有内容。 因此,我选择简短而简洁地谈论我认为在编写现代JavaScript时非常方便的那些。 为了深入了解,我建议你看一下这些书:
《设计模式:可复用面向对象软件的基础》,作者:Erich Gamma,Richard Helm,Ralph Johnson和John Vlissides(即“四人帮”)
《JavaScript设计模式》,作者:Addy Osmani
《JavaScript模式》,作者:Stoyan Stefanov
《JavaScript模式》,作者:Stoyan Stefanov
❤️ 看之后
- 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
- 关注公众号「新前端社区」,号享受文章首发体验!每周重点攻克一个前端技术难点。