48

Ben Cherry's excellent article explains hoisting in JavaScript adequately. My problem, however, is that I cannot conceive a use case for this notorious perpetrator of confusion. Please explain if there is a design pattern that actually takes advantage of this language feature.

Secondly, is scope hoisting unique to JavaScript?

UPDATE --- I'm adding a bounty for an answer that satisfies my curiosity: Which design pattern(s) actually take advantage of JavaScript's hoisting behavior? I understand why JavaScript supports hoisting, but I want to know how I can take advantage of this feature.

4

9 回答 9

21

Variable hoisting

One of the simplest uses of hoisting is variable hoisting. If we didn't have variable hoisting, this would throw a ReferenceError:

var bar = foo; 
var foo;

That doesn't seem immediately useful, but it allows us to do things like this:

var myCoolJS = myCoolJS || {};

This basically means what it looks like: myCoolJS is myCoolJS if it exists, or a new object if it doesn't. The second myCoolJS doesn't throw a ReferenceError if myCoolJS didn't already exist, because this variable declaration is hoisted.

This saves us from doing an awkward typeof myCoolJS != 'undefined' check.

Function hoisting

Function hoisting can be especially useful when combining multiple scripts into one. For example, I've created a lightweight build-time implementation of CommonJS modules. This provides the same module, require, and exports features that are found in node.js. I built the tool to allow required modules to be composed of multiple files. For example, require('/foo') could result in a module composed of two files, foo.js (the "body file") and foo.h.js (the "header file").

This allows the "body file" to have no knowledge of the free variables provided by the CommonJS modules environment; all of that is handled in the header. This makes code reusable and easy to test without building. However, since the headers are prepended to the body, we leverage function hoisting in the body file to allow exports in the headers. For example:

// dom.h.js

var util = require('util').util;

exports.css = css; // we can do this because "css" is hoisted from below

// ... other exports ...

...

// dom.js

function css(){}; // this would normally just be an object.

css.hasClass = function(element) { ... };
css.addClass = function(element) { ... };

// ...other code...
于 2012-03-05T16:51:50.287 回答
15

这是一个用于提升的用途:

(function() {
    var factorial = function(n) {
        if(n == 0)
            return 1;
        return n * factorial(n - 1);
    };
})();

如果没有提升,那将无法编译,因为factorial函数文字中尚不存在。您必须单独声明变量或使用命名函数。

JavaScript 还允许如下代码:

var test = function(b) {
    if(b) {
        var value = 2;
    } else {
        var value = 5;
    }
    console.log(value);
};

使用块作用域,您必须valueif.

公平地说,这段代码的工作是因为函数作用域,而不是提升。并且 JavaScript 可以在没有提升的情况下拥有函数范围。Ruby 可以更好地处理这个问题:Ruby 具有变量的方法作用域,但变量在您设置它们之前不存在:

def test(b)
    # unlike JavaScript, value is not accessible here
    if b
        value = 2
    else
        value = 5
    end
    puts value
end
于 2012-03-03T04:32:36.053 回答
11

JavaScript 没有块作用域(让我们暂时忘记let),因此任何变量声明都是为整个函数声明的,其中 JavaScript确实具有作用域。

如果你这样想,JavaScript 提升可能更有意义。

如果你记得提升,它不应该是错误和混乱的来源。这只是您必须理解和记住的怪癖之一。

我不确定提升是否仅限于 JavaScript。我从未在其他地方听说过它,但这并不一定意味着它在其他语言中不存在。

于 2011-12-21T23:02:43.047 回答
9

那篇文章中的前两个例子写得不好。糟糕的代码显然会导致错误和混乱。让我给你这些例子的重构版本。你会看到这里没有混淆......

示例 1 - 原始代码

