厨房编码噩梦:JavaScript作用域

最近在Recurse Center,我一直在开发我的殖民者游戏的一个JavaScript客户端。作为一名使用JavaScript的Perl开发者,这真是一个有趣的体验。JavaScript感觉非常Perl化 - 两种语言都有灵活的语法、一等函数和作为哈希的对象。而且两种语言都有宽松的解释器,本应一开始就使用严格模式(哈哈!)JavaScript与Perl一个非常不同的方面是其作用域规则。我因为这些问题多次受伤,所以如果你是JavaScript的新手,你可能觉得以下摘要和建议很有用。

函数式作用域

使用var关键字声明变量

var name = "David";

变量是函数式作用域的,这意味着如果在函数内声明,该变量是函数块的私有变量。函数外部声明的变量是全局作用域的。并且没有其他类型的块作用域(如if-else或for循环中)。

var name = "David";

function log_name (name) // private
{
  console.log(name);
}

var names = ["Jen", "Jim", "Jem", "Jon"];
for (var i = 0; i < names.length; i++)
{
  var name = names[i]; // overwriting the global
}
console.log(name); // Jon NOT David

函数作为变量

函数名按照普通变量的相同作用域规则存储。有两种声明函数的方式

function log_name () { }

var log_name = function () { }

这两个是相同的。这意味着有可能不小心用另一个变量声明覆盖一个函数

function name () { return "David"; }
var name = "John";
name(); // error, name is not a function anymore

提升

JavaScript解释器有一个初始运行时阶段(类似于Perl的BEGIN),在该阶段执行所有变量声明,然后是其他代码。这被称为“提升”,但实际上这意味着你可以在声明变量之前使用它!

console.log(name); // yep, this works
var name = "David";

绑定

JavaScript大量使用匿名函数和回调。为了修改函数的作用域,JavaScript1提供了bind。通过例子更容易理解。如果我有一个点对象,并且我想通过加载一个图片来绘制它到画布上,我可以这样

Point.prototype.draw = function()
{
  var ctx = get_canvas_context(); // declared elsewhere
  var img = new Image();
  img.onload = function () {  // anonymous function
    ctx.drawImage(img, this.x, this.y);   
  }.bind(this);
  img.src = "/point.png";
}

这里我使用bind将点对象的范围注入匿名函数中。否则,我就无法访问点对象的xy属性,因为this会引用其他东西。

关于JavaScript作用域的更详细解释,我推荐阅读Todd Motto的文章,关于JavaScript作用域你想要知道的一切

应对作用域

好吧,这是坏消息;好消息是有很多处理JavaScript作用域规则的技术。根据上下文,你可能觉得以下方法中的一些或全部都有用。

命名约定

你可以做的第一件事是采用命名约定。例如,所有函数使用动词-名词结构(如“get_address”)命名,所有值变量使用普通名词(如“addresses”)命名。这不是一个完整的解决方案,但至少这会减少函数被值变量替换的可能性。

每个作用域一个var

管理变量作用域的另一种技术是每个作用域只允许一个var语句。所以一个典型的程序可能看起来像这样

// declare global scope variables
var foo = "/root/assets",
    bar = 0;

function execute (foo)
{
  var i, j, bar; // functional scope

  for (i = 0; i < foo.length; i++)
  {
    for (j = 0; j < foo.length; j++)
    {
      // do something
    }
  }
}

使用严格模式

这是所有Perl程序员都应该熟悉的约定。在JavaScript中启用严格模式。就像Perl一样,JavaScript的严格模式可以捕获几个与变量相关的错误。使用以下命令全局启用

"use strict";

通常JavaScript专家推荐使用严格模式的功能性版本 - 在这种情况下,声明被放置在函数块内。这有助于防止脚本连接错误(即导入的脚本不满足严格规则)。

(function () {
   "use strict";
   // this function is strict...
}());

作为命名空间的对象

如果您认为使用模块可以简单解决所有命名空间冲突,请允许我先告诉您,JavaScript没有模块的概念(它是一种基于原型的语言)。没有import关键字。在HTML中,任何用script标签加载的代码都被简单地连接到当前作用域。

尽管存在这种限制的解决方案。在《JavaScript权威指南》中,作者David Flanagan建议使用对象作为命名空间(第六版,第9.9.1节)。每个对象的作用域可以用来封装特定领域的特定行为和数据。例如

// everything is scoped to point.*
var point = {};
point.Point = function (_x, _y) {
  this.x = _x;
  this.y = _y
}

point.Point.prototype.coordinates = function ()
{
  return [this.x, this.y];
}

// now lets try it out ...
var p = new point.Point(1,3);
console.log(p.coordinates());

要将代码打包成模块,可以使用模块模式。尽管JavaScript没有原生的导入方法,但有几个外部库可以提供这种行为,例如RequireJS

Let

JavaScript的下一个主要版本ES6提供了let关键字。这个关键字提供了变量块级作用域,类似于其他主流语言。ES6目前还没有得到广泛支持,但您可以使用转译器如Babel将ES6 JavaScript转换回ES5。

使用代码检查器

浏览器在处理JavaScript时不会抛出足够的异常。相反,它们试图坚持下去并执行程序员真正想要的,而不是他们输入的内容。这对用户来说很好2,因为他们可以获得无间断的浏览体验,但对于我们程序员来说这绝对是一个坏事情™。浏览器的健壮性使得JavaScript难以调试,这正是代码检查器介入的地方 - 它分析代码并报告它们发现的任何错误或警告。对于JavaScript,我喜欢使用JSHint

1在ES5 JavaScript中引入,这是所有现代浏览器都支持的。对于较旧版本的JavaScript解决方案,请使用callapply

2这对用户可能也是一个坏事情 - 处理语法错误代码的开销会降低性能,更糟糕的是,会鼓励编写更多错误代码。


这篇文章最初发表在PerlTricks.com上。

标签

David Farrell

David是一位职业程序员,他经常推文博客关于代码和编程艺术。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在GitHub上打开一个问题或拉取请求,帮助我们。