计算机 / 读书笔记 · 2023年5月4日 0

Professional JavaScript for Web Developers, 3rd Edition, Part 5

第7章 函数表达式

定义函数的两种方式,1.函数声明,2.函数表达式。

函数声明的语法:

function functionName(arg0, arg1, arg2) {}

函数声明的一个重要特征是函数声明提升(function declaration hoisting),即在执行代码前会先读取函数声明。这使得可以把函数声明放到调用它的语句后面:

sayHi();
function sayHi() {
    alert("Hi!");
}

函数表达式的语法:

var functionName = function(arg0, arg1, arg2) {}

即创建一个匿名函数(或称lambda函数),将其赋给一个变量。匿名函数的name属性是空字符串。

使用函数表达式时,必须在使用前先赋值。

sayHi(); // error, sayHi这个变量还不存在
var sayHi = function() {
    alert("Hi!");
}

函数提升不能按照下面的方式使用:

    var condition = true;
    
    //never do this!
    if(condition){
        function sayHi(){
            alert("Hi!");
        }
    } else {
        function sayHi(){
            alert("Yo!");
        }
    }

    sayHi();

这在ECMAScript中属于无效语法。

想实现上面代码的类似效果只能使用函数表达式:

var sayHi;
if (condition) {
  sayHi = ...;
} else {
  sayHi = ...;
}

1.递归

下面的递归代码会出错:

            function factorial(num){
                if (num <= 1){
                    return 1;
                } else {
                    return num * factorial(num-1);
                }
            }

            var anotherFactorial = factorial;
            factorial = null;
            alert(anotherFactorial(4));  //error!

因为factorial被赋值为null,导致调用anotherFactorial时代码对factorial进行调用出错。

正确的做法是重写factorial函数:

            function factorial(num){
                if (num <= 1){
                    return 1;
                } else {
                    return num * arguments.callee(num-1);
                }
            }

但是在严格模式下无法通过脚本访问arguments.callee,可以考虑使用匿名函数:

            function factorial = (function f(num){
                if (num <= 1){
                    return 1;
                } else {
                    return num * f(num-1);
                }
            });

2.闭包

闭包是指有权访问另一个函数作用域中的变量的函数。

当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,…直至作为作用域链终点的全局执行环境。

换句话说:闭包是指每个函数在执行时都要生成一个执行环境,这个执行环境里有一个作用域链,作用域链按优先级包含了用arguments和其他命名参数的值来初始化的该函数的活动对象,该函数的外部函数的活动对象,该函数的外部函数的外部函数的活动对象,…最后是作为作用域链终点的全局执行环境。这个作用域链把该函数运行需要的所有参数都包含进来了,因此成为闭包。但是正因为要包含这个作用域链,导致闭包会占用更多的内存。

1.闭包与变量

需要注意,闭包的作用域链中引用的外部函数(以及外部函数的外部函数,…)的活动对象中的变量是共享的(即引用的)。

下面的函数只能创建一个全部返还10的函数数组:

            function createFunctions(){
                var result = new Array();
                
                for (var i=0; i < 10; i++){
                    result[i] = function(){
                        return i;
                    };
                }
                
                return result;
            }
            
            var funcs = createFunctions();
            
            //every function outputs 10
            for (var i=0; i < funcs.length; i++){
                document.write(funcs[i]() + "<br />");
            }

而下面的代码会让每个函数返回不同的值:

            function createFunctions(){
                var result = new Array();
                
                for (var i=0; i < 10; i++){
                    result[i] = function(num){
                        return function(){
                            return num;
                        };
                    }(i);
                }
                
                return result;
            }
            
            var funcs = createFunctions();
            
            //every function outputs 10
            for (var i=0; i < funcs.length; i++){
                document.write(funcs[i]() + "<br />");
            }

2.关于this对象

this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于该对象。

this和arguments是两个特殊的变量,每个函数在被调用时都会自动获取这个变量。因此在作用域链的开始(即该函数的活动对象)就搜索到了这两个变量。无法访问外部函数中的this变量,除非把外部函数中的this变量保存到其他变量里。

下面的代码不能访问外部函数中的this变量:

        var name = "The Window";
        
        var object = {
            name : "My Object",
        
            getNameFunc : function(){
                return function(){
                    return this.name;
                };
            }
        };
        
        alert(object.getNameFunc()());  //"The Window"

下面的代码通过将外部函数的this变量保存到新的变量里实现了对外部函数的this变量的访问:

            var name = "The Window";
            
            var object = {
                name : "My Object",
            
                getNameFunc : function(){
                    var that = this;
                    return function(){
                        return that.name;
                    };
                }
            };
            
            alert(object.getNameFunc()());  //"MyObject"

理解下面几行函数调用的结果:

            var name = "The Window";
            
            var object = {
                name : "My Object",
            
                getName: function(){
                    return this.name;
                }
            };
            
            alert(object.getName());     //"My Object"
            alert((object.getName)());   //"My Object"
            alert((object.getName = object.getName)());   //"The Window" in non-strict mode

3.内存泄露

闭包会引用包含函数的整个活动对象。

换句话说:闭包会引用外部函数的活动对象,对于一些老版本的IE会导致引用计数无法变为0,无法回收内存的问题。

3.模仿块级作用域

JavaScript没有块级作用域的概念,变量不是定义在块语句中,而是存在于函数的活动对象中的。因此可以利用匿名函数来实现块级作用域:

(function(){
    //这里是块级作用域
})();


// 语法错误,函数声明后不能再接着()
// 除非通过上面那样的代码加()将函数声明转换为函数表达式
function(){
}();     

4.私有变量

JavaScript中没有私有成员的概念,所有对象属性都是共有的。但是有私有变量的概念,即函数内部定义的变量(包括函数的参数、局部变量和在函数内部定义的其他函数)是不能在外部进行访问的。

为了访问函数中的私有变量和私有函数,方法之一是在构造函数中创建特权方法(privileged method)。这种方法的缺点是每个实例都会创建一组特权方法。

            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"

1.静态私有变量

与上面的每个实例都有一组私有变量和方法不同,可以通过创建并调用一个匿名函数来实现一个私有作用域,并将该私有作用域的特权方法赋给类的原型对象来让所有的实例共享该私有作用域里的变量和方法。

2.模块模式

一个不相干的问题,下面三段代码的区别是什么,为什么第2个会报错:

(function(){
    //这里是块级作用域
})();


// 语法错误,函数声明后不能再接着()
// 除非通过上面那样的代码加()将函数声明转换为函数表达式
function(){
}();   

// 正常
var a = function(){}();

为了理解上述问题,应该要先理清函数表达式与函数声明的区别:

https://stackoverflow.com/questions/336859/var-functionname-function-vs-function-functionname

var functionOne = function() {
    // Some code
};

function functionTwo() {
    // Some code
}

functionOne是函数表达式,该函数定义只有在运行到这一行代码时才有定义,而functionTwo是函数声明,在开始运行整个脚本时就会有定义。

回到最上面的那三段代码,以个人理解其中的第二段应该是会按照函数声明的方式去解析这个”function(){}();”中的”function(){}”这就会产生缺少函数名的报错。但是如果是把这个”function(){}”加上括号表达式或者放到赋值语句里,JavaScript就会按照函数表达式的规则去解析代码就不会报错了。

回到本节的模块模式,具体指的是通过匿名函数返回一个对象来实现一个具有私有变量和方法的单例。

3.增强的模块模式

与前面的模块模式相比,这里的增强模块模式其实就是强行让匿名函数返回的对象变成指定类的实例。