Ruby's Louvre

每天学习一点点算法

导航

重新认识javascript的函数体系

看这篇文章前,自己起码要一定的javascript的基础。如果只是学了jQuery两三个月,请买本书补一下基础。本文内容非常多,而且我会讲得很快(我承认我从来不是一位好老师),因此做好觉悟吧。

目录

基础回顾

  • 如何定义与执行函数
  • 为函数传参
  • 重新定义泛化函数
  • 变量的作用域
  • 函数变量

高级技能

  • 匿名函数
  • 内部函数
  • currying函数
  • 惰性函数
  • 闭包

定义函数

function sum(a, b) {
    var c = a + b;
    return c;
}

调用函数

sum(1,2);

参数

function args() { return arguments; }
args("司徒正美","ruby")

参数

function args() { return arguments; }
args("司徒正美","ruby")

预定义的函数

parseInt() 
parseFloat() 
isNaN() 
isFinite() 
encodeURI() 
decodeURI() 
encodeURIComponent() 
decodeURIComponent() 
eval()

parseInt

这东西我们完成可以自己实现,用正则把它最前面的数字部分提取出来,再减零转换为数字类型。注意,默认第二个参数为10。

>>> parseInt('123')
       123
>>> parseInt('abc123')
       NaN
>>> parseInt('1abc23')
       1
>>> parseInt('123abc')
       123
>>> parseInt('FF', 10)
     NaN
>>> parseInt('FF', 16)
     255
>>> parseInt('0377', 10)
    377
>>> parseInt('0377', 8)
    255
>>> parseInt('377') //默认是十进制
       377 
>>> parseInt('0377') //默认是十进制
       255 
>>> parseInt('0x377') //默认是十进制
       887

parseFloat

>>> parseFloat('123')
       123
>>> parseFloat('1.23')
       1.23
>>> parseFloat('1.23abc.00')
       1.23
>>> parseFloat('a.bc1.23')
       NaN
>>> parseFloat('123e-2')
       1.23
>>> parseFloat('123e2')
       12300
>>> parseInt('1e10')//注意这里,paresInt把1e10当成一个普通的字符串解析
       1

isNaN

判断其类型是否不为数字,比较拗,你就想一想它是否能用于计数吧。

>>> isNaN(Number.NEGATIVE_INFINITY)
       false
>>> isNaN(NaN)
       true
>>> isNaN(123)
       false
>>> isNaN(1.23)
       false
>>> isNaN(parseInt('abc123'))
       true

isFinite

>>> isFinite(Infinity)
       false
>>> isFinite(-Infinity)
       false
>>> isFinite(12)
       true
>>> isFinite(1e308)
       true
>>> isFinite(1e309)
       false

javascript最大数为1.7976931348623157e+308,大于它都认为是无穷数。

Encode/Decode URIs

>>> var url = 'http://www.packtpub.com/scr ipt.php?q=this and that';
>>> encodeURI(url);
   "http://www.packtpub.com/scr%20ipt.php?q=this%20and%20that"
>>> encodeURIComponent(url);
   "http%3A%2F%2Fwww.packtpub.com%2Fscr%20ipt.php%3Fq%3Dthis% 
   20and%20that"

变量的作用域

var global = 1;
function f() {
  var local = 2;
  global++;
  return global;
}
>>> f();
   2
>>> f();
   3
>>> local
   local is not defned

local于外围作用域是不可见。

var a = 123;
function f() {
  alert(a);
  var a = 1;
  alert(a);
}
f(); 

又如这个,以函数f为界,我们可以划分出两个作用域,外围的a一目了然,不说了。内围的函数数中的,经过预编译后,内部实际是这个样子:

var a;
alert(a);
a = 1;
alert(a)

函数变量

我们都知道定义函数有两种方法,函数声明与函数表达式。我们看一下,函数表达式是不是好像把一个函数代入一个变量中呢?

var a = function(){alert("1")}

