和Javascript函数谈一场恋爱(下)
如果Javascript函数是我心中的那个女孩,那么,就让我们谈一场恋爱吧。
1 相恋
如果爱上一个人,就要接受她的全部。
函数就是对象,这是我们理解函数的第一原则。
function is_true() { return true; } console.log("is_true is", is_true());
直接用浏览器进行调试,你会对函数有更深的认识:
1.1 大家闺秀
在Javascript的世界里,如果函数是一个女孩,那她应该是一位大家闺秀。让我们先来了解一下她的长辈们吧。
我们所使用的函数其原型来源于系统内置的Function函数,它当然也是个对象:
console.log(typeof Function, Function);
输出结果:function function Function() { [native code] }
因为是系统内置的,所以你看不到源码,但是调试器还是帮我们列出来了:
是不是有很多熟悉的面孔?
Function对象的原型来源于Object。基本上,我们声明的函数的祖宗三代就是这个样子。
了解了这些,对函数进行扩展就是一件很简单的事情了:
Function.prototype.makeup = function() { return "精心化妆"; }; function a_girl() { return "一个女孩"; } console.log(a_girl(), a_girl.makeup());
输出结果:一个女孩 精心化妆
1.2 少女闺房
在古代,想进入大家闺秀的少女闺房那是很难的,现代,好吧,我承认,也很难。
在Javascript的世界里,一个函数代表着一个作用域。对,我们现在要说的就是刚才我们看到的:
下面我们直接来段代码,找到少女闺房的地址:
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函数是下面的样子:
作用域里是1个对象,一个全局对象
注意全局对象里的变量china还是undefined,因为还没开始运行和赋值呢
2、引擎开始运行,首先执行变量china的赋值,并输出,然后执行fun_beijing函数,在这里跨作用域了
依然重复上面的步骤,编译器会把funciton fun_haidian和var beijing提升。
编译器解析成功后的fun_haidian函数是下面的样子:
作用域里是2个对象,一个是fun_beijing对象,一个是全局对象
注意这里的变量beijing还是undefined,因为还没开始运行和赋值呢
3、引擎继续运行,执行变量beijing的赋值,并输出,然后执行fun_haidian函数,在这里跨作用域
依然重复上面的步骤,编译器会把function fun_address和var haidian提升。
编译器解析成功后的fun_address函数是下面的样子:
作用域里是3个对象,一个是fun_haidian对象,一个是fun_beijing对象,一个是全局对象
注意这里的变量haidian还是undefined,因为还没开始运行和赋值呢
4、引擎继续运行,执行变量haidian的赋值,并输出,然后执行fun_address函数,在这里跨作用域
fun_address函数的执行就简单了,因为已经到最里层的作用域了
上面关于函数作用域的描述,我不断地重复再重复,就是为了给大家一个最直观的影响,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 相守
恋爱是激情的,婚姻是平淡的。即使我们已经复习了这么多关于函数的知识,有些函数的经典问题,我们依旧要小心呵护。
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)); }
这段代码是可以正常运行的,那么为什么?我们来捋一捋:
me对象中有一个fib的方法,记为对象xx
wife对象中的fib方法用me.fib赋值,也就是xx
然后me对象喝多了,me.fib丢失
wife.fib函数调用时调用的是xx
因为是对象wife调用的xx,所以xx中的this应该是wife
wife中有fib吗?有,就是xx
所以,一切顺理成章
再增加点难度,加入我们把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)); }
又可以顺利运行了,这里又发生了什么?我们再来捋一捋:
me对象中有一个fib的方法,但是它是一个有名字的对象,就叫xx
wife对象中的fib方法用me.fib赋值,也就是xx
然后me对象喝多了,me.fib丢失
wife.fib函数调用时调用的是xx
xx函数执行时可以访问自己吗?没问题啊
所以,一切顺理成章
这个解决方案不需要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部分还没写完。但是,为了写这篇文章,我不小心重温了一遍星爷的《喜剧之王》,并毫不意外地再一次泪流满面。所以,就让这个不是后记的后记停在这里吧。
再次向星爷致敬:
共同学习,写下你的评论
评论加载中...
作者其他优质文章