Javascript是一种基于原型的面向对象语言,但是可以通过多种方式变为基于类的语言,方法是:


自己编写要用作类的函数
在框架中使用漂亮的类系统(例如mootools Class.Class)。
从Coffeescript生成它。但是最近我一直在使用Javascript框架和NodeJS,它们脱离了类的概念,而更多地依赖于代码的动态特性,例如:


异步编程,使用并编写使用回调/事件的编写代码
通过RequireJS加载模块(以使它们不会泄漏到全局名称空间)
功能编程概念,例如列表推导(映射,过滤器等)


到目前为止,我收集到的是大多数我已经阅读的OO原理和模式(例如SOLID和GoF模式)都是为基于类的OO语言编写的,例如Smalltalk和C ++。但是,其中有哪些适用于基于原型的语言(例如Javascript)?避免回调地狱,邪恶评估或任何其他反模式的原则。

#1 楼

经过多次编辑后,此答案的长度已变成怪物。我先向您道歉。延迟评估与延迟加载类似,但是延迟评估实际上是将代码存储在字符串中,然后使用eval()eval评估代码。如果您使用一些技巧,那么它将变得比邪恶更有用,但是如果您不这样做,则可能导致不良后果。您可以查看使用此模式的模块系统:https://github.com/TheHydroImpulse/resolve.js。 Resolve.js主要使用eval而不是new Function来建模每个模块中可用的CommonJS new Functionexports变量,而module将代码包装在一个匿名函数中,尽管如此,我最终还是将每个模块包装在一个函数中,我手动将其组合在一起带有eval。

您将在以下两篇文章中了解到更多信息,后面的文章也将参考第一篇。 > AMD不是答案

和谐生成器

现在,生成器终于在标志(new Function--harmony)下降落在V8中,并因此降落在Node.js中。这些大大减少了您拥有的回调地狱的数量。它使编写异步代码确实很棒。

利用生成器的最佳方法是采用某种控制流库。

概述/概述:

如果您不熟悉发电机,则是暂停发电机的一种做法。执行特殊功能(称为生成器)。这种做法称为使用--harmony-generators关键字产生。

示例:

function* someGenerator() {
  yield []; // Pause the function and pass an empty array.
}


因此,每当您第一次调用此函数时,它就会ll返回一个新的生成器实例。这使您可以在该对象上调用yield来启动或恢复生成器。

var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }


您将继续调用next(),直到next返回done为止。这意味着生成器已完全完成其执行,并且不再有true语句。

Control-Flow:

您可以看到,控制生成器不是自动的。您需要手动继续每个。这就是为什么使用诸如co之类的控制流库的原因。
示例:

var co = require('co');

co(function*() {
  yield query();
  yield query2();
  yield query3();
  render();
});