嗯,无错,的确是这样。而且javascript的引用机制非常强悍,引用总是只指向最终被引用的对象,而不会是引用本身。例如,在Perl语言里,很可能有一个引用指向另一个也是引用的变量。但在JavaScript里,它会沿着引用链向下追溯直到指向核心的对象。因此javascript世界才如此忌悍全局变量,它很容易很我们清晰的结果搞糊涂。

另,既然是作为变量,它就拥有变量的一切特征。如,在调用delete方法删除一个变量时,有以下一个有趣的现象:var 变量是不能删除,没有var的变量才能删除。

因为函数是作为一种数据而设计出来,因此它遵循其他变量的行为,如被拷贝,被删除,作为参数放进函数内。

x = 1
alert(delete x) // true
var y = 2
alert(delete y) // false

那么我们看看函数变量也是不是如此:

      sum= function (a, b) {return a + b;}
      var add = sum;
      alert(delete sum)
      //true
      alert(typeof sum)
      // "undefned"
      alert(typeof add)
      // "function"
      alert(add(1, 2))
      // 3

      var sum= function (a, b) {return a + b;}
      var add = sum;
      alert(delete sum)
      //false
      alert(typeof sum)
      // "function"
      alert(typeof add)
      // "function"
      alert(add(1, 2))
      // 3

nice!函数的“变量”特性能帮我们搞出许多花样,我们在高级部分再进一步发挖!

---------------------------------------------

匿名函数

没有名字的函数,但是没有名字,我们怎样引用它呢?看下面两个实现模式

模式一,回调函数。我们知道函数可以作为一个变量来使唤,那么变量能做的事,它也能做,如作为参数。

      function addition(a, b){
        return a() + b();
      }
      function one() {
        return 1;
      };
      function two() {
        return 2;
      };
      alert(addition(one,two))

我们把它的两个参数缩到主函数的括号中又如何?

      function addition(a, b){
        return a() + b();
      }
      alert(addition(function one() { return 1;},function two() {return 2;}))

但显然,函数名给我们引用用的,现在没有用处,我们可以把它们去掉!

      function addition(a, b){
        return a() + b();
      }
      alert(addition(function () { return 1;},function () {return 2;}))

看到它个样子,我突然想到scheme……javascript真是灵活啊!

回调函数还有一种情形,比如我们想对数组的元素进行统一操作。嗯,你应该想起javascript1.6的那三个迭代器吧,forEach,map,filter。IE下没有,我们通常自造一个。

      function forEach(arr,callback,bind){
        for(var i=0,n=arr.length;i<n;i++){
              i in arr &&  callback.call(bind,arr[i],i,arr);
         }
      }
     forEach([1,2,3],function(a){alert(a+10)});

第二个参数就是回调函数,也是没有名字,不过我们通常称之为回调参数。

第二种模式,就是自动执行函数。打开jQuery的源码,刚学javascript的人一下晕倒,这是什么啊?

(function(){
     alert("司徒正美!")
})()

除了优先表达式可以构造自动执行函数外,基本上可用的操作符加匿名函数都可以,如

(function(){
     alert("司徒正美!")
})();
~~function(){
   alert("司徒正美!")
}
//更多见http://www.cnblogs.com/rubylouvre/archive/2010/02/11/1667364.html

我们还可以为它传参数

(function(name){
    alert('Hello ' + name + '!');
  })('Javascript')

内部函数

亦叫私有函数,是javascript实现信息隐藏的重要手段!后面讲的currying函数就是基于它发展而来。

       function a(p1) {
        function b(p2) {
          return p2 * 2;
        };
        return 3 + b(p1);
      };
      alert(a(4))//11
      alert(b(4))//错误: b is not defined

currying函数

