JavaScript权威指南 第8章 函数

18 篇文章 0 订阅
1 篇文章 0 订阅

第8章 函数

函数是一个JavaScript代码块,定义之后,可以被执行或调用任意多次。JavaScript函数是参数化的,即函数定义可以包含一组标识符,称为参数或形参(parameter)。这些形参类似函数体内定义的局部变量。函数调用会为这些形参提供值或实参(argument)。函数通常会使用实参的值计算自己的返回值,这个返回值会成为函数调用表达式的值。除了实参,每个调用还有另外一个值,即调用上下文(invocation context),也就是this关键字的值。

如果把函数赋值给一个对象的属性,则可以称其为该对象的方法。如果函数是在对象上被调用或通过一个对象被调用,这个对象就是函数的调用上下文或this值。设计用来初始化一个新对象的函数成为构造函数。

JavaScript中的函数是对象,可以通过程序来操控,比如,JavaScript可以把函数赋值给变量,然后再传递给其他函数,因为函数是对象,所以可以在函数上设置属性,甚至调用函数的方法。

JavaScript函数可以嵌套定义在其他函数里,内嵌的函数可以访问定义在函数作用域的任何变量。这意味着JavaScript函数是闭包,基于闭包可以实现重要且强大的编程技巧。

8.1 定义函数

8.1.1 函数声明

函数声明由function关键字后跟如下组件构成。

  • 命名函数的标识符。这个作为函数名的标识符对于函数声明是必需的,它作为一个变量名使用,新定义的函数对象会赋值给这个变量。
  • 一对圆括号,中间包含逗号分隔的零或多个标识符。这些标识符是函数的参数名,它们就像是函数体内的局部变量。
  • 一对花括号,其中包含零或多个JavaScript语句。这些语句构成函数体,会在函数被调用时执行。

要理解函数声明,关键是理解函数的名字变成了一个变量,这个变量的值就是函数本身。函数声明语句会被“提升”到包含脚本、函数或代码块的顶部,因此调用这种方式定义的函数时,调用代码可以出现在函数定义代码之前。对此,另一种表述方式是:在一个JavaScript代码块中声明的所有函数在该块的任何地方都有定义,而且它们会在JavaScript解释器开始执行该块中的代码之前被定义

在ES6以前,函数声明只能出现在JavaScript文件或其他函数的顶部。虽然有些实现弱化了这个限制,但严格来讲在循环体、条件或其他语句中定义函数都不合法。不过在ES6的严格模式下,函数声明可以出现在语句块中。不过,在语句块中定义的函数只在该块中有定义,对块的外部不可见。

8.1.2 函数表达式

函数表达式看起来很像是函数声明,但它们出现在复杂表达式或语句的上下文中,而且函数名是可选的。以下是几个函数表达式的示例:

//这个函数表达式定义了一个对参数求平方的函数
//注意,我们把它赋值给一个变量
const square=function(x){ return x*x; };

//函数表达式可以包含名字,这对递归有用
const f=function fact(x){
    if(x<=1)
        return 1;
    else
        return x*fact(x-1);
};

//函数表达式也可以用作其他函数的参数
[3,2,1].sort(function(a,b){return a-b});
=>(3) [1, 2, 3]

//函数表达式也可以定义立即调用
let tensquared=(function(x){return x*x;}(10));

注意,函数名对定义为表达式的函数而言是可选的,前面看到的多数函数表达式都没有名字。函数声明实际上会声明一个变量,然后把函数对象赋值给它。而函数表达式不会声明变量,至于要把新定义的函数赋值给一个常量还是变量都取决于你,这样方便以后多次引用。最佳实践是使用const把函数表达式赋值给常量,以防止意外又给它赋予新值而重写函数。

如果需要引用自身,也可以带函数名,比如前面的阶乘函数。如果函数表达式包含名字,则该函数的局部作用域中也会包含一个该名字与函数对象的绑定。实际上,函数名就变成了函数体内的一个局部变量。多数定义为表达式的函数都不需要名字,这让定义更简洁。

使用函数声明定义函数f()与创建一个函数表达式再将其赋值给变量f有一个重要的区别。在使用声明形式时,先创建好函数对象,然后再运行包含它们的代码,而且函数的定义会被提升带顶部,因此在定义函数的语句之前就可以调用它们。但对于定义为表达式的函数就不一样了,这些函数在定义它们的表达式实际被求值以前是不存在的,不仅如此,要调用函数要求必需可以引用函数,在把函数表达式赋值给变量之前是无法引用函数的,因此定义为表达式的函数不能在它们的定义之前调用。

8.1.3 箭头函数

在ES6中,我们可以使用一种特别简洁的语法来定义函数,叫作“箭头函数”。它使用“箭头”分隔函数的参数和函数体。以为箭头函数是表达式而不是语句,所以不必使用function关键字,而且也不需要函数名。
箭头函数的一般形式:

const sum=(x,y)=>{ return x+y; }

但箭头函数还正常一种更简洁的语法。如果函数体只有一个return语句,那么可以省略return关键字、语句末尾的分号及花括号,将函数体写成一个表达式,它的值将被返回:

const sum=(x,y)=>x+y;

更进一步,如果箭头函数只有一个参数,也可以省略包含参数列表的圆括号:

const polynomal=x=>x*x+2*x+3;

不过要注意,对于没有参数的箭头函数则必须把空圆括号写出来:

const constanFunc=()=>42;

另外,如果箭头函数的函数体是一个return语句,但要返回的表达式对象字面量,那必须把这个对象字面量放在一对圆括号中,以避免解释器分不清花括号到底是函数体的花括号,还是对象字面量的花括号:

const f=x=>{return {value:x};};
const g=x=>{{value:x}}
const h=x=>{{value:x}}
const i=x=>{v:x,w:x}
VM1416:1 Uncaught SyntaxError: Unexpected token ':'

在上面代码的第3行,函数h()是有歧义的:本来想作为字面量的代码可能会解释为标签语句,因而会创建一个返回undefined的函数。而在第4行,更复杂的对象字面量并不是有效语句,这个不合法代码会导致语法错误。

箭头函数的简洁语法让它们非常适合作为值传给其他函数,而这在使用map()、filter()和reduce()等数组方法时是非常常见的:

//得到一个过滤掉null元素的数组
let filtered=[1,null,2,3].filter(x=>x!==null);
//求数值的平方
let squares=[1,2,3,4].map(x=>x*x);

相比其他方式定义的函数,箭头函数有一个及其重要的区别:它们从定义自己的环境继承this关键字的值,而不是像以其他方式定义的函数那样定义自己的调用上下文。这是箭头函数一个重要且非常有用的特性。箭头函数与其他函数还有一个区别,就是它们没有prototype属性。这意味着箭头函数不能作为新类的构造函数。

8.1.4 嵌套函数

在JavaScript中,函数可以嵌套在其他函数中。例如:

