1

该文章以收录: 《JavaScript深入探索之路》


前言

函数是这样的一段JavaScript代码,它只定义一次,但是可能被执行或调用任意次。你可能已经从诸如子例程或者过程这些名字里对函数的概念有所了解。<!-- more --> JavaScript函数时参数化的:函数的定义会包括一个称为形参的表示符列表,这些参数在函数体中像局部变量一样工作。函数调用会为形参提供实参的值,函数使用它们实参的值来计算返回值,成为该函数调用表达式的值。除了实参外,每次调用还会拥有另一个值 —— 本次调用的上下文 —— 这就是this关键字的值。

参数有形参(parameter)和实参(argument)的区别,形参相当于函数中定义的变量,实参是运行时的函数调用时传入的参数。

创建函数

一般我们有三种方法去创建函数:

  • 函数声明:function foo(){}

  • 函数表达式:var foo = function(){}

  • 函数构造法:var foo = new Function('a','b',"return a+b")

函数声明和函数表达式

1.什么是函数声明,什么是函数表达式

ECMA规范说:函数声明必须带有标示符(Identifier)(就是大家常说的函数名称),而函数表达式则可以省略这个标示符:

// 函数声明
function 函数名(可选参数){}

// 函数表达式
function 可选函数名(可选参数){}

我们可以很清楚的看出,如果没有标识符(函数名),该函数就一定是函数表达式,如果有标识符我们该怎么区分函数声明和函数表达式呢。

函数声明我们不用太多的考虑,很容易看出来,我们来看看函数表达式都有哪些自然就知道哪些是函数声明了。

第一种函数表达式:var声明的函数我们很常见

// 没有标识符一定是表达式
var fun = function(){}

//我们还可以这样写 

var fun = function foo(){}

第二种我们带有标识符,但将函数赋值给变量后,该函数就成了函数表达式。在这里foo标识符在该函数体外访问时并不起作用

var fun = function foo(a){
  
    if(a){
      console.log("外部调用了");  
      foo(false); //这会执行函数fun。
    }else{
      console.log("内部调用了");
    }

}

fun(true); //外部调用了 ,内部调用了

foo(); // 报错 ReferenceError: foo is not defined

我们可以看出,函数表达式中foo标识符只能在函数内部使用,去访问该函数,在函数外部访问不到任何东西。上面我们加判断是为了防止函数一直递归。

第二种函数表达式:()括号包住的函数

// 这是函数表达式
(function foo(){})

括号包住的函数是函数表达式是因为,()是一个分组操作符,分组操作符内只能是表达式。

例如:

//Unexpected token var
(var a = 0) 

上面报错,这是因为var是一个语句,而分组操作符内只能是表达式。还有我们在使用evel()时这样写 var ev = eval("(" + json + ")") ,这里是强制将{}解析为表达式而不是代码块。

2.函数声明和函数表达式的特点

函数声明在我们的代码执行前,会被整个提升,也就是说我们在函数声明之前就可以调用该函数,而函数表达式不能够被提升。

a() // 结果 我是函数a"
function a(){
    console.log("我是函数a");
}

fun() //结果 fun is not a function
var fun = function b(){
    console.log("我是函数b");
}

为什么会出现这种情况我们在以后的文章中会详细的讨论。同时,我们应该养成一种习惯,尽量不要在函数声明之前使用函数。这只是一种良好的代码书写习惯。

其次我们需要注意的是在if判断语句中我们不应该出现函数声明,即便是没有报错,也是不推荐这样去写的。

立即执行函数(表达式)和自执行函数

1. 立即执行函数(表达式)

我们来看一下:

(function (){
    console.log("one");
})();


(function (){
    console.log("two");

}());

上边两个函数都可以自己去执行,而值有个共同的特点就是括号左边的函数必须是一个函数表达式,而不是函数声明。

// 如果括号左边是函数声明,报错
function fun(){
    console.log()
}()



// 括号左边是函数时表达式,(函数表达式都可以自执行)

var F = function(){
    return 10;
}();

true && function (){}();

0 , function (){}();

//你甚至看还可以这样

!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();

new function(){}();

其实上边的些自执行函数表达式我们为了方便我们的阅读我们一般会用到第一个和第二个,当然如果你非要搞个性化,使用其他的也是可以。只不过你的代码并不美观,后期也不容易维护。

2. 什么是自执行函数,像这样

function fun1(){
    //code
}
fun1();


function fun2(){
    fun2() // 递归
}

