为了账号安全,请及时绑定邮箱和手机立即绑定

和Javascript函数谈一场恋爱(下)

如果Javascript函数是我心中的那个女孩,那么,就让我们谈一场恋爱吧。

1 相恋

如果爱上一个人,就要接受她的全部。

https://img1.sycdn.imooc.com//5b5d0aa20001026e05790688.jpg

函数就是对象,这是我们理解函数的第一原则。

function is_true() {
    return true;
}

console.log("is_true is", is_true());

直接用浏览器进行调试,你会对函数有更深的认识:

https://img1.sycdn.imooc.com//5b5d0c070001783503890466.jpg

1.1 大家闺秀

在Javascript的世界里,如果函数是一个女孩,那她应该是一位大家闺秀。让我们先来了解一下她的长辈们吧。

我们所使用的函数其原型来源于系统内置的Function函数,它当然也是个对象:

console.log(typeof Function, Function);

输出结果:function function Function() { [native code] }

因为是系统内置的,所以你看不到源码,但是调试器还是帮我们列出来了:

https://img1.sycdn.imooc.com//5b5d32030001fe9903600225.jpg

是不是有很多熟悉的面孔?

Function对象的原型来源于Object。基本上,我们声明的函数的祖宗三代就是这个样子。

了解了这些,对函数进行扩展就是一件很简单的事情了:

    Function.prototype.makeup = function() {
        return "精心化妆";
    };
    
    function a_girl() {
        return "一个女孩";
    }
    
    console.log(a_girl(), a_girl.makeup());

输出结果:一个女孩 精心化妆

1.2 少女闺房

在古代,想进入大家闺秀的少女闺房那是很难的,现代,好吧,我承认,也很难。

在Javascript的世界里,一个函数代表着一个作用域。对,我们现在要说的就是刚才我们看到的:

https://img1.sycdn.imooc.com//5b5d39880001288c03730035.jpg

下面我们直接来段代码,找到少女闺房的地址:

var china = "中国";
console.log(china);

function fun_beijing() {
    var beijing = "北京";
    console.log(china, beijing);
    
    function fun_haidian() {
        var haidian = "海淀区";
        console.log(china, beijing, haidian);
            
        function fun_address() {
            var address = "XX小区XX号楼XX单元XX";
            console.log(china, beijing, haidian, address);
        }
        fun_address();
    }
    fun_haidian();
}
fun_beijing();

输出结果:

中国
中国 北京
中国 北京 海淀区
中国 北京 海淀区 XX小区XX号楼XX单元XX

这里我故意不用立即执行函数表达式,而用最原始的写法,这是为了好调试。这段代码是怎么执行的?

1、编译器会把function fun_beijing()和var china提升,还记得我们的女士优先原则不?

编译器解析成功后的fun_beijing函数是下面的样子:

https://img1.sycdn.imooc.com//5b5d3b090001f94203540321.jpg

作用域里是1个对象,一个全局对象

注意全局对象里的变量china还是undefined,因为还没开始运行和赋值呢

2、引擎开始运行,首先执行变量china的赋值,并输出,然后执行fun_beijing函数,在这里跨作用域了

依然重复上面的步骤,编译器会把funciton fun_haidian和var beijing提升。

编译器解析成功后的fun_haidian函数是下面的样子:

https://img1.sycdn.imooc.com//5b5d3c7000014bd305520225.jpg

作用域里是2个对象,一个是fun_beijing对象,一个是全局对象

注意这里的变量beijing还是undefined,因为还没开始运行和赋值呢

3、引擎继续运行,执行变量beijing的赋值,并输出,然后执行fun_haidian函数,在这里跨作用域

依然重复上面的步骤,编译器会把function fun_address和var haidian提升。

编译器解析成功后的fun_address函数是下面的样子:

https://img1.sycdn.imooc.com//5b5d3dd10001dfd505560244.jpg

作用域里是3个对象,一个是fun_haidian对象,一个是fun_beijing对象,一个是全局对象