function hypotenuse(a,b){
    function square(x){ return x*x; }
    return Math.sqrt(square(a)+square(b));
}

关于嵌套函数,最重要的是理解它们的变量作用域规则:它们可以访问包含自己的函数(或更外层函数)的参数和变量。例如,在上面的代码中,内部函数square()可以读写外部函数hypotenuse()定义的参数a和b。嵌套函数的这种作用域规则是非常重要的,我们会在8.6再详细解释。

8.2 调用函数

构成函数体的JavaScript代码不在定义函数的时候执行,而在调用函数的时候执行。JavaScript函数可以通过5种方式来调用:

  • 作为函数
  • 作为方法
  • 作为构造函数
  • 通过call()或apply()方法间接调用
  • 通过JavaScript语言隐式调用(与常规函数调用不同)

8.2.1 函数调用

函数是通过调用表达式被作为函数或方法调用的。调用表达式包括被求值为函数对象的函数表达式。如果函数表达式是属性访问表达式,即函数是对象的属性或数组的元素,那么它是一个方法调用表达式。这种情况会在后面的例子中解释。下面这段代码包含几个常规的函数调用表达式:

printprops({x:1});
let total=distance(0,0,2,1)+distance(2,1,3,5);
let probability=factorial(5)/factorial(13);

在一次调用中,每个(位于括号中的)实参表达式都会被求值,求值结果会变成函数的实参。换句话说,这些值会被赋予函数定义的命名形参。在函数体内,对形参的引用会求值为对应的实参值。

对常规函数调用来说,函数的返回值会变成调用表达式的值。如果函数由于解释器到达末尾而返回,则返回值是undefined。如果函数由于解释器执行到return语句而返回,则返回值是return后面的值;如果return语句没有值就是undefined。

对于非严格模式下的函数调用,调用上下文(this值)是全局对象。但在严格模式下调用上下文是undefined。要注意的是,使用箭头语法定义的函数又有不同:它们总是继承自身定义所在环境的this值。

要作为函数(而非方法)来调用的函数通常不会在定义中使用this关键字。但是可以在这些函数中使用this关键字来确定是不是处于严格模式:

//定义并调用函数,以确定当前是不是严格模式
const strict=(function(){ return !this; }());
strict
=>false

8.2.2 方法调用

方法其实就是JavaScript的函数,只不过它保存为对象的属性而已。如果有一个函数f和一个对象o,那么可以像下面这样给o定义一个名为m的方法:

o.m=f;

对象o有了方法m()后,就可以这样调用:

o.m();

如果m()期待两个参数,可以这样调用:

o.m(x,y);

这个函数表达式本身是个属性表达式,这意味着函数在这里是作为方法而非常规函数被调用的。

方法调用的参数和返回值与常规函数调用的处理方式完全一致。但方法调用与函数调用有一个重要的区别:调用上下文。属性访问表达式由两部分构成:对象和属性名。在像这样的方法调用表达式中,对象o会成为调用上下文,而函数体可以通过关键字this引用这个对象。
eg:

let caculator={ //对象字面量
    openrand1:1,
    openrand2:1,
    add(){      //对这个函数使用了方法简写语法
        //注意这里使用this关键字引用了包含对象
        this.result=this.openrand1+this.openrand2;
    }
};
caculator.add();      //方法调用,计算1+1
caculator.result
=>2

多数方法调用使用点号进行属性访问,但使用方括号的属性访问表达式也可以实现方法调用。
eg:

o["m"](x,y);             //对o.m(x,y)的另一种写法
a[0](z)                  //也是一种方法调用(假设a[0]是函数)

方法和this关键字是面向对象编程范式的核心。任何用作方法的函数体实际上都会隐式收到一个参数,即调用它的对象。通常,方法会在对象上执行某些操作,而方法调用语法是表达函数操作对象这一事实的直观方式。
eg:

rect.setSize(width,height);
setRectSize(rect,width,height);

上面两行代码中的函数是假想的,这两个调用实际上会对(假想的)rect对象执行相同的操作。但第一行的方法调用语法更清晰地传达除了对象rect才是这个操作的焦点。

在这里插入图片描述
注意,this是个关键字,不是变量也不是属性名。JavaScript语法不允许给this赋值。

this关键字不具有变量那样的作用域机制,除了箭头函数,嵌套函数不会继承包含函数的this值。如果嵌套函数被当作方法来调用,那它的this值就是调用它的对象。如果嵌套函数(不是箭头函数)被当作函数来调用,则它的this值要么是全局对象(非严格模式),要么是undefined(严格模式)。
这里有一个很常见的错误,就是对于定义在方法中的嵌套函数,如果将其当作函数来调用,以为可以使用this获得这个方法的调用上下文。以下代码演示了这个问题:

let o={
    m:function(){
        let self=this;
        this===o;
        f();
        
        
       function f(){   //嵌套函数f
           this===o         //=>false:this是全局对象或undefined值
           self===o         //=>true:self是外部的this值
       }
       }
    };
o.m();       //在对象上调用方法m

在嵌套函数f()内部,this关键字不等于对象o。这被广泛认为是JavaScript语言的一个缺陷,所以了解这个问题很重要。上面的代码演示了一个常见的技巧。在方法m中,我们把this值赋给变量self,然后在嵌套函数f中,就可以使用self而非this来引用包含对象。

在ES6及之后的版本中,解决这个问题的另一个技巧是把嵌套函数f转换为箭头函数,因为箭头函数可以继承this值:

const f=()=>{
    this===o    //true,因为箭头函数继承this
};

函数表达式不像函数声明语句那样会被提升,因此为了让上面的代码有效,需要将这个函数f的定义放到方法m中调用函数f的代码之前。

还有一个技巧是调用嵌套函数的bind()方法,以定义一个在指定对象上被隐式调用的新函数:

const f=(function(){
    this===o   //true,因为我们把这个函数绑定到了外部的this
}).bind(this)

8.2.3 构造函数调用

如果函数或方法调用前面加了一个关键字new,那它就算构造函数调用。构造函数调用与常规函数和方法调用的区别在于参数处理、调用上下文和返回值。

如果构造函数调用在圆括号中包含参数列表,则其中的参数表达式会被求值,并以与函数和方法调用相同的方式传给函数。不过,假如没有参数列表,构造函数调用时其实也可以省略空圆括号。
例如,下面这两行代码是等价的:

o=new Object();
o=new Object;

构造函数调用会创建一个新的空对象,这个对象继承构造函数的propotype属性指定的对象。构造函数就是为初始化对象而设计的,这个新创建的对象会被用作函数的调用上下文,因此在构造函数中可以通过this关键字引用这个新对象。注意,即使构造函数调用看起来像方法调用,这个新对象也仍然会被用作调用上下文。换句话说,在表达式new o.m()中,o不会用作调用上下文。