返回函数的函数,这样可以充许我们分派传入参数?为什么要分派传入的需求呢?如forEach函数。

      function forEach(arr,callback,bind){//数组与回调函数是用户决定的,是否绑定对象也一样,没有默认是全局对象window
        for(var i=0,n=arr.length;i<n;i++){
              i in arr &&  callback.call(bind,arr[i],i,arr);//回调函数究竟要传入是什么,是我们决定的,绑定对象,数组元素,索引,数组本身
         }
      }
     forEach([1,2,3],function(a){alert(a+10)});

又如数组的sort方法,我们自定排序方法,用系统为我们的方法传入当前数组的某两个元素。

      function add(p1) {
        return function (p2) {
          return p1 + p2
        };
      };
      alert(add(4)(6))//10

实战用的currying函数:

http://www.cnblogs.com/rubylouvre/archive/2009/11/09/1598761.html
      function curry(f) {
        if (f.length == 0) return f;
        function iterate(args) {
          if (args.length <= f.length)
            return f.apply(null, args);
          return function () {
            return iterate(args.concat(Array.prototype.slice.call(arguments)));
          };
        }
        return iterate([]);
      }

闭包

闭包体现javascript的“函数为一等公民”的思想。不过在了解闭包前,我们还需要了解两样东西:

Scope Chain

有名的作用域链,基本上可以想象成一个对象套着一个对象,最外层的东西叫做window。查找一个变量时,基本遵循从里到外的原则。

>>> var a = 1; function f(){var b = 1; return a;}
>>> f();
       1
>>> b
       b is not defned

在定义a的作用域内,我们可以访问a,但b则是不可见的。

在定义b的作用域内,我们可以访问a与b。

显然,内围的作用域可用的资源更丰富些。

Lexical Scope

在javascript中,函数都有一个词法作用域,意思是函数会创建一个运行环境(作用域)于它定义的时候,而非执行它的时候。不过说创建,怎么也等于把代码放加到DOM树的那一刻。嘛,基本上取决于源码,eval,with等变态情况例外。

>>> function f1(){var a = 1; f2(); alert(a)}
>>> function f2(){ a = a + 1;}
>>> f1();
      a is not defned

我们在f1的里面调用f2,并通过f2对a做点来料加工什么的,但是报错了!不是说内部函数无敌吗,能访问外围的资料吗?!但是f2并不是定义在f1里面,因此它不算是f1的内部函数。f2正如f1一样,只有接受它里面的东西与它外围作用域的东西。在f2里面的a,它基本与f1的a是不同一个东西,像山西有个张三,山东有个张三,尽管同名同姓,但终究不是一个东西。而且f2的a没有定义就用了,不报错也难。

      function f1(){ var a = 1; return f2(); }
      function f2(){ return a;}
      alert(f1());
      var a = 5;
      alert(f1());
      //   5
      a = 55;
      alert(f1());
      //  55

利用闭包破坏作用域链

为了方便大家理解,我绘制了如下一幅图,蓝色的为全局作用域,红色白边与黑色白边的大圆圈为函数,绿色的小圆为变量。 如果你位于a,你就暴露于全局作用域下。如果位于b,你就在函数F中,那么你可以访问全局作用域中的a,但反过来不行。如果位于c,则可以访问b与a。一切都井井有条,但有一天,出于一个意外,一个奇特的现象——闭包——出现了,N函数不知所何从F函数中逃离出来,跑到全局变量中去了。

这样会造成什么效果呢?N函数与a变量一样悠哉优哉地游戈于全局作用域中,并且既然函数能记住它定时所在的作用域,那么N函数可以访问b变量,N函数里的c变量当然也可以访问b变量。作为N的同僚a,却没有这么好待遇。

但N函数是如何做到的呢?

#closure1
      var a = "a"
      function F(){
        var b = "b";
        return function(){//图中的N函数
          return b;
        }
      }
     alert(b) //如果在全局作用域下访问b会报错: b is not defined

我们必须先让F执行,好获取N的引用,由于N会返回b,因此我们再执行N。

>>> var N = F();//N成为一个全局函数了
>>> alert(N());
     "b"

另一种写法

