当前位置: 首页 > 科技观察

《JavaScript 闯关记》的函数

时间:2023-03-12 03:38:19 科技观察

函数是一段只定义一次但可以执行或调用任意次的代码。在JavaScript中,函数是对象,程序可以随意操作它们。例如,您可以将函数分配给变量,将它们作为参数传递给其他函数,设置它们的属性,甚至调用它们的方法。如果函数作为对象的属性挂载在对象上,则称为对象的方法。如果函数是嵌套在其他函数中定义的,那么它们可以访问定义它们的范围内的任何变量。函数是在JavaScript中定义的。函数实际上是对象,每个函数都是Function构造函数的一个实例。因此,函数名实际上是指向函数对象的指针,不会绑定到函数上。函数通常有以下三种定义方式。例如://写法1:函数声明(推荐写法)functionsum(num1,num2){returnnum1+num2;}//写法2:函数表达式(推荐写法)varsum=function(num1,num2){returnnum1+num2;};//写法三:构造函数(不推荐)varsum=newFunction("num1","num2","re??turnnum1+num2");由于函数名只是指向函数的指针,所以函数名和包含的对象指针的其他变量没有区别。换句话说,一个函数可能有多个名称。例如:functionsum(num1,num2){returnnum1+num2;}console.log(sum(10,10));//20varanotherSum=sum;console.log(anotherSum(10,10));//20sum=null;console.log(anotherSum(10,10));//20没有重载把函数名看成指针也有助于理解为什么JavaScript中没有函数重载的概念。functionaddSomeNumber(num){returnnum+100;}functionaddSomeNumber(num){returnnum+200;}varresult=addSomeNumber(100);//300显然,在这个例子中,声明了两个同名的函数,结果是后面的函数覆盖前面的函数。上面的代码其实和下面的代码没什么区别。varaddSomeNumber=function(num){returnnum+100;};addSomeNumber=function(num){returnnum+200;};varresult=addSomeNumber(100);//重写代码后300容易理解,创建第二个函数后,它实际上覆盖了引用第一个函数的变量addSomeNumber。函数声明和函数表达式解析器在将数据加载到执行环境时不会平等对待“函数声明”和“函数表达式”。解析器将首先读取函数声明并使其可用(可访问),然后再执行任何代码;至于函数表达式,它必须等到解析器执行到它所在的代码行,才真正被解释执行。例如:console.log(sum(10,10));//20functionsum(num1,num2){returnnum1+num2;}以上代码可以正常运行。因为在代码开始执行之前,解析器已经经历了一个称为函数声明提升的过程,该过程将函数声明读取并添加到执行环境中。在评估代码时,JavaScript引擎会在第一遍声明函数并将它们放在源代码树的顶部。因此,即使声明函数的代码在调用它的代码之后,JavaScript引擎也可以将函数声明提升到顶部。把上面的“函数声明”改成等价的“函数表达式”会导致执行时出错。例如:console.log(sum(10,10));//UncaughtTypeError:sumisnotafunctionvarsum=function(num1,num2){returnnum1+num2;};除了以上区别外,“函数声明”和“函数表达式”的语法是等价的。作为值的函数因为JavaScript中的函数名本身就是变量,所以函数也可以作为值。也就是说,一个函数不仅可以像参数一样传递给另一个函数,还可以将一个函数作为另一个函数的结果返回。看看下面的函数。functioncallSomeFunction(someFunction,someArgument){returnsomeFunction(someArgument);}这个函数接受两个参数。第一个参数应该是一个函数,第二个参数应该是一个要传递给函数的值。然后,您可以像下面的示例一样传递该函数。functionadd10(num){returnnum+10;}varresult1=callSomeFunction(add10,10);console.log(result1);//20functiongetGreeting(name){return"Hello,"+name;}varresult2=callSomeFunction(getGreeting,"Nicholas");console.log(result2);//"你好,Nicholas"这里的callSomeFunction()函数是通用的,即无论第一个参数传入什么函数,都会返回并执行第一个之后的结果参数。要在不执行函数的情况下访问函数指针,必须删除函数名后的一对括号。所以在上面的例子中,传递给callSomeFunction()的是add10和getGreeting,而不是执行它们的结果。当然,也可以从另一个函数返回一个函数,这是一个非常有用的技术。例如,假设我们有一个对象数组,我们想根据某个对象属性对数组进行排序。传递给数组sort()方法的比较函数接收两个参数,即要比较的值。但是,我们需要一种方法来指示按哪个属性排序。为了解决这个问题,你可以定义一个接收属性名的函数,然后根据属性名创建一个比较函数。下面是这个函数的定义。functioncreateComparisonFunction(propertyName){returnfunction(object1,object2){varvalue1=object1[propertyName];varvalue2=object2[propertyName];if(value1value2){return1;}else{return0;}};}这个函数定义看起来有点复杂,其实无非就是在一个函数中嵌套另一个函数,在内部函数前面加一个return运算符。内部函数接收到propertyName参数后,使用方括号表示法获取给定属性的值。一旦获得了想要的属性值,定义比较函数就非常简单了。上面的函数可以像下面的例子一样使用。vardata=[{name:"Zachary",age:28},{name:"Nicholas",age:29}];data.sort(createComparisonFunction("name"));console.log(data[0].name);//Nicholasdata.sort(createComparisonFunction("age"));console.log(data[0].name);//Zachary在这里,我们创建了一个包含两个对象的数组数据。其中,每个对象都包含一个name属性和一个age属性。默认情况下,sort()方法调用每个对象的toString()方法来确定它们的顺序;但结果往往不符合人类的思维习惯。因此,我们调用createComparisonFunction("name")方法来创建一个比较函数,以根据每个对象的name属性的值对其进行排序。结果中最上面的项目是名称为“Nicholas”且年龄为29的对象。然后我们再次使用createComparisonFunction("age")返回的比较函数,这次按对象的age属性对对象进行排序。结果是name值为“Zachary”,age值为28的对象排在第一位。函数的形参和实参都在函数内部,有两个特殊的对象:arguments和this。其中,arguments是一个类数组对象,包含了所有传递给函数的参数。虽然arguments的主要目的是保存函数参数,但这个对象还有一个名为callee的属性,它是指向拥有arguments对象的函数的指针。考虑下面非常经典的阶乘函数。functionfactorial(num){if(num<=1){return1;}else{returnnum*factorial(num-1)}}阶乘函数的定义一般采用递归算法,如上代码所示,在function,而且名字以后不会变,这个定义是没有问题的。但问题是这个函数的执行是和函数名factorial紧密耦合的。为了消除这种紧耦合,arguments.callee可以像下面这样使用。functionfactorial(num){if(num<=1){return1;}else{returnnum*arguments.callee(num-1)}}在重写的factorial()函数的函数体中,不再引用函数名factorial.这样,无论使用什么名称来引用函数,都可以保证递归调用正常完成。例如:vartrueFactorial=factorial;factorial=function(){return0;};console.log(trueFactorial(5));//120console.log(factorial(5));//0这里,变量trueFactorial取值的factorial值实际上是一个指向存储在另一个位置的函数的指针。然后,我们分配一个简单地将0返回给阶乘变量的函数。如果arguments.callee不像原来的factorial()那样使用,调用trueFactorial()将返回0。但是,将函数体中的代码和函数名解耦后,trueFactorial()仍然可以正常计算阶乘;至于factorial(),它现在只是一个返回0的函数。函数内部的另一个特殊对象是this,它在Java和C#中的行为大致如此。也就是说,this指的是函数执行的环境对象(在网页全局范围内调用函数时,this对象指的是window)。看看下面的例子。window.color="red";varo={color:"blue"};functionsayColor(){console.log(this.color);}sayColor();//"red"o.sayColor=sayColor;o.sayColor();//"blue"上面的函数sayColor()定义在全局范围内,它引用了这个对象。由于this的值直到调用函数时才确定,因此在代码执行期间this可能会引用不同的对象。当在全局范围内调用sayColor()时,this指的是全局对象window;换句话说,评估this.color转化为评估window.color,返回“红色”。而当这个函数被赋值给对象o并调用o.sayColor()时,this引用了对象o,所以评估this.color被转换为评估o.color,结果是“blue”。请记住,函数的名称只是一个包含指针的变量。因此,即使在不同的环境中执行,全局的sayColor()函数和o.sayColor()指向同一个函数。ECMAScript5还规范化了函数对象的另一个属性调用者。此属性保存“调用当前函数的函数的引用”。如果在全局范围内调用当前函数,则其值为空。例如:functionouter(){inner();}functioninner(){console.log(arguments.callee.caller);}outer();上面的代码将使outer()函数的源代码显示在警告框中。因为outer()调用inter(),arguments.callee.caller指向outer()。在严格模式下,访问arguments.callee属性,或为函数的caller属性赋值,将导致错误。函数的属性和方法JavaScript中的函数是对象,所以函数也有属性和方法。每个函数包含两个属性:长度和原型。其中,length属性表示函数期望接收的命名参数的个数,如下例所示。functionsayName(name){console.log(name);}functionsum(num1,num2){returnnum1+num2;}functionsayHi(){console.log("hi");}console.log(sayName.length);//1console.log(sum.length);//2console.log(sayHi.length);//0对于JavaScript中的引用类型,原型才是真正保存它们所有实例方法的地方。换句话说,toString()和valueOf()等方法实际上存储在原型名称下,但通过相应对象的实例访问。在创建自定义引用类型和实现继承时,原型属性的作用极其重要。在ECMAScript5中,原型属性不可枚举,因此无法使用for-in发现它。每个函数包含两个非继承方法:apply()和call()。这两个方法的目的是在特定范围内调用函数,实际上相当于在函数体中设置了this对象的值。首先,apply()方法接收两个参数:一个是运行函数的范围,另一个是参数数组。其中,第二个参数可以是Array的实例,也可以是arguments对象。例如:functionsum(num1,num2){returnnum1+num2;}functioncallSum1(num1,num2){returnsum.apply(this,arguments);//传入参数对象}functioncallSum2(num1,num2){returnsum.apply(this,[num1,num2]);//传入数组}console.log(callSum1(10,10));//20console.log(callSum2(10,10));//20上例中,callSum1()时执行sum()函数,this(因为是在全局范围内调用,所以传入的是window对象)和arguments对象。而callSum2也调用了sum()函数,只不过传入的是this和一个参数数组。两个函数都正常执行并返回正确的结果。call()方法与apply()方法具有相同的功能,唯一不同的是接收参数的方式。对于call()方法来说,第一个参数是this的值没变,变的是其余参数直接传给函数。换句话说,当使用call()方法时,传递给函数的参数必须一一枚举,如下例所示。functionsum(num1,num2){returnnum1+num2;}functioncallSum(num1,num2){returnsum.call(this,num1,num2);}console.log(callSum(10,10));//20正在使用call()方法,callSum()必须显式传递每个参数。结果与使用apply()没有什么不同。至于是用apply()还是call(),就看你给函数传递参数最方便的方式了。如果打算直接传入arguments对象,或者included函数先接收一个数组,使用apply()肯定更方便;否则,call()可能更合适。(在不向函数传递任何参数的情况下,使用哪种方法并不重要。)事实上,传递参数并不是真正的apply()和call()有用的地方;它们的真正力量在于能够扩展函数运行的范围。让我们看一个例子。window.color="red";varo={color:"blue"};functionsayColor(){console.log(this.color);}sayColor();//redsayColor.call(this);//redsayColor.call(window);//redsayColor.call(o);//blue这个例子修改自前面解释这个对象的例子。这一次,sayColor()也被定义为一个全局函数,当在全局范围内调用时,它确实显示“红色”,因为this.color的评估转换为window.color的评估。而sayColor.call(this)和sayColor.call(window)是在全局范围内显式调用函数的两种方式,结果当然会显示“红色”。但是运行sayColor.call(o)时,函数的执行环境不同了,因为此时函数体中的this对象指向o,所以结果显示为“blue”。使用call()或apply()来扩大作用域的最大好处是对象不需要和方法有任何耦合关系。在前面例子的第一个版本中,我们首先将sayColor()函数放在对象o中,然后通过o调用它;在这里重写的例子中,前面的冗余加强了。Level//挑战1,组合任意数量的字符串varconcat=function(){//要实现的方法体}console.log(concat('st','on','e'));//stone//挑战2,在指定位置输出斐波那契数列varfioacciSequece=function(count){//要实现的方法体}console.log(fioacciSequece(12));//0,1,1,2,3,5,8,13,21,34,55,89//挑战3,三维数组或n维数组去重,使用arguments重写vararr=[2,3,4,[2,3,[2,3,4,2],5],3,5,[2,3,[2,3,4,2],2],4,3,6,2];varunique=function(arr){//方法执行Body}console.log(unique(arr));//[2,3,4,5,6]关注微信公众号“勉哥舍”回复“解答”获取详细解释的水平。按照https://github.com/stone0090/javascript-lessons获取最新更新。