构造函数正常情况下不适用return关键字,而是初始化新对象并在到达函数体末尾时隐式返回这个对象。此时,这个新对象就是构造函数调用表达式的值。但是,如果构造函数显式使用了return语句返回某个对象,那该对象就会变成调用表达式的值。如果构造函数使用return但没有返回值,或者返回的是一个原始值,则这个返回值会被忽略,仍然以新创建的对象作为调用表达式的值。

8.2.4 间接调用

JavaScript函数是对象,与其他JavaScript对象一样,JavaScript函数也有方法。其中有两个方法——call()和apply(),可以用来间接调用函数。这两个方法允许我们指定调用时的this值,这意味着可以将任意函数作为任意对象的方法来调用,即使这个函数实际傻瓜并不是该对象的方法。这两个方法都支持指定调用参数。其中,call()方法使用自己的参数列表作为函数的参数,而apply()方法则期待数组值作为参数。

8.2.5 隐式函数调用

有一些JavaScript语言特性看起来不像函数调用,但实际上会导致某些函数被调用。要特别关注会被隐式调用的函数,因为这些函数设计的bug、副效应和性能问题都比常规函数更难排查。因为只看代码可能无法知晓什么时候会调用这些函数。

以下是可能导致隐式函数调用的一些语言特性。

  • 如果对象有获取方法或设置方法,则查询或设置其属性值可能会调用这些方法。
  • 当对象在字符串上下文中使用时(比如当拼接对象与字符串时),会调用对象的toString()方法。类似地,当对象用于数值上下文时,则会调用它的valueof()方法。
  • 在遍历可迭代对象的元素时,会涉及一系列方法调用。
  • 标签模板字面量是一种伪装的函数调用。
  • 代理对象的行为完全由函数控制。这些对象上的几乎任何操作都会导致一个函数被调用。

8.3 函数实参与形参

JavaScript函数定义不会指定函数形参的类型,函数调用也不对传入的实参进行任何类型检查。事实上,JavaScript函数调用连传入实参的个数都不检查。

8.3.1 可选形参与默认值

当调用函数时传入的实参少于声明的形参时,额外的形参会获得默认值,通常是undefined。有时候,函数定义也需要声明一些可选参数。
eg:

//把对象o的可枚举属性名放到数组a中,返回a
//如果不传a,则创建一个新数组
function getPropertyNames(o,a){
    if(a===undefined) a=[];     //如果是undefined,创建一个新数组
    for(let property in o) a.push(property);
    return a;
}

//调用getPropertyNames()可以传一个参数,也可以传两个参数
let o={x:1},p={y:2,z:3};   //两个用于测试的对象
let a=getPropertyNames(o);
getPropertyNames(p,a);
=>(3) ["x", "y", "z"]

这个函数的第一行也可以不使用if语句,而是像下面这样约定熟成地使用||。

a=a||[];

注意,在设置可选参数时,一定要把可选参数放在参数列表后面,这样在调用时才可以省略。调用函数的程序员不可能不传第一个参数而只传第二个参数,他必须在第一个参数的位置显式地传undefined。

在ES6及更高的版本中,可以在函数形参列表中直接为每个参数定义默认值。语法是在形参名后面加上等于号和默认值,这样在没有给该形参传值时就会使用这个默认值:

//把对象o的可枚举属性名放到数组a中,返回a
//如果不传a,则传入一个新数组
function getPropertyNames(o,a=[]){
    for(let property in o) a.push (property);
    return a;
}

函数的形参默认值表达式会在函数调用时求值,不会在定义时求值。因此每次调用getPropertyNames()函数时如果只穿一个参数,都创建并传入一个新的空数组。如果形参默认值是常量(或类似[]、{}这样的字面量表达式),那函数是最容易了解的。但这不是必需的,也可以使用遍历或函数调用计算形参的默认值。对此有一种情形,即如果函数有多个形参,则可以使用前面参数的值来定义后面参数的默认值:

//这个带啊吗返回一个表示矩形尺寸的对象
//如果只提供width,则height就是它的两倍
const rectangle=(width,height=width*2)=>({width,height});

rectangle(1)
=>{width: 1, height: 2}

以上代码演示了形参默认值也可以在箭头函数中使用。同样,对于方法简写函数,以及其他各种形式的函数定义也都是一样的。

8.3.2 剩余形参与可变实参列表

形参默认值让我们可以编写用少于形参个数的实参来调用的函数。剩余形参的作用恰好相反:它让我们能够编写在调用时传入比形参多任意数量的实参的函数。下面是一个示例函数,接收一个或多个实参,返回其中最大的一个:

function max(first=-Infinity,...rest){
    let maxValue=first;          //假设第一个参数是最大的
    //遍历其他参数,寻找更大的数值
    for(let n of rest){
        if(n>maxValue){
            maxValue=n;
        }
    }
    //返回最大的数值
    return maxValue;
}
max(1,10,100,2,3,1000,4,5,6)
=>1000

剩余形参前面有三个点,而且必需是函数声明中最后一个参数。在调用剩余形参的函数时,传入的实参首先会赋值到非剩余形参,然后所有剩余的实参(也是剩余参数)会保存在一个数组中赋值给剩余形参。最后一点很重要:在函数体内,剩余形参的值始终是数值。数组有可能为空,但剩余形参永远不可能是undefined(相应地,也要记住,永远不要给剩余形参定义默认值,这样既没有用,也不合法)。

类似前面例子中那样可以接收任意数量实参的函数称为可变参数函数(variadic function),可变参数数量函数(variable arity function)或变长函数(vararg function) 。本书使用最通俗的“变长函数” (vararg),这个称呼可以追溯到C编程语言诞生的时期。

8.3.3 Arguments对象

剩余形参是ES6引入JavaScript的。在ES6之前,变长函数是基于Arguments对象实现的。也就是说,在任何函数体内,标识符arguments引用该次调用的Arguments对象。Arguments对象是一个类数组对象,它允许通过数值而非名字取得传给函数的参数值。下面是之前展示的max()函数,使用Arguments对象重写了一下,没有使用剩余参数:

function max(x){
    let maxValue=-Infinity;
    //遍历arguments,查找并记住最大的数值
    for(let i=0;i<arguments.length;i++){
        if(arguments[i]>maxValue) maxValue=arguments[i];
    }
    //返回最大的数值
    return maxValue;
}
max(1,10,100,2,3,1000,4,5,6);
=?1000

Arguments对象可以追溯到JavaScript诞生之初,也有一下奇怪的历史包袱,导致它效率低且难优化,特别是在非严格模式下。阅读代码的时候,我们还是可以看到Arguments的身影,但在新写的代码中应该避免使用它。在重构老代码时,如果碰到了使用arguments的函数,通常那个可以将其替换为…args剩余形参。由于Arguments对象属于历史遗留问题,在严格模式下,arguments会被当成保留字。因此不能用这个名字开声明函数形参或局部变量。

8.3.4 在函数调用中使用扩展操作符

在期待单个值的上下文中,扩展操作符…用于展开或“扩展”数组(或任何可迭代对象,如字符串)的元素。我们已经看到过如何对数组字面量使用扩展操作符。这个操作符同样可以用在函数调用中。

