JavaScript语言精粹——函数

JavaScript中最好的特性就是它对函数的实现。但也并非万能。
函数包含一组语句,它们是JavaScript的基础模块单元,用于代码复用、信息隐藏和组合调用。函数用于指定对象的行为。一般来说,所谓编程就是将一组需求分解成一组函数与数据结构的技能。
1.函数对象
①在JavaScript中函数就是对象,对象是名/值对的集合并拥有一个连到原型对象的隐藏连接。对象字面量产生的对象连接到Object.prototype,函数对象连接到Function.prototype。
②每个函数在创建时附有两个附加的隐藏属性:函数的上下文和实现函数行为的代码。
③每个函数对象在创建时也带一个prototype属性,它的值是一个拥有constructor属性且值为该函数的对象。
④因为函数是对象,所以它们可以像任何其他值一样被使用,函数可以存放在变量、对象和数组中。函数可以被当作参数传递给其他函数,函数也可以再返回函数。因为函数是对象,所以函数也可以拥有方法。
⑤函数的与众不同之处在于它们可以被调用。

2.函数字面量
①函数对象可以通过函数字面量来创建:
var add=function(a,b){ return a+b;};
函数字面量包括四个部分
<1>第一个部分是保留字function
<2>第二个部分是函数名,可以被省略。函数可以用它的名字递归调用自己,若如上例没给函数命名,它会被认为是匿名函数
<3>第三部分是包围在圆括号中的一组参数,每个参数用逗号分隔,这些名称被定义为函数中的变量。它们不像普通的变量那样被初始化为undefined,而是在该函数被调用时初始化为实际提供的参数的值。
<4>第四部分是包围在花括号中的一组语句,这些语句是函数的主体,它们在函数被调用时执行。
②函数字面量可以出现在任何允许表达式出现的地方,函数也可以被定义在其他函数中,一个内部函数自然可以访问自己的参数和变量,同时它也能访问被嵌套在其中的那个函数的参数与变量。通过函数字面量创建的函数对象包含一个连到外部上下文的连接,这被成为闭包。闭包是JavaScript强大表现力的根基。

3.调用
调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数。除了声明时定义的形式参数,每个函数接收两个附加的参数:this和arguments,参数this在面向对象编程中很重要,它的值取决于调用的模式。
在JavaScript中共有四种调用模式:方法调用模式、函数调用模式、构造器调用模式和apply调用模式。这些模式在如何初始化关键参数this上存在差异。

调用运算符是跟在任何产生一个函数值的表达式之后的一对圆括号。圆括号内可包含0到多个用逗号隔开的表达式,每个表达式产生一个参数值,每个参数值被赋予函数声明时定义的形式参数名。
当实际参数(arguments)的个数与形式参数(parameters)的个数不匹配时不会导致错误,如果实际参数过多,超出的参数会被忽略,如果实参过少,缺少的值会被替换为undefined,任何类型的值都可以被传递给参数。
①方法调用模式
<1>当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this被绑定到该对象。如果一个调用表达式包含一个属性存取表达式(即.点表达式或[subscript]下标表达式),那么它被当作一个方法来调用。
<2>方法可以用this访问对象,所以它能从对象中取值或者修改该对象。this到对象的绑定发生在调用的时候。这个超级迟绑定(very late binding)使得函数可以对this高度复用。 
<3> 通过this可以取得它们所属对象的上下文的方法称为公共方法。

②函数调用模式
当一个函数并非一个对象的属性时,它被当作一个函数来调用。var sum=add(3,4);//sum值为7
当函数以此模式调用时,this被绑定到全局对象,这是语言设计的一个错误。倘若正确,当内部函数被调用时,this应该仍然绑定到外部函数的this变量。解决方案:如果该方法定义一个变量并且给它赋值为this,那么内部函数就可以通过那个变量访问到this,按照约定,那个变量命名为that.
     myObject.double=function(){//内部函数被调用时this被绑定到全局对象
     var that=this;//解决方法为定义变量并赋值this,那么内部函数就可以通过这个变量访问到this
     var helper=function(){
          that.value=add(that.value,that.value);
       };
       helper();//以函数形式调用helper
    };
    myObject.double();//以方法的形式调用double
    alert(myObject.getValue());//6

③构造器调用模式  不推荐使用
JavaScript是一门基于原型继承的语言,意味着对象可以直接从其他对象继承属性。
如果在一个函数前边带上new来调用,那么将创建一个隐藏连接到该函数的prototype成员的新对象,同时this将会绑定到那个新对象上。结合new前缀调用的函数被称为构造器函数。
new前缀也会改变return语句的行为。

④Apply调用模式
JavaScript是一门函数式的面向对象编程语言,所以函数可以拥有方法。
apply方法让我们构造一个参数数组并用其去调用函数,也允许我们选择this的值。
apply方法接受两个参数,一个是将被绑定给this的值,第二个就是一个参数数组。第二个参数不用逐个列出

