厨房编码噩梦: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
将点对象的范围注入匿名函数中。否则,我就无法访问点对象的x
和y
属性,因为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解决方案,请使用call
或apply
。
2这对用户可能也是一个坏事情 - 处理语法错误代码的开销会降低性能,更糟糕的是,会鼓励编写更多错误代码。
这篇文章最初发表在PerlTricks.com上。
标签
反馈
这篇文章有什么问题吗?请在GitHub上打开一个问题或拉取请求,帮助我们。