注意,从求值并产生一个值的角度说,…并不是真正的操作符。应该说,它是一种可以针对数组字面量或函数调用使用的特殊JavaScript语法。

如果在函数定义(而非函数调用时)使用同样的…语法,那么会产生与扩展操作符相反的作用。如8.3.2节所示,在函数定义中使用…可以将多个函数实参收集到一个数组中。剩余形参和扩展操作符经常同时出现,如以下函数所示,它接收一个函数实参并返回该函数的可测量版本,以用于测试:

function timed(f){
    return function(...args){    //把实参收集到一个剩余形参数组args中
        console.log(`Entering function ${f.name}`);
        let startTime=Date.now();
        try{
            //把收集到的实参传给包装后的函数
            return f(...args);   //把args扩展回原来的形式
        }finally{
            //在返回被包装的返回值之前,打印经过的时间
            console.log(`Exiting ${f.name} after ${Date.now()-startTime}ms`)
        }
    };
}

function benchmark(n){
    let sum=0;
    for(let i=1;i<=n;i++) sum+=i;
    return sum;
}

timed(benchmark)(1000000)
=>VM4521:3 Entering function benchmark
=>VM4521:10 Exiting benchmark after 4ms
=>500000500000

8.3.5 把函数实参解构成形参

调用函数时如果传入了一个实参列表,则所有参数值都会被赋给函数定义时声明的形参。函数调用的这个初始化阶段非常类似变量赋值。因此对于函数使用解构赋值计算并不奇怪。

如果我们定义了一个函数,它的形参包含在方括号中,那说明这个函数期待对每对方括号都传入一个数组值。作为调用过程的一部分,我们传入的数组实参会被解构赋值为单独的命名形参。例如,假设要用数组来表示两个数值的2D向量,数组的第一个元素是X坐标,第二个元素是Y坐标。基于这个简单的数据结构,可以像下面这样写一个把它们相加的函数:

function vectorAdd(v1,v2){
    return [v1[0]+v2[0],v1[1]+v2[1]];
}
vectorAdd([1,2],[3,4]);
(2) [4, 6]

如果换成把两个向量实参解构为命名更清晰的形参,以上代码就更好理解了:

function vectorAdd([x1,y1],[x2,y2]){
    return [x1+x2,y1+y2];
}

vectorAdd([1,2],[3,4])
=>(2) [4, 6]

类似地,如果定义的函数需要一个对象实参,也可以把传入的对象解构赋值给形参。还以前面的向量计算为例,但这次假设要求我们传入一个有x和y参数的对象:

//用标量乘以向量{x,y}
function vectorMutiply({x,y},scalar){
    return {x:x*scalar,y:y*scalar};
}

vectorMutiply({x:1,y:2},2)
=>{x: 2, y: 4}

这个例子把一个对象实参解构为两个形参,由于形参名与对象的属性名一致,所以相当清晰。但是,如果需要把解构的属性赋值给不同的名字,那代码会更长更不好理解。下面是一个向量加法的例子,用基于对象的向量实现:

function vectorAdd({x:x1,y:y1},{x:x2,y:y2}){
    return {x:x1+x2,y:y1+y2};
}

vectorAdd({x:1,y:2},{x:3,y:4})
=>{x: 4, y: 6}

对于像{x:x1,y:y1}这样的解构语法,关键是要记住哪些是属性名,哪些是实参名。无论是解构赋值还是解构函数调用,要记住的是声明的变量或参数都位于对象字面量中期待值的位置。因此,属性名始终在冒号左侧,而形参(或变量)名则在冒号右侧。

在解构赋值中也可以为形参定义默认值。下面是针对2D或3D向量乘法的例子:

//用标量乘以向量{x,y}或{x,y,x}
function vectorMutiply({x,y,z=0},scalar){
    return {x:x*scalar,y:y*scalar,z:z*scalar};
}

vectorMutiply({x:1,y:2},2)
=>{x: 2, y: 4, z: 0}

解构数组时,可以为被展开数组中的额外元素定义一个剩余形参。注意,位于方括号中的剩余形参与函数真正的剩余形参完全不同:

//这个函数七大i一个数组参数。数组的前两个元素
//会展开赋值给x和y,而剩下的所有元素则保存在
//coords数组中。第一个数组之后的参数则会保存
//到rest数组中
function ff([x,y,...cords],...rest){
    return [x+y,...rest,...cords];    //注意,这里是扩展操作符
}

ff([1,2,3,4],5,6)
=>(5) [3, 5, 6, 3, 4]

在ES2018中,解构对象也可以使用剩余形参。此时剩余形参的值是一个对象,包含所有违背解构的属性。对象剩余经常与对象扩展操作一起使用,后者也是ES2018中新增的特性:

//用标量乘以向量{x,y}或{x,y,z},其他属性不变
function vectorMultiply({x,y,z=0,...props},scalar){
    return {x:x*scalar,y:y*scalar,z:z*scalar,...props};
}
vectorMultiply({x:1,y:2,w:-1},2)
=>{x: 2, y: 4, z: 0, w: -1}

最后,要记住一点,除了解构作为参数的对象和数组,也可以解构对象的数组、有数组属性的对象,以及有对象属性的对象,无论层级多深。

8.3.6 参数类型

JavaScript方法的参数没有预定义的类型,在调用传参时也没有类型检查。可以用描述性强的名字作为函数参数,同时通过在注释中解释解释函数的参数来解决这个问题。

正如3.9节所介绍的,JavaScript会按需执行任意的类型转换。因此如果你的函数接收字符串参数,而调用时传的是其他类型的值,则这个值在函数想把它当成字符串使用时,会尝试将它转换为字符串。所有原始类型的值都可以转换为字符串,所有对象都有toString()方法(尽管有些不一定真正有用),因此这种情况下永远不会出错。

不过也并非没有例外。仍以前面的arraycopy()方法为例。该方法期待一个或多个数组参数,如果参数类型不对就会失败。除非你写的是个私有函数,只会在自己的代码内部调用,否则就必须增加像下面这样用于参数类型转换的代码。对函数来说,在发现传入的值不对是立即失败,一样好过先执行逻辑再以出错告终,而且前者比后者更清晰。
eg:

function sum1(a){
    let total=0;
    for(let element of a){             //如果a不是可迭代对象则抛出TypeError
        if(typeof element !== "number"){
            throw new TypeError("sum():element must be numbers");
        }
        total+=element;
    }
    return total;
}
sum1([1,2,3])
=>6
sum1(1,2,3)
VM5972:3 Uncaught TypeError: a is not iterable
    at sum1 (<anonymous>:3:24)
    at <anonymous>:1:1
sum1([1,2,"3"])
VM5972:5 Uncaught TypeError: sum():element must be numbers
    at sum1 (<anonymous>:5:19)
    at <anonymous>:1:1
