JavaScript权威指南学习笔记(四)

函数定义

函数声明语句"被提前"到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被在它定义之前出现的代码所调用。不过,以表达式定义的函数就另当别论了,为了调用一个函数,必须能引用它,而要使用一个以表达式方式定义的函数之前,必须把它赋值给一个变量。变量的声明提前了,但给变量赋值是不会提前的,所以,以表达式方式定义的函数在定义之前无法调用。

方法调用

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

var o = {
    m:function(){
        var self = this;                 //将this的值保存至一个变量中
        console.log(this===o);           //输出true,this就是这个对象
        f();
        function f(){
            console.log(this === 0);    //"false":this的值是全局对象或undefined
            consle.log(self === o);     //"true":self指外部函数的this值
        }
    }
}

构造函数调用

如果函数或者方法调用之前带有关键字new,它就构成函数调用。构造函数调用和普通的函数调用以及方法调用在实参处理、调用上下文和返回值方面都有不同。

凡是没有形参的构造函数调用都可以省略圆括号,比如,下面两行等价:

var o = new Object();
var o = new Object;

构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显示返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值,然而如果构造函数是显示使用return语句返回一个对象,那么调用表达式的值就是这个对象。如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。

可选形参

当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined。

//将对象o中可枚举的属性名追加至数组a中,并返回这个数组a
//如果省略a,则创建一个新数组并返回这个新数组
function getPropertyNames(o,/* optional */ a){
    if(a===undefined) a = [];       //如果未定义,则使用新数组
    for(var property in o) a.push(property);
    return a;
}
//这个函数调用可以传入1个或2个实参
var a = getPropertyNames(o);        //将o的属性存储到一个新数组中
getPropertyNames(p,a);              //将p的属性追加至数组a中

如果在第一行代码中不使用if语句,可以使用"||"运算符:

a = a || [];

注意:

  • 那些调用你的函数的程序员是没办法省略第一个实参并传入第二个实参的,它必须将undefined作为第一个实参显示传入。同样注意在函数定义中使用注释/optional/来强调形参是可选的。
  • 使用"||"运算符代替if语句的前提是a必须预先声明,否则a=a||[]会报引用错误、在这个例子中a是作为形参传入的,相当于var a,即已经声明了a,所以这样用是没问题的。

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

标识符arguments是指向实参对象的引用,实参对象是一个类数组对象。

实参对象有一个重要的用处,就是让函数可以操作任意数量的实参:

function max(/*....*/){
    var max = Number.NEGATIVE_INFINITY;
    //遍历实参,查找并记住最大值
    for(var i=0;imax) max = arguments[i];
    //返回最大值
    return max;
}
var largest = max(1,10,100,2,3,1000,4,5,10000,6); //=> 10000

类似这种函数可以接收任意个数的实参,这种函数也称为"不定实参函数"。需要记住,arguments并不是真正数组,它是一个实参对象。每个实参对象都包含以数字为索引的一组元素以及length属性,但它毕竟不是真正的数组。可以这样理解,它是一个对象,只是碰巧具有以数字为索引的属性。

通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值:

function f(x){
    console.log(x);         //输出实参的初始值
    arguments[0] = null;    //修改实参数组的元素同样会修改x的值
    console.log(x);         //输出"null"
}

arguments[0]和x指代同一个值,修改其中一个的值会影响到另一个。

将对象属性用做实参

可以通过名/值对的形式来传入参数,这样参数的顺序就无关紧要了。

//将原始数组的length元素复制至目标数组
//开始赋值原始数组的from_start元素
//并且将其复制至目标数组的to_start中
//要记住实参的顺序并不容易
function arraycopy(/* array */from,/* index */from_start,
                    /* array */to,/* index */to_start,
                    /* integer */length
                    )
{
    //逻辑代码
}
//这个版本的实现效率稍微有些低,但不必再去记住实参的顺序
//并且from_start和to_start都默认为0
function easycopy(args){
    arraycopy(args.from,
            args.from_start || 0,   //注意这里设置了默认值
            args.to,
            args.to_start || 0,args.length);
}
//来看如何调用easycopy()
var a = [1,2,3,4],b=[];
easycopy({from:a,to:b,length:4});

自定义函数属性

当函数需要一个"静态"变量来在调用时保持某个值不变,最方便的方式是给函数定义属性,而不是定义全局变量,显然定义全局变量会让命名空间变得更加杂乱无章:

//初始化函数对象的计数器属性
//由于函数声明被提前了,因此这里是可以在函数声明
//之前给它的成员赋值的
uniqueInteger.counter = 0;

//每次调用这个函数都会返回一个不同的整数
//它使用一个属性来记住下一次将要返回的值
function uniqueInteger(){
    return uniqueInteger.counter++; //先返回计数器的值,然后计数器自增1
}

