JavaScript函数


引子

首先得明白一点,在JavaScript中函数其实就是对象。使函数不同于其他对象的决定性特点是函数存在一个被称为 [[Call]] 的特殊内部属性,包含了函数的执行指令。内部属性无法通过代码访问而是定义了代码执行时的行为,[[Call]] 属性是函数独有的,表明该对象可以被执行。由于仅函数拥有该属性,ECMAScript定义 typeof 操作符对任何具有 [[Call]] 属性的对象返回“function”。


声明/表达式

函数具有两种字面形式。一种是函数声明,如下:

function add(num1, num2) {
    return num1 + num2;
}

一种是函数表达式,如下:

var add = function(num1, num2) {
    return num1 + num2;
};  //不要遗漏分号,因为这是一条JavaScript赋值语句

上述两段代码的结果是一样的,它们都定义了一个函数。但是这儿有一点区别:从形式上来看,函数表达式比函数声明少了函数名;函数表达式比函数声明多了一个句末的分号,这是因为本质上是将函数作为值赋给变量,按照语法规则,赋值语句需要以分号结尾。

另外,它们还有一个非常重要的区别。函数声明会被提升至上下文(要么是函数被声明时所在的函数的范围,要么是全局范围)的顶部。这意味着我们可以先使用函数后声明:

var result = add(5, 5);
console.log(result);    // 10

function add (num1, num2) {
    return num1 + num2;
}

上面这段代码,看起来似乎会出错,但事实上它的确能正常工作,那是因为JavaScript引擎将函数声明提升至顶部来执行;就好像它被写成下面这样:

//JavaScript引擎会像下面这样解释代码
function add(num1, num2) {
    return num1 + num2;
}
var result = add(5, 5);
console.log(result);

JavaScript引擎能对函数声明进行提升,这是因为引擎提前知道了函数的名字。而函数表达式仅能通过变量引用,因此无法提升。所以下面这段代码会导致错误:

var result = add(5, 5);     //运行会报错“add is not a function”
console.log(result);

var add = function(num1, num2) {
    return num1 + num2;
};

所以,建议保持良好的编码习惯,始终在函数使用之前定义它们,在代码中也只使用自己已经定义过的函数。


构造函数形式

函数是对象,所以存在一个 Function 构造函数。同创建对象一样,也可以使用 Function 构造函数创建新的函数。如下:

事实上,很少有人使用 Function 构造函数形式来创建新的函数,因为它会使代码难以理解和调试。但是有时候可能不得不使用这种用法,例如在函数的真实形式直到运行时才能确定的时候。


函数就是值

前面说过,函数就是对象,本质上是引用类型,所以我们可以像使用对象一样使用函数。可以将它们赋给变量,可以在对象中添加它们,可以将它们当成参数传递给其他的函数,可以从别的函数中返回……基本上只要是可以使用引用值的地方,就可以使用函数。看下面这个例子:

function sayHi(){
    console.log("Hi");
}

sayHi();    // Hi

var sayHi2 = sayHi;

sayHi2();   // Hi

上面这段代码运行结果是会输出两次 Hi,我们可以按以下步骤分析这段代码:
1.声明一个函数 sayHi
2.执行函数 sayHi,会输出 Hi
2.创建变量 sayHi2
3.将 sayHi 的值赋给 sayHi2,现在 sayHi 和 sayHi2 指向同一个函数(本质上是对象,引用类型)
4.执行 sayHi2,会输出 Hi
为了更好地展示这背后的过程,我们可以使用构造函数形式来定义 sayHi 函数重写上面代码:

var sayHi = new Function("console.log(\"Hi\");");

sayHi();    // Hi

var sayHi2 = sayHi;

sayHi2();   // Hi

Function 构造函数更加清楚地表明 sayHi 能够像其他对象一样被传来传去。只需记住函数就是可执行的对象。


参数

JavaScript函数的另一个独特之处在于可以给函数传递任意数量的参数却不造成错误。那是因为函数参数实际上是被保存在一个被称之为 arguments 的类似数组的对象(注意,arguments 不是数组的实例)中,如同一个普通的JavaScript数组,arguments 可以自由增长来包含任意个数的值,这些值可以通过数字索引来引用(中括号语法)。arguments 对象自动存在于函数中,也就是说,函数的命名参数不过是为了方便,并不是真的限制了该函数可接受参数的个数。