// 这是一个自执行匿名函数
var foo = function () { arguments.callee(); };

看了这些例子,我们应该清楚了什么是自执行,什么是立即调用。

函数构造器

Function()构造函数并不需要通过传入实参以指定函数名,就像函数直接量一样,Function()构造函数创建一个匿名函数。

关于Function()构造函数有几点需要特别注意

  • Function()构造函数允许JavaScript在运行时动态的创建并编译函数。

  • Function()构造函数每次执行时都会解析函数主体,并创建一个新的函数对象,所以当在一个循环或频繁执行的函数中调用Function()构造函数效率是非常低的。而函数字面量(函数表达式)却不是每次遇到都会重新编译的,用Function()构造函数创建一个函数时并不遵循典型的作用域,它一直把它当作是顶级函数来执行

  • 它所创建的函数并不是使用词法作用域,相反,函数体代码的编译总是会在顶层函数执行,

用函数构造器创建的函数不会在上下文中创建闭包,它们总是被创建在全局作用域中,当执行被创建的函数时,它们只能使用自己的局部变量或者全局变量。例如下面:

var scop = 'global';
function fun(){
    var scop  = 'fun';
    return new Function('return scope') // global
}

函数的属性、方法和构造函数

1. length属性

函数体中,arguments.length表示传入函数的实参个数。而函数本身的length表示该函数的形参。

 function fun(x,y,z){
     console.log(arguments.length);// 2
     console.log(arguments.callee.length); //3
 }

 fun(1,2);

我们这里提一下,在函数中的arguments,函数中arguments包含所用实参,它并不是一个真正的数组,而是一个实参的对象。另外他还有两个特别的属性calleecaller属性。

callee属性只带党庆正在执行的函数。
caller属性时非标准的,但大多数浏览器都实现了这个属性它指带调用当前正在执行的函数的函数。

通过caller属性可以访问调用栈。callee属性在某些时候会非常的有用,比如在匿名函数中通过callee来递归调用自身,上面我们已经提到过。

2. prototype属性

每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称作"原型对象"。每一个函数都包含不同的原型对象,当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性。

3. call()方法和apply()方法

call 和 apply 是为了改变某个函数运行时的 context 即上下文而存在的,换句话说,就是为了改变函数体内部 this 的指向。因为 JavaScript 的函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

它们的用法也是很简单的。例如:

var obj = {
        msg:"我是obj对象",
    fun:function(){
       console.log(this.msg);
    },
}

var objTwo = {
    msg:"我是objTwo对象"
};

// 这里我们想让通过objTwo 对象调用 obj对象的fun方法,
// 此时obj的fun方法中的this已经改变了指向。
obj.fun.call(objTwo) // 我是objTwo对象

call 和 apply 的区别在于他们传参的形式不同:

var obj = {
        msg:"我是obj对象",
    fun:function(one,two){
       console.log(this.msg);
       console.log("我是参数:"+ one + ',' +two);
    },
}

var objTwo = {
    msg:"我是objTwo对象"
};

// 这里我们想让通过objTwo 对象调用 obj对象的fun方法,
// 此时obj的fun方法中的this已经改变了指向。
obj.fun.call(objTwo,1,2) // 我是objTwo对象 ,我是参数:1,2

obj.fun.apply(objTwo,[1,2]) // 我是objTwo对象 ,我是参数:1,2

高阶函数

所谓高阶函数就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数。我们来看一个例子:

这个例子的主要作用是,fun函数接收f()g()两个函数,并返回一个新的函数用以计算f(g())

function fun(f,g){
    return function(){
      return f.call(this,g.apply(this,arguments));
    }
}

var square = function(x){
    return x * x
}

var sum = function(x,y){
    return x + y;
}

var suqareOfSum = compose(square,sum)

squareOfSum(2,3) // 25

在这里函数fun() 就是高阶函数。

不完全函数

不完全函数是一种函数变换技巧,即把一次完整的函数调用折成多次函数调用,每次传入的实参都是完整实参的一部分,每个拆分开的函数叫做不完全函数,每次函数调用叫做不完全调用,这种函数变换的特点是每次调用都反一个函数,知道得到最终运行结果为止,例如:将函数f(1,2,3,4,5)的调用修改为等价的f(1,2)(3,4)(5)后者包含三次调用,和每次调用相关的函数就是 不完全函数”。

结束

参考文献: 《JavaScript权威指南》


webxiaoma
747 声望27 粉丝

代码搬运工