声明函数
函数声明的方式有:
使用
function
操作符声明函数,这种方式又分为具名函数、匿名函数和函数表达式。使用全局方法
Function()
。使用 λ(拉姆达)表达式,又称为“箭头函数”(ES6 新增),这种函数都是匿名函数;当函数内只有一个
return
语句时,花括号和return
关键字都可以(要一并)省略;函数参数只有一个时,包裹参数的括号可以省略;例如:(x, y) => {return x + y}
可简写为(x, y) => x + y
(x) => {return x * 2}
可简写为x => x * 2
匿名函数无法直接使用,需要赋值为一个变量,或者将其转换为表达式。
如果将一个具名函数赋值给一个变量,那么它的函数名会变更为变量名。
函数有一个
name
属性,如果是匿名函数,这个值是空字符串;如果是具名函数,这个值是函数声明时的名称,即使将这个函数赋值给一个变量,也不会改变,但是如果此时使用这个名字访问函数,是无法访问的;而匿名函数赋值给一个变量后,这个值会变成变量名,实际上此时,匿名函数已成为具名函数,name
值也就遵循具名函数的特性。关于函数的
name
属性,还有一个特殊情况:使用new Function()
方式声明的函数,它的name
属性值是字符串'anonymous'
,意思是“匿名的”,也无法通过赋值操作改变,但又无法使用这个名字调用这个函数……
函数的本质
函数是一段可以反复调用的代码块;函数还能接收输入的参数,不同的参数会返回不同的值。
要了解函数的本质,要先了解
eval()
这个全局方法,它可以将传入的字符串,计算后当作脚本代码来执行。函数的调用,实际上是在调用函数的
call()
方法,其内部有一个类似eval()
的方法将函数体解析为脚本代码后运行;直接使用函数名后加括号的调用方法,实际上是 JavaScript 的一种语法糖,真正的“硬核(hard cored)” 方法是函数调用自己的call()
方法。从前面的内容可以看出,函数是一个可以执行代码的对象。
函数原型包含的三个重要方法:
call()
、apply()
和bind()
。
谈谈 this 和 arguments
函数的
call()
方法中的第一个参数,可以用this
得到,其他的参数,可以用arguments
得到,前面说过,arguments
是一个伪数组。在普通模式(相对于严格模式)下,
this
的值如果是undefined
,JavaScript 会将其自动引用为window
对象(全局对象),这是一个“潜规则”。JavaScript 中的
new
和this
都是要模拟 Java 语言,但是画虎不成反类其犬……
调用栈 Call Stack
函数调用分为:普通调用、嵌套调用和递归调用(自己嵌套调用自己)。
函数被调用时,都会在调用栈里记录一个位置;嵌套调用也一样,当调用并完成相应的运算后,会跳回之前记录的位置,并将记录抹去,再继续往下运行,直至函数结束调用。
当函数未运行完成时,它会暂时保存在调用栈里,这个叫做“压栈”;递归调用和嵌套调用都会压栈。
当压栈超过系统提供的栈的最大值时,运行就会报错,称为“栈溢出(Stack Overflow)”。
作用域
当遇到一些关于作用域的问题,一定不要忘记变量声明提前这回事。
作用域按照语法树,遵循就近原则。
作用域与变量的关系,主要理解的是变量在哪个作用域,变量是哪一个变量,而不是变量是哪一个变量的值,因为变量的值是可变的。
理解了作用域和变量的关系,以及变量与值的关系,就能理解为什么在循环为一个菜单列表添加事件后,触发事件时打印出的下标,都是列表的长度而不是每个列表项对应的下标;因为当你触发事件的时候,循环已经完成并停止,此时的循环下标,已经是列表的长度了,虽然下标还是那个下标,但是值已经发生了改变。
闭包
- 如果一个函数使用了它范围外的变量,那么(这个函数加这个变量)就叫做闭包。
课后拾遗
在严格模式下(
'use strict'
),this
值如果是基本类型,那么打印时便是这个值,而普通模式下,打印时是这个值包装的对象。语句都会返回
undefined
。多用硬核方式调用函数,
fn.call(undefined, arg1, arg2...)
,就会理解每个函数都有自己的this
。