哪种设计模式利用了JavaScript的提升行为?

时间:2011-12-21 22:56:50

标签: javascript scope hoisting

Ben Cherry的优秀文章充分解释了hoisting in JavaScript。然而,我的问题是,我不能为这个臭名昭着的混淆犯罪者设想一个用例。请解释是否存在实际利用此语言功能的设计模式。

其次,是JavaScript独有的范围提升吗?

更新 ---我正在为满足我好奇心的答案添加赏金:哪些设计模式实际上利用了JavaScript的提升行为?我了解为什么 JavaScript支持提升,但我想知道如何利用此功能

9 个答案:

答案 0 :(得分:19)

可变吊装

吊装最简单的用途之一是可变吊装。如果我们没有变量提升,则会抛出ReferenceError

var bar = foo; 
var foo;

这似乎没有立即有用,但它允许我们做这样的事情:

var myCoolJS = myCoolJS || {};

这基本上意味着它的样子:myCoolJS如果存在则为myCoolJS,如果不存在则为myCoolJS。如果ReferenceError尚未存在,则第二个myCoolJS不会抛出typeof myCoolJS != 'undefined',因为此变量声明已提升。

这使我们免于进行尴尬的module检查。

功能提升

将多个脚本合并到一个中时,功能提升特别有用。例如,我创建了CommonJS modules的轻量级构建时实现。这提供了在node.js中找到的相同requireexportsrequire('/foo')功能。我构建了工具,允许所需的模块由多个文件组成。例如,foo.js可能会生成一个由两个文件foo.h.js(“正文文件”)和// dom.h.js var util = require('util').util; exports.css = css; // we can do this because "css" is hoisted from below // ... other exports ... (“标题文件”)组成的模块。

这允许“body文件”不知道CommonJS模块环境提供的自由变量;所有这些都在标题中处理。这使得代码可以重复使用,并且无需构建即可轻松测试。但是,由于标题预先到正文,我们利用正文文件中的函数提升来允许标题中的导出。例如:

// dom.js

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

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

// ...other code...

...

{{1}}

答案 1 :(得分: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);
};

使用区块范围设定,您必须在value之前添加另一行以声明if

公平地说,这段代码因功能范围而不是吊装而起作用。 JavaScript可能没有提升功能范围。 Ruby更好地处理这个问题:Ruby有变量的方法范围,但变量不存在直到你设置它们:

def test(b)
    # unlike JavaScript, value is not accessible here
    if b
        value = 2
    else
        value = 5
    end
    puts value
end

答案 2 :(得分:11)

JavaScript没有块范围(现在让我们忘记let),因此任何变量声明都声明整个函数,其中JavaScript 具有范围。

如果您这样考虑,JavaScript提升可能会更有意义。

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

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

答案 3 :(得分: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”。明显。这里也没有混淆。

答案 4 :(得分:6)

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

实施例

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

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

因此Javascript会将该代码插入:

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

这就是为什么“Java the Good parts”有一个指导原则,说明你应该在函数的顶部声明变量。

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

“请解释是否存在实际利用此语言功能的设计模式。” “hoisting”不是一个特性,而是Javascript解释器如何构造代码的结果,因为该语言使用了函数作用域。

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

答案 5 :(得分: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 ()
   {
      //...
   }
}

我认为他们可能只有悬挂的功能对象,但我认为这与功能应该被视为语言中的一等公民的哲学不一致。

答案 6 :(得分:4)

这是一个真实的用例(虽然简化为伪代码),来自那些真正想要在野外使用吊装的好处。

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

  1. 逻辑序列:代码有一个顺序流,这意味着在声明函数之前总是会调用函数。这样做的好处在于,当使用低复杂度函数时,它会使事物保持相对平坦,并让您在其源之前不久了解函数的调用上下文。你只需要向下滚动(从不向上)来遵循代码,并且 - 尽可能 - 滚动不是或根本不滚动。
  2. 低参考开销:我喜欢将所有变量声明保存在每个范围的顶部,以便读者在阅读其正文之前了解该函数所需的所有活动部分,但没有人愿意读取每个调用函数的源代码,以了解当前作用域的作用。使用此方法,您将永远不会在声明之前遇到函数引用。一开始听起来很愚蠢,但它实际上减少了认知开销:你永远不会给隐含的函数来源记住这个 - 我们稍后会用它 - 相反你只读过一次函数源你知道它被调用的背景。
  3. $( 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' );
      }
    } );
    

答案 7 :(得分: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被拒绝。

再次。不要这样做!

答案 8 :(得分:0)

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