JavaScript详细教程(5) - 函数

一、调用函数

在JavaScript中有4种方式来调用JavaScript函数

  • 作为函数
  • 作为方法
  • 作为构造函数
  • 通过他们的call()和apply()方法间接调用

作为函数调用

根据es3和非严格的es5对函数调用的规定,调用上下文(this的值)是全局对象。在严格模式下,调用上下文则是undefined

以函数形式调用的函数通常不适用this关键字。不过,”this”可以用来判断当前是否是严格模式

作为方法调用

作为方法调用时,对象o成为调用上下文,函数体可以使用关键字this引用该对象

关键字this没有作用于的限制,嵌套的函数不会从调用它的函数中继承this.如果嵌套函数作为方法调用,其this的值指向调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)

注意:很多人会误认为调用嵌套函数时this会指向调用外层函数的上下文。如果你想要访问这个外部函数的this值,需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。通常使用变量self来保存this

1
2
3
4
5
6
7
8
9
10
11
12
13
var o = {
m: function(){
var self = this;
console.log(this === o);
f();
function f(){
console.log(this === o);
console.log(self === o);
}
}
};
o.m();

构造函数调用

构造函数调用创建一个新的空对象,这个对象继承自构造函数的prototype属性,构造函数试图初始化这个新创建的对象,并将这个对象用做其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象(详见下面“this与上下文(Context)”章节)

尽管构造函数看起来像一个方法调用,它依然会使用这个新对象作为调用上下文,也就是说在表达式new o.m()中调用上下文并不是o而是新创建的这个对象

间接调用

使用call()和apply()可以用来间接调用函数,这两个方法都允许显式指定调用所需的this值,也就是说任何函数都可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。

两个方法都可以指定调用的实参,call()方法使用它自有的实参列表作为函数的实参,apply()方法则要求以数组的形式传入参数

二、函数的实参和形参

可选形参

当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。因此在调用函数时形参是否可选以及是否可以省略应当保持较好的适应性,为了做到这一点,应当给省略的参数赋一个合理的默认值

1
2
3
function getpropertyNames(o, /*optional*/ a){
if(a == undefined) a = [];
}

第一行代码也可以不适用if语句,可以使用“||”运算符,

1
a = a || [];

可变长的实参列表:实参对象

当调用函数的时候传入的实参个数超过函数定义时的形参个数时,没有办法直接获得未命名值得引用。参数对象解决了这个问题,在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标就能访问传入函数的实参值

假设定义了函数f,它的实参只有一个x。如果调用这个函数时传入两个实参,第一个实参可以通过参数名x来获得,也可以通过arguments[0]来得到。第二个实参智能通过用以标识其所含元素的个数。因此,如果调用函数f()时传入两个参数。arguments,length的值就是2

将对象属性用做实参

当一个函数包含超过三个形参时,尽量使用名值对的形式来传入参数。为了实现这种风格的方法调用,定义函数的时候,传入的实参都写入一个单独的对象之中

实参类型

JavaScript方法的形参并未声明类型,在形参传入函数提之前也未做到任何类型检查,可以通过语义化的单词来给函数实参命名,也可以给实参补充注释,对于可选的实参来说,可以在注释中补充下这个实参是可选的,当一个方法可以接收任意数量的实参时,可以使用省略号

三、自定义函数属性

JavaScript中的函数并不是原始值,而是一种特殊对象,也就说,函数可以拥有属性,当函数需要一个“静态”变量来在调用时保持某个值不变,最方便的方式的方式就是给函数定义属性,而不是定义全局变量

1
2
3
4
5
6
7
8
//初始化函数对象的计数器属性
//由于函数声明被提前了,因此这里是可以在函数声明致歉
//给它的成员赋值
uniqueInterger.counter = 0;
function uniqueInteger(){
return uniqueInteger.counter++;
//先返回计数器的值,然后计数器自动加一
}

四、作为命名空间的函数

在函数中声明的变量在整个函数体内都是可见的(包括嵌套的函数中),在函数的外部是不可见的。不在任何函数内声明的变量是全局变量,在整个JavaScript程序中都是可见的。在JavaScript中是无法声明只在一个代码块内可见的变量的,基于这个原因,我们常常简单地定义一个函数用做临时的命名空间,这个命名空间内定义的变量都不会污染到全局命名空间

