在JavaScript中如何声明命名空间?

1074

如何创建JavaScript中的命名空间,以便我的对象和函数不会被其他同名的对象和函数覆盖?我已经使用了以下代码:

if (Foo == null || typeof(Foo) != "object") { var Foo = new Object();}

有没有更优雅或更简洁的方法来做这件事?


21
我能理解你的想法,即检查命名空间是否被占用,但由于如果此操作失败,对象将不会被创建,因此我认为更好的方法是在命名空间已被占用时发出警告。坦白地说,在大多数 JavaScript 情况下,这种情况应该不会发生,并且应该很快在开发过程中被捕获。 - annakata
19
拥有一个顶层“命名空间”(窗口属性)。早期测试应该能够检测到冲突。不要费心添加所有这些“如果”的检查。重复的“命名空间”是致命问题,应该像这样处理。你可以像jQuery一样采取一种方法来允许使用自定义“命名空间”,但这仍然是设计时的问题。 - user166390
请参见 https://dev59.com/PkvSa4cB1Zd3GeqPeXKv 以了解 JavaScript 命名空间技术的性能问题。 - Tim Abell
请参见https://dev59.com/Am855IYBdhLWcg3w0H93,了解有关JavaScript中使用对象和函数命名空间的区别。 - Tim Abell
这是大量的信息,但真正阐述了不同JS设计模式之间的差异。它对我帮助很大:http://addyosmani.com/resources/essentialjsdesignpatterns/book/ - Justin
1
现在我们有符号和模块,所以重复的命名空间甚至不应该成为一个问题。 - Sebastian Simon
29个回答

17

模块模式最初是作为一种在传统软件工程中为类提供私有和公共封装的方式而定义的。

在使用模块模式时,我们可能会发现定义一个简单的模板来开始使用它很有用。这个模板可以涵盖命名空间、公共和私有变量。

在 JavaScript 中,模块模式用于进一步模拟类的概念,以这样的方式包括单个对象内的公共/私有方法和变量,从而使特定部分不受全局范围的影响。这导致函数名与页面上其他脚本中定义的函数发生冲突的可能性减少。

var myNamespace = (function () {

  var myPrivateVar, myPrivateMethod;

  // A private counter variable
  myPrivateVar = 0;

  // A private function which logs any arguments
  myPrivateMethod = function( foo ) {
      console.log( foo );
  };

  return {

    // A public variable
    myPublicVar: "foo",

    // A public function utilizing privates
    myPublicFunction: function( bar ) {

      // Increment our private counter
      myPrivateVar++;

      // Call our private method using bar
      myPrivateMethod( bar );

    }
  };

})();

优点

为什么模块模式是一个好选择?首先,从JavaScript的角度来看,相对于真正的封装思想,它更适合有面向对象背景的开发者。其次,它支持私有数据 - 所以,在模块模式中,我们的代码的公共部分可以访问私有部分,但外部世界无法访问类的私有部分。

缺点

模块模式的缺点在于,由于我们以不同的方式访问公共和私有成员,当我们希望改变可见性时,我们实际上必须更改使用该成员的每个地方。

我们还不能访问后期添加到对象中的方法的私有成员。尽管如此,在许多情况下,模块模式仍然非常有用,并且在正确使用时,肯定有潜力改善应用程序的结构。

揭示模块模式

现在我们对模块模式稍微熟悉一些了,让我们来看一下稍微改进的版本 - Christian Heilmann的揭示模块模式。

揭示模块模式的产生是因为Heilmann对这样一个事实感到沮丧,即当我们希望从一个公共方法调用另一个公共方法或访问公共变量时,必须重复主对象的名称。他还不喜欢模块模式要求他必须将希望公开的内容切换为对象字面量表示法。

他努力的结果是更新了一种模式,在该模式中,我们只需在私有作用域中定义所有函数和变量,并返回一个匿名对象,其中包含指向我们希望公开功能的指针。

下面是如何使用揭示模块模式的示例

var myRevealingModule = (function () {

        var privateVar = "Ben Cherry",
            publicVar = "Hey there!";

        function privateFunction() {
            console.log( "Name:" + privateVar );
        }

        function publicSetName( strName ) {
            privateVar = strName;
        }

        function publicGetName() {
            privateFunction();
        }


        // Reveal public pointers to
        // private functions and properties

        return {
            setName: publicSetName,
            greeting: publicVar,
            getName: publicGetName
        };

    })();

