Ghost 开源博客平台

Ghost 是一个简洁、强大的写作平台。你只须专注于用文字表达你的想法就好,其余的事情就让 Ghost 来帮你处理吧。

JavaScript 的常见“陷阱”

PHP是我的第一门编程语言,之后通过像jQuery这样的库首次接触JavaScript。由于JavaScript与PHP的工作原理不同,开始时总有一些JavaScript问题困扰着我。即使现在,仍有让我感到疑惑的东西。我想分享一些开始使用JavaScript时我苦苦思考的问题。这些问题将涉及:全局命名空间、this、ECMAScript 3 和ECMAScript 5的区别、异步调用、原型以及 简单的JavaScript继承。

全局命名空间

在PHP中,特别地,当你在类(或命名空间块)之外声明一个函数变量时,你实质上往全局命名空间添加了一个函数。在JavaScript中,没有所谓的命名空间,然而所有的东西都附属于全局对象。在Web浏览器中,全局对象就是windows对象。另一个主要区别是:在JavaScript中,函数和变量均是全局对象的属性,即我们通常指代的properties。

因为,当你覆盖一个全局函数或属性,JavaScript不会给出任何警告,所以这会很麻烦,实际上是相当危险的。

function globalFunction() {  
  console.log('I am the original global function');
}

function overWriteTheGlobal() {  
  globalFunction = function() {
    console.log('I am the new global function');
  }
}
globalFunction(); //输出 "I am the original global function"  
overWriteTheGlobal(); //重写最初的全局函数  
globalFunction(); //输出 "I am the new global function"  

JavaScript中的一种技术是立即执行函数表达式,一般称为自执行匿名函数。它对于保持变量和函数的独立性是非常有用的。通常,我通过传入对象方式将函数暴露给外界。这是模块模式的一个变种。

var module = {};

(function(exports){

  exports.notGlobalFunction = function() {
    console.log('I am not global');
  };  

}(module));

function notGlobalFunction() {  
  console.log('I am global');
}

notGlobalFunction(); //输出 "I am global"  
module.notGlobalFunction(); //输出 "I am not global"  

在自执行匿名函数里,所有的全局作用域是封闭的,通过将之依附到module变量实现。从技术上讲,你可以直接把属性附加到module变量,但我们将它传递给函数的原因是:明确地表明函数所附加的地方。它允许我们给传入函数的对象起别名。这里的关键是:我们预先声明依赖于模块变量,而不依赖于全局变量。

你也许已经注意到了var关键字。如果你不知道如何使用它,基本的解释是:在声明变量前使用var,为离之最近的函数创建了一个属性。如果省略var关键字,那么意味着给现有变量分配一个新值,并提升了作用域链。这个作用域链可能是全局范围的。

var imAGlobal = true;

function globalGrabber() {  
  imAGlobal = false;
  return imAGlobal;
}

console.log(imAGlobal); //输出 "true"  
console.log(globalGrabber()); //输出 "false"  
console.log(imAGlobal); //输出 "false"  

正如你所看到的一样,在你的函数中依赖全局变量是相当危险的,因为可能产生副作用,造成难以预料的冲突。当你使用var关键字时,会发生什么呢?

var imAGlobal = true;

function globalGrabber() {  
  var imAGlobal = false;
  return imAGlobal;
}

console.log(imAGlobal); //输出 "true"  
console.log(globalGrabber()); //输出 "false"  
console.log(imAGlobal); //输出 "true"  

JavaScript将var声明变量提升到函数块顶部,接着初始化变量。这就是所谓的变量提升。

总结:所有变量的作用于一个函数内(函数本身就是一个对象),并使用var声明这些变量就确定它们的函数作用域,不使用var意味着声明一个全局变量。

让我们来看看使用变量提升的情况:

function variableHoist() {  
  console.log(hoisty);
  hoisty = 1;  
  console.log(hoisty);
  var hoisty = 2;
  console.log(hoisty);
}

variableHoist();  
//输出 undefined (如果在作用域内不存在var声明,将得到一个引用错误(ReferenceError))
//输出 "1"
//输出 "2"

try {  
  console.log(hoisty); //输出 ReferenceError (不存在全局变量"hoisty")
} catch (e) {
  console.log(e);
}

之所以像你所看到的一样:把var声明放在函数的哪个位置实际上并不重要,是因为属性是在函数执行任何代码之前创建好的。在目前实践中,通常把var声明放在函数顶部,因为其作用域终止于该函数。在函数的顶部初始化变量也是完全可以接受的,只可以清楚事件的执行顺序。