sum1 @ VM5972:5
(anonymous) @ VM6076:1

8.4 函数作为值

函数最重要的特性在于可以定义和调用它们。函数定义和调用是JavaScript和大多数语言的语法特性。但在JavaScript中,函数不仅仅是语法,也是值。这意味着可以把函数赋值给变量、保存对象的属性或数组的元素、作为参数传给其他函数,等等。

要理解函数既是JavaScript数据又是JavaScript语法到底意味着什么,可以看看下面的例子:

function square(x) { return x*x; }

这个定义创建了一个新函数对象并把它赋值给变量square。函数的名字其实不重要,因为它就是引用函数对象的一个变量名。把函数赋值给另一个变量同样可以调用:

let a=square;        //=>s也引用了与square相同的函数对象
square(4)            //=>16
s(4)                 //=>16

除了变量,也可以把函数赋值给对象的属性。如前所诉,这时候把函数称作“方法”:

let o={square:function(x){ return x*x }};   //对象字面量
let y=o.square(16);                         //y===256

函数甚至可以没有名字,比如可以把匿名函数作为一个数组元素:

let a=[x=>x*x,20];    //数组字面量
a[0](a[1])            //=>400

为了更好地理解把函数作为值由多大用处,可以想一想Array.sort()方法。这个方法可以对数组元素进行排序。因为可能的排序方式有很多种,所以sort()方法可选的接收一个函数作为参数,并根据这个函数的返回值决定如何排序。这个函数的任务非常简单:对于传给它的两个值,它要返回一个表示哪个值在排序后的数组中排在前面的值。这个函数参数让Array.sort()变得非常通用,而且无比灵活。可以通过它把任何可以想象出来的方式进行排序。

示例8-1演示了把函数当作值可以做什么事。
在这里插入图片描述在这里插入图片描述

8.4.1 定义自己的函数属性

函数在JavaScript中并不是原始值,而是一种特殊的对象。这意味着函数也可以有属性。如果一个函数需要一个“静态变量”,且这个变量的值需要在函数每次调用时都能访问到,则通常把这个变量定义为函数本身的一个属性。比如,假设我要写一个每次调用都返回唯一整数的函数,那么每次调用都不能返回相同的值。为保证这一点,函数需要记录自己已经返回过的值,这个信息必须在每次调用时都能访问到。可以把这个信息保存一个全局变量中,但其实没有这个必要,因为这个信息只有函数自己会用到。更好的方式是把这个信息保存在函数对象的一个属性中。下面就是一个每次调用都返回唯一整数值的函数实现:

//初始化函数对象的计数器(counter)属性
//函数声明会被提升,因此我们可以在函数声明
//之前在这里就给它赋值
uniqueInteger.counter=0;

//这个函数每次被调用时都返回一个不同的整数
//它使用自身的属性记住下一个要返回什么值
function uniqueInteger(){
     return uniqueInteger.counter++;   
}
uniqueInteger()
uniqueInteger()

8.5 函数作为命名空间

在函数体内声明的变量在函数外部不可见。为此,有时候可以把函数用作临时的命名空间,这样可以保证在其中定义的变量不会污染全局命名空间。

假设有一段JavaScript代码,你像在几个不同的JavaScript程序中使用它(或者在客服端JavaScript中,在不同网页中使用它)。再假设这段代码跟多数代码一样定义了存储计算结果的变量。问题来了:这段代码可能被很多程序用到,我们不知道那些程序中创建的变量会不会跟这段代码中的变量冲突。解决方案就是把这段代码放到一个函数中,然后调用这个函数。这样,原本可能定义在全局的变量就变成函数地局变量了:

function chunkNamespace(){
    //要复用的代码放在这里
    //在这里定义的任何变量都是函数的局部变量
    //不会污染全局命名空间
}
chunkNamespace();           //别忘了调用这个函数!

以上代码只定义了一个全局变量,即函数chunkNamespace。如果就连定义一个属性也嫌多,那可以在一个表达式中定义并调用匿名函数。

(function(){     //将chunkNamespace()函数重写为一个无名表达式
   //要复用的代码放在这里
}());           //函数结束后立即调用它     

在一个表达式中定义并调用匿名函数的技术非常常用,因此甚至有了别称,叫“立即调用函数表达式”。注意前面代码中括号的使用。位于function关键字前面的左开括号是必需的,因为如果没有,JavaScript解释器会把function关键字作为函数声明语句来解析。有了这对括号,解释器会把它正确地识别为函数定义表达式。而且开头地括号也便于程序员知道这是等以后立即调用的函数,而不是为后面使用而定义的函数。

函数作为命名空间真正的用武之地,还是在命名空间中定义一个或多个函数,而这些函数又使用该命名空间中的变量,最后这些函数又作为命名空间函数的返回值从内部传递出来。类似这样的函数被称为闭包。

8.6闭包

与多数现代编程语言一样,JavaScript使用词法作用域。这意味着函数执行时使用的是定义函数时生效的变量作用域,而不是调用函数时生效的变量作用域。为了实现词法作用域,JavaScript函数对象的内部状态不仅要包括函数代码,还要包括对函数定义所在作用域的引用。这种函数对象与作用域(即一组变量绑定)组合起来解析函数变量的机制,在计算机科学文献中被称为闭包。

严格来说,所有JavaScript函数都是闭包。但由于多数函数调用与函数定义都在同一作用域内,所以闭包的存在无关紧要。必要真正值得关注的时候,是定义函数与调用函数的作用域不同的时候。最常见的情形就是一个函数返回了在它内部定义的嵌套函数。很多强大的编程技术都是建立在这种嵌套函数闭包之上的,因此嵌套函数比好在JavaScript程序中也变得比较常见。

要理解闭包,第一步需要先回顾嵌套函数的词法作用域规则。来看下面代码:

let scope="global scope";          //全局变量
function checkscope(){
    let scope="local scope";       //局部变量
    function f(){
        return scope;              //返回当前作用域中的值
    }
    return f();
}
checkscope()
=>"local scope"

checkscope()函数声明了一个局部变量,然后定义了一个返回该变量的函数并调用了该函数。很显然,调用checkscope()应该返回“local scope”。现在,我们稍微改一改代码。

let scope="global scope";
function checkscope(){
    let scope="local scope";      //局部变量
    function f(){
        return scope;             //返回作用域中的值
    }
    return f;
}
let s=checkscope()();
s
=>"local scope"

在上面的代码中,我们把checkscope()中的一对圆括号转移到了外部。转移前调用的是嵌套函数并返回结果,而现在checkscope()返回的是嵌套函数。

还记得词法作用域的基本规则吧:JavaScript函数是使用定义它们的作用域来执行的。在定义嵌套函数f()的作用域中,变量scope绑定的值是“local scope”,该绑定在f执行时仍然有效,无论它在哪里执行。因此前面代码示例最后一行返回“local scope”,而非“global scope”。简言之,这正是闭包惊人且强大的本质:它们会破获自身定义所在外部函数的局部变量(及参数)绑定。