1
2
3
4
5
6
7
8
9
10
function mymodule(){
//模块代码,这个模块所使用的所有变量都是局部变量
}
mymodule();//定义完成后好记得调用这个函数
以上代码可以简化成以下写法:
(function(){
//模块代码
}())

这种定义匿名函数并立即在单个表达式中调用它的写法非常常见,已经成为一种惯用用法

五、作用域与上下文

执行上下文(execution context)

执行上下文(简称上下文)决定了Java执行过程中可以获取哪些变量、函数、数据。一段程序可能被分割成许多不同的上下文,每一个上下文都会绑定一个变量对象(variable object),它就像一个容器,用来存储当前上下文中所有已定义或可获取的变量、函数等。位于最顶端或最外层的上下文称为全局上下文(global context),全局上下文取决于执行环境,如Node中的global和Browser中的window:

作用域

执行上下文与作用域(scope)是不同的概念。Js本身是单线程的,每当有function被执行时,就会产生一个新的上下文,这一上下文会被压入Js的上下文堆栈(context stack)中,function执行结束后则被弹出,因此Js解释器总是在栈顶上下文中执行。在生成新的上下文时,首先会绑定该上下文的变量对象,其中包括arguments和该函数中定义的变量;之后会创建属于该上下文的作用域链(scope chain),最后将this赋予这一function所属的Object,

this与上下文(Context)

上下文通常取决于函数是如何被调用的。当一个函数被作为对象中的一个方法被调用的时候,this被设置为调用该方法的对象上,看以下代码:

1
2
3
4
5
6
var obj = {
foo: function(){
alert(this === obj);
}
};
obj.foo(); // true

这个准则也适用于当调用函数时使用new操作符来创建对象的实例的情况下。在这种情况下,在函数的作用域内部this的值被设置为新创建的实例,看以下代码:

1
2
3
4
5
function foo(){
alert(this);
}
new foo() // foo
foo() // window

当调用一个为绑定函数时,this默认情况下是全局上下文,在浏览器中它指向window对象。需要注意的是,ES5引入了严格模式的概念, 如果启用了严格模式,此时上下文默认为undefined。

六、闭包

JavaScript采用词法作用域,也就是说,函数的执行以来于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域相互关联起来,函数体内部的变量都可以保存在作用域内,这种特性称之为“闭包”

从技术的角度讲,所有的JavaScript函数都是闭包:他们都是对象,他们都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效,但这并不影响闭包。当调用函数时闭包指向的作用域链和定义函数时的作用域链不是同一个作用域链时,就引起很微妙的变化。

当外部函数将嵌套的函数对象作为返回值返回的时候往往会发生这种情况

注意一下函数的区别和打印值不同

1
2
3
4
5
6
7
var scope = "global scope"
function checkscope(){
var scope = "local scope";
function f(){return scope;}
return f();
}
checkscope() //=> "local scope";

checkscope()函数声明了一个局部变量,并定义了一个函数f(),函数返回了这个变量的值,最后将函数f()的执行结果返回,对上面代码做一些更改如下

1
2
3
4
5
6
7
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){ return scope;}
return f;
}
checkscope()(); //=> "local scope"

作用域链是定义的时候根据词法作用域创建的,嵌套函数f()定义在这个作用域链里,其中的变量scope一定是局部变量

利用闭包实现变量私有化

我们可以利用闭包将局部变量变成私有状态

1
2
3
4
var uniqueInteger = (function(){
var counter = 0 ;
return function(){return counter++;}
}());

这里定义了一个立即调用的函数(函数开始带有左圆括号),因此是这个函数的返回赋值给uniqueInteger,当外部函数返回之后,其他任何代码都无法访问counter变量,只有内部函数才能访问它

利用闭包实现变量共享

以上代码中的counter变量不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也可以访问它,这多个嵌套函数都共享一个作用域链,如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
function counter(){
var n = 0;
return {
count:function(){return n++;}
reset:function(){n = 0;}
}
}
var c = counter(), d = counter();
c.count(); //=> 0
d.count(); //=> 0 c和d互不重叠
c.reset(); //=> 0 reset()和count()方法共享状态
d.count(); //=> 1

以上代码说明在同一个作用域定义了两个闭包,这两个闭包共享同样的变量。但是要特别小心不要把那些不希望共享的变量共享给了其他闭包,下面代码:

1
2
3
4
5
6
7
8
9
function constfunc(v){
return function(){
return v;
}
}
var funcs = [];
for(var i = 0; i < 10; i++){
funcs[i] = constfunc(i);
}

