我们可以通过4种方式调用一个函数,每种方式之间有一些细微的差别。
- 作为一个函数调用(function)--skulk(),直接调用。
- 作为一个方法(method)-- ninja.skulk(),关联在一个对象上,实现面向对象编程。
- 作为一个函数(constructor)-- new Ninja(),实例化一个新的对象。
- 通过函数的apply或者call方法-- skulk.apply(ninja)或者skulk.call(ninja)。例如:
function skulk(name){}
function Ninja(name){}
//作为函数调用
skulk("yongja");
(function(who){return who})('Hattori');
var ninja = {
skulk:function(){}
}
//作为对象方法调用
ninja.skulk('yongjar');
//作为构造函数调用
ninja = new Ninja('Yongjar');
//通过call方法调用
skulk.call(ninja,'Yongjar');
//通过apply方法调用
skulk.apply(ninja,['Yongjar']);
除了call和apply的方式外,函数调用的操作符都是函数表达式加一对圆括号,下面来开始探讨--作为函数直接被调用。
作为函数直接被调用
作为函数调用?听起来是多么愚蠢,函数当然要被作为函数调用,实际上,这我们说的函数"作为一个函数"被调用是为了区别其他的调用方式:方法、构造函数和apply/call。如果一个函数没有作为方法,构造函数或者通过apply和call被调用的话,我们就成为作为函数被直接调用。
通过()运算符调用一个函数,且被执行的函数表达式不是作为一个对象的属性存在时,就属于这种调用类型。(当执行的函数表达式是一个对象属性时,属于接下来将要讨论的方法调用类型)这里有一些简单的示例:
// 函数定义作为函数被调用
function ninja() {}
ninja();
// 函数表达式作为函数被调用
var samurai = function(){}
samurai();
//会被立即调用的函数表达式,作为函数被调用。
(function(){})()
当以这种方式调用时,函数上下文(this关键字的值)有两种可能性,在非严格模式下,它将是全局上下文(window对象),而在严格模式下,它将是undefined。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../unitl/test.js"></script>
<style>
#results li.pass {color:green;}
#results li.fail {color:red;}
</style>
</head>
<body>
<ul id="results"></ul>
</body>
<script>
//非严格模式下的函数
function ninja() {
return this;
}
//严格模式下的函数
function samurai() {
"use strict";
return this;
}
//非严格模式下的函数以window对象作为函数上下文
assert(ninja() === window,"In a 'nonstrict ninja function the context is the global window object'");
assert(samurai() === undefined, "In a 'strict' function,the Context is undefined");
</script>
</html>
注意
很显然,在多数情况下,严格模式比非严格模式更简单易懂,例如,在清本例中使用的函数调用的方式(而不是作为方法被调用),并没有指定函数被调用的对象。因此在我们看来,this
关键字的确应该被设置为undefined(在严格模式下),而不应该是全局的window对象(在非严格模式下)。一般而言,严格模式修复了很多javaScript中类似的怪异表现(如arguments的用法)。
作为方法被调用
- 当一个函数被给被赋值给一个对象的属性,并且通过对象属性应用的方式调用函数时,函数会作为对象的方法被调用。示例如下:
var ninja = {};
ninja.skulk = function(){};
ninja.skulk();
这种情况下函数被称为方法,如果你有面向对象编程的经历,一定会联想到是否可以在方法内部通过this方法访问对象主题。这种情况下同样适用。当函数作为某个对象的方法被调用时,
该对象成为函数的上下文,并且在函数内部可以通过参数访问到。这也是javaScript实现面向对象编程的主要方式之一。(构造函数是另外一种方式)。
通过下面例子来说明函数作为函数调用和作为方法调用的异同点。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../unitl/test.js"></script>
<style>
#results li.pass {color:green;}
#results li.fail {color:red;}
</style>
</head>
<body>
<ul id="results"></ul>
</body>
<script>
//返回函数上下文,从而让我们能从函数外面检查函数上下文
function whatsMyContext(){
return this;
}
//作为函数被调用并将其上下文设置为window对象。
assert(whatsMyContext() === window,"Function call on window");
var getMythis = whatsMyContext;
//使用变量getMythis来调用函数,该函数仍然作为函数被调用,函数上下文依然是window对象。
assert(getMythis() === window,"Anthor function call in window");
//创建一个对象ninja1其属性getMythis得到了函数whatsMyContext的引用。
var ninja1 = {
getMythis:whatsMyContext
}
// 使用ninja对象的方法getMythis来调用函数。函数的上下文现在是ninja1了。这就是面向对象。
assert(ninja1.getMythis() === ninja1,"Working with 1st ninja");
//创建一个对象ninja2其属性getMythis得到了函数whatsMyContext的引用。
var ninja2 = {
getMythis:whatsMyContext
};
//使用ninja2对象的方法getMythis来调用函数。函数上下文现在是ninja2.
assert(ninja2.getMythis() === ninja2,"Working with 2nd ninja");
</script>
</html>
这段测试代码中设置了一个名为whatMyContext的函数,在整个程序中都将用到它。这个函数的唯一功能就是返回它的函数上下文,这样就可以在函数外部看到调用函数的上下文。
function whatsMyContext() {
return this;
}
当直接通过函数名调用,也就是将函数作为函数调用时,因为实在非严格模式下执行,因此预期的函数上下文结果应当是全局上下文(window)。断言如下:
assert(whatsMyContext() === window,...);
然后通过变量getMythis创建了whatMyContext函数的一个引用,var getMythis = whatsMyContext。这样就不会重复创建函数的示例。他仅仅是创建了原函数的一个引用(因为函数是第一类对象)。
因为函数调用操作符可以应用任何表示函数的表达式,所以可通过变量调用函数,这里再一次将函数作为函数调用。我们预期的函数上下文是window,断言如下:
assert(getMythis()===window,"Anther function call in window");
现在,假设遇到了一个棘手的问题,需要定义一个名为ninja1的对象,并包含一个名为getMythis的属性,属性值为函数whatMyContext的引用。这样我们就在对象上创建了一个名为getMythis的方法。
不要认为whatsMyContext成为了ninja1的一个方法,whatsMyContext是一个独立的函数,它可以有多种调用方式:
var ninja1 = {
getMyThis:whatsMyContext
}
正如之前提到的,当通过方法应用调用函数时,我们期望的函数上下文是该方法所在的对象,因此断言如下:
assert(ninja1.getMythis() === ninja1,"Working with 1st ninja");
注意
将函数作为方法调用对于实现javascript面向对象编程至关重要。这样就可以通过this在任何方法中获取该方法的"宿主"对象--这也是面向对象编程的基本概念。
为了验证这一点,我们通过创建一个新的对象ninja2来进一步测试,他也包含了一个应用whatsMyContext函数的名为getMythis的属性。当我们通过ninja2对象调用这个函数时,他的上下文对象即为ninja2:
var ninja2 = {
getMyThis:whatsMyContext
};
assert(ninja2.getMyThis() === ninja2,"Working with 2nd ninja");
即使在整个示例中使用的是相同的函数--whatsMyContext,但通过this返回的函数上下文依然取决于whatsMyContext的调用方式。例如,ninja1和ninja2共享了完全相同的函数,但当执行函数时,该函数可以访问并操作所属对象的其他方法。因此我们不需要创建一个单独的函数副本来操作不同的对象进行相同的处理。这也是面向对象编程的魅力所在。