var foo = 1;
function bar() {
    if (!foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();

示例 1 - 重构代码(消除混淆)

var foo = 1;

function bar() {
    var foo;

    if ( !foo ) {
        foo = 10;
    }

    alert( foo );
}

bar();

警报显示“10”,原因很清楚。这里没有混淆。

示例 2 - 原始代码

var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);

示例 2 - 重构代码(消除混淆)

var a = 1;

function b() {
    var a = function () {}; 
    a = 10;
    return; 
}

b();

alert( a );

警报显示“1”。明显地。这里也没有混淆。

于 2011-12-21T23:32:40.987 回答
7

“提升”不是 ECMAScript 标准的一部分,但它确实说函数内的变量在函数的开头声明,无论它在代码中的函数位置。

例子

(function() {
  alert(myvar); // undefined
  var myvar = 'local value';
})();

在内部,Javascript 会在警报之前声明 myvar,显示警报,然后将 myvar 分配给“本地值”。

所以 Javascript 会将该代码解释为:

(function() {
  var myvar;
  alert(myvar); // undefined
  myvar = 'local value';
})();

这就是为什么“Java 的好部分”有一个指导方针,说你应该在函数的顶部声明变量。

资料来源: http: //net.tutsplus.com/tutorials/javascript-ajax/quick-tip-javascript-hoisting-explained/

“请解释是否有一种设计模式实际上利用了这种语言特性。” “提升”不是一项功能,而是 Javascript 解释器如何构造代码的结果,因为该语言使用函数范围。

“哪种设计模式实际上利用了 JavaScript 的提升行为?” 答:没有。

于 2012-03-06T15:35:18.563 回答
5

我认为提升很有用的一个领域是因为函数被视为第一类对象。例如:

function foo()
{
   function a()
   {
      //...
   }

   function b()
   {
      //...
   }
}

也可以写成:

function foo()
{
   var a = function ()
   {
      //...
   }

   var b = function ()
   {
      //...
   }
}

如果没有提升,以下将导致错误:

function foo()
{
   var a = function ()
   {
      b();
   }
   a(); //Error in function since b is not yet defined

   var b = function ()
   {
      //...
   }
}

我想他们只能提升函数对象,但我认为这与函数应该被视为语言中的一等公民的哲学不一致。

于 2011-12-21T23:19:34.093 回答
4

这是一个真实的用例(尽管简化为伪代码),来自那些真正想利用野外吊装好处的人。

我最近编写了这个脚本来处理简单的表单验证和提交。尽可能地,每个函数声明调用以下内容。这对可读性有两个主要好处:

  1. 逻辑顺序:代码有一个顺序流,这意味着函数总是在声明之前被调用。这是一个好处,因为当与低复杂度的函数一起使用时,它可以保持相对平坦,并让您了解函数在其源代码之前不久的调用上下文。你只需要向下滚动(从不向上)来跟随代码,并且——尽可能地——根本不滚动或滚动很少。
  2. 低引用开销:我喜欢将所有变量声明保留在每个作用域的顶部,以便读者在通读其主体之前了解函数所需的所有移动部分,但没有人愿意阅读每个调用函数的源代码以获得想法当前范围的作用。使用此方法,您将永远不会在声明之前遇到函数引用。起初这听起来很愚蠢,但它实际上减少了认知开销:你永远不会得到一个函数的源代码,其中隐含记住 this——我们稍后会使用它——相反,你只有在知道它被调用的上下文后才阅读函数源代码在。
$( function emailHandler(){
  var $form      = …
  var $email     = …
  var $feedback  = …
  var value      = …
  var validation = …

  // All initialisation is right here. Executes immediately.
  // Therefore, all future code will only ever be invoked
  // by call stacks passing through here.
  void function bindEvents(){
    $email.on( 'input', filterInput );

    $form.on( 'submit', preSubmit );
  }();

  function filterInput( inputEvent ){
    if( inputEvent && inputEvent.which === '13' ){
      return presubmit( inputEvent );
    }

    return validate();
  }

  function validate(){
    var presentValue = $email.val();

    if( validation.done || presentValue === value ){
        return;
    }
    else if( presentValue === placeholder || presentValue === '' ){
        validation.message = 'You must enter an email address to sign up';
        validation.valid   = false;
    }
    else if( !validation.pattern.test( presentValue ) ){
        validation.message = 'Please enter a valid email address';
        validation.valid   = false;
    }
    else {
        validation.message = '';
        validation.valid   = true;
    }

    validation.ever = true;

    clearFeedback();
  }

  function preSubmit( inputEvent ){
    if( inputEvent instanceof $.Event ){
      inputEvent.preventDefault();
    }

    if( !validation.ever ){
      validate();
    }
    if( validation.pending || validation.done ){
      return;
    }
    else if( validation.valid ){
      return submit();
    }
    else {
      return feedback();
    }
  }

  function submit(){
    $form.addClass( 'pending' );

    // Create an XHR base on form attributes
    $.ajax( {
      cache : false,
      url   : $form.attr( 'action' ),
      data  : $form.serialize(),
      type  : $form.attr( 'method' ).toUpperCase()
    } )
      .done( success )
      .fail( failure )
      .always( feedback );
  }

  function success( response ){
    validation.message = response.message;
    validation.valid   = response.valid;
  }

  function failure(){
    validation.message = 'Our server couldn\'t sign you up. Please try again later.';
    validation.error   = true;
  }

  function feedback(){
    clearFeedback();

    if( validation.message ){
      $feedback
        .html( validation.message )
        .appendTo( $placeholder );
    }

    if( !validation.valid || validation.error ){
      $form.addClass( 'invalid' );

      $email.trigger( 'focus' );
    }
    else {
      $form.addClass( 'done' );

      validation.done = true;
    }

    validation.pending = false;
  }

  function clearFeedback(){
    $form.removeClass( 'invalid pending done' );
  }
} );
于 2014-01-28T13:35:07.067 回答
1

我喜欢提问的风格,基于对语言的好奇。显然,没有人应该真正使用提升作为一项功能,除非他们绝对确定他们的家庭住址不会被以后可能使用它的人发现。

我只能想象一些琐碎的案例。要利用的基本属性是变量可以声明(但未定义),然后仅在一行代码中分配,但事件在两个不同的不同点进行解释。

使用循环结束时的声明(当然不是 .forEach 设置范围),您可以使用它来检测第一次迭代。

var notfirst = true;  // this is actually never referenced.

(function () {  
  var number, stack = [1, 2, 3, 4, 5];

  while (number = stack.pop()) {
    if (notfirst) console.log(number);
    var notfirst = true;
  }
})();

清空堆栈的输出是 4、3、2、1。5 被拒绝。

再次。不要这样做!

于 2012-03-07T01:01:55.217 回答
0

如果您考虑其他语言的编写方式(C++/Java)以及如何使用它们的类模式,可以利用提升来编写类似的模式来构建原型。

于 2016-05-02T18:08:27.657 回答