- 简介(函数是对象)
- 没有重载
- 函数声明和函数表达式
- 变量提升和函数声明提升
- 作为值的函数
- 函数内部属性(callee和caller)
- 函数属性和方法
简介
函数实际上是对象,每个函数都是function的实例,而且都与其他引用类型一样具有属性和方法。
定义函数跟定义对象或者数组一样都是使用构造函数,function构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体,例如:
var sum = new Function('num1', 'num2', 'return num1 + num2')
从技术上看,这是一个函数表达式,但是我们不推荐这种写法,不过这种语法对于理解‘函数是对象,函数名是指针’的概念倒是非常直观。
注意:函数名后不带括号是访问函数,带括号是调用函数
没有重载
函数重载是指在同一范围内声明不同功能的同名函数,函数名是相同的,但是函数的功能是不同的,这样子调用同一个函数就可以实现不同的功能叫函数重载。
但是javascript的函数没有这个功能,如果函数名相同就会造成后者覆盖前者。例如:
function a(num) {
return num + 100
}
function a(num) {
return num + 200
}
var result = a(100) // 300
等同于
var a = function (num) {
return num + 100
}
a = function (num) {
return num + 200
}
var result = a(100) // 300
通过观察上面代码可以看到创建第二个函数时,后者会覆盖前者,因为a只是一个函数对象的指针。
函数声明与函数表达式
两者区别:(主要是函数声明提升和变量提升的区别)解析器在向执行环境中加载数据时,会率先读取函数声明,并使其在执行任何代码之前可用,至于表达式,则必须等到解析器执行到它所在的代码才会真正被解释执行。例如:
console.log(sum(10, 10))
function sum(num1, num2) {
renturn num1 + num2
}
上面的代码中函数调用写在函数声明的前面并且不会报错,是因为解析器在第一遍解析代码时,会将函数声明通过函数声明提升的过程把它们提升到源代码树的顶部。如果像下面例子所示的,把上面的函数声明改为等价的函数表达式,就会在执行期间报错。
console.log(sum(10, 10))
var sum = function (num1, num2) {
return num1 + num2
}
上面代码之所以会报错,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量sum中是不会保存函数的引用。
除了什么时候可以通过变量访问函数这一点区别之外,函数声明和函数表达式的语法是等价的。
变量提升和函数声明提升
函数声明提升就是把函数声明提升到函数声明所在作用域中(或者说一个函数体内)的顶端,变量提升只是提升变量的声明到所在作用域的顶端不会把值一起提升上来,例如:
function add() {
var a = 1;
var b = 2;
var c = 3
}
等同于
function add() {
var a, b, c
a = 1;
b = 2;
c = 3;
}
注:随便函数声明和变量都会提升,但是函数声明提升是提升整个函数体,变量提升只是提升变量声明,不会提升值,并且函数声明比变量提升优先级要高,也就是函数声明提升要在变量提升上面
如果函数声明的函数名和变量是一样的会不会覆盖呢?看下面的例子:
function sum() {
return 100
}
var sum
console.log(sum)
sum = 200
console.log(sum)
输出的结果为:第一个:function sum() {
return 100
}
第二个:200
第一次变量sum没有赋值所以输出还是function而不是undefined(谁赋值谁优先,变量赋值才会覆盖上面
的值,哪怕赋的值是undefined也会覆盖上面的值),第二次变量赋值所以输出的是200,
这个地方比较特别。
正是因为var声明会发生提升,所以在es6引入了let声明:
(1)let 声明的变量的作用域是块级的;
(2)let 不能重复声明已存在的变量;
(3)let 有暂时死区,不会被提升;
作为值的函数
因为函数名本身就是变量,函数体作为值被保存在变量中,变量可以被作为参数传递给函数,所以函数名也可以被当做参数,把函数当做参数传递给另一个函数,其实就是把函数名(变量)当做参数传递给另一个函数,例:
function add10(num) {
return num + 10;
}
function call(some, argument) {
return some(argument)
}
var result = call(add10, 10)
console.log(result) // 20
要访问函数的指针而不执行函数的话,必须去掉函数名后面的那对大括号。因此上面例子中传递给
call()的是add10,而不是add10()
函数内部属性(callee和caller)
callee
arguments是一个类数组对象,包含传入函数中的所有参数,该对象还有一个callee属性,该属性是一个指针,指向拥有这个arguments对象的函数。例如:
function factorial(num) {
if (num<=1) {
return 1
} else {
return num * factorial(num-1)
}
}
定义阶乘函数一般都要用到递归算法;在函数名不会变的情况下,这么写没有问题,但问题是这个函数的执行与函数名factorial紧紧耦合在了一起。为了消除这种耦合,可以像下面使用arguments.callee。
function factorial(num) {
if (num<=1) {
return 1
} else {
return num * arguments.callee(num-1)
}
}
在这个重写后的factorial()函数的函数体内,没有再引用函数名factorial。 这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。
caller
这个属性中保存着调用当前函数的函数的引用,如果在全局作用域中调用当前函数,它的值为null。例如:
function outer() {
inner()
console.log(outer.caller)
}
function inner() {
console.log(inner.caller)
}
outer()
因为outer是自己执行,没有哪个函数调用所以第一个打印出来是null,第二个是outer()调用inner(),所以inner.caller()就指向outer(),为了解除代码耦合,也可以通过arguments.callee.caller来访问。
函数属性和方法
因为函数是对象,所以函数也都有属性和方法(值),每个函数都包含两个属性:length和prototype。其中length属性表示函数希望接受的命名参数的个数,例如:
function sayName(name) {
console.log(arguments.length) // 2
}
sayName(name, age)
console.log(sayName.length) // 1
sayName实际上传入了2个参数,所以arguments的长度是2,但是sayName接受了一个参数,所以sayName的长度是1。prototype在后面会详细讲解。
网友评论