1

函数

  • JavaScript中,函数指只定义一次,但可以多次被多次执行或调用的一段JavaScript代码。与数组类似,JavaScript中函数是特殊的对象,拥有自身属性和方法

  • 每个函数对象都有prototypelength属性,bindapply()call()方法。函数的特殊性在于:可以通过函数调用执行函数体中的语句。

  • 函数是对象,所以可以赋值给变量、作为参数传递进其他函数、挂载到对象上作为方法

1 函数定义

函数定义总共有三种方法:函数定义表达式、函数声明语句和new Function()

  • 但是new Function()使用很少,因为通过它创建的函数不使用词法作用域,创建的函数都在全局作用域被调用。

  • 函数定义表达式和函数声明语句都利用关键字function来定义函数

    // 函数声明语句
    function funcName([arg1 [, arg2] [..., argn]]) {
      statements
    }
    //函数定义表达式
    var funcName = function([arg1 [, arg2] [..., argn]]) {
      statements
    }
  • 函数名标识符funcName:引用新定义的函数对象

  • 参数列表:函数中的参数与函数体中的局部变量相同,function(x)相当于在函数体中var x;

  • { statments }:构成函数体的语句,调用函数后执行的语句

1.1 变量提升

JavaScript中由var关键字声明的变量存在变量提升:将变量声明提升到作用域的顶部,但赋值仍保留在原处。所以函数声明语句和函数定义表达式有本质的区别

  • 函数声明语句:将函数声明和函数的赋值都提升到作用域的顶部,在同一个作用域中可以出现调用在函数定义之前;

ECMAScript允许函数声明语句作为顶级语句,可以出现在全局作用域中、也可以出现在嵌套函数中,但不能出现在循环、判断、try-catch-finallywith语句中。函数定义表达式没有限制

  • 函数定义表达式:与var声明的普通变量相同,只是将变量声明提升到作用域顶部,但赋值仍然保留在原处,不能在定义前使用

    //没有显式指明返回值的函数,默认返回undefined
    //输出对象o的每个属性的名称
    function printPrps(o) {
      for(var prop in o) {
        console.log(prop + ": " + o[prop] + "\n");
      }
    }
    
    //计算笛卡尔坐标系中两点间的距离
    function distance(x1, y1, x2, y2) {
      var dx = x2 - x1;
      var dy = y2 - y1;
      return Math.sqrt(dx * dx + dy * dy);
    }
    
    // 计算阶乘的递归函数,x!是x到1间(步长1)的累乘
    function factorial(x) {
      //递归结束标志
      if(x <= 1) {
        return 1;
      }
      return x * factorial(x - 1);
    }
    
    //将函数表达式赋值给变量
    var square = function (x) {return x * x;};
    // 函数表达式可以包含函数名,在递归时很有用
    //
    var f = function fact(x) {
      if(x <= 1) {return 1;}
      return x * fact(x -1);
    };
    
    // 函数表达式可以作为参数传递给其他函数
    data.sort(function(a, b) {return a  - b;});
    
    //定义后立即调用函数表达式
    var tensquare = (function(x) {return x * x;})(10);

1.2 嵌套函数

JavaScript中,函数可以嵌套在其他函数中。内部函数可以访问外部函数的局部变量和参数。

//  内部函数square可以访问到外部函数的参数a、b和局部变量c
function hypotenuse(a, b) {
  var c = 10;
  function square(x) {return x * x;}
  return Math.sqrt(square(a) + square(b) + square(c));
}

2 函数的调用

在定义函数时,函数体中的代码不会执行,只有在调用函数时,才执行函数体中的语句。有四种方式可以调用函数:

  1. 作为普通函数

  2. 作为对象的方法

  3. 作为构造器函数

  4. 使用函数的call()apply()方法间接调用

2.1 调用函数

使用调用表达式来调用普通函数,每个调用表达式由多个函数表达式组成。每个函数表达式包括函数对象、括号和传入的实参组成。

  • 每次调用会拥有本次调用的上下文this;在ES5非严格模式下,普通函数的this值是全局对象;在严格模式下是undefined

  • 以函数形式调用的函数通常不使用this关键字

  • 如果函数没有显式return语句返回一个值,默认返回undefined

  • 传入的实参是由逗号分隔的0个或多个函数表达式

    // 调用printProps()函数,传入对象作为实参即可
    printPrps({x: 1});  
    // 调用distance()函数
    var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5);
    // 调用factorial()函数
    var probability = factorial(5) / factorial(13);