在8.4.1节,我们定义了一个unicodeInteger()函数,该函数使用一个函数自身的属性来跟踪药返回的下一个值。该方式有一个缺点,就是容易被错误或而已代码重置计数器,或者把计数器设置为整数,导致unicodeInteger()函数违法自己契约的“唯一”(unique)或“整数”规则。闭包可以捕获一次函数调用的局部变量,可以将这些变量作为私有状态。为此,可以像下面这样改写unicodeInteger()函数,使用立即调用函数表达式定义一个命名空间,再通过一个闭包利用该命名空间来保证自己的状态私有:

let unicodeInteger=(function(){   //定义并调用
    let counter=0;                //下面函数的私有状态
    return function(){ return counter++; };
}());
unicodeInteger()
=>0
unicodeInteger()
=>1

为了理解这段代码,必需仔细看一遍。乍一看,这一行代码像一条赋值语句,把一个函数赋值给变量uniqueInteger。实际上(如第一行代码开头所提示的),这行代码定义并调用了一个函数,因此真正赋给uniqueInteger的是这个函数的返回值。再仔细看一下函数体,你会发现它的返回值是另一个函数。换句话说,这个嵌套的函数最终被赋值给了uniqueInteger。这个嵌套函数有权访问器作用域中的变量,而且可以使用定义在外部函数中的变量counter。外部函数一但返回,就没有别的代码能够看到变量counter了,此时内部函数拥有对它的专有访问权。

类似counter这样的私有变量并非只能由一个闭包独享。同一个外部函数中完全可以定义两个或更多嵌套函数,而它们共享相同的作用域。来看下面的代码:

function counter(){
    let n=0;
    return{
        count:function(){
            return n++;
        },
        reset:function(){
            n=0;
        }
    };
};
let c=counter(),d=counter();    //创建两个计数器
c.count()
=>0
d.count()                       //它们分别计数
=>0
c.reset();                      //reset()和count()方法共享状态
c.count()                       //因为重置了c
=>0
d.count()
=>1

这个counter()函数返回一个“计数器”对象,该对象有两个方法:count()和reset()。前者返回下一个整数,后者重置内部状态。首先要理解的是,这两个方法都有权访问私有变量n。其次是要知道,每次调用counter()都会创建一个新作用域(与之前调用插件的作用域相互独立),还有作用域中的一个新私有变量。因此如果调用两次counter(),就会得到拥有两个不同私有变量的计数对象。在一个计数器上调用count()或reset()不会影响另一个计数器。

有一点需要指出的是,可以将这种闭包技术与属性获取方法和设置方法组合使用。下面这个counter()函数是6.10.6节中代码的变体,但它使用了闭包保存私有状态而非常规对象属性:

function counter(n){   //函数参数n是私有变量
    return{
        //属性获取方法,返回递增后的私有计数器值
        get count(){
            return n++;
        },
        //属性设置方法,不允许n的值减少
        set count(m){
            if(m>n) n=m;
            else throw Error("count can only be set to a larger value");
        }
    };
}

let c=counter(1000);

c.count
=>1000

c.count
=>1001

c.count=2000;
=>2000

c.count
=>2000

c.count=2000         //错误:计数只能设置为更大的值
VM825:10 Uncaught Error: count can only be set to a larger value
    at Object.set count [as count] (<anonymous>:10:24)
    at <anonymous>:1:8

注意这个版本的counter()函数并没有声明局部变量,只使用自己的参数n保存供属性访问器方法共享的私有状态。这样可以让counter()的调用者指定私有变量的初始值。

示例8-2基于前面介绍的闭包技术实现了一个通用的共享私有状态的函数。例子中的函数addPrivateProperty()定义了一个私有变量和两个分别用于获取和设置该变量值的嵌套函数,而在将这两个嵌套函数作为方法添加到了调用时指定的对象。

示例8-2:使用闭包的私有属性访问器方法

//这个函数按照指定的名字为对象o添加属性访问器方法
//方法命名为get<name>和set<name>
//如果提供断言函数,则设置
//方法用它测试自己的参数,在存储前先验证
//如果断言返回false,则设置方法抛出异常
//
//
//称作的值并没有保存在对象o上,而是保存在了
//这个函数的一个局部变量中。获取方法和设置方法
//也是在函数局部定义的,因而可以访问这个
//局部变量。这意味着该变量对两个访问器方法
//而言是私有的,除了设置方法,没有别的途径可以
//设置或修改这个变量的值
function addPrivateProperty(o,name,predicate){
    let value;     //这是属性值
    
    //获取方法简单地返回属性值
    o[`get${name}`]=function(){
        return value;
    };
    
    //设置方法保存值或在断言函数失败时抛出异常
    o[`set${name}`]=function(v){
        if(predicate&&!predicate(v)){
            throw new TypeError(`set${name}:invalid value ${v}`);
        }else{
            value=v;
        }
    };
}

let o={};              //创建一个变量

//添加属性访问器方法getName()和setName()
//确保只能设置字符串值
addPrivateProperty(o,"Name",x=>typeof x==="string");

o.setName("Frank")      //设置属性的值
o.getName();
=>"Frank"
o.setName(0)            //尝试设置一个错误类型的值
VM1375:12 Uncaught TypeError: setName:invalid value 0
    at Object.o.<computed> [as setName] (<anonymous>:12:19)
    at <anonymous>:1:3

前面几个例子都是在相同作用域中定义两个闭包,共享访问相同的私有变量或变量。这个技术很重要,但同样重要的是应该知道什么情况下闭包会意外地共享访问不该被共享地变量。比如下面的代码:

//这个函数返回一个始终返回v的函数
function constfunc(v){
    return ()=>v;
}

//创建一个常量函数的数组
let funs=[]
for(var i=0;i<10;i++)
    funs[i]=constfunc(i);

//索引5对应的函数返回值5
funs[5]()
=>5

在编写这种循环创建多个闭包的代码时,一个常见的错误是把循环转移到定义闭包的函数中。比如下面的代码:

//返回一个函数数组,其中的函数返回值为0-9
function constfuncs(){
    let funcs=[];
    for(var i=0;i<10;i++){
        funcs[i]=()=>i;
    }
    return funcs;
}
let funcs=constfuncs();
funcs[5]()
10

以上代码创建了10个闭包并将它们保存在一个数组中。而闭包全部都是在同一个函数调用中定义的,因此它们可以共享访问变量i。当constfuncs()返回后,变量i的值是10,全部10个闭包共享这个值。因此,返回的函数数组中的所有函数都返回相同的值。这绝对不是我们想要的结果。关键是要记住,与闭包关联的作用域是“活的”。嵌套函数不会创建作用域的私有副本或截取变量绑定的静态快照。从根本上说,这里的问题在于通过var声明的变量在整个函数作用域内都有定义。代码中的for循环使用var i声明循环变量,因此变量i的作用域是整个函数体,而不是更小的循环体。这段代码演示了ES5及之前版本代码中常见的一类错误,而ES6增加的块级作用域变量解决了这个问题。只要把这里的var替换成let 或 const ,问题马上就会消失。因为let和const是块级作用域的标志,这意味着每次循环都会定义一个与其他循环不同的独立作用域,而每个作用域中都有自己独立的i绑定。