注意这里的变量haidian还是undefined,因为还没开始运行和赋值呢

4、引擎继续运行,执行变量haidian的赋值,并输出,然后执行fun_address函数,在这里跨作用域

fun_address函数的执行就简单了,因为已经到最里层的作用域了

https://img1.sycdn.imooc.com//5b5d47a700012ef701830055.jpg

上面关于函数作用域的描述,我不断地重复再重复,就是为了给大家一个最直观的影响,Javascript的编译器其实很傻很天真,Javascript的作用域其实也很傻很天真。这是由于Javascript这门语言的原始基因造成的,它在网页里运行,要保证快速响应速度,就不能像其它编译语言一样,要把所有的东西都编译的妥妥当当才运行,它只能采用见到点东西就快速编译快速运行的策略。所以,哎,你就把它当做一个贪吃嘴馋的吃货少女吧,早晚会长成圆球的。

1.3 珠光宝气

如果你让一位爱美的女人把她的首饰拿出一些来送人,估计她会杀了你。函数的Scopes作用域链就好像是函数的首饰。如果你已经了解了函数的作用域问题,那么下面这个函数闭包的问题就是一件很简单的事情了。

让我们把刚才的代码稍微改一改,返回最里面的函数:

var china = "中国";
console.log(china);

function fun_beijing() {
    var beijing = "北京";
    console.log(china, beijing);
    
    function fun_haidian() {
        var haidian = "海淀区";
        console.log(china, beijing, haidian);
            
        function fun_address() {
            var address = "XX小区XX号楼XX单元XX";
            console.log(china, beijing, haidian, address);
        }
        return fun_address;
    }
    return fun_haidian();
}

var fun = fun_beijing();
fun();

输出结果:

中国
中国 北京
中国 北京 海淀区
中国 北京 海淀区 XX小区XX号楼XX单元XX

在很多语言中,退出作用域会导致该作用域内的各种对象被清除。Javascript语言其实也是这样的,但是如果我们返回的是一个函数,引擎就不敢清除了,因为它必须要保证返回函数的完整性,和其它语言不同的是,Javascript函数的完整性除了函数声明以外,还包括函数调用时的执行环境,也就是那个Scopes作用域链了。是的,一个函数的每次执行都有独立的执行环境,哪怕它是同一个函数。这就是Javascript要保证函数是一类公民而必须要付出的代价。

在上面的代码中,变量fun被fun_address函数对象赋值,如果该函数对象的Scopes作用域链没有被清除,当fun执行的时候,自然会输出上面的结果。

哎,这就是所谓的闭包,不过是这个小姑娘舍不得丢掉她任何首饰的天性而已。但是正如首饰对一个女人的重要性一样,闭包的存在让Javascript语言在众多开发语言名媛中大放异彩。

2 相守

恋爱是激情的,婚姻是平淡的。即使我们已经复习了这么多关于函数的知识,有些函数的经典问题,我们依旧要小心呵护。

https://img1.sycdn.imooc.com//5b5d6d000001528115940804.jpg

2.1 递归

Javascript函数很容易实现递归,我们就以经典的计算菲波那切数列为例吧:

    function fib(n) {
        if (n < 3) {
            return 1;
        }
        
        return fib(n - 1) + fib(n - 2);
    }
    
    for (var i = 1; i <= 10; i++)
    {
        console.log("fib", i, "=", fib(i));
    }

输出结果:

fib 1 = 1
fib 2 = 1
fib 3 = 2
fib 4 = 3
fib 5 = 5
fib 6 = 8
fib 7 = 13
fib 8 = 21
fib 9 = 34
fib 10 = 55

直接用函数名做递归是可以的,别忘了,函数就是对象。

升级一下,将函数升级为方法:

    var me = {
        fib : function(n) {
            if (n < 3) {
                return 1;
            }
            
            return me.fib(n - 1) + me.fib(n - 2);
        }        
    }
    
    for (var i = 1; i <= 10; i++)
    {
        console.log("fib", i, "=", me.fib(i));
    }

这样就可以了,本质上还是用的对象。