来看另外一个例子,下面这个函数factorial()使用了自身的属性来缓存上一次的计算结果:

//计算阶乘,并将结果缓存至函数的属性中
function factorial(n){
    if(isFinite(n) && n>0 && n==Math.round(n)){
        if(!(n in factorial))                       //如果没有缓存结果
            factorial[n] = n * factorial(n-1);      //计算结果并缓存
        return factorial[n];
    }
    else return NaN; //如果输入有误
}
factorial[1] = 1; //初始化缓存以保存这种基本情况-

闭包

函数指向依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。函数对象可以通过作用域链相互关联起来,函数体内的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为"闭包"。

当调用函数时有时闭包所指向的作用域和定义函数时的作用域不是同一个作用域,事情就会变得很微妙:

var scope = "global scope";             //全局变量
function checkscope(){
    var scope = "local scope";          //局部变量
    function f() { return scope; }      //在作用域中返回这个值
    return f();
}
checkscope()                            //=>"local scope"

checkscope()函数声明了一个局部变量,并定义了一个函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。现在做些改动:

var scope = "global scope";             //全局变量
function chekscope(){
    var scope = "local scope";          //局部变量
    function f() { return scope };      //在作用域中返回这个值
    return f;
}
checkscope()()                          //返回值是什么

这个作用域链是函数定义的时候创建的。嵌套的函数f()定义在这个作用域链里、其中的变量scope一定是局部变量,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回"local scope",而不是"global scope"。

实现闭包

函数定义时的作用域链到函数执行时依然有效。

我们将作用域描述为一个对象列表,不是绑定的栈。每次调用JavaScript函数的时候,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当作垃圾回收掉。如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域,并且这个作用域链指向指向一个变量绑定对象。但如果这些嵌套的函数对象在外部函数中保存下来,那么它们也会和所指向的变量绑定对象一样当作垃圾回收。但是如果这个函数定义可嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当作垃圾回收,并且它所指向的变量绑定对象也不会被当作垃圾回收。

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.reset()                           //reset()和count()方法共享状态
    c.count()                           //=>0:因为它们重置了c
    d.count()                           //=>1:而没有重置d 
}

counter()函数返回一个"计数器"对象,这个对象包含两个方法:count()返回下一个整数,reset()将计数器重置为内部状态。首先要理解,这两个方法都可以访问私有变量n。每次调用counter()都会创建一个新的作用域和一个新的私有变量。因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或reset()不会影响到另外一个对象。

从技术角度看,其实可以将这个闭包合并为属性存取器getter和setter。下面这段代码私有状态的实现是利用了闭包,而不是利用普通的对象属性来实现:

function counter(n){
    retrun {
        //属性getter方法返回并给私有计数器var递增
        get count(){ return n++; },
        //属性setter不允许n递减
        set count(m){
            if(m>=n) n = m;
            else throw Error("count can only be set to a larger value");
        }
    }
}
var c = counter(1000);
c.count                     // =>1000
c.count                     // =>1001
c.count=2000                
c.count                     //=>2000
c.count                     //=>Error

这个版本的counter()函数并未声明局部变量,而只是使用参数n来保存私有状态,属性存取器方法可以访问n。这样的话,调用counter()的函数可以指定私有变量的初始值。

这个例子定义了addPrivateProperty()函数,这个函数定义了一个私有变量,以及两个嵌套的函数用来获取设置这个私有变量的值。它将这些嵌套函数添加为指定对象的方法:

//这个函数给对象o增加了属性存取器方法
//方法名称为get和set。如果提供了一个判定函数
//setter方法就会用它来检测参数的合法性,然后在存储它
//如果判定函数返回false,setter方法抛出一个异常
//这个函数有一个非同寻常之处,就是getter和setter函数
//所操作的属性值并没有存储在对象o中
//相反,这个值仅仅是保存在函数中的局部变量中
//getter和setter方法同样是局部函数,因此可以访问这个局部变量
//也就是说,对于两个存取器方法来说这个变量是私有的
//没有办法绕过存取器方法来设置或修改这个值
function addPrivateProperty(o,name,predicate){
    var value; //这是一个属性值

    //getter方法简单地将其返回
    o["get"+name] = function(){ return value; };

    //setter方法首先检查值是否合法,若不合法就抛出异常
    //否则就将其存储起来
    o["set"+name] = function(v){
        if(predicate && !predicate(v))
            throw Error("set" + name + ":invalid value " + v);
        else
            value = v;
        }
    }
}
//下面的代码展示addPrivateProperty()方法
var o = {};         //设置一个空对象

//增加属性存取器方法getName()和setName()
//确保只允许字符串值
addPrivateProperty(o,"Name",function(x){ return typeof x == "string"; });