在写闭包的时候,要注意:this是JavaScript关键字,不是变量。如前所述,箭头函数继承包含它们的函数中的this值,但使用function定义的函数并非如此。因此如果你要写的闭包需要使用其包含函数的this值,那应该在返回闭包之前使用箭头函数或调用bind(),也可把外部的this值赋给你的闭包将继承的变量:

const self=this;     //让嵌套函数可以访问外部this值

8.7 函数属性、方法与构造函数

8.7.1 length 属性

函数有一个只读的length属性,表示函数的元数(arity),即函数在参数列表中声明的形参个数。这个值通常表示调用函数时应该传入的参数个数。如果函数有剩余形参,则这个剩余形参不包含在length属性内。

8.7.2 name属性

函数有一个只读的name属性,表示定义函数时使用的名字(如果是用名字定义的),如果是未命名的函数,表示在第一次创建这个函数时赋给该函数的变量名或属性名。这个属性主要用于记录调试或排错消息。

8.7.3 propotype属性

除了箭头函数,所有函数都有一个prototype属性,这个属性引用一个被称为原型对象的对象。每个函数都有自己的原型对象。当函数被作为构造函数使用时,新创建的对象从这个原型对象继承属性。原型和prototype属性在6.2.3节讨论过,第9章还会再次介绍。

8.7.4 call()和apply()方法

call()和apply()允许间接调用(参见8.2.4)一个函数,就像这个函数是某个其他对象的方法一样。call()和apply()的第一个参数都是要在其上调用这个函数的对象,也就是函数的调用上下文,在函数体内它会变成this关键字的值。要把函数f()作为对象o的方法进行调用(不传参数),可以使用call()或apply():

f.call(o);
f.apply(o)

这两行代码都类似于下面的代码(假设o并没有属性m):

o.m=f;              //把f作为o的一个临时方法
o.m();              //调用它,不传参数
delete o.m;         //删除这个临时方法

我们知道,箭头函数从定义它的上下文中继承this值。这个this值不能通过call()和apply()方法重写。如果对箭头函数调用这两个方法,那第一个参数实际上会被忽略。

除了作为调用上下文传给call()的第一参数,后续的所有参数都会传给调用的函数(调用箭头函数时不会忽略这些参数)。比如,要将函数f()作为对象o的方法进行调用,并同时传给函数f()传两个参数,可以这样写:

f.call(o,1,2);

apply()方法与call()方法类似,只不过要传给函数的参数需要以数组的形式提供:

f.apply(o,[1,2]);

如果函数定义时可以接收多个参数,则使用apply()方法可以在调用这个函数时把任意长度的数组内容传给它。在ES6及之后的版本中可以使用扩展操作符,但我们也有可能看到使用apply()的ES5代码。例如,在不使用扩展操作符的情况下,想知道一个数值数组中的最大值,可以使用apply()方法把数组的元素传给Math.max()函数:

let biggest=Math.max.apply(Math,arrOfNumbers);

下面定义的trace()函数与8.3.4节定义的timed()函数类似,但trace()函数操作的是方法而不是函数,它使用apply()方法而不是扩展操作符,这样一来,就可以用与包装方法(新方法)相同的参数和this值调用被包装的方法(原始方法):

//将对象o的方法m替换成另一个版本
//新版本在调用原始方法前、后会打印日志
function trace(o,m){
    let original=o[m];         //在闭包中记住原始方法
    o[m]=function(...args){
        console.log(new Date(),"Entering:",m);         //打印消息
        let result=original.apply(this,args);     //调用原始方法
        console.log(new Date(),"Exiting:",m);          //打印消息
        return result;
    };
}

8.7.5 bind()方法

bind()方法的主要目的是把函数绑定到对象。如果在函数f()上调用bind()方法并传入对象o,则这个方法会传入一个新函数。如果作为函数来调用这个新函数,就会像f是o的方法一样调用原始函数。传给这个新函数的所有参数都会传给原始函数。例如:

function f(y){                //这个函数需要绑定
    return this.x+y;
}

let o={x:1};              //要绑定的对象
let g=f.bind(o);          //调用g(x)会在o上调用f()
g(2)
let p={x:10,g};           //作为这个对象的方法调用g()
p.g(2)                    //=>3:g仍然绑定到o,而非p
=>3                    

箭头函数从定义它们的环境中中继承this值,且这个值不能被bind()覆盖,因此如果前面代码中的函数f()是以非箭头函数定义的,则绑定不会起作用。不过,由于调用bind()最常见的目的是让非箭头函数变得像箭头函数,因此这个关于绑定箭头函数的限制在实践中通常不是问题。

事实上,除了把函数绑定到对象,bind()方法还会做其他事。比如,bind()也可以执行“部分应用”,即在第一个参数值传给bind()的参数也会随着this值一起被绑定。部分应用是函数式编程中的一个常用技术,有时候也被称为柯里化。下面是几个使用bind()方法实现部分应用的例子:

let sum=(x,y)=>x+y;
let succ=sum.bind(null,1);    //把第一个参数绑定为1
succ(2);
=>3
function f(y,z){
    return this.x+y+z;
}

let g=f.bind({x:1},2);       //绑定this和y
g(3)                         //this.x绑定到1,y绑定到2,z是3
=>6

bind()返回函数的name属性由单词"bound"和调用bind()的函数的name属性构成。

8.7.6 toStringO 方法

与所有JavaScript对象一样,函数也有toString()方法。ECMAScript规范要求这个方法返回一个符合函数声明语句的字符串.实践中,这个方法的多数(不是全部)实现都返回函数完整的源代码。内置函数返回的字符串中通常包含“[native code]”,表示函数体。

Function()构造函数

因为函数是对象,所以就有一个Function()构造函数可以用来创建新函数:

const f=new Function("x","y","return x*y");

这行代码创建了一个新函数,差不多相当于使用如下语法定义的函数:

const f=function(x,y){ return x*y };

Function()构造函数可以接收任意多个字符串参数,其中最后一个参数是函数的文本。这个函数文本中可以包含任意JavaScript语句,相互以分号分隔。传给这个构造函数的其他字符串都用于指定新函数的参数名。如果新函数没有参数,可以只给构造函数传一个字符串(也就是函数体)。

注意,Function()构造函数不接收任何指定新函数名字的参数。与函数字面量一样,Function()构造函数创建的也是匿名函数。