另一方面,函数有一个属性名为 length,它用来保存函数所期望的参数个数,即定义函数时设置的形参个数。下面是一个具体用法:

function reflect(value) {
    return value;
}

console.log(reflect("Hi"));     // Hi
console.log(reflect("Hi", 25)); // Hi
console.log(reflect.length);    // 1

reflect = function() {
    return arguments[0];
}

console.log(reflect("Hi"));     // Hi
console.log(reflect(25, "Hi")); // 25
console.log(reflect.length);    // 0

使用 arguments 对象的版本有点儿让人莫名其妙,因为没有命名参数,不得不浏览整个函数来确定是否使用了参数,所以大多数情况下避免使用 arguments。当然了,arguments 也是有用武之地的,arguments 对象有一个名为 length 的属性,可以告诉我们 arguments 对象目前保存着多少个值 。一个例子:


function sum() {
    var result = 0;
    var i = 0;
    var len = arguments.length;

    while (i < len) {
        result += arguments[i];
        i++;
    }

    return result;
}

console.log(sum(1, 2));         // 3
console.log(sum(3, 4, 5, 6));   // 18
console.log(sum(50));           // 50
console.log(sum());             // 0

sum() 可以接受任意数量的参数并在 while 循环中遍历它们的值为求和。由于 result 初始值为0,所以该函数就算没有参数也能正常工作。


重载

大多数面向对象语言支持函数重载,它能让一个函数具有多个签名。函数签名由函数的名字、参数的个数及其类型组成。之前已经提过,JavaScripta函数可以接受任意数量的参数且参数类型完全没有限制。这说明JavaScript函数其实根本没有签名,因此也不存在重载。看下例:

function sayMessage(message) {
    console.log(message);
}

function sayMessage() {
    console.log("Default Message");
}

sayMessage("Hello");    // Default Message

如果在支持函数重载的语言中,上例代码运行结果就会是 “Hello”。然而在JavaScript中,上述代码会输出“Default Message”,这是因为在JavaScript里,当试图定义多个同名的函数时,只有最后定义的有效,之前函数声明被完全删除,只使用最后那个。可以使用函数的构造函数形式来帮助理解:

var sayMessage = new Function("message", "console.log(message);");

var sayMessage = new Function("console.log(\"Defalut Message\");");

sayMessage("Hello");    // Default Message

这样看代码,前一个函数为什么不能工作一目了然。对变量 sayMessage 连续赋了两次对象,第一个自然就丢失了。

虽然JavaScript没有函数,但是我们可以通过使用 arguments 对象获取传入的参数个数并决定怎么处理来模仿函数重载。

function sayMessage(message) {
    if (arguments.length === 0) {
        message = "Default Message";
    }

    console.log(message);
}

sayMessage("Hello");    // Hello

上面的例子通过检查 arguments.length 的值,检查传入的函数参数,从而实现了 sayMessage() 函数的行为视传入参数的个数而定。有时还需检查参数的数据类型,我们可以使用 typeof 的 instanceof 操作符。
注意:在实际应用中,检查命名参数是否未定义比依靠 arguments.length 更为常见。


改变this

JavaScript所有函数的作用域内部都有一个 this 对象代表调用该函数的对象。在全局作用域中,this 对象代表全局对象(浏览器里的 windows)。当一个函数作为对象的方法被调用时,默认 this 的值等于那个对象。

在JavaScript中,使用和操作函数中 this 的能力是良好地面向对象编程的关键。函数会在各种不同上下文中被使用,它们必须到哪儿都能正常工作。一般 this 会被自动设置,但是我们可以改变它的值来完成不同的目标。有3种函数方法允许我们改变 this 的值。


call()方法

call() 方法接受多个参数,传入 call() 方法的第一个参数指定函数执行时的 this 值,其后传入的所有参数则为函数本身执行时所需要传入的实参。下面是个例子:

function sayNameForAll(label) {
    console.log(label + " : " + this.name);
}

var person1 = {
    name : "feng"
};

var person2 = {
    name : "dong"
};

var name = "Ming";

//指定 this 对象为 person1
sayNameForAll.call(person1, "global");  // global : feng
sayNameForAll.call(person1, "person1"); // person1 : feng
sayNameForAll.call(person1, "person2"); // person2 : feng

