# 函数
函数可以封装任意多条语句,而且可以在任何地方、任何时候调用执行。函数实际上是对象。每个函数都是 Function
类型的实例. 都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
定义函数的方式有三种:一种是函数声明,另一种就是函数表达式。还有一种是使用 Function
构造函数
函数声明的语法是这样的:
使用 function
关键字来声明,函数名后跟一组参数以及函数体
function functionName(arg0, arg1, arg2) {
//函数体
}
函数声明, 它的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。
函数表达式的语法是这样的:
var functionName = function(arg0, arg1, arg2){
//函数体
};
这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量。这种情况下创建的函数叫做匿名函数(anonymous function),因为 function
关键字后面没有标识符。
Function 构造函数 可以接收任意数量的参数,但最后一个参数始终都被看成是函数体, 前面的参数则枚举出了新函数的参数
var sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
# 传递参数
ECMAScript 中所有函数的参数都是 "按值传递" 的, 也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。
引用传递是指在调用函数时将实际参数的地址传递到函数中,
值传递的精髓是:传递的是存储单元中的内容,而非地址或者引用!
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用 ECMAScript 的概念来说,就是 arguments
对象中的一个元素)
在向参数传递引用类型的值时,会把这个值所指向在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。
function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
# 内部属性
# arguments 对象
函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。ECMAScript 中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数, 函数体内可以通过 arguments
对象来访问函数调用时传进来的实参组成的数组.
arguments
是对象, 不是 Array
的实例, 但是可以像数组一样用方括号语法访问它的每一个属性, 使用 length
属性来确定传递进来多少个参数( "arguments" ), 函数对象自身也有一个 length
属性, 返回希望接收的命名/形式参数( "parameters" )个数.
实参("argument"):全称为 "实际参数" 是在调用时传递给函数的参数
形参("parameter"):全称为 "形式参数" 由于它不是实际存在变量,所以又称虚拟变量。是在定义函数名和函数体的时候使用的参数, 目的是用来接收调用该函数时传入的参数. 在调用函数时,实参将赋值给形参。
有点像下面这样:
var arguments = {
"0": arg0,
"1": arg1,
"2": arg2,
...
"N-1": argN,
}
console.log(arguments[2]); // arg2
arguments
的值永远与对应命名参数的值保持同步, arguments
对象中的值会自动反映到对应的命名参数, 修改 arguments
对象属性的值, 也会修改对应的命名参数
# arguments.callee
arguments
对象还有一个名叫 callee
的属性,该属性是一个指针,指向拥有这个 arguments
对象的函数
一个经典用法是:
function factorial(num){
if (num <=1) {
return 1;
} else {
return num * factorial(num-1)
}
}
定义阶乘函数一般都要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名 factorial
紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用 arguments.callee
。
function factorial(num){
if (num <=1) {
return 1;
} else {
return num * arguments.callee(num-1);
}
}
# caller
函数内的 caller
属性返回函数被调用时所在的作用域.
如果一个函数 f
是在全局作用域内被调用的,则 f.caller
为 null
,相反,如果一个函数是在另外一个函数作用域内被调用的,则 f.caller
指向调用它的那个函数.
该属性的常用形式 arguments.callee.caller
替代了被废弃的 arguments.caller
.
function myFunc() {
if (myFunc.caller == null) {
return ("该函数在全局作用域内被调用!");
} else
return ("调用我的是函数是" + myFunc.caller);
}
为了让函数名和函数体解耦, 可以改成下面这样:
function myFunc() {
if (arguments.callee.caller == null) {
return ("该函数在全局作用域内被调用!");
} else
return ("调用我的是函数是" + arguments.callee.caller);
}
# this
this
指向是函数执行时所在的环境对象.
具体可以参考 this
# 没有重载
ECMAScript 函数不能像传统意义上那样实现重载, 即为一个函数编写两个定义.
两个同名函数出现, 后面的覆盖前面的.
function addSomeNumber(num){
return num + 100;
}
function addSomeNumber(num) {
return num + 200;
}
var result = addSomeNumber(100); //300
因为函数名保存的是指向函数对象的指针, 以上代码实际上等价于下面的代码:
var addSomeNumber = function (num){
return num + 100;
};
addSomeNumber = function (num) {
return num + 200;
};
var result = addSomeNumber(100); //300
因为用了相同的函数名, 则指针指向了后者.
# 递归
递归函数是在一个函数内通过名字调用自身的情况下构成的
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
前面提到过, 用 arguments.callee
是一个指向正在执行的函数(arguments
对象所在函数)的指针, 用他可以让函数名和函数体解耦.
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
但在严格模式下,不能通过脚本访问 arguments.callee
,访问这个属性会导致错误。不过,可以使用命名函数表达式来达成相同的结果。
命名函数表达式 = 函数表达式 + 函数声明
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});
# 闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数
function createComparisonFunction(propertyName) {
return function (object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量 propertyName
s。之所以还能够访问这个变量,是因为内部函数的作用域链中包含 createComparisonFunction()
的作用域。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用 arguments
和其他命名参数的值来初始化函数的活动对象.
在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
在函数执行过程中,需要在作用域链中查找变量。
# 闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
上面例子中, 每个 result
函数结果都是 10. 因为每个函数的作用域链中都保存着 createFunctions()
函数的活动对象,所以它们引用的都是同一个变量 i
, 且是 i
的最后一个值.
但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
在调用每个匿名函数时,我们传入了变量 i
。由于函数参数是按值传递的,所以就会将变量 i
的当前值复制给参数 num
。这样一来,result
数组中的每个函数都有自己 num
变量的一个副本
# this
的指向
this
对象是在运行指向函数的执行环境. 在全局函数中,this
等于 window
,而当函数被作为某个对象的方法调用时,this
等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this
对象通常指向 window
。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"(在非严格模式下)
因为 object.getNameFunc()
返回一个匿名函数, 所以 this
指向 window
. 前面曾经提到过,每个函数在被调用时都会自动取得两个特殊变量:this
和 arguments
。内部函数在搜索这两个变量时,只会搜索到其活动对象为止, 不过,把外部作用域中的 this
对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My Object"
# 私有变量
严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的, 不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量, 因为不能在函数的外部访问这些变量。
通过闭包可以访问私有变量, 因为不能在函数的外部访问这些变量。我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)
下面是几种在对象上创建特权方法的方式。
# 实例私有变量
在构造函数中定义特权方法:
function MyObject(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
对这个例子而言,变量 privateVariable
和函数 privateFunction()
只能通过特权方法 publicMethod()
来访问。在创建 MyObject
的实例后,除了使用 publicMethod()
这一个途径外,没有任何办法可以直接访问 privateVariable
和 privateFunction()
。
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据:
function Person(name){
this.getName = function(){
return name;
};
this.setName = function (value) {
name = value;
};
}
var person = new Person("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
# 静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法:
(function(){
var name = "";
Person = function(value){
name = value;
};
Person.prototype.getName = function(){
return name;
};
Person.prototype.setName = function (value){
name = value;
};
})();
var person1 = new Person("Nicholas");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"
这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。在私有作用域中,首先定义了私有变量和私有函数,然后又定义了构造函数及其公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。没有在声明 Person
时使用 var
关键字, 可以创建一个全局变量, 能够在私有作用域之外被访问到。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。
这个例子中的 Person
构造函数与 getName()
和 setName()
方法一样,都有权访问私有变量 name
。在这种模式下,变量 name
就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用 setName()
会影响所有实例
# 模块模式
模块模式为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
};
}();
在匿名函数内部先定义私有变量和函数, 然后返回一个对象字面量, 对象字面量中包含公开的属性和方法, 从本质上来讲,这个对象字面量定义的是单例的公共接口.
简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。