[No0000104]JavaScript-基础课程4
要说 JavaScript 和其他较为常用的语言最大的不同是什么,那无疑就是 JavaScript 是函数式的语言,函数式语言的特点如下:
函数为第一等的元素,即人们常说的一等公民。就是说,在函数式编程中,函数是不依赖于其他对象而独立存在的(对比与 Java,函数必须依赖对象,方法是对象的方法)。
函数可以保持自己内部的数据,函数的运算对外部无副作用(修改了外部的全局变量的状态等),关于函数可以保持自己内部的数据这一特性,称之为闭包。
由于 JavaScript 支持函数式编程,我们随后会发现 JavaScript 许多优美而强大的能 力,这些能力得力于以下主题:匿名函数,高阶函数,闭包及柯里化等。熟悉命令式语言的开发人员可能对此感到陌生,但是使用 lisp,scheme 等函数式语言的开发人员则觉得非常亲切。
匿名函数在函数式编程语言中,术语成为 lambda 表达式。顾名思义,匿名函数就是没有 名字的函数,这个是与日常开发中使用的语言有很大不同的,比如在 C/Java 中,函数和方法必须有名字才可以被调用。在 JavaScript 中,函数可以没有名字,而且这一个特点有着非凡的意义
通常,以一个或多个函数为参数的函数称之为高阶函数。高阶函数在命令式编程语言中有对应的实现,比如 C 语言中的函数指针,Java 中的匿名类等,但是这些实现相对于命令式编 程语言的其他概念,显得更为复杂。
虽然在 C 语言中可以通过函数指针的方式来实现高阶函数,但是随着高阶函数的“阶” 的增高,指针层次势必要跟着变得很复杂,那样会增加代码的复杂度,而且由于 C 语言是 强类型的,因此在数据类型方面必然有很大的限制。
柯里化就是预先将函数的某些参数传入,得到一个简单的函数,但是预先传入的参数被保存在闭包中,因此会有一些奇特的特性。
var adder = function(num){ return function(y){ return num + y; } } var inc = adder(1); var dec = adder(-1); //这里的 inc/dec 两个变量事实上是两个新的函数,可以通过括号来调用,比如下例中的用法: //inc, dec现在是两个新的函数,作用是将传入的参数值(+/-)1 print(inc(99));//100 print(dec(101));//100 print(adder(100)(2));//102 print(adder(2)(100));//102
根据柯里化的特性,我们可以写出更有意思的代码,比如在前端开发中经常会遇到这样的情况,当请求从服务端返回后,我们需要更新一些特定的页面元素,也就是局部刷新的概 念。使用局部刷新非常简单,但是代码很容易写成一团乱麻。而如果使用柯里化,则可以很大程度上美化我们的代码,使之更容易维护。
//update会返回一个函数,这个函数可以设置id属性为item的web元素的内容 function update(item){ return function(text){ $("di##+item).html(text); } } //Ajax请求,当成功是调用参数callback function refresh(url, callback){ var params = { type : "echo", data : "" }; $.ajax({ type:"post", url:url, cache:false, async:true, dataType:"json", data:params, //当异步请求成功时调用 success: function(data, status){ callback(data); }, //当请求出现错误时调用 error: function(err){ alert("error : "+err); } }); } refresh("action.do?target=news",update("newsPanel")); refresh("action.do?target=articles",update("articlePanel")); refresh("action.do?target=pictures",update("picturePanel")); //其中,update 函数即为柯里化的一个实例,它会返回一个函数,即: update("newsPanel") = function(text){ $("div#newsPanel").html(text); }
由于 update(“newsPanel”)的返回值为一个函数,需要的参数为一个字符串,因此在 refresh 的 Ajax 调用中,当 success 时,会给 callback 传入服务器端返回的数据信息,从而实现 newsPanel 面板的刷新,其他的文章面板 articlePanel,图片面板 picturePanel 的刷新均采取这种方式,这样,代码的可读性,可维护性均得到了提高。
通常来讲,函数式编程的谓词(关系运算符,如大于,小于,等于的判断等),以及运算 (如加减乘数等)都会以函数的形式出现
因此,可以首先对这些常见的操作进行一些包装,以便于我们的代码更具有“函数式”风格:
function abs(x){ return x>0?x:-x;} function add(a, b){ return a+b; } function sub(a, b){ return a-b; } function mul(a, b){ return a*b; } function div(a, b){ return a/b; } function rem(a, b){ return a%b; } function inc(x){ return x + 1; } function dec(x){ return x - 1; } function equal(a, b){ return a==b; } function great(a, b){ return a>b; } function less(a, b){ return a<b; } function negative(x){ return x<0; } function positive(x){ return x>0; } function sin(x){ return Math.sin(x); } function cos(x){ return Math.cos(x); }
函数式编程的特点当然不在于编码风格的转变,而是由更深层次的意义。
使用 Y-结合子,可以做到对匿名函数使用递归。
var Y = function(f) { return (function(g) { return g(g); })(function(h) { return function() { return f(h(h)).apply(null, arguments); }; }); }; // var factorial = Y(function(func){ return function(x){ return x == 0 ? 1 : x * func(x-1); } }); factorial(10); // Y(function(func){ return function(x){ return x == 0 ? 1 : x * func(x-1); } })(10); //不要被上边提到的 Y-结合子的表达式吓到,事实上,在 JavaScript 中,我们有一种简单的方法来实现 Y-结合子: var fact = function(x){ return x == 0 : 1 : x * arguments.callee(x-1); } fact(10); // (function(x){ return x == 0 ? 1 : x * arguments.callee(x-1); })(10);//3628800
其中,arguments.callee 表示函数自身,而 arguments.caller
表示函数调用者,因此省去了很多复杂的步骤。
//函数的不动点 function fixedPoint(fx, first){ var tolerance = 0.00001; function closeEnough(x, y){return less( abs( sub(x, y) ), tolerance)}; function Try(guess){//try 是javascript中的关键字,因此这个函数名为大写 var next = fx(guess); //print(next+" "+guess); if(closeEnough(guess, next)){ return next; }else{ return Try(next); } }; return Try(first); } // 数层嵌套函数, function sqrt(x){ return fixedPoint( function(y){ return function(a, b){ return div(add(a, b),2);}(y, div(x, y)); }, 1.0); } print(sqrt(100));
fiexedPoint 求函数的不动点,而 sqrt 计算数值的平方根。
正如第三章提到的,JavaScript 对象是一个属性的集合,另外有一个隐式的对象:原型对象。原型的值可以是一个对象或者 null。一般的引擎实现中,JS 对象会包含若干个隐 藏属性,对象的原型由这些隐藏属性之一引用,我们在本文中讨论时,将假定这个属性的名 称为"__proto__"(事实上,SpiderMonkey 内部正是使用了这个名称,但是规范中并未做要求,因此这个名称依赖于实现)。
由于原型对象本身也是对象,根据上边的定义,它也有自己的原型,而它自己的原型对象又可以有自己的原型,这样就组成了一条链,这个链就是原型链。
JavaScritp 引擎在访问对象的属性时,如果在对象本身中没有找到,则会去原型链中查找,如果找到,直接返回值,如果整个链都遍历且没有找到属性,则返回 undefined.原 型链一般实现为一个链表,这样就可以按照一定的顺序来查找。
var base = { name : "base", getInfo : function(){ return this.name; } } var ext1 = { id : 0, __proto__ : base } var ext2 = { id : 9, __proto__ : base } print(ext1.id); print(ext1.getInfo()); print(ext2.id); print(ext2.getInfo());
0
base
9
base
var base = { name : "base", getInfo : function(){ return this.name; } } var ext1 = { id : 0, name : "ext1", __proto__ : base } print(ext1.id); print(ext1.getInfo()); // 0 ext1
这个运行效果同样验证了原型链的运行机制:从对象本身出发,沿着__proto__
查找, 直到找到属性名称相同的值(没有找到,则返回 undefined)。
var base = { name : "base", getInfo : function(){ return this.id + ":" + this.name; } } var ext1 = { id : 0, __proto__ : base } print(ext1.getInfo());
我们在 getInfo 函数中加入 this.id
,这个 id 在 base 对象中没有定义。同时,删掉了 ext1 对象中的 name 属性,执行结果如下:
0:base
应该注意的是,getInfo 函数中的 this 表示原始的对象,而并非原型对象。上例中的 id 属性来自于 ext1 对象,而 name 来自于 base 对象。如果对象没有显式的声明自己的”__proto__”属性,这个值默认的设置为Object.prototype
,而 Object.prototype
的”__proto__”属性的值为”null”,标志着原型链的终结。
我们在来讨论一下构造器,除了上边提到的直接操作对象的__proto__属性的指向以外,JavaScript 还支持构造器形式的对象创建。构造器会自动的为新创建的对象设置原型 对象,此时的原型对象通过构造器的 prototype 属性来引用。
我们以例子来说明,将 Task 函数作为构造器,然后创建两个实例 task1, task2:
function Task(id){ this.id = id; } Task.prototype.status = "STOPPED"; Task.prototype.execute = function(args){ return "execute task_"+this.id+"["+this.status+"]:"+args; } var task1 = new Task(1); var task2 = new Task(2); task1.status = "ACTIVE"; task2.status = "STARTING"; print(task1.execute("task1")); print(task2.execute("task2")); //execute task_1[ACTIVE]:task1 execute task_2[STARTING]:task2
构造器会自动为 task1,task2 两个对象设置原型对象 Task.prototype,这个对象被 Task(在此最为构造器)的 prototype 属性引用,参看下图中的箭头指向。
由于 Task 本身仍旧是函数,因此其”__proto__”属性为 Function.prototype
, 而内 建的函数原型对象的”__proto__”属性则为Object.prototype
对象。最后 Obejct.prototype
的”__proto__”值为 null.