以上代码的for循环每次创建的闭包都是会同时创建一个自己独享的作用域链和变量,但是当把循环移入定义这个闭包的函数之内时,多个闭包就会共享这个变量

1
2
3
4
5
6
7
function constfunc(){
var funcs = [];
for(var i = 1; i < 10; i++){
funcs[i] = function(){return i;}
}
return funcs;
}

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

length属性

在函数中arguments.length表示实际传入的实参个数,而arguments.callee.length表示期望传入的实参个数

prototype属性

函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做“原型对象”(prototype obejct)。每一个函数都包含不同的原型对象。当将函数用做构造函数的时候,新创建的喜爱那个会从原型对象上继承属性

call()和apply()方法

我们可以将call()和apply()看做是某个对象的方法,通过调用方法的形式来间接调用函数。call()和apply()的第一个实参是要调用用函数的母对象,它是调用上下文,在函数内通过this来获得对它的引用,要想以对象o的方法来调用函数f(),可以这样使用call()和apply()

1
2
f.call(o);
f.apply(o);

对于call()来说,第一个调用上下文实参之后的所有实参就是要传入待调用函数的值。比如,以对象o的方法的形式调用函数f(),并传入两个参数,如下面代码:

1
f.call(o, 1, 2);

apply()方法和call()类似,但传入实参的形式和call()有所不同,它的实参都放入一个数组当中:

1
f.apply(o, [1, 2]);

如果一个函数的实参可以是任意数量,给apply()传入的参数数组可以是任意长度的

bind()方法

bind()方法主要用于将函数绑定至某个对象。当在函数f()上调用bing方法并传入一个对象o作为参数,这个方法将返回一个新的函数。然后(以函数调用的方式)调用新的函数将会把原始的函数f()当做o的方法来调用。传入新函数的任何实参都将传入原始函数,例如:

1
2
3
4
5
6
function f(y){
returen this.x + y
}
var o = {x: 1};
var g = f.bind(o)
g(2) //=> 3

ECMAScript5中bind()方法不仅仅是将函数绑定至一个对象,它还附带一些其他应用:除了第一个实参之外,传入bind()的实参也会绑定至this,这个附带的应用是一种常见的函数式编程技术,有时也被称为“柯里化”,参照下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var sum = function(x, y){
return x+y;
}
//创建一个类似sum的新函数,但是this的值绑定到null
//并且第一个参数绑定到1,这个新的函数期望只传入一个实参
var succ = sum.bind(null, 1);
succ(2) //=> x绑定到1,并传入2作为实参y
function f(y, z){
return this.x + y + z;
}
var g = f.bind({x:1}, 2);
g(3) //=> 6:this.x绑定到1, y绑定到2, z绑定到3

toString()方法

和所有的Javascript对象一样,函数也有toString()方法。大多数函数的toString()芳芳的实现都返回函数的完整源码。内置函数往往返回一个类似[native code]的字符串为函数体

Function()构造函数

不管是通过函数定义语句还是函数直接量表达式,函数的定义都要使用function关键字。但函数还可以通过Function()构造函数来定义,例如:

1
var f = new Function("x", "y", return x*y;");

这一行代码创建一个新的函数,这个函数和通过下面代码定义的函数几乎等价:

1
var f = function(x, y){return x*y;}

关于Function构造函数需要特别注意:

  • Function()构造函数运维JavaScript在运行时动态地创建并编译函数
  • 每次调用Function()构造函数都会解析函数体,并创建新的函数对象。如果是在一个循环或者多次调用的函数中执行这个构造函数,执行效率会受影响。相比之下循环中嵌套函数和函数定义表达式则不会每次执行时都重新编译
  • 最重要的一点,由Function创建的函数并不是使用词法作用域,相反,函数体代码的编译总是会在顶层函数中执行,如下面代码:
    1
    2
    3
    4
    5
    6
    7
    var scope = "global";
    function constructFunction(){
    var scope = "local";
    return new Function("return scope") //无法捕获局部作用域
    }
    //这一行代码返回global,因为通过Function()构造函数所返回的函数使用的不是局部作用域
    constructFunction()(); //=> "global"

我们可以将Function()构造函数认为是在全局作用域中执行的eval(),eval()可以在自己的私有作用域内定义新变量和函数,Function构造函数在实际编程中很少用到

-------------本文到此结束,感谢您的阅读-------------