在JavaScript中,使用function关键字声明的函数(不赋给变量)也会被提升。实际上,整个函数被提升,可供执行。

myFunction(); //输出 "i exist"

function myFunction() {  
  console.log('i exist');
}

当使用var形式来声明函数时,整个函数不会被提升:

try {  
  myFunction();
} catch (e) {
  console.log(e); //抛出 "Uncaught TypeError: undefined is not a function"
}
var myFunction = function() {  
  console.log('i exist');
}

myFunction();  //输出 "i exist"  

理解“this”

由于JavaScript使用了函数域,this的意义与PHP中的截然不同,引起了诸多疑惑。考虑下面的情形:

console.log(this); // 输出 window object

var myFunction = function() {  
  console.log(this);
}

myFunction(); //输出 window object

var newObject = {  
  myFunction: myFunction
}

newObject.myFunction(); //输出 newObject  

默认情况下,this指向包含在函数内的对象。由于myFunction()是全局对象的一个属性,this是全局对象window的引用。现在,当我们把myFunction()封装到newObject中,现在this指向newObject。在PHP和其他类似的语言中,this往往指向包含该方法的类的实例。你可能认为JavaScript在这里干着一些蠢事,但确实很多JavaScript语言的力量正来源于此。事实上,当我们使用call()或apply()来调用JavaScript函数时,甚至可以替换this的值。

var myFunction = function(arg1, arg2) {  
  console.log(this, arg1, arg2);
};

var newObject = {};

myFunction.call(newObject, 'foo', 'bar'); //输出 newObject "foo" "bar"  
myFunction.apply(newObject, ['foo', 'bar']); //输出 newObject "foo" "bar"  

但不要急着往下看,让我们认真思考一下。这里我们所做的是:通过传入对象值作为函数内部this的替代值来调用函数myFunction函数。 call()与apply()的根本区别在于传入参数的方式:call()函数接收第一个参数后,接着可接收无限量的参数;apply()函数则规定将参数数组作为第二个参数。

像jQuery这样的库,采用上述方式进行调用,表现出惊人能力。让我们看看jQuery的$.each()方法:

var $els = [$('div'), $('span')];  
var handler = function() {  
  console.log(this);
};

$.each($els, handler);

//迭代器 1 输出包裹在div里面的jquery dom元素
//迭代器 2 输出包裹在tag里面的jquery dom元素

handler.apply({}); //输出 object  

jQuery经常重写this的值,所以你要试图理解this在jQuery事件处理程序上下文中或其他类似结构中的含义。

弄清楚ECMAScript 3和ECMAScript 5的区别

长久以来,ECMAScript 3已经成为大多数浏览器的标准,但最近ECMAScript 5融入大部分现代浏览器(IE浏览器仍然滞后)。ECMAScript 5向JavaScript中引入了很多常见的功能以及一些你以前只能靠某个库提供的原生函数,如String.trim()和Array.forEach()。然而问题是:如果你使用Internet Explorer浏览器,这些方法在浏览器环境中仍不可用。

看看当你在IE8中试图使用String.trim会发生什么呢:

var fatString = "   my string   ";

//in modern browsers
console.log(fatString); //输出 "   my string   "  
console.log(fatString.trim()); //输出 "my string"

//in IE 8
console.log(fatString.trim()); //error: Object doesn't support property or method 'trim'  

因此在此期间,我们可以像使用jQuery.trim方法来做到这一点。我相信如果浏览器支持,jQuery会回退到String.trim,以提高性能(浏览器原生函数更快)。

你可能不关心甚至不需要了解ECMAScript3和ECMAScript5之间的所有差异,但首先查阅作为函数参考手册的Mozilla开发者网络(MDN),看看函数适用的语言版本:这通常是一个好主意。一般来说,如果你使用库像jQuery或者underscore这样的库来处理,效果应该也不错。 如果您有兴趣在旧版浏览器中使用类似ECMAScript 5的插件,请查阅https://github.com/kriskowal/es5-shim

了解异步

在实践中开始编写JavaScript代码时,特别是jQuery,让我吃尽苦头的是一些异步操作。有很多次遇到这样的情况:我所编写过程式代码希望立即返回一个结果,但并未发生。

看看下面的代码片段:

var remoteValue = false;  
$.ajax({
  url: 'http://google.com',
  success: function() {
    remoteValue = true;
  }
});

console.log(remoteValue); //输出 "false"  

我花了一段时间才弄清楚,当进行异步编程时,需要使用回调函数来处理ajax返回的结果。

var remoteValue = false;