2.2 方法调用

方法是保存在JavaScript对象属性中的函数。

  • 对方法调用的参数和返回值处理与函数调用相同

  • 方法调用由两个部分组成:对象.属性名(),其中属性名是值为函数的属性

  • 方法调用中:调用上下文指调用方法的对象,使用this关键字引用

    printProps({x: 1});
    var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5);
    var probability = factorial(5) / factorial(13);
    
    
    var calculator = {  //对象字面量
      operand1: 1,
      operand2: 2,
      add: function() {
        //用this关键字指代当前对象calculator
        this.result = this.operand1 + this.operand2;
      }
    };
    calculator.add();      //调用其add方法,使calculator对象获得result属性
    calculator.result;   //   ==> 3
    

方法和this关键字是面向对象的核心,任何函数作为方法调用时会传入一个隐式实参(指代调用方法的对象this),基于this的方法可以执行多种操作。

  • this是一个关键字,不是变量名、属性名,JavaScript不允许为this赋值,但是可以将其赋值给其他变量

  • this没有作用域限制,但是嵌套的函数不会从调用它的函数中继承this

    • 嵌套函数如果作为方法调用,this的值指向调用它的对象;

    • 嵌套函数如果作为函数调用,this不是全局变量(ES5非严格模式),就是undefined(ES5严格模式)

    • 嵌套函数的this并不指向调用它的外层函数的上下文

  • 在外层函数中使用变量将外层函数的this对象及arguments属性保存下来,在嵌套函数中便可以访问

    var o = {
      m: function() {
        var self = this;             // 保存this(指向o对象)在变量self中
        console.log(this === o);    // ==> true,this指向o对象
        f();                        //将f()作为函数调用
    
        function f() {
          console.log(this);  //  ==> window严格模式下,嵌套函数作为函数来调用,其this是undefined;非严格模式下是全局对象
          console.log(this === o);   //false,此处的this指向全局对象或undefined
          console.log(self === o);   //true,self指向外部函数的this值
        }
      }
    };
    o.m();       //调用对象o的方法m()

2.3 构造函数调用

如果函数或方法调用前有关键字new,函数或者方法便作为构造函数来调用。构造函数会创建一个新对象,新对象继承构造函数的prototype属性。

  • 作为构造器函数的调用,会将新创建的对象作为其调用上下文(this指向新创建的对象),在构造器函数中使用this引用新创建的对象。

2.4 间接调用call()apply()

函数是对象,每个函数都有call()apply()两个方法,作用是改变函数运行时的上下文context--改变函数体内部this的指向
,因为JavaScript中有函数定义时上下文函数运行时上下文函数中上下文可以改变的概念。

  • call()apply()作用都是动态改变函数体内this指向,只是接受参数形式不太一样。

  • call()需要将参数按顺序传递进函数,并且知道参数的数量(参数数量确定时使用)

  • apply()将参数放在数组中传进函数(参数数量不确定时使用)

call()apply()存在的意义

在JavaScriptOOP中,使用原型实现继承,call()apply()是用于不同对象间的方法复用。当一个object没有某个方法,但是另一个objAnother对象有,可以借助call()apply()使object可以操作objAnother对象的方法。

function Cat() {}
function Dog() {}

Cat.prototype = {
  food: "fish",
  say: function () {
    console.log("I love " + this.food);
  }
};

Dog.prototype = {food: "bone"};

var bCat = new Cat();
var bDog = new Dog();
bCat.say();                 //  ==> "I love fish"
bCat.say.call(bDog);          //==>"I love bone",bDog对象使用bCat对象的say方法,输出自身的`this.food`属性

3 函数的实参和形参