再增加些难度,不仅你要会算,你还要让你的媳妇也会算,但不幸的是,你不小心喝多了:

    var me = {
        fib : function(n) {
            if (n < 3) {
                return 1;
            }
            
            return me.fib(n - 1) + me.fib(n - 2);
        }        
    }
    
    var wife = {
        fib : me.fib,
    }
    
    me = {}
    
    for (var i = 1; i <= 10; i++)
    {
        console.log("fib", i, "=", wife.fib(i));
    }

这下麻烦了,当wife.fib开始计算时要用me.fib,而此时你的me.fib已经找不到了。

这个问题可以这样来解决:

    var me = {
        fib : function(n) {
            if (n < 3) {
                return 1;
            }
            
            return this.fib(n - 1) + this.fib(n - 2);
        }        
    }
    
    var wife = {
        fib : me.fib,
    }
    
    me = {}
    
    for (var i = 1; i <= 10; i++)
    {
        console.log("fib", i, "=", wife.fib(i));
    }

这段代码是可以正常运行的,那么为什么?我们来捋一捋:

  1. me对象中有一个fib的方法,记为对象xx

  2. wife对象中的fib方法用me.fib赋值,也就是xx

  3. 然后me对象喝多了,me.fib丢失

  4. wife.fib函数调用时调用的是xx

  5. 因为是对象wife调用的xx,所以xx中的this应该是wife

  6. wife中有fib吗?有,就是xx

  7. 所以,一切顺理成章

再增加点难度,加入我们把wife对象中的fib改成fib2,根据上面的分析,不用想,程序又挂了。但是,还可以这样改:

    var me = {
        fib : function xx(n) {
            if (n < 3) {
                return 1;
            }
            
            return xx(n - 1) + xx(n - 2);
        }        
    }
    
    var wife = {
        fib2 : me.fib,
    }
    
    me = {}
    
    for (var i = 1; i <= 10; i++)
    {
        console.log("fib", i, "=", wife.fib2(i));
    }

又可以顺利运行了,这里又发生了什么?我们再来捋一捋:

  1. me对象中有一个fib的方法,但是它是一个有名字的对象,就叫xx

  2. wife对象中的fib方法用me.fib赋值,也就是xx

  3. 然后me对象喝多了,me.fib丢失

  4. wife.fib函数调用时调用的是xx

  5. xx函数执行时可以访问自己吗?没问题啊

  6. 所以,一切顺理成章

这个解决方案不需要this的中转,效率更好,可读性也更好。它被称为“内联命名函数”。这也符合我们一直提倡的尽量给函数一个名字的原则。

还没完,上面的fib函数有个致命的问题,那就是效率太低。想一想,如果我们要算1个fib(100),这是第1排,就要先计算1个fib(99)和1个fib(98),这是第2排,1个变成了2个,同理,要完成第2排的2个计算,我们就要先完成第3排的4个计算等等。这是一个2的n次幂的裂变,效率太低。

解决的方法是用函数缓存:

(function () {
    var me = {
        fib : function xx(n) {
            if (!xx.caches) {
                xx.caches = {};
            }
            if (xx[n] != null) {
                return xx[n];
            }
            
            if (n < 3) {
                return 1;
            }
            
            return xx[n] = (xx(n - 1) + xx(n - 2));
        }        
    }
    
    var wife = {
        fib2 : me.fib,
    }
    
    me = {}
    
    for (var i = 1; i <= 1000; i++)
    {
        console.log("fib", i, "=", wife.fib2(i));
    }
}());

没错,现在我们已经可以大胆地算到fib(1000)了。当然了,输出结果我就不写了。

3 后记

其实,后记本不应该出现在这个地方的,因为第2部分还没写完。但是,为了写这篇文章,我不小心重温了一遍星爷的《喜剧之王》,并毫不意外地再一次泪流满面。所以,就让这个不是后记的后记停在这里吧。

再次向星爷致敬:

https://img1.sycdn.imooc.com//5b5d887f0001556915001014.jpg


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消