myRevealingModule.setName( "Paul Kinlan" );

优点

该模式使我们的脚本语法更加一致,同时也使得模块结束时哪些函数和变量可以被公开访问更加清晰,从而提高了可读性。

缺点

这种模式的一个缺点是,如果私有函数引用了公共函数,则如果需要打补丁,就无法覆盖公共函数。这是因为私有函数将继续引用私有实现,该模式只适用于函数而不适用于公共成员。

引用私有变量的公共对象成员也受到上述不可打补丁规则的影响。


16

如果您需要私有作用域:

var yourNamespace = (function() {

  //Private property
  var publicScope = {};

  //Private property
  var privateProperty = "aaa"; 

  //Public property
  publicScope.publicProperty = "bbb";

  //Public method
  publicScope.publicMethod = function() {
    this.privateMethod();
  };

  //Private method
  function privateMethod() {
    console.log(this.privateProperty);
  }

  //Return only the public parts
  return publicScope;
}());

yourNamespace.publicMethod();

否则,如果您永远不会使用私有作用域:

var yourNamespace = {};

yourNamespace.publicMethod = function() {
    // Do something...
};

yourNamespace.publicMethod2 = function() {
    // Do something...
};

yourNamespace.publicMethod();

15

你可以声明一个简单的函数来提供命名空间。

function namespace(namespace) {
    var object = this, tokens = namespace.split("."), token;

    while (tokens.length > 0) {
        token = tokens.shift();

        if (typeof object[token] === "undefined") {
            object[token] = {};
        }

        object = object[token];
    }

    return object;
}

// Usage example
namespace("foo.bar").baz = "I'm a value!";

10

虽然我晚了7年才参加这个派对,但在8年前就已经进行了大量的工作:

重要的是能够轻松有效地创建多个嵌套命名空间,以使复杂的 web 应用程序更有组织性和可管理性,同时尊重 JavaScript 全局命名空间(防止命名空间污染),并且不会在此过程中覆盖命名空间路径中的任何现有对象。

基于上述内容,以下是我大约在2008年提出的解决方案:

var namespace = function(name, separator, container){
  var ns = name.split(separator || '.'),
    o = container || window,
    i,
    len;
  for(i = 0, len = ns.length; i < len; i++){
    o = o[ns[i]] = o[ns[i]] || {};
  }
  return o;
};

这不是创建命名空间,而是提供一个用于创建命名空间的函数。

这可以压缩为一行的迷你代码:

var namespace=function(c,f,b){var e=c.split(f||"."),g=b||window,d,a;for(d=0,a=e.length;d<a;d++){g=g[e[d]]=g[e[d]]||{}}return g};

使用示例:

namespace("com.example.namespace");
com.example.namespace.test = function(){
  alert("In namespaced function.");
};

或者,用一个陈述:

namespace("com.example.namespace").test = function(){
  alert("In namespaced function.");
};

然后执行其中的任意一个:

com.example.namespace.test();
如果您不需要支持旧版浏览器,则使用更新版本:
const namespace = function(name, separator, container){
    var o = container || window;
    name.split(separator || '.').forEach(function(x){
        o = o[x] = o[x] || {};
    });
    return o;
};
现在,我会谨慎地将 namespace 暴露给全局命名空间本身。 (很遗憾这不是基础语言为我们提供的!)因此,我通常会在闭包中使用它自己,例如:

(function(){
 const namespace = function(name, separator, container){
  var o = container || window;
  name.split(separator || '.').forEach(function(x){
   o = o[x] = o[x] || {};
  });
  return o;
 };
 const ns = namespace("com.ziesemer.myApp");
 
 // Optional:
 ns.namespace = ns;
 
 // Further extend, work with ns from here...
}());

console.log("\"com\":", com);

在大型应用程序中,这只需要在页面加载的开始时被定义一次(适用于客户端Web应用程序)。 如果保留名称空间函数,则其他文件可以重复使用它(在上面作为“可选项”包含)。 最坏的情况是,如果该函数被重复声明几次 - 它只是几行代码,如果被压缩则更少。