o.setName("Frank");         //设置属性值
console.log(o.getName())    //得到属性值
o.setName(O);               //试图设置一个错误类型的值

要特别小心那些不希望共享的变量往往不经意间共享了其他的闭包,了解这一点也很重要。:

//这个函数返回一个总是返回v的函数
function consfunc(v){ return function() { return v; } }

//创建一个数组用来存储常数函数
 var funcs = [];
 for(var i=0;i<10;i++) funcs[i] = constfunc(i);

 //在第5个位置的元素所表示的函数返回值为5
 funcs[5]()     //=>5

这段代码利用循环创建了很多个闭包,当写类似这种代码的时候往往会犯一个错误:那就是试图将循环代码移入定义这个闭包的函数之内,看一下这段代码:

//返回一个函数组成的数组,它们的返回值是0~9
function consfuncs(){
    var funcs = [];
    for(var i=0;i<10;i++){
        funcs[i] = function(){ return i; }
    }
    return funcs;
}
var funcs = constfuncs();
funcs[5]()          //返回值是?

当constfuncs()返回时,变量i的值是10,所有的闭包都共享这一个值,因此,数组中的函数返回值都是同一个,这不是我们想要的结果。关联到闭包的作用域都是"活动的",记住这一点非常重要。嵌套的函数不会将作用域内的私有成员赋值一份,也不会对所绑定的变量生成静态快照。

prototype属性

每一个函数都包含一个prototype属性,这个属性式指向一个对象的引用,这个对象称做"原型对象"。每一个函数都包含不同的原型对象。当作函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。

call()方法和apply()方法

call()和aplly()的第一个实参是要调用函数的母对象吗,它是调用上下文,在函数体内通过this来获得对它的引用。要想以对像o的方法来调用函数f(),可以这样使用call()和apply():

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

每行代码和下面代码的功能类似(假设对象o中预先不存在名为m的属性)。

o.m = f;            //将f存储为o的临时方法
o.m();              //调用它,不传入参数
delete o.m;         //将临时方法删除

在ECMAScript 5 的严格模式中,call()和apply()的第一个实参都会变为this的值,哪怕传入的实参是原始值甚至是null或undefined。在ECMScript3和非严格模式中传入的null和undefined都会被全局对象代替,
而其他原始值则会被相应的包装对象所替代。

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

f.call(o,1,2);

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

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

如果一个函数的实参可以是任意数量,给apply()传入的参数数组可以是任意长度的。比如,为了找出数组中最大的数值元素,调用Math.max()方法的时候可以给apply()传入一个包含任意个元素的数组:

//将对象o中名为m()的方法替换为另一个方法
//可以调用原始的方法之前和之后记录日志消息
function trace(o,m){
    var original = o[m];    //在闭包中保存原始方法
    o[m] = function(){
        console.log(new Date(),"Entering:",m);      //输出日志消息
        var reuslt = orginal.apply(this,arguments); //调用原始函数
        console.log(new Date(),"Exiting:",m);       //输出日志消息
        return result;
    };
}

bind()方法

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

function f(y){ return this.x + y; }     //这个是待绑定的函数
var o = { x:1 };                        //将要绑定的对象
var g = f.bind(o);                      //通过调用g(x)来调用o.f(x)
g(2) //=>3

可以通过如下代码轻易实现这种绑定:

//返回一个函数,通过调用它来调用o中的方法f(),传递它所有的实参
function bind(f,o){
    if(f.bind)  return f.bind(o);           //如果bind()方法存在的话,使用bind()方法
    else return function(){
        return f.apply(o,arguments);
    };
}

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

var sum = function(x,y){ return x+y };      
//创建一个类似sum的新函数,但this的值绑定到null
//并且第一个参数绑定到1,这个新的函数期望只传入一个实参
var succ = sum.bind(null,1);
succ(2)     //=>3: x绑定到1,并传入2作为实参y

function f(y,z){ return this.x+y+z };       //另外一个累加计算的函数
var g = f.bind({x:1},2);                    //绑定this和y

ECMAScript3版本的Funtion.bind方法

if(!Function.prototype.bind){
    Funtion.prototype.bind = function(o /*,args */){
        //将this和arguments的值保存至变量中
        //以便在后面嵌套的函数中可以使用它们
        var self = this,boundArgs = arguments;

        //bind()方法的返回值是一个函数
        return function(){
            //创建一个实参列表,将传入bind()的第二个及后续的实参传入这个函数
            var args= [],i;
            for(i=1;i

Function()构造函数

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

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

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

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

Function()构造函数并不需要通过传入实参以指定函数名。就像函数直接量一样,Function()构造函数创建一个匿名函数。

关于Function()构造函数有几点需要特别注意:

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

你可能感兴趣的:(JavaScript权威指南学习笔记(四))