4.参数
①函数被调用时,会自然得到一个参数,arguments数组。通过它函数可以访问所有它被调用时传递给它的参数列表,包括那些没有被分配给函数声明时定义的 形式参数的多余参数,因此可以编写无须指定参数个数的函数。
②因为语言的设计错误,arguments并不是一个真正的数组,它只是一个类数组对象,arguments拥有length属性,但它缺少所有的数组方法。

5.返回
①当一个函数被调用时,它从第一个语句开始执行,并在遇到关闭函数体的}时结束,那使得函数把控制权交还给调用该函数的程序部分。
②return语句可以用来使函数提前返回,当return被执行时,函数立即返回而不再执行余下的语句。
③一个函数总是会返回一个值,如果没有指定返回值,则返回undefined.
④如果函数以在前边加上new前缀的方式调用,且返回值不是一个对象,则返回this.

6.异常
JavaScript提供可一套异常处理机制。异常是干扰程序的正常流程的非正常事故,但并非完全是出乎意料的。当查出这样的事故时,程序应该抛出一个异常:
var add=function(a,b){//当查出非正常事故时,程序应该抛出异常
      if(typeof a!=='number'||typeof b!=='number'){
        throw{            //throw语句终端函数的执行,它应该抛出一个exception对象,该对象包含可识别异常类型的name属性和一个描述性的message属性,也可以添加其他属性
          name:'TypeError',
          message:'add needs numbers'
        };
      }
      return a+b;
    }
    //构造try_It函数,以不正确的方式调用之前的add函数
    var try_It=function(){
      try{
        add("seven");
      }catch(e){
        document.writeln(e.name+": "+e.message);
      }
    }
    try_It();
如果在try代码快内抛出了一个异常,控制权就会跳转到它的catch从句。
一个try语句只会有一个将捕获所有异常的catch代码快,如果处理手段取决于异常的类型,那么异常处理器必须检查异常对象的name属性以确定异常的类型。
7.给类型增加方法
①JavaScript允许给语言的基本类型增加方法,比如可以通过给Function.prototype增加方法来使得该方法对所有函数可用:
//给类型增加方法
Function.prototype.method=function(name,func){//通过给Function.prototype增加method方法,就不必键入prototype属性名
      this.prototype[name]=func;
      return this;
    };
②JavaScript没有单独的整数类型,因此有时候提取数字中的整数部分是必要的。它本身提供的取整方法有些丑陋,可以通过给Number.prototype添加一个integer方法改善,它可以根据数字的正负判断是使用Math.ceiling还是Math.floor.
  
/* Number.method('integer',function(){
      return Math[this<0 ? 'ceiling' : 'floor'](this);
    });
    document.writeln((-10 / 3).integer());   无法得出结果,not a function ,原因暂不明*/


//为string添加移除末端空白方法
    String.method('trim',function(){
      return this.replace(/^\s+|\s+$/g,'');
    });
    document.writeln(' " '+"  neat  ".trim()+' " ')
③通过给基本类型增加方法,可以大大提高语言的表现力,因JavaScript原型动态本质,新的方法立刻被赋予到所有对象实例上,哪怕对象实例在方法创建前就创建好了
④基本类型的原型是公共的结构,只在确定没有该方法时再添加它比较保险。
Function.prototype.method=function(name,func){
if(!this.prototype[name])//有条件的增加一个方法
      this.prototype[name]=func; } ;

8.递归
①递归函数是会直接或者间接调用自身的一种函数,递归将一个问题分解为一组相似的子问题,每一个都用一个寻常解来解决,一般来说一个递归函数调用自身解决它的子问题。
var hanoi=function(disc,src,aux,dst){
      if(disc>0){
        hanoi(disc-1,src,dst,aux);
        document.writeln('move disc'+disc+' from '+src+' to '+dst+"<br/>");
        hanoi(disc-1,aux,src,dst);
       }
    }
    hanoi(10,'Src','Aux','Dst');
hanoi函数把该问题分解成三个子问题,首先移动源柱子中最小的圆盘到辅助柱,露出较大的圆盘,然后将较大的圆盘移动到目标柱,最后将较小的圆盘从辅助柱移动到目标柱,通过递归调用自身去处理一对圆盘的移动,从而解决那些子问题。
②一些语言提供了尾递归优化,意味着如果一个函数返回自身递归调用的结果,那么调用的过程会被替换为一个循环,它可以显著提高速度。但JavaScript并未提供,深度递归的函数可能会因为返回堆栈溢出而运行失败。

9.作用域
①在编程语言中,作用域控制着变量与参数的可见性及生命周期,它减少了名称冲突,并且提供了自动内存管理。
②大多数使用C语言语法的语言都有块级作用域,在一个代码块中(在花括号内的语句集)定义的变量在代码块外部是不可见的。定义在代码块中的变量在代码块执行结束后会被释放。
③JavaScript不支持块级作用域,而有函数作用域,定义在函数中的参数和变量在函数外部不可见,而且在一个函数的任何位置定义的变量在该函数的任何地方都可见。最好的做法是在函数体的顶部声明函数中可能用到的所有变量。