//指定 this 对象为 person2
sayNameForAll.call(person2, "global");  // global : dong
sayNameForAll.call(person2, "person1"); // person1 : dong
sayNameForAll.call(person2, "person2"); // person2 : dong

// 指定 this 对象为全局对象(浏览器里的 window)
sayNameForAll.call(this, "global");     // global : Ming
sayNameForAll.call(this, "person1");    // person1 : Ming
sayNameForAll.call(this, "person2");    // person2 : Ming

上例中的 sayNameForAll() 函数接受一个 label 参数用于输出表明当前使用的是哪个对象,同时输出的还有函数作用域内部 this 对象的 name 属性值。由于使用了 call() 方法,显示指定了 this 值,所以只要传入 call() 方法的第一个参数维持不变,以后传入的参数无论怎么变化,最终输出的 this 对象的 name 属性值始终不会变。


apply()方法

事实上,apply() 工作的方法和 call() 完全一样,区别是它只接受两个参数:this 的值和一个数组或者说类似数组的对象,内含需要被传给函数的参数(也就是说,可以把 arguments 对象作为 apply() 方法的第二个参数)。即与 call() 方法相比,apply() 方法接受传给函数的众多参数是通过向其传递包括这些参数的一个数组实现的。

function sayNameForAll(label) {
    console.log(label + " : " + this.name);
}

var person1 = {
    name : "feng"
};

var person2 = {
    name : "dong"
};

var name = "Ming";
//指定 this 对象为 person1
sayNameForAll.apply(person1, ["global"]);   // global : feng
sayNameForAll.apply(person1, ["person1"]);  // person1 : feng
sayNameForAll.apply(person1, ["person2"]);  // person2 : feng

//指定 this 对象为 person2
sayNameForAll.apply(person2, ["global"]);   // global : dong
sayNameForAll.apply(person2, ["person1"]);  // person1 : dong
sayNameForAll.apply(person2, ["person2"]);  // person2 : dong

//指定 this 对象为全局对象(在浏览器中是 window)
sayNameForAll.apply(this, ["global"]);      // global : Ming
sayNameForAll.apply(this, ["person1"]);     // person1 : Ming
sayNameForAll.apply(this, ["person2"]);     // person2 : Ming

这段代码借用了前一个例子并用 apply() 方法替换了 call() 方法;结果完全相同。这二者在本质上并没有区别,实际工作中通常根据需要传入的数据决定用哪个方法:若是一个个单独的变量,使用 call();若是一个数组,使用 apply() 方法。


bind()方法

改变 this 的第三个方法是 bind(),ECMAScript 5中新加的。按惯例,bind() 方法的第一个参数是要传给函数的 this 值,而其他所有参数代表需要被永久设置在新函数中的命名参数。

function sayNameForAll(label) {
    console.log(label + " : " + this.name);
}

var person1 = {
    name : "feng"
};

var person2 = {
    name : "dong"
};

/*注意:bind() 方法与 call()、apply() 的区别,像下面这样的代码是没有结果的,但是如果换做 call() 就可直接运行输出
* sayNameForAll.bind(person1, "person1");
* sayNameForAll.bind(person2, "person2");
*/

//仅仅为 person1 创建一个函数
var sayNameForPerson1 = sayNameForAll.bind(person1);
sayNameForPerson1("person1");   // person1 : feng

//仅仅为 person2 创建一个函数
var sayNameForPerson2 = sayNameForAll.bind(person2, "person2");
sayNameForPerson2();            // person2 : dong

person2.sayName = sayNameForPerson1;
person2.sayName("person2");

sayNameForPerson1() 没有绑定参数,所以在使用时还需先传入参数。sayNameForPerson2() 不仅绑定了 this 值为 person2,同时也绑定了第一个参数为“person2”;这意味着w我们可以直接调用 sayNameForPerson2() 而不需要传入任何额外参数。例子最后将 sayNameForPerson1() 设置为 person2 的 sayName 方法。由于其 this 值已经绑定,所以虽然 sayNameForPerson1 现在是 person2 的方法,它仍然输出 person1.name 的值;这同前面 call() 方法例子中的

sayNameForAll.call(person1, "person2"); // person2 : feng

有些相似。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值