#closure2
var n;
function f(){
  var b = "b"; 
  n = function(){
    return b;
  }
}
f();
alert(n());

上面两个写法都能让我们在全局作用域下访问到b,它们都有如下特征:函数都是相嵌的,换言之,一定存在内部函数;想要取得的内部变量并没有定义在内部函数中,从另一个角度看,内部函数调用它外围作用域的变量;运行结果会造成,内部函数被它外部函数外的一个变量引用,于是我们总需要两个函数,两个变量。当外围的函数执行完毕,除了内部函数外,还有一些东西没有跟着完蛋(被回收)。整个现象(闭包)必须要在外部函数执行后才产生,换言之,它是执行期的产物。

闭包产生的原因,因为javascript函数存在词法作用域,让它能访问定义它时作用域的东西,于是一个矛盾产生了,当函数return之后,除了return的东西,其他函数内部的东西怎么办?跟着外部函数被回收吗?这样肯定会跟词法作用域现范发生冲突,活下来的内部函数(现在是全局函数了),不能访问它原先作用域的变量了。因此肯定有一个东西产生了,保存着这变量与函数。这东西就是闭包,至于内部机制是怎样做的就不得人知了。

但这有什么用呢?看下面例子:

      function f() {
        var a = [],i;
        for(i = 0; i < 3; i++) {
          a[i] = function(){
            alert(i);
          }
        }
        return a;
      }
      var a = f();//★
      a[0]()//★★
      a[1]()//★★★
      a[2]()//★★★★

为什么都是3呢?当程序执行一星处时,返回一个函数组数赋给a。到二星处时,取出数组的第一个元素执行,这个函数要alert(i),但是i不是在此函数内部声明,它往外找。怎样往外找呢?记住,此时f函数已完蛋了,但它不在全局作用域,而是根据词法作用域,它按照定义时顺序找(内部函数内部--->内部函数外部--->f函数外部)。但请记住,但f()执行完后,i已经是3了,因此弹出3。其他两个情形相同。于是i怎么消亡,那它肯定产生了一个闭包,保存着这个函数数组与i。

我发现另一个有趣的现象,不一定需要内部函数,返回一个引用外部变量的对象,也可以产生闭包。

这时,我们再回顾官方的定义吧:闭包,是指语法域位于某个特定的区域,具有持续参照(读写)位于该区域内自身范围之外的执行域上的非持久型变量值能力的段落。这些外部执行域的非持久型变量神奇地保留它们在闭包最初定义(或创建)时的值(深连结)。JS版主winter补充说——“根据ECMA262的描述,当函数被执行时,会建立一个Activation Object,这个对象没有原型,是一个真正的空对象,之后所有函数内的局部变量都会作为这个对象的成员被添加上去。而在函数执行期间,嵌套定义的inner function,会保存一个到此对象的引用,这样inner function在别处执行时,就可以访问定义时的局部变量了。”

但是这种程度的闭包没有达到我们的目的。它只生了一个闭包。我们需要三个闭包来保存i的值。

      function f() {
        var a = [];
        var i;
        for(i = 0; i < 3; i++) {
          a[i] = (function(x){//内部函数
            return function(){//内部函数
              alert(x);
            }
          })(i);
        }
        return a;
      }

      var a = f();
      a[0]();
      a[1]();
      a[2]();

这里有一个自动执行函数,传0进去时,x=0,由于返回的函数要引用外部的变量,于是闭包产生,这个闭包的结构如下:

--------------------------------
|    x        | 1                        |
--------------------------------
|  匿名函数 |function(){alert(x)} |
--------------------------------

传1进去时,又一个闭包产生:

--------------------------------
|    x        | 2                        |
--------------------------------
|  匿名函数 |function(){alert(x)} |
--------------------------------

第三个相同

--------------------------------
|    x        | 3                        |
--------------------------------
|  匿名函数 |function(){alert(x)} |
--------------------------------

posted on 2010-04-12 22:48  司徒正美  阅读(544)  评论(0)    收藏  举报