要理解Function()构造函数,需要理解一下几点。

  • Function()函数允许在运行是动态创建和编译JavaScript函数。
  • Function()构造函数每次被调用时都会解析函数体并创建一个新函数对象。如果在循环中或者频繁调用的函数中出现了对它的调用,可能会影响程序性能。相对而言,出现在循环中的嵌套函数和函数表达式不会每次都被重新编译。
  • 最后,也是关于Function()非常重要的一点,就是它创建的函数不适用词法作用域,而是始终编译为顶级函数一样。如以下例子所示:
let scope="global";
undefined
function constructFunction(){
    let scope="local";
    return new Function("return scope");                 //不会捕获局部作用域
}

//构造函数返回的函数不使用局部作用域
constructFunction()()
=>"global"

最后将Function()构造函数作为在自己私有作用域中定义新变量和函数的eval()的全局作用域版。我们自己写的代码中可能永远也用不到这个构造函数。

8.8 函数式编程

8.8.1 使用函数处理数组

假设有一个数值数组,我希望计算这些数值的平均值和标准差。如果使用非函数式风格的代码,可能会这样写:

let data=[1,1,3,5,5];          //这是数值数组

//平均值
let total=0;
for(let i=0;i<data.length;i++)
    total+=data[i];
=>15
let mean=total/data.length;
mean
=>3

//标准差
total=0
for(let i=0;i<data.length;i++){
    let deviation=data[i]-mean;
    total+=deviation*deviation;
}
=>16
let stddev=Math.sqrt(total/(data.length-1));
stddev
=>2

而使用数组方法map()和reduce()(参考7.8.1节),可以像下面这样以间接的函数式风格实现同样的计算:

//首先,定义两个简单的函数
const sum=(x,y)=>x+y;
const square=x=>x*x;

//然后,使用数组方法计算平均值和标准差
let date=[1,1,3,5,5];
let mean=data.reduce(sum)/data.length;
let deviations=data.map(x=>x-mean);
let stddv=Math.sqrt(deviations.map(square).reduce(sum))/(data.length-1);
stddv      //=>2

新版本的代码看起来与第一个版本差别很大,但仍然调用对象上的方法,因此还可以看出一些面向对象的痕迹。下面我们再来定义map()和reduce()方法的函数版:

const sum=(x,y)=>x+y;
const square=>x=>x*x;

let data=[1,1,3,5,5];
let mean=reduce(data,sum)/data.length;
let deviations=map(data,x=>x-mean);
let stddev=Math.sqrt(reduce(map(deviations,square),sum)/(data.length-1))
=>2

8.8.2 高阶函数

高阶函数就是操作函数的函数,它接收一个或多个函数并返回一个新函数。例如:

//这个高阶函数返回一个新函数
//新函数把参数传给f并传给f返回值的逻辑非
function not(f){
    return function(...args){       //返回一个新函数
        let result=f.apply(this,args);  //新函数调用f
        return !result;            //对结果求逻辑非
    };
}
const even=x=>x%2===0;            //确定不是偶数的函数
const odd=not(even);              //确定数值是不是奇数的新函数
[1,1,3,5,5].every(odd)           
=>true

8.8.3 函数的部分应用

函数f的bind()方法,返回一个新函数,这个新函数在指定的上下文中以指定的参数调用f。我们说它把这个函数绑定到了一个对象并部分应用了参数。bind()方法在左侧部分应用参数,即传给bind()的参数会放在传给原始函数的参数列表的开头。但是也有可能在右侧部分应用参数:

function patialLeft(f,...outerArgs){
    return function(...innerArgs){   //返回这个函数
        let args=[...outerArgs,...innerArgs];       //构造参数列表
        return f.apply(this,args);   //然后通过它调用f
    };
}
function patialRight(f,...outerArgs){
    return function(...innerArgs){   //返回这个函数
        let args=[...innerArgs,...outerArgs];       //构造参数列表
        return f.apply(this,args);   //然后通过它调用f
    };
}

//这个函数的参数列表作为一个模板。这个参数列表中的undefined值
//会被来自内部参数列表的值填充
function partial(f,...outerArgs){
    return function(...innerArgs){
        let args=[...outerArgs];     //外部参数模板的局部版本
        let innerIndex=0;         //下一个是哪个内部参数
        //循环遍历args,用内部参数填充undefined值
        for(let i=0;i<args.length;i++){
            if(args[i]===undefined)
                args[i]=innerArgs[innerIndex++];
        }
        //现在把剩余的内部参数加进来
        args.push(...innerArgs.slice(innerIndex));
        return f.apply(this,args);
    };
}


const f=function(x,y,z){ return x*(y-z) };

patialLeft(f,2)(3,4)
=>-2                       //绑定第一个参数:2*(3-4)
patialRight(f,2)(3,4)
=>6                        //绑定最后一个参数:3*(4-2)
partial(f,undefined,2)(3,4)
=>-6                       //绑定中间的参数:3*(2-4)

8.8.4 函数记忆

在8.4.1节,我们定义了一个缓存自己之前计算结果的阶乘函数。在函数式编程中这种缓存被称为函数记忆。下面的代码展示了高阶函数memoize()可以接收一个函数参数,然后返回这个函数的记忆版:

function memorize(f){
    const cache=new Map();  //cache保存在这个闭包中
    return function(...args){
        //创建参数的字符串版,以用作缓存键
        let key=args.length+args.join("+");
        if(cache.has(key)){
            return cache.get(key);
        }else{
            let result=f.apply(this,args);
            cache.set(key,result);
            return result;
        }
    };
}

这个memorize()函数创建了一个新对象作为缓存使用,并将这个对象赋值给一个局部变量,从而让它(在闭包中)成为被返回的函数的私有变量。返回的函数将其参数数组转换为字符串,并使用该字符串作为缓存对象的属性。如果缓存存在某个值,就直接返回该值;否则,就调用指定的函数计算这些参数的值,然后缓存这个值,最后返回这个值。下面是使用memorize()的例子:

//使用欧几里得算法返回两个整数得最大公约数
function memorize(f){
    const cache=new Map();  //cache保存在这个闭包中
    return function(...args){
        //创建参数的字符串版,以用作缓存键
        let key=args.length+args.join("+");
        if(cache.has(key)){
            return cache.get(key);
        }else{
            let result=f.apply(this,args);
            cache.set(key,result);
            return result;
        }
    };
}

function gcd(a,b){        //省略了对a和b的类型检查
    if(a<b){
        [a,b]=[b,a];      //用结构赋值交换变量
    }
    while(b!==0){         //这是求最大公约数的欧几里德算法
        [a,b]=[b,a%b];
    }
    return a;
}
const gcdmemo=memorize(gcd);
gcdmemo(85,187)
=>17

factorial=memorize(function(n){
    return (n<=1)? 1:n*factorial(n-1);
});
ƒ (...args){
        //创建参数的字符串版,以用作缓存键
        let key=args.length+args.join("+");
        if(cache.has(key)){
            return cache.get(key);
        }else{
            let result=f.apply(this,arg…
factorial(5)     //因为4、3、2和1缓存了值
=>120
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值