函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。
1.概述
1.1函数的声明
JavaScript有三种声明函数的方法:
- function命令:function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里。函数声明(Function Declaration)
- 函数表达式:除了用function命令声明函数,还可以采用变量赋值的写法,这种写法将一个匿名函数赋值给变量。此时这个匿名函数又被称为函数表达式(Funtion Expression),因为赋值语句的等号右侧只能放表达式。若函数表达式中加入函数名,则只在内部可用,指代函数表达式本身,其他地方不可用。此写法有两个用处:可以在函数体内部调用自身;方便除错。
- Function构造函数:这种声明函数的方式非常不直观,几乎无人使用。
1.2函数的重复声明
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。而且,由于函数名的提升,前一次声明在任何时候都是无效的。
1.3圆括号运算符,return语句和递归
- 调用函数时,要使用圆括号运算符,圆括号之中,可以加入函数的参数。
- 函数体内部的return语句,表示返回。JavaScript引擎遇到return语句,就直接返回return后面那个表达式的值,即使后面还有语句也不会执行。return语句所带的那个表达式就是函数的返回值。return语句不是必须的,若没有则函数不返回任何值或返回undefined。
- 函数可以调用自身,这就是递归(recursion)。计算斐波那契数列的代码。
1.4第一等公民
JavaScript语言将函数看作一种值,与其他值(数值、字符串、布尔值等)地位相同。凡是可以使用值得地方,都可以使用函数:
- 把函数赋值给变量和对象的属性
- 把函数当作参数传入其他函数(高级函数/回调函数)
- 把函数当作其他函数的结果返回 函数只是一个可以执行的值,此外并无特殊之处。由于函数与其他数据类型地位平等,所以在JavaScript语言中又称函数为第一等公民。 (函数不能用a命名?)
1.5函数名的提升
JavaScript引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。但是,如果采用赋值语句定义函数(函数表达式),先调用再定义,JavaScript就会报错。
- 如果采用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义(后面的赋值覆盖了前面的)。(函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被变量赋值后覆盖)
2.函数的属性和方法
2.1name属性
- 函数的name属性返回函数的名字(具名函数)
- 如果是通过变量赋值定义的函数,那么name属性返回变量名(函数表达式)
- 若函数表达式中变量的值是一个具名函数,那么name属性返回function关键字之后的那个函数名
- name属性的一个用处,就是获取参数函数的名字(即传递到函数中作为参数的函数的名字)
2.2length属性
- 函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数.
- 不管调用函数时输入了多少个参数,length属性始终等于函数定义时的参数个数
- length属性提供一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)
2.3toString()
- 函数的toString()方法返回一个字符串,内容是函数的源码,包含换行符
- 对于原生的函数,toString()方法返回function(){[native code]}
- 函数内部的注释也可以返回,利用多行注释的特点可以变相实现多行字符串
3.函数作用域
3.1定义
作用域(scope)指变量存在的范围。ES5中,JavaScript只有两种作用域:全局作用域(变量在整个程序中存在,所有地方可读取);函数作用域(局部作用域,变量只在函数内部存在)。(ES6新增块级作用域)
- 对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),可以在函数内部读取
- 在函数内部定义的变量,外部无法读取,称为局部变量(local variable)
- 函数内部定义的变量,会在该作用域内覆盖同名全局变量(就近原则)
- 对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量
3.2函数内部的变量提升
var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
3.3函数本身的作用域
函数本身也是一个值,也有自己的作用域。作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
- 函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域
- 函数体内部声明的函数,作用域绑定函数体内部(正是这种机制,构成了“闭包”现象)
4.参数
4.1概述
函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。
4.2参数的省略
函数参数不是必须的,JavaScript允许省略参数。没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined。
4.3传递方法
- 函数参数如果是原始类型的值(number、string、boolean),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
- 函数参数如果是复合类型的值(Array、Object、function),传递方式是传址传递(pass by reference)。这意味着,传入函数的原始值的地址,在函数内部修改参数,将会影响到原始值。(传入函数的参数对象是地址,因此在函数内部修改对象的属性,会影响到原始值。)
- 注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。(形参的值实际是参数)
4.4同名参数
- 如果有同名的参数,则取最后出现的那个值
- 如果要获取第一个参数的值,可以使用arguments对象
4.5arguments对象
由于JavaScript允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数,这就是arguments对象的由来。
- arguments对象包含了函数运行时所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,只有在函数体内部才可以使用。
- 正常模式下,arguments对象可以在运行时修改
- 严格模式下,arguments对象与函数参数不具有联动关系,也就是说,修改arguments对象不会影响到实际的函数参数
- 通过arguments对象的length属性,可以判断函数调用时到底带几个参数 与数组的关系:
- 需要注意的是,arguments虽然很像数组,但它是一个对象;数组专有的方法(slice、forEach),不能在arguments对象上直接使用
- 如果要让arguments对象使用数组方法,真正的解决方法是将arguments对象转为真正的数组:slice方法和逐一填入新数组 callee属性:
- arguments对象带有一个callee属性,返回它所对应的原函数
- 可以通过arguments.callee达到调用函数自身的目的,但是该属性在严格模式里禁用,因此不建议使用
5.函数的其他知识点
5.1闭包
闭包(closure)是JavaScript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
- 理解闭包,首先必须理解变量作用域:全局作用域和函数作用域;函数内部可以直接读取全局变量;函数外部无法读取函数内部声明的变量
- 由于种种原因,需要得到函数内的局部变量,但是正常情况下办不到,只有通过变通方法实现。即在函数内部,再定义一个函数
- JavaScript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量,所以父对象的所有变量对子对象可见,反之不成立
- 闭包就是返回的函数,即能够读取其他函数内部变量的函数。由于在JavaScirpt语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”,闭包最大的特点,就是它可以“记住”诞生的环境。本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁 闭包的用处:
- 可以读取外层函数内部的变量
- 让这些变量始终保存在内存中,即闭包可以使得它诞生环境一直存在
- 闭包使用外层变量,导致外层函数不能从内存释放
- 只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取
- 闭包的另一个用处,是封装对象的私有属性和私有方法
- 函数Person的内部变量_age,通过getAge和setAge,变成了返回对象p1的私有变量
- 注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大,因此不能滥用闭包,否则会造成网页的性能问题
5.2立即调用的函数表达式(IIFE)
圆括号()跟在函数名之后,表示调用该函数。
- 有时,我们需要在定义函数之后立即调用该函数,此时不能再函数定义后加上圆括号,会产生语法错误Unexpected token
- function这个关键字既可以当作语句,可以当作表达式
- 当作表达式时,函数可以定义后直接加圆括号调用,function作为表达式,引擎就把函数定义当作一个值,这种情况不会报错
- 当作语句时,function关键字出现在行首,JavaScript引擎认为这一段都是函数的定义,不应该以圆括号结尾,所以报错 函数定义后立即调用的解决方法,就是不要让function出现在行首,让引擎将其理解成一个表达式:
- 将语句放在一个圆括号里面,引擎就会认为后面跟的是一个表达式,而是函数定义语句,避免报错
- 立即调用的函数表达式(Immediately Invoked Function Expression),简称IIFE
- 两种写法最后的分号是必须的,避免JavaScript将它们连在一起解释
- 推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果 通常情况下,只对匿名函数使用这种“立即执行的函数表达式”,目的有两个:
- 不必为函数命名,避免全局污染
- IIFE内部形成单独的作用域,可以封装一些外部无法读取的私有变量
6.eval命令
6.1基本用法
eval命令接受一个字符串作为参数,并将这个字符串当作语句执行.
- 放在eval中的字符串,应该有独自存在的意义,不能用来与eval以外的命令配合使用
- 如果eval的参数不是字符串,那么会原样返回
- eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题
- eval的本质是在当前作用域中,注入代码 由于安全风险和不利于JavaScript引擎优化执行速度,一般不推荐使用。 通常情况下,eval最常见的场合是解析JSON数据的字符串,不过正确的做法应该是使用原生的JSON.parse方法