# 函数

函数可以封装任意多条语句,而且可以在任何地方、任何时候调用执行。函数实际上是对象。每个函数都是 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.callernull ,相反,如果一个函数是在另外一个函数作用域内被调用的,则 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;
        }
    };
}

内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量 propertyNames。之所以还能够访问这个变量,是因为内部函数的作用域链中包含 createComparisonFunction() 的作用域。

当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象. 在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。 在函数执行过程中,需要在作用域链中查找变量。

10.d07z.02

# 闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。

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. 前面曾经提到过,每个函数在被调用时都会自动取得两个特殊变量:thisarguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止, 不过,把外部作用域中的 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() 这一个途径外,没有任何办法可以直接访问 privateVariableprivateFunction()

利用私有和特权成员,可以隐藏那些不应该被直接修改的数据:

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();
        }

    };
}();

在匿名函数内部先定义私有变量和函数, 然后返回一个对象字面量, 对象字面量中包含公开的属性和方法, 从本质上来讲,这个对象字面量定义的是单例的公共接口.

简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。

上次更新: 7/4/2020, 4:14:54 AM