细说javascript(二)—— 函数基础

函数,对于js来说,应该是最重要的部分之一了。它本身也是一个对象。本文将介绍函数的基础部分相关知识。

(ES6函数新特性请戳阮老师的链接http://es6.ruanyifeng.com/#docs/function,本文不涉及)

1.定义函数

首先要明确的一点是,函数也是一个对象,所以函数名可以看做一个指向这个对象的指针。js中的函数定义是不需要定义返回值的。定义一个函数有如下三种方式。它们各有不同。

1.1 函数声明

语法:funciton funcName(){  //函数代码  } ;如下示例:


1.2 函数表达式

语法:var functionName = function(){  //函数代码  }

这里是使用一个变量指向一个匿名函数,调用的时候只要只用这个变量加上()就可以了。如下示例:


1.3 new Function()

语法:var funcName = new Function(参数,....,函数体)

这种函数定义方式可能你没见过,这的确很少用,但是也是一种合理的定义函数的方式。它接收若干个参数,其中,最后一个参数是函数体的语句,前面的都是函数接收的参数。如下例所示,该函数接收一个msg参数,函数体的内容是在控制台输出msg:


1.4 三种方式的比较

先来思考这个例子:

            

我们使用使用函数声明的方式定义了一个sayHello(),利用函数表达式的方式定义了一个sayHi(),并且都在函数定义之前调用了,但是呈现的结果却不同,前者正常执行,后者却报错了,这是为什么呢?

其实,在js中有一个函数声明提升的过程,在执行流进入一个执行环境之前,解释器会读取该执行环境中的函数声明并且将它放到代码的执行环境顶部。所以即使在函数声明之前调用该函数,也可以正常执行。而且用函数表达式方式定义的函数就没有这种“特权”了,它只能乖乖等到解释器执行到它所在的代码行时才会执行,所以当你sayHi的时候,根本找不到这个方法,所以报错了,并且后面的代码也不会执行了。

这种机制,在任意的执行环境中都是存在的,如下在一个匿名函数的执行环境中:


再看看看为什么第三种方式很少用。

我们看看第三种定义函数的语法

var fn=new Function('num1','num2','return num1+num2')
这种方式的缺点在于,它会引起 解释器 的两次解析,第一次解析是像常规代码一样,创建一个对象,给变量赋值,但是这是一个构造函数,所以势必会对构造函数的参数做第二次解析,以形成一个可以调用的function。

2. 理解函数

2.1 参数

我们知道,js的函数的参数是很有趣的,传入的参数无所谓多少,也无所谓类型。什么意思呢?看下面的例子:

我们定义了一个add函数,表面上看上去像是两个数字的求和,但是实际上你传递多少个参数都是可以的,而且传递什么参数也都是可以的。它不像java等强语言那样做编译检查,如果你函数内部的操作不区分数据类型的话,那整个调用函数的过程都是正常的。所以我们平时在写代码的时候,为了保证程序的健壮性,对于函数的参数做类型检查是很有必要的。

2.2 返回值

每个函数都有返回值。你可以在函数内显式的用return语句来定义,也可以省略return语句表示返回undefined。return之后的语句都不会执行。

2.3 重载

先来了解两个概念:

函数签名:函数的名称及其参数列表组合在一起,就定义了一个唯一的特性,称为函数签名。它是用法区分函数的凭证。(不包括返回类型)

重载:函数名相同,参数列表不同(个数,类型等)。(与返回值无关)

好了。理解这两个概念,我们就好理解js为什么没有重载这一说法了。

首先,js的参数个数和类型都是不固定的,所以无法确定参数列表究竟相不相同,其次也是最重要的是,js函数实际上是一个对象,我们不管用上述哪种方式定义了一个函数,都会创建一个指向该对象的指针,再次定义一个不同参数列表的参数,实际上只会改变该变量的指针指向,相当于覆盖了原变量的值(指针地址)。看下例:

我们定义了两个fn函数,一个不接受参数,直接输出hello,一个接收一个name,输出'hi:’+name,接着我们调用函数fn()可以看出执行的第二个函数,第一个函数已经没有任何指针指向它了。可以参考下图做个理解:


2.4 函数作为值的使用

函数可以作为普通的js值来使用,比如用作函数的参数:

上述方法定义个一个doSomethingAfter2s的方法用于2秒后执行传进来的参数。这种用法经常用做回调函数(callback function)

另外,也可以将函数作为函数的返回值。比如我们递归调用自身的时候:

这是一个经典的阶乘案例。我们使用arguments.callee来调用自身可以有效的将函数体和函数名解耦。这个函数在入参大于1的时候返回自身,这样递归调用最终获取值。一定要注意的是,这里的return不能省略。

2.5 函数内部属性  

  • arguments

           我们上面了解到,函数的入参是很随意的,无论怎么传内部都是可以正确接收到的。这是因为参数在函数内部是用数组表示的。函数接收的始终是这个数组,不是很关心你数组里有什么。函数体内可以通过arguments来访问这个数组。再次强调,arguments不是Array的实例。

                                        

上例中的函数定义没有参数,但是调用的时候却传了两个参数,函数体内使用arguments来获取传入的参数。可以看到,函数正常的执行了。

关于arguments,还有一点需要了解,它的值永远与命名参数保持一致,比如在函数内部修改了arguments[0]的值,对应的参数的值也会得到修改(仅仅是值同步而已,并不代表两者访问的是同一内存空间),但是修改命名参数的值不会影响到arguments的值。

  • this
这里只要了解,this代表的是该函数的执行环境对象。

2.6 函数的属性和方法

  • 属性

函数有length属性,表示函数希望接收到的参数个数,也就是函数定义时的参数的个数:

                              

函数还有name属性,表示函数名称:

函数还有prototype属性,将在下下一章讨论面向对象的时候详细介绍。

  • 方法

函数有两个特殊的方法,call和apply。这两方法彻底解耦了对象与方法。

我们平时在使用对象调用方法时,总会将方法作为对象的实例,例如:

这种方式将对象与方法耦合到一起,不好。下面我们来看看call和apply是如何解耦这种关系的。

apply():接收两个参数,第一个是函数执行的作用域,第二个是参数数组

call():与apply类似,唯一不同的是接收参数时要一一列出这些参数。

如下例:


我们可以看到,call和apply都正确的执行了。但是怎么用这两个函数实现对象与方法的解耦呢?

是不是很简单!!这种方式才真正体现了apply和call的能力:扩充函数的作用域

3.思考题

1.函数返回值问题

思考下面的代码会返回什么:

var tag=1;
function fun1(){
	tag++;
	if(tag<5){
		fun1()
	}else{
		return tag
	}
}
乍一看很简单呀,最后会返回tag的值,经过几轮自增以后,会返回5,但是真的这样吗?不会有什么蹊跷吗?实践是检验真理的唯一标准,我们来执行一下:

你也许会惊讶,undefined是从哪里冒出来的??我这方法不是一直递归吗?应该对tag有操作啊?好歹也返回个数字啊!!!

的确,你是对tag进行操作了,tag的值也确实变成5了。但是,这真的跟函数的返回值有关系吗?换句话说,if条件里面的fun1() 真的是最外层函数fun1()的返回值吗?

答案是绝对不是!上面说过了,函数不谢return语句,是默认返回undefined的。而这里一直调用fun1(),注意,只是调用,并没有作为值来返回。有点抽象吗?其实很简单,转换下思路就好了,看下改编的例子:

我不递归调用自身了,我调用一个肯定能返回数的一个函数test,结果还是一样的。有没有点头绪了?内部调用的函数,只要是没有作为值被外层函数返回的,都影响不了外层函数的返回值。

可以再看一个改编的例子:

OK,你应该已经焕然大悟了。所以要想让原函数正确的返回预期值(5)的话,只要在fun1()前面加return把调用函数后获取到的返回值作为最外层函数的返回值返回就可以了。如图:

2.函数声明提升问题

再来看一个例子呗:

这比较简单,应该是输出两个“你好不好”。以为函数声明提升,会将所有函数按声明顺序放到代码的最顶部。本例中很显然下面的函数覆盖了上面的函数。然后就是两个sayHi了,都是执行的sayHi最后一次声明的函数。

3.函数参数传递问题

关于这个问题的讨论,可以看下这个经典的网址:https://stackoverflow.com/questions/518000/is-javascript-a-pass-by-reference-or-pass-by-value-language

上面有一个很好的例子,我们可以看看:

function changeStuff(a, b, c)
{
  a = a * 10;
  b.item = "changed";
  c = {item: "changed"};
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);//  10
console.log(obj1.item); // "changed"    
console.log(obj2.item); // "unchanged"

如果是按引用传递的话,那么num的值不可能是10,应该是100;如果是按值传递的话,那么obj1在函数内的修改,不会影响到外面的变化,obj1.item应该是unchanged。

事实上,的确是按值传递的,但是传递的值,本身就是一个引用,技术上来说,这是按共享传递。当我们改变值本身的时候,不会对该值造成改变;但是修改值的内部属性的时候,会导致值的变化。

可以这么理解:简单数据类型的话,你函数内部再怎么操作,都不会改变外面的值;对象类型的话,你可以修改对象内部属性,但是你修改不了对象本身。

可以看下图理解一下(注意:函数的执行环境与全局执行环境不一样,所以参数的值拷贝与全局对象不应该在同一个栈里面,这里为了演示仅做简化处理)



其实js高程里面说的按值传递与stackoverflow上call-by-sharing的说法的并无本质差别,两者都是说明对象参数的传递实际上是一个对象的地址。



以上内容如有错误,还请及时指正,不胜感激!





posted @ 2018-04-14 00:33  JerryYJ  阅读(160)  评论(0编辑  收藏  举报