var doSomethingWithRemoteValue = function() {  
  console.log(remoteValue); //当调用成功时,输出 true 
}

$.ajax({
  url: 'https://google.com',
  complete: function() {
    remoteValue = true;
    doSomethingWithRemoteValue();    
  }
});

另一个厉害的东西是deferred对象(有时称为promises),可用于倾向过程风格的编程:

var remoteValue = false;

var doSomethingWithRemoteValue = function() {  
  console.log(remoteValue); 
}

var promise = $.ajax({  
  url: 'https://google.com'
});

//输出 "true"
promise.always(function() {  
  remoteValue = true;
  doSomethingWithRemoteValue();    
});

//输出 "foobar"
promise.always(function() {  
  remoteValue = 'foobar';
  doSomethingWithRemoteValue();    
});

您可以使用promises完成链式回调。在我看来,这种方式比嵌套回调方式更容加易用,额外地,这些对象还提供了大量其他好处。

浏览器中的动画也是异步的,因此这也是容易混乱的地方。这里不打算深究,但是你应该像处理ajax请求一样,通过回调函数来处理动画。然而,我还不算是这方面的专家,所以请自行参考 jQuery .animate() method

JavaScript的简单继承

粗略概括一下,JavaScript通过克隆对象来扩展它们,而PHP、Ruby、Python和Java使用、继承类。在JavaScript中每个对象都拥有一个所谓的原型。事实上,所有的函数,字符串,数字以及对象有一个共同的祖先:Object。关于原型有两件事情要记住:蓝本(blueprints)和链。

基本上,每个原型本身就是一个对象。在创建一个对象的实例时,它描述了可用的属性。原型链就是允许原型继承其他原型的东西。事实上,原型本身也有原型。当对象实例没有某个方法或属性时,那么它会在该对象的原型、原型的原型中寻找。依此类推,直至发现该属性不存在,最终报错undefined。

值得庆幸的是,初学者一般不必要沾惹这东西,因为创建一个对象字面量相当容易,然后在运行时附加属性即可。

var obj = {};

obj.newFunction = function() {  
  console.log('I am a dynamic function');
};

obj.newFunction();  

一直以来,我使用jQuery.extend()这种简单的方法来继承对象:

var obj = {  
  a: 'i am a lonely property'
};

var newObj = {  
  b: function() {
    return 'i am a lonely function';
  }
};

var finalObj = $.extend({}, obj, newObj);

console.log(finalObj.a); //输出 "i am a lonely property"  
console.log(finalObj.b()); //输出 "i am a lonely function"  

ECMAScript 5提供了Object.create()方法,你可用它从对现有对象进行继承。然而,如果你需要支持旧版浏览器,请勿使用。在创建属性和设置属性的属性(是的,属性也有属性)方面,它确实具有明显的优势。

var obj = {  
  a: 'i am a lonely property'
}; 

var finalObj = Object.create(obj, {  
  b: {
    get: function() {
      return "i am a lonely function";
    }
  }
});

console.log(finalObj.a); //输出 "i am a lonely property"  
console.log(finalObj.b); //输出 "i am a lonely function"  

由于JavaScript具有强大能力和灵活性,你可以深入JavaScript继承方面。而好在这里又不必深究。

意外陷阱:在for循环中忘记使用var

var i = 0;

function iteratorHandler() {  
  i = 10;
}

function iterate() {  
 //迭代器仅运行一次
  for (i = 0; i < 10; i++) {
    console.log(i); //输出 0
    iteratorHandler();
    console.log(i); //输出 10
  } 
}

iterate();  

该例子是刻意的,但你可意识到这里的危险。解决的办法是,使用var声明迭代器变量。

var i = 0;

function iteratorHandler() {  
  i = 10;
}

function iterate() {  
  //迭代器会运行10次
  for (var i = 0; i < 10; i++) {    
    iteratorHandler();
    console.log(i);
  }
}

iterate();  

这一切可追溯到我们之前说的作用域规则。请记住,恰当使用var。

总结

JavaScript或许是唯一一门使用前不需要学习的语言,但最终你将陷入一些不明原因的麻烦之中。在这些日子里,除了避免犯错之外,学习JavaScript收获良多,考虑到其重生与广泛适用性。这篇博文并不是尝试着解决所有问题的灵丹妙药,但希望在人们被迫编写可怕的JavaScript代码之前,可以帮助他们了解一些基本情况;暗地里希望重回幸福之地 — 充满着数据库查询的PHP后端项目。


英文原文:Common JavaScript “Gotchas”
中文译文:JavaScript 的常见“陷阱”

王赛
关于作者 王赛