JavaScript函数之美~
Posted 张松任
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript函数之美~相关的知识,希望对你有一定的参考价值。
这篇文章,我将就以下几个方面来认识javascript中的函数。
- 函数为什么是对象,如何定义函数?
- 如何理解函数可以作为值被传递
- 函数的内部对象、方法以及属性
第一部分:函数为什么是对象,如何定义函数
JavaScript中最有意思的恐怕是函数了,因为和其他语言不同,在JavaScript中,每个函数都是Function类型的实例,而我们知道:Object是一个基础类型,其他所有类型都是从Object继承了基本的行为。也就是说Function也是从Object引用类型继承而来的,那么作为Function类型的实例,函数也就是对象这一点就不难理解了。
那么如何定义一个函数呢?一般来说,有四种种方式可以来定义函数。
第一:函数声明。这种语法和其他语言的形式一样,是我们最为常见的一种方式。如下:
1
2
3
|
function add(num){ return num+10; } |
第二:函数表达式。如下:
1
2
3
|
var add= function (num){ return num+10; }; |
我们可以注意到函数表达式的定义函数方法把函数看作了一个表达式,因此最后要以分号;结尾,并且在function关键字之后没有标识符作为函数名,看起来好像就是常规的变量赋值语句,即创建一个函数并将它赋值给变量add,这种情况下创建的函数叫做匿名函数(又称拉姆达函数)。那么怎么调用呢?实际上,通过add即可引用了。(实际上,这里的add是全局对象global在浏览器中表现为window对象的一个属性或方法)
第三:使用Function构造函数。如下:
1 | var add= new Function( "num" , "return num+10" ); |
Function构造函数可以接收任意多的参数,其中最后一个是函数体,前面所有的是函数的参数。这种方法是我们所不推荐的,因为它会导致解析两次代码(第一次是解析常规的ECMAScript代码,第二次是解析传入构造函数中的字符串),从而影响了性能。但是这种方法有利于我们理解:函数是对象,函数名是指针。
第四:使用命名函数表达式。如下:
1
2
3
4
5
6
7
8
9
10
|
var factorial=( function f(num){ if (num<=1){ return 1; } else { return num*f(num-1); } }); var anotherFactorial=factorial; factorial= null ; alert(anotherFactorial(5)); //120 |
特点是等式右边需要用圆括号括起来,并且与匿名函数不同的是,等式右边有函数名,在后面介绍arguments对象时我还会提到这种定义函数的方式。
由于我们不推荐第三种方法来创建函数,并且第四种方法在实际中用的很少。
那么前两种方法又有什么区别呢?
A 实际上,区别之一在于是否函数声明的方法会使得代码在开始执行之前,解析器就已经通过了函数声明提升读取并将函数添加到了执行环境中去,于是在代码执行前,JavaScript引擎会将它们放到源代码树的顶部,而函数表达式则不会。这里可能不好理解,看下面例子:
a
1
2
3
4
|
function say(){ console.log( "I like coding" ); } say(); |
此时在控制台输出了I like coding.
我们可以从下面的例子中看出来:
即执行到第一句的时候,实际上a已经被声明了(没有定义),且b也已经声明了。 再看下面一个例子:
console.log(a); // undefined b(); // "b" var a = 10; function b () { console.log("b"); }
a仅仅是声明没有定义,而b不仅仅声明了还定义了。
b
1
2
3
4
|
var say= function (){ console.log( "I like coding" ); } say(); |
同样,这时在控制台也输出了I like coding.
c
1
2
3
4
|
say(); function say(){ console.log( "I like coding" ); } |
这里我们将say()这个调用函数的语句放在最上面了,同样控制台也输出了I like coding.
d
1
2
3
4
|
say(); var say= function (){ console.log( "I like coding" ); }; |
然而,这里却出现了错误。让我们捕获以下错误。
1
2
3
4
5
6
7
8
|
try { say(); var say= function (){ console.log( "I like coding" ); }; } catch (err){ console.log( "错误是:" +err); } |
控制台提示:错误是:TypeError: say is not a function。以上代码之所以会在运行期间产生错误,是因为函数没有位于一个函数声明中,而是位于一个初始化的语句中,这样如果没有执行到该语句,那么变量sum就不会保存有对函数的引用。而使用函数声明,JavaScript引擎将函数声明提升到了最顶端,那么函数名sum就保存了对函数的引用。
B
区别之二在于下面这种情况:
第一种方式:声明函数
1
2
3
4
5
6
7
8
9
10
|
if (5>3){ function sayHi(){ alert( "Hi!" ); } } else { function sayHi(){ alert( "Yo!" ); } } sayHi(); |
即在满足条件时,会定义一个函数,在不满足条件时,会定义另外一个函数,这个看似没有问题,实际上,这在ECMAScript中属于无效语法,JavaScript引擎会尝试修正错误,将其转换为合理的状态。但问题时浏览器在不同浏览器的状态表现不一致,在chrome、firefox和opera中表现正常,但是在safari中它始终都会返回else下定义的函数,而对于IE,由于我电脑上没有装,暂时无法测试。
第二种方式:函数表达式
1
2
3
4
5
6
7
8
9
10
|
if (5>3){ sayHi= function (){ alert( "Hi!" ); } } else { sayHi= function (){ alert( "Yo!" ); } } sayHi(); |
将函数声明修改为函数表达式,这样在各个浏览器中都执行的很好。推荐!
为了更深刻的理解函数是对象,函数名是指针,我们可以以下面的例子讲解:
1
2
3
4
5
6
7
8
9
10
|
function add(num){ return num+10; } console.log(add(10)); //20 var addCopy=add; //这时我们把add这个指针赋值给addCopy,于是addCopy也指向了同一个函数对象 console.log(addCopy(10)); //20 sum= null ; //null 的一大作用就是用于保存空的对象,这时sum指向了一个空的对象 console.log(sum(10)); //Uncaught TypeError: sum is not a function(…) sum是一个空对象,因此会出现语法错误 console.log(addCopy(10)); //20 而addCopy指向了那个函数对象,便可以得到正确的答案 |
也正是因为函数是对象,其函数名(指针)可以有多个(指向的是同一个对象),因此也不难理解函数没有重载。
第二部分:如何理解函数可以作为值来传递。
一般,函数中的参数都是变量。而因为函数名是指针,也是变量,因此我们就可以把函数作为值来使用了。
如下:
1
2
3
4
5
6
7
|
function onefunc(antherfunc,argum){ return antherfunc(argum); } function antherfunc(str){ return str+ "I am js" ; } console.log(onefunc(antherfunc, "ha " )); //ha I am js |
除此之外,一个函数也可以作为另一个函数的结果返回。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
function createComparisonFunction(propName){ return function (object1,object2){ //这是一个比较函数,虽然有形参,但是不需要传递实参,只是为了便于下面调用 var value1=object1[propName]; var value2=object2[propName]; //这里使用方括号操作符的好处在于:当定义一个对象数组时,里面的对象是用对象字面量方法来创建的,于是属性名可以加引号,这时属性名即使不规范也不会影响结果,应当注意的是,这时,最后调用的时候还是需要使用方括号调用。。 if (value1<value2){ return -1; } else if (value1>value2){ return 1; } else { return 0; } }; //注意:虽然这里不用分号也可以运行,但是最好写上,这样才规范。 } var data=[{name: "zhuzhen" ,age:21},{name: "heting" ,age:18}]; //这里表示按照何种属性排序 data.sort(createComparisonFunction( "name" )); console.log(data[0].name); //heting data.sort(createComparisonFunction( "age" )); console.log(data[0].age); //18 |
第三部分:函数的属性和方法
我们可以先来总结一下函数一共有哪些属性和方法且函数有哪些对象。
函数的内部对象:this ,arguments(它还有一个callee属性和length属性)
函数的方法:继承自Object的有toString0(),valueOf(),toLocaleString()。函数自身添加的方法有call() apply() bind()
函数的属性:length prototype caller
A函数的内部对象:arguments 和 this
1.arguments对象
在javascript的函数中,函数是不介意传过来多少参数的。即最终传进来的参数个数不一定就是在声明时希望的接受的参数个数。这就是因为函数中存在一个类数组对象arguments,函数接收到的就是这个“数组”,我们可以通过这个“数组”获取传递给函数的每一个参数。说它是类数组对象,而不是数组对象,是因为它并不是引用类型Array的实例,只是我们可以使用方括号法来访问其中的每一个元素罢了。第三部分的开头我介绍到arguments有两个属性,一个是length一个是callee。
其中length属性返回的是函数接收到的实际参数个数。比如:
1
2
3
4
5
6
7
|
function number(){ console.log(arguments.length+ " " +arguments[0]); } number(12,45); //2 12 number( "hah" , "hei" , "you" ); //3 hah number(); //0 undefined |
从上述代码可以看出,虽然我们在声明函数时,并不希望接收参数,但是到了实际调用,我们可以传递任意多的参数。
且我们在长度后面也输出了传入的第一个参数,由于Number()并没有传入参数,所以它的第一项是undefined。
而callee属性也是函数内部对象arguments的一个属性,这个属性是一个指针,指向拥有这个arguments对象的函数。通过这个属性我们可以改写求阶乘的递归函数。首先,我们,我们看看最普通的递归函数。
1
2
3
4
5
6
7
8
|
function factorial(num){ if (num<=1){ return 1; } else { return num*factorial(num-1); } } console.log(factorial(3)); //6 |
这中递归函数很好理解。即我们使用factorial(3)进行调用时,这时立马进入了factorial的函数执行环境,于是立即创建了num这个局部变量(形式参数虽然没有用var,但它的确是函数内部的局部变量)并赋值为3,判断后返回3*factorial(2),因为我们认为return多少,这个函数最终就得到多少。接下来,由于出现了factorial(2),则再次进入了factorial执行环境,最终返回了3*2*factorial(1).由于出现了factorial(1),相当于又调用了一次函数,这时,再次进入函数执行环境最终得到3*2*1=6。这时跳出了局部执行环境进入全局执行环境并销毁了num这个局部变量。
但是,这里有一个问题,由于函数是对象,函数名是指针,如果我们把factorial指针赋值给另外一个指针比如anotherFactorial,并让这个factorial指针指向一个空对象,那么此时调用anotherFactorial会怎么样呢?见下面代码:
1
2
3
4
5
6
7
8
9
10
|
function factorial(num){ if (num<=1){ return 1; } else { return num*factorial(num-1); } } var anotherFactorial=factorial; factorial= null ; console.log(anotherFactorial(3)); //Uncaught TypeError: factorial is not a function(…) |
这是没有用的,因为内部的factorial依然存在,它紧紧地和函数耦合在了一起。但是,只要使用arguments的callee这个属性就可以很好的解决问题了。
1
2
3
4
5
6
7
8
9
10
|
function factorial(num){ if (num<=1){ return 1; } else { return num*arguments.callee(num-1); } } var anotherFactorial=factorial; factorial= null ; console.log(anotherFactorial(3)); //6 |
另外,在严格模式下,就不能通过脚本访问arguments.callee了,访问这个属性会导致错误。如下:
1
2
3
4
5
6
7
8
9
10
11
|
"use strict" ; function factorial(num){ if (num<=1){ return 1; } else { return num*arguments.callee(num-1); } } var anotherFactorial=factorial; factorial= null ; console.log(anotherFactorial(3)); |
此时,控制台提示错误: Uncaught TypeError: \'caller\', \'callee\', and \'arguments\' properties may not be accessed on strict mode functions or the arguments objects for calls to them(…)
这时,我们可以使用另外一种方式解决这个问题,即命名函数表达式:
1
2
3
4
5
6
7
8
9
10
|
var factorial=( function f(num){ if (num<=1){ return 1; } else { return num*f(num-1); } }); var anotherFactorial=factorial; factorial= null ; alert(anotherFactorial(5)); //120 |
我们发现,在函数表达式的右边依然有一个f函数名,只是等式右边用了一个()“圆括号”包了起来,我们称之为命名函数表达式。最终也很好地解决了问题。
最后,arguments对象可以弥补函数不能重载的缺陷。
1
2
3
4
5
6
7
8
9
10
11
12
|
function onefunc(){ if (arguments.length==1){ console.log(arguments[0]+10); } else if (arguments.length==2){ console.log(arguments[0]+arguments[1]); } else { console.log( "please input one or two numbers" ); } } onefunc(45); //55 onefunc(12,37); //49 onefunc(); //please input one or two numbers |
2.this对象
在 JavaScript设计模式 中我又谈到了this,可以参考。
this对象我们一般又称为上下文对象,这个对象是在函数运行是自动生成的一个内部对象,且由于它是属于函数的,故它只能在函数中使用(在函数中出现为this. 如果在函数外最多出现this,后面没有.)。我们可以分为四种情况来做讲解。理解:一般,this在在哪个环境下被调用,this就指向哪里。
第一种:纯粹的函数调用。
这时函数的最通常的用法,属于全局属性调用,因此this就代表window。可以看以下例子:
1
2
3
4
5
6
7
|
var color= "red" ; var color= "pink" ; var o={color: "blue" }; function sayColor(){ console.log( this .color); } sayColor(); // pink |
这里最终得到了pink,是因为这时是纯粹的函数调用,且在全局环境中调用,最终指向的是window。即我们将得到window.color,而这时候得到了pink,这时因为pink在后,覆盖了red,color最终为pink 。同理,如果pink在前,red在后,最终将得到red而非pink。
提出疑问:this可能指向的是sayColor函数吗?这里是因为sayColor中没有color,所以沿着作用域链向上一直搜索到window对象呢?
请看下面这个例子:
1
2
3
4
5
6
7
|
var color= "red" ; var o={color: "blue" }; function sayColor(){ var color= "black" ; console.log( this .color); } sayColor(); //red |
这时,又得到了red,而没有得到函数中的black,说明this确实指向的是window对象,而不是sayColor函数,即并不是沿着作用域链向上搜寻的结果。
那如果我们把sayColor()函数中的color定义为全局变量呢?如下:
1
2
3
4
5
6
7
8
|
color= "red" ; var o={color: "blue" }; color= "pink" ; function sayColor(){ color= "black" ; alert( this .color); } sayColor(); //black |
这里为什么没有得到pink而是得到sayColor中的black了呢? 这说明,sayColor函数一经调用,这时即进入了sayColor()函数的执行环境,于是函数内部的变量开始创建,由于函数内部的color没有用var声明,因此函数知道,这时创建了一个全局变量,于是函数想:你心不在我这我干嘛要你,并随即将该全局变量扔到了全局环境中,但是总得有个先来后到吧,于是color="black"被扔到了color="pink"的后面。或者说就是按照先来后到的过去,无论如何,它最后会覆盖前面定义的color。
第二种:作为对象方法的调用。
函数也可以作为某个对象的方法调用(这句话需细细理解),这时this就指向了这个上级对象。如下面的例子:
1
2
3
4
5
6
7
8
9
|
color= "red" ; var o={color: "blue" }; function sayColor(){ var color= "black" ; alert( this .color); } sayColor(); //red o.sayColor=sayColor; o.sayColor(); //blue |
其中的o.sayColor=sayColor;这句意思是将函数作为对象的方法。所以this指向的是这个上级对象o,最终得到blue就不难理解了。值得注意的是:函数的名字仅仅是包含指针的变量而已,因此,即使在不同的环境中执行(引用),全局的sayColor()函数和o.sayColor()函数仍指向的是同一个函数。
第三种:作为构造函数
所谓构造函数,即通过这个函数生成一个新的对象,此时,this就指向了这个新的对象。如下面的例子所示:
1
2
3
4
5
|
function Num(x){ this .x=x; }; var o= new Num(2); console.log(o.x); //2 |
我们通过Num这个构造函数并初始化了对象o,于是this就指向了这个对象o,也就是说this.x实际上就成了o.x,我们在初始化时传入了2,实际上就是o.x=2。
第四种情况:函数的apply()方法、函数的call()方法以及函数的bind()方法。
这些方法我将在下面进行讲解。
举例:我们见过比较经典的考察this的题目如下:
var name = "htt"; var obj = { name: "zzw", sayName: function () { console.log(this.name); } }; obj.sayName(); // zzw var sayAnothername = obj.sayName; sayAnothername(); // htt
其实不难理解,第一次调用是对象调用的,第二次调用是在全局环境下调用的,this与在哪里调用是密切相关的,我们还可以看下面的方法更为直观,这对于我们在实际问题中解决问题会有很大的帮助:
可以看到这里处在Local作用域(即局部作用域)(根据Scope可知),现在所处的执行环境是sayName,this为Object,根据Call Stack中的栈底我们知道刚刚在全局匿名环境下是在16行暂停的,所以刚刚是从第16行代码进入的。
可以看到,我们现在是在第13行,且Scope中Local下的this指的是Window,由于在Call Stack的栈底是全局环境,所以我们是从18行暂停并进入sayName的执行环境的,所以刚刚是执行的sayAnothername()。
总结: 利用这种方式判断this会十分的方便。
B函数的方法
函数作为Function引用类型的实例,那么它一定继承了一些Object的方法,但又为了保证它的独特性,它一定还拥有非继承的方法。
第一:继承的方法
函数从Object引用类型继承得到的方法有valueOf()方法、toString()方法以及toLocaleString()方法。它们最终都会返回函数的代码:
1
2
3
4
5
6
|
function add(){ return 20; } console.log(add.toString()); console.log(add.toLocaleString()); console.log(add.valueOf()); |
不出意外地,它们最终都返回了函数本身,即:
1
2
3
|
function add(){ return 20; } |
第二:非继承的方法。
非继承的方法包括apply(),call()以及bind(),这些方法也正是刚刚我在讲this对象的第四种情况时省略的方法,这里我将讲解。
apply()方法和call()方法两者的作用都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。同时,两者也都只接收两个参数,第一个参数都是运行函数的作用域,而第二个参数略有不同,apply()方法的第二个参数要接收一个数组(Array的实例)或者是一个arguments对象,而call()方法的第二个参数为列举出所有要传递给函数的参数。举例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function sum(num1,num2){ return num1+num2; } function callSum(num1,num2){ return sum.call( this ,num1,num2); //call()方法必须全部列举出来 } function callSum1(num1,num2){ return sum.apply( this ,arguments); //apply()方法可以传递一个arguments对象 }<br> function callSum3(){<br> return sum.apply( this ,arguments); //注意:这里即使没有形参也可以<br> } function callSum2(num1,num2){ return sum.apply( this ,[num1,num2]); //apply()方法还可以传递数组(Array的实例) } console.log(callSum(10,10)); //20 console.log(callSum1(10,10)); //20 console.log(callSum2(10,10)); //20<br> console.log(callSum3(10,10));//20 我们没有给callSum3传递形参也可以 |
上面的例子并没有改变this的作用域,知识介绍了第二个参数的传递方法。而apply()和call()方法的真正作用在于扩充函数来以运行的作用域。
1
2
3
4
5
6
7
8
9
10
11
|
window.color= "red" ; var o={color: "blue" }; function sayColor(){ console.log( this .color); } sayColor(); //red sayColor.call( this ); //red sayColor.call(window); //red sayColor.call(o); //blue |
第一个是隐式地在全局作用域中调用函数,而第二第三个是显式地在全局作用域中调用函数,最用一个是在对象o中调用函数,故this指向o,最后的到的是o.color。总结就是,call和apply的第一个参数是什么,就是在哪里调用函数,亦即this指向哪里。关于call和apply方法的更多应用可以看《JavaScript之继承(原型链)》的第二部分。
最后一个方法即为bind(),它只能接收一个参数,表示调用函数的作用域,也就是说它和前两种方法的效果一致。只是,它必须先赋值给一个变量,再调用这个变量。
1
2
3
4
5
6
7
8
|
window.color= "red" ; var o={color: "blue" }; function sayColor(){ console.log( this .color); } sayColor(); //red var s=sayColor.bind(o); s(); //blue |
C.函数的属性
我们说过,函数一般具有三种属性:length,caller,name,prototype
第一:length属性
这个属性很简单,由函数调用,返回函数希望接受的参数的个数。如下所示:
1
2
3
4
5
6
7
8
|
function a(num1,num2){ return num1+num2; } function b(num1,num2,num3){ return num1+num2+num3; } console.log(a.length); //2 console.log(b.length); //3 |
注意区分:函数的length属性是函数希望接受的参数的个数,而函数的内部对象arguments的length属性是真正传入函数的参数的个数,两者是不一样的。
第二:caller属性
这个属性会返回调用当前函数的函数。如下所示:
1
2
3
4
5
6
7
|
function a(){ b(); } function b(){ console.log(b.caller); } a(); |
在调用a()之后,我们可以在控制台中看到:
1
2
3
|
function a(){ b(); } |
注意区分:caller返回的是调用当前函数的函数,而callee返回的是arguments对象所从属的函数。
第三:name属性
name属性非常简单,即返回一个函数的函数名。观察如下代码:
A.声明函数
1
2
3
4
5
|
function add(num){ return 20+num; } console.log(add(10)); alert(add.name); |
声明函数的方式在各个主流浏览器中都支持name属性
B.函数表达式
1
2
3
4
5
|
var add= function (num){ return 20+num; } console.log(add(10)); alert(add.name); |
但是,函数表达式的方式name属性在火狐浏览器和safari浏览器中是不支持的。
第四:prototype属性
这个属性非常重要,我在《深入理解JavaScript中创建对象模式的演变(原型)》这篇博文中做了详细的介绍。
点击这里回到博文开头
如果把你的人生拍成一部电影,你能吸引多少观众?
以上是关于JavaScript函数之美~的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript 数据结构与算法之美 - 你可能真的不懂递归