实参和形参是相对的概念,在函数定义时指定的参数叫做形参;在函数调用时传入的参数叫做实参。对于需要省略的实参,可以使用null或undefined`作为占位符。

3.1 参数默认值

如果调用函数时,传入的实参个数arguments.length小于定义时形参的个数arguments.callee.length,剩余的形参都被设置为undefined。对可以省略的值应该赋一个合理的默认值。

//  将对象obj中可枚举的自身属性追加到数组a中,并返回数组a
//  如果省略a,则创建一个新数组,并返回这个新数组
function getPropertyNames(obj, /*optional*/ a) {
  if(!a) { a = []; }       //如果未传入a,则使用新数组。
  // a = a || [];代替写法更有语义
  for(var prop in obj) {
    if(!obj.hasOwnProperty(prop)) {continue;}
    a.push(prop);
  }
  return a;
}
// 调用,出入一个参数或两个参数
var a = getPropertyNames(obj);   //将obj的属性存储到一个新数组中
getPropertyNames(obj, arr);   //将obj的属性追加到arr数组中

函数中的参数等同于函数体内的局部变量,具有函数的作用域。

3.2 参数对象

函数体内,标识符arguments指向实参对象的引用,实参对象是一个类数组对象,可以通过下标访问每个传入的参数。

  • arguments仅是一个标识符,严格模式下不能赋值;

  • 应用场景:函数包含固定个数的必须参数,随后包含不定数量的可选参数

    // 可以接收任意个数的实参,
    // 接收任意数量的实参,返回传入实参的最大值,内置的Math.max()方法功能类似
    function max(/*...optional*/) {  //实参个数不能为0
      var maxNum = Number.NEGATIVE_INFINITY;   //将保存最大值的变量初始化
      for(var i in arguments) {
        maxNum = (arguments[i] > maxNum) ? arguments[i] : maxNum;
      }
      return maxNum;
    }

3.3 calleecaller属性

  • callee是ECMAScript规范中arguments对象的属性:代表当前正在执行的函数。

  • caller是非标准的,只是浏览器基本都实现了这个属性:带表调用当前函数的函数。

  • 在严格模式中,对这两个属性读写都会产生错误

    // arguments的callee属性用在匿名函数的递归实现
    var factorial = function(x) {
      if(x <= 1) {return 1;}
      return x * arguments.callee(x - 1);
    }

3.4 将对象属性作为参数

在定义一个函数时,如果传入的参数多于3个,在调用时按顺序传入会变得很麻烦。一种解决方式是传入key/value形式的参数,无需关注参数的顺序。

  • 在定义函数时,形参指定为一个对象;

  • 调用函数时,将整个对象传入函数,无需关心每个属性的顺序。(性能会差,参数需要在对象中去查找值)

    // 将原始数组的length复制到目标数组
    // 开始复制原始数组的from_start元素
    // 并且将其复制至目标数组的to_start中
    // 参数复杂,调用时顺序难以控制
    function arrayCopy(array, from_start, target_arr, to_start, length) {
      // (原始数组, index, 目标数组, index, length)
      {
        // 实现逻辑
      }
    }
    
    // 无需关心参数顺序的版本,效率略低
    // from_start和to_start默认为0
    function easyCopy(args) {
      arrayCopy(args.array,
                args.from_start || 0,
                args.target_arr,
                args.to_start || 0,
                args.length);
    }
    // easyCopy()的调用
    var a = [1, 2, 3, 4];
    var b = [];
    easyCopy({array: a, target_arr: b, length: 4});

3.5 实参类型

JavaScript在定义函数时并未声明形参类型,形参整体传入函数体前不会做类型检查,如果对传入的实参有某种限制,最好在函数体内增加类型检查的代码。

// 返回数组或类数组a中元素的累加和
// 数组a中的元素必须是数字,null和undefined被忽略
// 类型检查严格,但是灵活性很差
function sum(a) {
  if(isArrayLike(a)) {  // a是数组或类数组
    var result = 0;
    for(var i in a) {
      var element = a[i];
      if(element == null) {continue;}   //跳过null和undefined
      if(isFinite(element)) {
        result += element;
      } else {
        throw new Error("sum(): elements must be finite number");
      }
    }
    return result;
  } else {
    throw new Error("sum(): arguments must be array-like");
  }
}

4 函数作为值

函数定义及调用是JavaScript中的词法特性;同时JavaScript中函数是一个对象:

  • 可以赋值给变量

  • 存储在对象的属性中或者数组的元素中

  • 作为参数传入另一个函数:例如Array.sort()方法,用来对数组元素进行排序。但是排序的规则有很多中,将具体规则封装在函数中,传入sort()。函数实现对任意两个值都返回一个值,指定它们在排序好数组中的先后顺序

    // 简单函数
    function add(x, y) {return x + y;}
    function subtract(x, y) {return x - y;}
    function mutiply(x, y) {return x * y;}
    function divide(x, y) {return x / y;}
    
    // 这个函数以上面一个函数作为参数,并传入两个操作数,使用传入的函数来调用
    // 过程抽象:两个数可以执行加、减、乘、除四个操作,将四个运算抽象为操作符,根据操作符不同,执行不同的函数
    function operate(operator, operand1, operand2) {
      return operator(operand1, operand2);
    }
    // 执行(2 + 3) + (4 * 5)
    var i = operate(add, 2, 3) + operate(mutiply, 4, 5);   // ==>25
    
    // 另外一种实现
    var  operators = {
      add: function(x, y) {return x + y;},
      subtrack: function(x, y) {return x + y;},
      mutiply: function(x, y) {return x + y;},
      divide: function(x, y) {return x + y;},
      pow: Math.pow
    };
    function operate2(operator, operand1, operand2) {
      if(typeof operators[operator] === 'function') {
        return operators[operator](operand1, operand2);
      } else {
        throw "unknown operator";
      }
    }
    // 计算("hello" + " " + "world")的值
    operate2("add", "hello", operate2("add", " ", "world"));   //  ==> "hello world"
    operate2("pow", 10, 2);    // ==> 100

自定义属性

函数是对象,可以拥有属性。对于函数中的静态变量,可以直接存入函数的属性中。

// 初始化函数对象的计数器属性,函数声明会被提前,所以可以先给他的属性赋值
uniqueInteger.counter = 0;
// 每次调用这个函数,都会返回一个不同的整数,使用counter属性保存下次要返回的值
function uniqueInteger() {
  return uniqueInteger.counter++;   // 先返回计数器的值,再自增1
}

5 函数作为命名空间

JavaScript中只存在函数作用域和全局作用域,没有块级作用域。可以使用自执行函数用作临时命名空间,这样不会污染全局变量。

(function() {/* 模块代码 */})();  //注意调用括号的位置,两种写法均可
(function() {/* 模块代码 */} ());

6 闭包

编程界崇尚优雅简洁唯美,很多时候如果你觉得一个概念很复杂,那么可能是你理错了

闭包在JavaScript中,指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使外部函数被返回(调用结束)。

  • Closure使JavaScript使当前作用域能够访问到外部作用域中的变量;

  • 函数是JavaScript中唯一拥有自身作用域的结构,所以Closure的创建依赖于函数

6.1 如何理解

var scope = "global scope";
function checkScope() {
  var scope = "local scope";
  function f() {return scope;}
  return f;   //将函数对象返回
}
checkScope()();    //  ==>  "local scope"

图片描述

  1. 在JavaScript中,每个函数在定义时会创建一个与之相关的作用域链,并且在程序执行期间一直存在

    • 外部函数checkScope有自身的作用域链,内部函数f有自身单独的的作用域链)

  2. 每次调用函数会创建一个新对象来保存参数和局部变量,并将其添加到作用域链。

    • 当函数返回时,将绑定的新对象从作用域链上删除。如果没有其他变量引用该对象、或该对象没有保存在某个对象的属性中,它会被当做垃圾回收。

    • 如果没有外部变量引用checkScope调用函数时创建的临时对象,函数return后便被垃圾回收

  3. 如果checkScope定义有嵌套函数f,并将f作为返回值或保存在某个对象的属性中。相当于有一个外部引用指向嵌套函数。

    • f有自身的作用域链和保存参数与局部变量的对象

    • fcheckScope函数体内,可以访问外部函数中所有的变量和参数

综上所述:JavaScript中的函数,通过作用域链和词法作用域两者的特性,将该函数定义时的所处的作用域中的相关函数进行捕获和保存,从而可以在完全不同的上下文中进行引用

6.2 注意点

  1. 每个函数调用都有一个this值和arguments对象,需要在外部函数中用变量保存this值和arguments对象,Closure才可以访问到外部函数的这两个值。that = thisouterArguments = arguments

  2. Closure是通过调用外部函数返回内部嵌套函数创建的,每次调用外部函数都会创建一个Closure但是每个Closure共享外部函数声明的变量,不会为每个Closure单独创建一份外部作用域的副本

    // 函数返回一个返回v的函数
    function constFunc(v) {
      return function() {return v;};
    }
    //创建一个数组用来保存常数
    var funcs = [];
    for(var i=0; i<10; i++) {
      funcs[i] = constFunc(i);  // 创建了10个Closure,每个Closure的值不同,因为每次传入外层函数constFunc的值不同
    }
    console.log(funcs[6]());   //  ==> 6
    
    function  constFuncs() {
      var funcs = [];
      for(var i=0; i<10; i++) {
        funcs[i] = function() {return i;};   // 创建10个Closure,但10个Closure在同一个外层函数constFuncs内,共享它的局部变量。
      }                                     // 10个Closure创建完毕后,i的值变为0,所以每个Closure返回的值都是0
      return funcs;
    }
    var foo = constFuncs();
    console.log(foo[4]());    //  ==> 10
  3. CLosure中部分资源不能自动释放,容易造成内存泄漏

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存(即不再利用的值或对象依然占据内存空间)


7 函数的属性、方法和构造函数

JavaScript中函数是对象,每个函数都有lenghtprototype属性;每个函数都有call()apply()bind()方法,并且可以利用函数的构造函数Function()来创建函对象。

7.1 length属性

函数对象的length属性是只读的,用于获取定义函数时指定的形参个数。可以用来检验定义的参数与传入的参数是否相同。

// arguments.callee不能在严格模式下工作
function check(args) {
  var actual = args.length;
  var expected = args.callee.length;      // arguments.callee指代函数本身
  if(expected !== actual) {
    throw Error("Expected:" + expected + " args; got " + actual + "args;");
  }
}
// 测试函数,只有传入三个函数才不会报错
function f(x, y, z) {
  check(arguments);
  return x + y + z;
}

7.2 prototype属性

每个函数都有一个prototype属性,指向一个原型对象的引用,每个函数的原型对象都不同

  • 将函数用作创建对象的构造器函数使用时,新创建的对象会从函数的原型对象上继承属性

7.3 call()apply()

call()apply()用于动态改变this的指向,使对象的方法可以借用给别的对象使用。

7.4 bind()

bind()方法的作用是将函数绑定至某个对象,bind()方法的返回值是一个新的函数对象

  • f()函数调用bind()方法绑定至对象o,用变量g来接收bind()返回的函数,(以函数调用形式)调用g时,会将原始函数f当做对象o的方法来使用。

    var f = function(y) {return this.x + y;};
    var o = {x: 2};
    var g = f.bind(o);   // 将f()绑定到o对象上
    console.log(g(6));    //  ==>  以函数调用的方式调用g(x),相当于调用o.f(x)
    
    // 实现bind()绑定
    function bind(f, o) {
      if(f.bind) {   //如果bind()方法存在,使用bind()方法
        return f.bind(o);
      } else {
        return function() {  //利用apply()使o对象来调用f()方法,并且传入类数组对象参数arguments
          return f.apply(o, arguments);  //arguments是调用绑定函数时传入的参数
        };
      }
    }
  • bind()第一个实参是要绑定方法的对象(本质是将函数的this指向改为传入的对象),同时后面的实参也会绑定至this,函数式编程中的currying 柯里化。

    var sum = function(x, y) {return x + y;};
    // 创建一个类似sum的新函数,但是this绑定到null
    // 并且第一个参数绑定为1,新的函数只期望传入一个参数
    var g = sum.bind(null, 1);  //  将sum的第一个参数x绑定为1
    console.log(g(3));   // ==> 4,因为x绑定为1,将3作为参数传入y
    
    function f(y, z) {return this.x + y + z;}
    var g = f.bind({x: 2}, 3); // 将f函数绑定到对象{x: 2},将3绑定到函数的第一个参数y,新创建的函数传入一个参数
    console.log(g(1));   // ==>6
  • 模拟实现bind()方法:bind()方法返回的是一个Closure

    if(!Function.prototype.bind) {  //不支持bind方法
      Function.prototype.bind = function (o) {
        var self = this;     // 保存bind()中的this与arguments,便于在嵌套函数中使用
        var boundArgs = arguments;
        // bind()方法返回一个函数对象
        return function() {
          // 创建一个实参列表,将传入bind()的第二个及以后的实参都传入这个函数
          var args = [];
          // 传入bind()函数的参数处理,从第二位开始
          for(var i=1; i<boundArgs.length; i++) {args.push(boundArgs[i]);}
          // 将调用新函数时传入的参数继续添加到args中
          for (var j=0; j < arguments.length; j++) {args.push(arguments[j]);}
    
          // 将self作为o的方法来调用
          return self.apply(o, args);
        };
      };
    }
注意点

bind()方法的某些特性是上述模拟方法不能替代的。

  1. bind()方法返回一个真正的函数对象,函数对象的length属性是绑定函数的形参个数减去绑定的实参个数(length的值不能小于0

    function f(y, z) {return this.x + y + z;}  // 绑定函数f的形参个数时2
    var g = f.bind({x: 2}, 3); // 绑定的实参个数是1(从第二位开始是传入绑定函数的实参),即将3传递给f的第一个参数y
    g(1);   // ==> 6,继续将1传递给函数f的形参z
  2. ES5的bind()方法可以顺带做构造函数,此时将会忽略传入bind()方法的this,原始函数以构造函数的形式调用,其实参已经绑定。

  3. bind()方法返回的函数并不包含prototype属性(普通函数的固有prototype属性是不能删除的);并且将绑定的函数用作构造器函数时所创建的对象,从原始为绑定的构造器函数中继承prototype

    • 如果将g()作为构造函数,其创建的对象与直接利用f当做构造函数创建的对象原型是同一个prototype

7.5 toString()方法

根据ECMAScript规范,函数的toString()方法返回一个字符串,字符串与函数声明语句的语法有关。

  • 大多数函数的toString()方法都返回函数的完整源码

  • 内置函数的toString()方法返回一个类似"[native code]"的字符串作为函数体

7.6 Function()构造函数

  • Function()构造函数运行JavaScript在运行时动态创建并编译函数

  • 每次调用Function()构造函数都会解析函数体,并创建新的函数对象。如果在循环中执行Function(),会影响效率;

  • Function()创建的函数不使用词法作用域,函数体的代码编译总在全局作用域执行

Function()在实际编程中使用很少。

8 函数式编程

JavaScript并非函数式编程语言,但JavaScript中函数是对象,可以像对象一样操控,所以可以应用函数式编程技术

8.1 使用函数处理数组

假设有一个数组,元素都是数字,要计算所有元素的平均值与标准差。

  • 非函数式编程风格

    var data = [1, 1, 3, 5, 5];
    var total = 0;  //平均数是所有元素的和除以元素的个数
    data.forEach(function(value) {
      total += value;
    });
    var mean = total / data.length;
    //标准差:先计算每个元素与平均值的差的平方的和
    var sum = 0;
    data.forEach(function(value) {
      var tmp = value - mean;
      sum += tmp * tmp;
    });
    //标准差stddev
    var stddev = Math.sqrt(sum / data.length-1);
  • 函数式编程风格,利用map()reduce()来实现,抽象出两个过程:

    • 求平均值和标准差会用到求一个数组中所有元素的和:使用reduce()

    • 求数组中每个元素的平方:使用map()

      // 定义求和、求积两个过程
      var add = function(x, y) {return x + y;};
      var square = function(x) {return x * x;};
      
      var data = [1, 1, 3, 5, 5];
      // reduc()实现数组求和
      var avg = data.reduce(add) / data.length;
      // map()实现差的平方,返回操作后的数组,再调用reduce()
      var sum = data.map(function(value) {return value - avg;});
      var stddev = Math.sqrt(sum.map(square).reduce(add) / (data.length - 1));

8.2 高阶函数

高阶函数higher-order function指操作函数的函数,接收一个或多个函数作为参数,并返回一个新函数。

// 高阶函数not()返回一个新函数,新函数将它的实参传入f()
function not(f) {
  return function() {    // 返回一个新函数
    var result = f.apply(this, arguments);    // 调用f()
    return !result;   // 对结果求反
  };
}
var even = function (x) {   //判断一个数是否是偶数
  return x % 2 === 0;
};
var odd = not(even);     // 一个新函数,所做的事情与even()相反
[1, 1, 3, 5, 5].every(odd);    // ==> true每个元素都是奇数

// mapper()返回的函数的参数是数组,对每个元素执行函数f()
// 返回所有计算结果组成的数组
function mapper(f) {
  return function(a) {
    return map(a, f);
  };
}
var increment = function(x) {return x + 1;};
var incrementer = mapper(increment);
incrementer([1, 2, 3]);

8.3 不完全函数

将一次完整的函数调用拆分为多次函数调用,每次传入的实参都是完整实参的一部分,每个拆分开的函数叫做不完全函数partial function,每次函数调用叫做不完全函数调用partial application特点是每次调用都返回一个函数,知道得到最终运行结果为止

   if(!Function.prototype.bind) {  //不支持bind方法
  Function.prototype.bind = function (o) {
    var self = this;     // 保存bind()中的this与arguments,便于在嵌套函数中使用
    var boundArgs = arguments;
    // bind()方法返回一个函数对象
    return function() {
      // 创建一个实参列表,将传入bind()的第二个及以后的实参都传入这个函数
      var args = [];
      // 传入bind()函数的参数处理,从第二位开始
      for(var i=1; i<boundArgs.length; i++) {args.push(boundArgs[i]);}
      // 将调用新函数时传入的参数继续添加到args中
      for (var j=0; j < arguments.length; j++) {args.push(arguments[j]);}

      // 将self作为o的方法来调用
      return self.apply(o, args);
    };
  };
}
  • 函数f()bind()方法返回一个新函数,给新函数传入特定的上下文和一组指定的参数,然后调用函数f()。传入bind()的实参都是放在传入原始参数的实参列表开始的位置。
    但有时希望将传入bind()的实参放在完整实参列表的右侧:

// 实现一个工具函数,将类数组对或对象转化为真正的数组
// 将arguments对象转化为真正的数组
function array(a, n) {return Array.prototype.slice.call(a, n || 0);}

// 这个函数的实参传递至左侧
function partialLeft(f) {
  var args = arguments;   // 保存外部的实参数组
  return function() {    // 返回一个函数
    var a = array(args, 1);  // 开始处理外部的第一个args
    a = a.concat(array(arguments));  //然后增加所有的内部实参
    return f.apply(this, a);   // 基于这个实参列表调用f()
  };
}

// 这个函数的实参传递至右侧
function partialRight(f) {
  var args = arguments;   // 保存外部的实参数组
  return function() {    // 返回一个函数
    var a = array(arguments);  // 从内部参数开始
    a = a.concat(array(args, 1));  //然后从外部第一个args开始添加
    return f.apply(this, a);   // 基于这个实参列表调用f()
  };
}

// 这个函数的实参被用作模板,实参列表中的undefined值都被填充
function partial(f) {
  var args = arguments;
  return function() {
    var a = array(args, 1);
    var i = 0, j = 0;
    // 遍历args,从内部实参填充undefined值
    for(; i<a.length; i++) {
      if(a[i] === undefined) {a[i] = arguments[j++];}
    }
    a = a.concat(array(arguments, j));
    return f.apply(this, a);
  };
}

// 函数带有三个实参
var f = function(x, y, z) {
  return x * (y - z);
};
// 注意三个不完全调用间的区别
partialLeft(f, 2)(3, 4);   //  ==> -2:绑定第一个实参 2*(3-4)
partialRight(f, 2)(3, 4);   //  ==> 6:绑定最后一个实参 3*(4-2)
partial(f, undefined, 2)(3, 4);   //  ==> -6:绑定中间的实参 3*(2-4)
  • 利用不完全函数的编程技巧,可以利用已有的函数来定义新的函数

8.4 记忆

在函数式编程中,把将上次计算记过缓存的技术叫做记忆memerization

本质上是牺牲算法的空间复杂度以换取更优的时间复杂度。因为在客户端中JavaScript代码的执行速度往往成为瓶颈。

// 返回f()的带有记忆功能的版本(缓存上次计算结果)
// 只有在f()的实参字符串表示都不相同时才工作
function memorize(f) {
  var cache = {};   //将值保存在闭包内
  return function() {
    // 将实参转为字符串形式,并将其用作缓存的键
    var key = arguments.length + Array.prototype.join.call(arguments, ",");
    if(key in cache) {
      return cache[key];
    } else {
      return cache[key] = f.apply(this, arguments);
    }
  };
}
// memorize()创建新对象cache并将其保存在局部变量中,对于返回的函数来说它是私有的(在闭包中)。
// 返回的函数将它的实参数组转化为字符串,并将字符串用作缓存对象的属性名。如果在缓存中有这个值,则直接返回
// 如果没有,调用既定函数对实参进行计算,将计算结果缓存并返回

// 返回两个整数的最大公约数
function gcd(a, b) {
  var t;
  if(a < b) { t= b; b = a; a = t; }
  while(b !== 0) {
    t = b;
    b = a %  b;
    a = t;
  }
  return a;
}
var gcdmemo = memorize(gcd);
gcdmemo(85, 187);   //  ==> 17
//注意写一个递归函数时,往往需要记忆功能
// 调用实现了记忆功能的递归函数
var factorial = memorize(function(n) {
  return (n <= 1)? 1 : n * factorial(n - 1);
});
factorial(5);   // ==> 120,同时缓存了1~4的值。

Kyxy
316 声望10 粉丝