这使得可以在Node中编写所有内容(并且带有Facebook Regenerator的浏览器,它以同步方式使用和谐生成器并分离出完全兼容的ES5代码作为源代码作为输入。

生成器仍然很新,因此需要Node.js> = v11.2。在撰写本文时,v0.11.x仍然不稳定,因此许多本机模块都已损坏,直到v0.12为止,本机API会平静下来。


为了补充我的原始答案:

我最近一直更喜欢JavaScript中功能更强大的API。约定确实在必要时在幕后使用OOP,但是它简化了所有操作。

例如,查看系统(客户端或服务器)。

view('home.welcome');


比以下内容容易阅读或遵循:

var views = {};
views['home.welcome'] = new View('home.welcome');


yield函数只是检查是否本地地图中已经存在相同的视图。如果该视图不存在,它将创建一个新视图并向地图添加一个新条目。

function view(name) {
  if (!name) // Throw an error

  if (view.views[name]) return view.views[name];

  return view.views[name] = new View({
    name: name
  });
}

// Local Map
view.views = {};


非常基本,对吧?我发现它极大地简化了公共界面并使其更易于使用。我还采用了链功能...在大多数公开接口中都使用这种功能方法。

有些人利用光纤来避免“回叫地狱”。这是使用JavaScript的完全不同的方法,我不是它的忠实拥护者,但是许多框架/平台都在使用它。包括Meteor,因为他们将Node.js视为线程/每个连接平台。

我宁愿使用抽象方法来避免回调地狱。它可能会很麻烦,但是会大大简化实际的应用程序代码。在帮助构建TowerJS框架时,它解决了许多问题,显然,您仍然会具有一定程度的回调,但是嵌套并不深入。

view('home.welcome')
   .child('menus')
   .child('auth')


我们正在开发中的路由系统和“控制器”的示例,尽管与传统的“类似导轨的”完全不同。但是该示例功能非常强大,可以最大程度地减少回调,并使事情变得显而易见。

这种方法的问题在于所有内容都是抽象的。什么都不能照原样运行,并且需要背后的“框架”。但是,如果在框架内实现这些功能和编码样式,那将是一个巨大的胜利。

对于JavaScript中的模式,它确实取决于。仅当使用CoffeeScript,Ember或任何“类”框架/基础结构时,继承才真正有用。当您处于“纯” JavaScript环境中时,使用传统原型接口的工作就像一种魅力:

一种不同的构造对象的方法。而不是单独构造每个原型方法,您将使用类似模块的界面。基本。 br />
基于事件/组件的设计

基于事件和基于组件的模型是IMO的赢家,或者最容易使用,特别是在使用带有内置EventEmitter组件的Node.js时,尽管实现此类发射器很简单,但这只是一个不错的选择。

// app/config/server/routes.js
App.Router = Tower.Router.extend({
  root: Tower.Route.extend({
    route: '/',
    enter: function(context, next) {
      context.postsController.page(1).all(function(error, posts) {
        context.bootstrapData = {posts: posts};
        next();
      });
    },
    action: function(context, next) {
      context.response.render('index', context);
      next();
    },
    postRoutes: App.PostRoutes
  })
});


只是一个示例,但这是一个很好的模型。尤其是在面向游戏/组件的项目中。

组件设计本身就是一个独立的概念,但我认为与事件系统结合使用时效果非常好。传统上,游戏以基于组件的设计而闻名,在这种情况下,面向对象的编程才可以带您到现在为止。

基于组件的设计有它的用途。这取决于建筑物的系统类型。我敢肯定它可以与Web应用程序一起使用,但是由于对象的数量和单独的系统,它在游戏环境中可以非常好地工作,但是肯定还有其他示例。

Pub / Sub模式

事件绑定和pub / sub相似。由于使用统一语言,因此发布/订阅模式确实在Node.js应用程序中大放异彩,但是它可以在任何语言中使用。

function Controller() {
    this.resource = get('resource');
}

Controller.prototype.index = function(req, res, next) {
    next();
};


观察者

这可能是一个主观的,因为有些人选择将Observer模式视为pub / sub,但是它们有所不同。

“观察者是一种设计模式,其中一个对象(称为主题)维护一个依赖于它的对象列表(观察者),并自动将状态的任何更改通知它们。” -观察者模式

观察者模式是超越典型发布/订阅系统的一步。对象之间具有严格的关系或通信方法。对象“主题”将保留依赖项“观察者”列表。该主题将使它的观察者保持最新。

响应式编程

响应式编程是一个更小,更未知的概念,尤其是在JavaScript中。 (据我所知)有一个框架/库公开了一种易于使用的API来使用此“反应式编程”。

反应式编程资源:



什么是(功能性)反应式编程?

响应式编程

基本上,它具有一组同步数据(例如变量,函数等)。

Ember.Controller.extend({
   index: function() {
      this.hello = 123;
   },
   constructor: function() {
      console.log(123);
   }
});


我相信反应式编程被相当多地隐藏了,尤其是在命令式语言中。这是一个非常强大的编程范例,尤其是在Node.js中。流星已经创建了它自己的反应引擎,该引擎基本上基于该引擎。流星的反应性在幕后如何运作?是对其内部工作原理的很好概述。

event.on("update", function(){
    this.component.ship.velocity = 0;
    event.emit("change.ship.velocity");
});


这将正常执行,显示view的值,但是如果我们更改它,

Session.set('name','Bob');

它将重新输出显示name的console.log。一个基本示例,但是您可以将此技术应用于实时数据模型和事务。您可以在此协议后面创建功能非常强大的系统。

流星的...


上下文实现
ContextSet实现
会话实现

反应模式和观察者模式非常相似。主要区别在于,观察者模式通常用整个对象/类描述数据流,而反应式编程则用特定属性描述数据流。

流星是反应式编程的一个很好的例子。由于JavaScript缺乏本机值更改事件(和谐代理更改了该值),因此运行时有点复杂。其他客户端框架Ember.js和AngularJS也利用反应式编程(在某种程度上)。

后两个框架最显着地在模板上使用反应模式(即自动更新)。 Angular.js使用一种简单的脏检查技术。我不会称其为完全反应式编程,但是它很接近,因为脏检查不是实时的。 Ember.js使用不同的方法。 Ember使用Hello Bobset()方法,使它们可以立即更新依赖的值。凭借其运行循环,它非常高效,并且可以在角度有理论极限的情况下提供更多的相关值。

承诺

不是回调的修补程序,但可以减少一些缩进,并将嵌套函数保持在最低限度。它还为该问题添加了一些不错的语法。

model.subscribe("message", function(event){
    console.log(event.params.message);
});

model.publish("message", {message: "Hello, World"});


还可以扩展回调函数,使它们不内联,但这是另一个设计决定。

另一种方法是将事件和诺言结合到适当的位置,以使您有适当的功能来分配事件,然后将实际的功能函数(其中具有真实逻辑的函数)绑定到特定事件。但是,您需要在每个回调位置内传递dispatcher方法,但是您必须弄清楚一些麻烦,例如参数,知道要分派给哪个函数等...

单个函数函数

不要将大量的回调混为一谈,而是将单个函数保留给单个任务,并很好地完成该任务。有时您可以超越自己并在每个功能中添加更多功能,但是请问自己:这可以成为一个独立的功能吗?命名该函数,这将清理您的缩进,并因此清理回调地狱问题。

最后,我建议开发或使用一个小的“框架”,该框架基本上只是您的应用程序的中坚力量,并花一些时间进行抽象,确定基于事件的系统或“独立”系统。我曾在多个Node.js项目中工作过,这些项目的代码特别令人头疼,尤其是回调地狱,但在他们开始编码之前也缺乏思想。花点时间思考一下API和语法方面的各种可能性。我将重点介绍一些不错的文章:



关于封装和直接对象引用的思考
保持使用信号和中介符解耦的模块
托管依赖与依赖注入RequireJS

控制反转

尽管与回调地狱不完全相关,但它可以帮助您总体架构,尤其是在单元测试中。

控制反转的两个主要子版本是依赖注入和服务定位器。与依赖注入相反,我发现Service Locator在JavaScript中最简单。为什么?主要是因为JavaScript是一种动态语言,并且不存在静态类型。 Java和C#等因依赖项注入而“闻名”,因为您能够检测类型,并且它们内置了接口,类等。这使事情变得相当容易。但是,您可以在JavaScript中重新创建此功能,尽管它并不完全相同,但有点笨拙,我更喜欢在系统中使用服务定位器。

任何类型的控制反转都将极大地将您的代码分离为可以随时模拟或伪造的单独模块。设计了渲染引擎的第二版?太棒了,只需用旧界面替换新界面即可。服务定位器对于新的Harmony Proxies尤其有趣,尽管它仅可在Node.js中有效使用,但它提供了更好的API,而不是使用get()而不是Service.get('render');。我目前正在使用这种系统:https://github.com/TheHydroImpulse/Ettore。 Java,C#,PHP中的注入-它不是静态类型的,但是具有类型提示。)可能被视为一个消极点,您绝对可以将其转变为一个强项。因为一切都是动态的,所以您可以设计一个“伪”静态系统。结合服务定位器,可以将每个组件/模块/类/实例绑定到一个类型。

 var a = 1;
 var b = 2;
 var c = a + b;

 a = 2;

 console.log(c); // should output 4


一个简单的示例。对于现实世界中的有效用法,您需要进一步推广此概念,但是如果您确实希望使用传统的依赖项注入,则可以帮助分离系统。您可能需要弄一点这个概念。在前面的示例中,我并没有花太多时间。几年前,JQuery风靡一时,因此,JQuery插件诞生了。您不需要客户端上的完整框架,只需使用jquery和一些插件即可。

现在,发生了巨大的客户端JavaScript框架大战。其中大多数使用MVC模式,并且使用方式各不相同。 MVC并不总是实现相同的。

如果您使用的是传统的原型接口,那么在使用MVC时可能很难获得语法糖或不错的API,除非您想做一些手工工作。 Ember.js通过创建“类” /“对象”系统来解决此问题,控制器可能看起来像:

引入视图助手(成为视图)和模板(成为视图)


JavaScript的新功能:

这仅在使用Node时有效.js,但它仍然是无价的。BrendanEich在NodeConf上的演讲带来了一些很酷的新功能,建议的函数语法,尤其是Task.js js库。函数嵌套的问题,由于缺少函数开销,会带来更好的性能

我不太确定V8是否本身支持此功能,最后我检查了是否需要启用一些标志,但是这可以在使用SpiderMonkey的Node.js端口中使用。

其他资源:Pro JavaScript设计模式(食谱:问题解决方案Ap)
JavaScript:The Good Par ts
学习JavaScript设计模式
面向对象的JavaScript:创建可缩放,可重用的高质量JavaScript应用程序和库


评论


不错的文章。我个人没有使用MV吗?库。我们拥有组织大型更复杂应用程序的代码所需的一切。他们都让我想起Java和C#太多了,它们试图将自己的各种废话丢在服务器-客户端通信中实际发生的事情上。我们有一个DOM。我们有活动委托。我们得到了OOP。我可以将自己的事件绑定到数据更改tyvm。

–埃里克·雷彭(Erik Reppen)
13年6月13日在5:41

“与其将混乱的回调弄得一团糟,请对单个任务保留单个功能,并做好该任务。” - 诗歌。

– CuriousWebDeveloper
2014年7月7日在6:04



当Javascript在2000年代初到中期处于非常黑暗的时代时,很少有人了解如何使用Javascript编写大型应用程序。就像@ErikReppen所说的那样,如果您发现JS应用程序看起来像Java或C#应用程序,那么您做错了。

–backpackcoder
17年1月10日在10:46

#2 楼

添加到Daniels的答案中:

可观察的值/组件

这个想法是从MVVM框架Knockout.JS(ko.observable)借用的,其想法是值和对象可以成为可观察的主题,并且一旦一个值或对象发生更改,它将自动更新所有观察者。它基本上是用Javascript实现的观察者模式,而不是大多数pub / sub框架的实现方式,“键”本身就是主题,而不是任意对象。

用法如下:

// the subjects
// plain old javascript object with observable values
var shipComponent = {
    velocity : observable(0)
};

// the observer, a player user interface
// implemented with revealing module pattern
var playerUi = (function(ship) {

  var module = {
    setVelocity: function (x) { 
      // ... sets the velocity on the player user interface
    },

    // only called once
    init: function() {

      // subscribe to changes on the velocity value
      // using the module's function as callback
      module.velocity.onChange(playerUi.setVelocity);
    }
  };

  return module;
})(shipComponent).init();

// the player ui will change when the velocity value is changed
shipComponent.velocity.set(10);


这个想法是观察者通常知道主题在哪里以及如何订阅。如果您必须进行大量更改,则此代码代替pub / sub的优势非常明显,因为在重构步骤中,删除主题很容易。我的意思是因为删除主题后,所有依赖该主题的人都会失败。如果代码快速失败,那么您知道在哪里删除其余的引用。这与完全解耦的主题(例如在pub / sub模式中使用字符串键)形成对比,并且有更大的机会保留在代码中,尤其是在使用了动态键并且维护程序员没有意识到这一点的情况下(死机维护编程中的代码是一个烦人的问题。)

在游戏编程中,这减少了旧的更新循环模式的需要,并且更多地成为了事件/反应性编程习惯,因为一旦改变了主题将自动更新所有观察者的更改,而不必等待更新循环执行。更新循环有很多用途(用于需要与游戏时间同步的事物),但是有时候您只是不想在组件本身可以使用此模式自动更新时将其弄乱。

observable函数的实际实现实际上非常容易编写和理解(特别是如果您知道如何使用javascript和观察者模式处理数组):

var observable = function(v) {
    var val = v, subscribers = [];

    // the observable object,
    // as revealing module
    var output = {

        // subscribes to event
        onChange : function(func) {
            // idiomatic JS to add object to the
            // subscribers array
            subscribers.push(func);

            return output: // enables chaining
        },

        // the method that changes the observable object
        // and emits the event
        set : function(v) {
            var i;
            val = v;
            for (i = 0, i < subscribers.length; i++) {
                // this is hardly fault tolerant but as long
                // as subscribers are functions it'll work
                subscribers[i](v);
            }

            return output;
        }

    };

    return output;
};


我已在JsFiddle中实现了可观察对象的实现,该实现在此基础上继续进行观察,并能够删除订户。随时尝试JsFiddle。