这很好,但我有一个关于 letconst 的问题。一旦你创建了一个命名空间 namespace("com.ziesemer.myApp"),是否可以将属性/对象指定为 constlet?比如 com.ziesemer.myApp.logger - 在我看来,这似乎永远不能声明为 const。我认为这样做就无法采用一些较新的语言特性。注意:不幸的是,采用新的Javascript模块不是一个选项。 - onefootswill
@onefootswill - 你所寻找的并不适用于这里,因为你并没有严格地声明变量,而是在现有对象上创建新属性。 - ziesemer
是的,我意识到了。这个问题是我们的架构师问我的,他在反对我采用这种方法。他似乎满足于在老旧、低质量的代码库中污染全局命名空间。有些人就是这样 ¯_(ツ)_/¯。 - onefootswill

9
我创建了一个叫做namespace的东西,它受到了Erlang模块的启发。这是一种非常实用的方法,但这就是我现在编写JavaScript代码的方式。
它为闭包提供了全局命名空间,并在该闭包内公开了一组定义好的函数。
(function(){

  namespace("images", previous, next);
  // ^^ This creates or finds a root object, images, and binds the two functions to it.
  // It works even though those functions are not yet defined.

  function previous(){ ... }

  function next(){ ... }

  function find(){ ... } // A private function

})();

8
我使用以下语法来定义命名空间:

var MYNamespace = MYNamespace|| {};

 MYNamespace.MyFirstClass = function (val) {
        this.value = val;
        this.getValue = function(){
                          return this.value;
                       };
    }

var myFirstInstance = new MYNamespace.MyFirstClass(46);
alert(myFirstInstance.getValue());

jsfiddle: http://jsfiddle.net/rpaul/4dngxwb3/1/


8

在将我的几个库移植到不同的项目中后,我不断地改变顶层(静态命名)命名空间。因此,我现在使用这个小型(开源)助手函数来定义命名空间。

global_namespace.Define('startpad.base', function(ns) {
    var Other = ns.Import('startpad.other');
    ....
});

我的博客文章中介绍了这些好处。您可以在此处获取源代码。

我最喜欢的好处之一是模块之间的隔离,与加载顺序有关。您可以在其加载之前引用外部模块。当代码可用时,您将获得对象引用。


1
我已经创建了一个改进版(2.0)的命名空间库:http://code.google.com/p/pageforest/source/browse/appengine/static/src/js/namespace.js - mckoss
你的所有链接似乎都失效了。 - snoob dogg

6
我认为你们在解决这个简单问题时使用了过多的代码。没有必要为此创建一个仓库。这里有一个单行函数。
namespace => namespace.split(".").reduce((last, next) => (last[next] = (last[next] || {})), window);

试一下:

// --- definition ---
const namespace = name => name.split(".").reduce((last, next) => (last[next] = (last[next] || {})), window);

// --- Use ----
const c = namespace("a.b.c");
c.MyClass = class MyClass {};

// --- see ----
console.log("a : ", a);


5

ES6模块命名空间引入

// circle.js
export { name, draw, reportArea, reportPerimeter };

// main.js
import * as Circle from './modules/circle.js';

// draw a circle
let circle1 = Circle.draw(myCanvas.ctx, 75, 200, 100, 'green');
Circle.reportArea(circle1.radius, reportList);
Circle.reportPerimeter(circle1.radius, reportList);

这会获取 circle.js 内可用的所有导出内容,并将它们作为 Circle 对象的成员进行使用,从而有效地给它赋予了自己的命名空间。


3

我最近最喜欢的模式是这个:

var namespace = (function() {
  
  // expose to public
  return {
    a: internalA,
    c: internalC
  }

  // all private
  
  /**
   * Full JSDoc
   */
  function internalA() {
    // ...
  }
  
  /**
   * Full JSDoc
   */
  function internalB() {
    // ...
  }
  
  /**
   * Full JSDoc
   */
  function internalC() {
    // ...
  }
  
  /**
   * Full JSDoc
   */
  function internalD() {
    // ...
  }
  
})();

当然,返回语句可以放在最后,但如果只有函数声明跟在其后,那么更容易看出命名空间的具体内容以及暴露了哪些API。在这种情况下使用函数表达式的模式会导致无法知道哪些方法已经被暴露,需要浏览整个代码才能确定。

你好,你如何从代码片段中调用公共函数?我尝试过 namespace.a(); - olimart
@olivier 是的,那就是我的想法。虽然现在用 ES6,我通常使用对象字面量的简写语法(https://ponyfoo.com/articles/es6-object-literal-features-in-depth)。 - nomæd
我只想强调函数定义末尾的 ()。它们是必需的,很容易被忽略。我遇到了与 @olimart 相同的问题,并通过添加它们来解决了这个问题。 - Andres

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接