什么是函数
函数是用function关键字定义的一组用来执行特定功能的语句。定义函数有三种方式:函数声明,函数表达式,构造函数。
函数声明
这是我们工作中用到最多的一种方式。
function sum(num1,num2){
return sum1+sum2;
}
函数表达式
var sum=function(num1,num2){
return num1+num2;
}
函数表达式与函数声明的区别是,函数表达式会将函数当做数据赋值给一个变量。
构造函数
var sum=new Function('num1','num2','return a+b')
Function 构造函数创建一个新的 Function 对象。直接调用此构造函数可用动态创建函数,但会遇到和 eval 类似的的安全问题和(相对较小的)性能问题。然而,与 eval 不同的是,Function 创建的函数只能在全局作用域中运行。其参数必须是字符串。因为定义比较复杂,所以一般用得少。
函数没有重载
function addNumber(num){
return num+10;
}
function addNumber(count){
return count+20;
}
addNumber(10);//30
即在JavaScript中如果定义了两个相同的函数,则该名字只属于后定义的函数。函数没有重载的好处在于能重写之前的代码功能,坏处就在于一不小心把别人的代码重写了,导致难以察觉的bug,慎用。
函数声明与函数表达式的区别
- 函数声明是函数名跟在关键字function后面,函数表达式是把函数赋值给一个变量;
- 函数声明可以在执行环境的任何位置,调用不受限制,而函数表达式的定义必须在函数调用之前。
函数声明提升
前面我们提到变量和函数都有声明提升,即变量和函数的定义会被提升到程序的最顶端。举个例子:
sum(1,2);//3
function sum(num1,num2){
return num1+num2;
}
上面代码函数调用在函数声明之前,但是能正常输出结果。变量声明提升虽然可以在变量声明前使用变量,但是值是undefined,而函数声明提升是函数的调用和声明可以在执行环境的任何位置。
函数的参数是按值传递
把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。 要理解上面这段话有点费劲。既然是复制,不是基本类型复制后互不影响,引用类型复制后会互相影响吗,为什么是按值传递不是按引用传递?
在函数传递一个基本类型参数时,被传递的值会复制给函数内一个局部变量(arguments中的一个),传递一个引用类型的值时,会把该引用类型的指针地址复制给函数内一个局部变量,当局部变量发生变化时会反映在函数的外部。举个例子:
函数参数传递一个数字,函数内部修改后:
function addNumber(num){
num=num+10;
console.log(num);//20
}
var num=10;
addNumber(num);
console.log(num);//10
如果传递的是一个object,在函数内部修改对象属性时,外部对象也发生变化了。
function setName(obj){
obj.name="李四"
console.log(obj.name);//李四
}
var obj={
name:"张三"
};
setName(obj);
console.log(obj);//obj.name="李四"
这不是与上面说的还值传递自相矛盾?我们修改几行代码再看:
function setName(obj){
obj.name="李四"
obj=new Object();
obj.name="王麻子";
console.log(obj.name);//王麻子
}
var obj={
name:"张三"
};
setName(obj);
console.log(obj);//obj.name="李四"
这段代码与上面的区别是obj传递给函数后,其属性name被重新赋值,创建一个新对象赋值给obj,再修改obj.name。如果obj是按引用传递,那么修改obj后应该也反映到函数外部,但原始引用并没有发生变化。原因是在函数内部重写obj后实际上创建了一个新的对象引用,该变量引用是局部变量,函数执行完后即被销毁。
总结一下:
函数的参数都是按值传递,参数都是局部变量。对于基本数据类型的参数来说,这个值是变量的副本,在函数体内修改参数不会影响外面,对于引用数据类型来说,这个值是指针地址,在函数体内修改数据会反应到外部,但是如果重写了这个引用类型数据,则不会反应到外面。
为什么说JavaScript函数是一等公民
在编程语言中,一等公民需要具备几个条件:可以作为函数的参数,可以作为函数返回值,可以赋值给变量。 按照这个标准,其实JavaScript任何数据都可以是一等公民,一般说JavaScript函数是一等公民是相对其他编程语言而言的,不是所有的编程语言中函数都能满足上述条件~
函数作为函数的参数
这就是我们常说的回调函数:
function doSomething(callback){
callback(1,2);//3
}
function getResult(num1,num2){
console.log(num1+num2);
}
doSomething(getResult)
作为函数的返回值
function doSomething(){
var name="沉默术士";
return function(){
return name;
}
}
var getName=doSomething();
getName();
作为数据变量
function doSomething(){
return "hello world";
}
var getValue=doSomething;
getValue();
尾调优化
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
function f(x){
return g(x);
}
但是以下情况就不属于尾调优化
// 情况一
function f(x){
let y = g(x);
return y;
}
// 情况二
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
尾调用不一定出现在函数尾部,只要是最后一步操作即可。
函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
如果改写成尾递归,只保留一个调用记录:
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
ES6函数拓展
1.默认参数
ES5我们定义函数默认参数的时候通常这样写:
function doSomething(value){
value=value||'world';
retrun "hello"+value;
}
ES6中我们定义默认参数可以这样写:
function doSomething(value=world){
retrun "hello"+value;
}
注意1:参数变量是默认声明的,所以不能用let或者const再次声明。默认参数应该在参数的末尾
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
注意2:使用默认参数时,函数参数不能有同名参数
// 不报错
function foo(x, x, y) {
// ...
}
// 报错
function foo(x, x, y = 1) {
// ...
}
注意3:参数默认值是惰性求值的
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。
2. 参数解构
function foo({x, y = 5}) {
console.log(x, y);
}
3. rest参数
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function sum(...values){
let sum=0;
for(let i of values){
sum+=i
}
return sum
}
sum(1,2,3);//6
4. 箭头函数
var f = () => 5;
// 等同于
var f = function () { return 5 };
箭头函数使用注意点:
1 . 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
2 . 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
3 . 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
4 . 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
总结:
1、创建函数的方式有:函数声明、函数表达式,构造函数;
2、函数没有重载;
3、函数声明提升指的是函数可以在执行环境任何地方声明和调用;
4、函数的参数都是按值传递;
5、函数也是数据,可以作为函数的参数,赋值给变量,作为函数的返回值; 6、ES6拓展了函数非常丰富有效的能力,比如默认参数,rest参数,参数解构,箭头函数等等。