10.闭包
①作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments)。
//定义一个函数,设置一个DOM节点为黄色,然后渐变为白色
    var fade=function(node){
      var level=1;
      var step=function(){
        var hex=level.toString(16);
        node.style.backgroundColor="#FFFF"+hex+hex;
        if(level<15){
          level+=1;
          setTimeout(step,100);
        }
      };
      setTimeout(step,100);
    };
    fade(document.body);

11.回调
函数可以让不连续事件的处理变得容易。假如用户交互开始向服务器发送请求,再显示响应:
request=prepare_the_request();
response=send_request_synchronously(request);
display(response);
这种方式问题在于,网络上的同步请求将会导致客户端进入假死状态,若网络或者服务器很慢,响应性将会很低

更好的方式是发起异步的请求,提供当服务器响应到达时将被调用的回调函数,异步函数立即返回,这样不会阻塞客户端。
request=prepare_the_request();
send_request_asynchronously(request,function(response){
    display(response)'
});

12.模块
①可以使用函数和闭包构造模块,模块是一个提供接口却隐藏状态与实现的函数或对象。通过是用函数去产生模块,我们几乎可以完全摒弃全局变量的使用。
②假如想给String增加一个deentityfy方法,它的任务是寻找字符串的HTML字符实体并替换为对应字符,但把字符实体的名字与对应的字符存放在全局变量中不合理,定义在函数本身会有运行损耗,因为函数每次执行该字面量都会被求值一次,最好将其放入闭包。
String.method("deentityify",function(){
      //字符实体表
      var entity={
        quot: '"',
        lt:   '<',
        gt:   '>'
      };
      //返回deentityify方法
      return function(){
          return this.replace(/&([^&;]+);/g,
          function(a,b){
            var r=entity[b];
            return  typeof r==='string' ? r : a;
          }
        );
      };
    }());
    document.write('&gt;&quot;&gt;'.deentityify()); 
③模块模式利用了函数作用域和闭包创建绑定对象与私有成员的关联,这个例子中只有deentityify方法有权访问字符实体表。
④模块模式的一般形式是:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数或者把它们保存到一个可访问到的地方。
⑤使用模块模式就可以摒弃全局变量的使用,它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装或者构造其他单例对象模块模式非常有效。(JavaScript的单例就是用对象字面量表示法创建的对象)
⑥模块模式也可以用来产生安全的对象。

13.级联
①一些方法没有返回值,比如一些设置或者修改对象的某个状态却不返回任何值的方法就是典型的例子。如果让这些方法返回this而不是undefined,就可以启用级联,在一个级联中,我们可以在单独一条的语句中依次调用同一个对象的很多方法。
②级联可以产生出具备很强表现力的接口,也能帮助控制那种构造视图一次做很多事情的接口的趋势。
如一个启用级联的Ajax类库允许这样编码:
getElement('#myDiv').move(350,150).width(100).color('red').padding('4px').on('mousedown',function(){})....

14.套用
①函数也是值,套用允许我们将函数与传递给它的参数结合产生一个新的函数。
 Function.method('curry',function(){
      var slice=Array.prototype.slice;
      args=slice.apply(arguments),
      that=this;
      return function(){
        return that.apply(null,args.concat(slice.apply(arguments)));
        };
    });
    var  add1=add.curry(1);
    document.write(add1(6));                      

15.记忆
①函数可以用对象去记住先前操作的结果,从而能避免无谓的运算,这种优化被称为记忆。JavaScript的对象和数组可以方便的实现这种优化。
var fibonacci=function(n){
    return n<2 ? n : fibonacci(n-1)+fibonacci(n-2);
  };
  for(var i=0;i<=10;i+=1){
    document.write('//'+i+': '+fibonacci(i));
  }       
 //这样是可以工作的,但函数被调用了453次,如果该函数具备记忆功能就能显著减少运算量     

 var fibonacci1=function(){
    var memo=[0,1];
    var fib=function(n){
      var result=memo[n];
      if(typeof result!=='number'){
        result=fib(n-1)+fib(n-2);
        memo[n]=result;
      }
      return result;
    };
    return fib;
  }();
  document.write('//'+i+': '+fibonacci1(i));
  //这个函数返回同样的结果,但他只被调用了29次
②我们可以把这种形式一般化,编写函数帮助我们构造带记忆功能的函数。
 //一般化
  var memoizer=function(memo,fundamental){
    var shell=function(n){
      var result=memo[n];
      if(typeof result!=='number'){
        result=fundamental(shell,n);
        memo[n]=result;
      }
      return  shell;
    };
    return shell;
  };
   //用memoizer定义fibonacci函数
  var fibonacci2=memoizer([0,1],function(shell,n){
    return shell(n-1)+shell(n-2);
  });
  //产生一个可记忆的阶乘函数
  var factorial1=memoizer([1,1],function(shell,n){
    return n*shell(n-1);
  });

你可能感兴趣的:(JavaScript语言精粹——函数)