分析javascript
javascript比较好的思想:函数、弱类型、动态对象、对象字面量表示法
不好的思想:基于全局变量的编程模型
函数
函数对象
函数就是对象,新创建的函数会连接到Function.prototype上,没和函数创建时附带有两个隐藏属性:函数上下文和实现函数行为的代码。(后者使得函数可以被调用)
函数字面量
通过函数字面量创建的函数对象包含一个连接到外部上下文的连接,被称为闭包,这是javascript强大表现力的根基。
调用
除了声明的形式参数外,函数接收两个附带的参数:this和arguments。参数this在面向对象编程中非常重要,他的值取决于调用的模式
在javascript中一共有四种调用模式:方法调用模式、函数调用模式、构造器调用模式和apply调用模式。
函数调用时如果传入的参数个数和定义的参数个数不一致,不会产生运行时错误,实际参数多于形式参数,超出的参数值会被忽略,少于的话直接被定义为undefined。
方法调用模式
当函数作为对象的一个方法被调用时,this会绑定到该对象。this到对象的绑定发生在函数调用的时候。这个超级迟邦定使得函数可以对this高度复用。通过this可取得他们所属对象的上下文的方法被成为公共方法。
函数调用模式
当一个函数并非一个对象的属性时,此时是函数调用。这时this被绑定到全局对象,这是语言设计的一个错误,这个设计错误的后果是方法不能被利用内部函数来帮他工作,因为内部函数的this被绑定到了全局对象上,并不是外部对象,不能共享方法对对象的访问权,解决这个问题的方案是在该方法中定义一个变量,将外部的this赋值给这个变量,然后在内部函数中使用。
var that = this
构造器调用模式
javascript是一门基于原型继承的语言,这意味着对象可以直接从其他对象继承属性。该语言是无类别的。
如果在一个函数前面加上new来调用,那么将创建一个隐藏连接到该函数的prototype成员的新对象,同时this将会绑定到新对象上。new前缀也会改变return语句的行为。
var Quo = function(string){this.status = string;
}
Quo.prototype.get_status = function(){return this.status;
}
var myQuo = new Quo("confused");
但是并不推荐使用这种形式的构造器函数。在之后将会看到更好的替代方式。
Apply调用模式
因为javascript是一门函数式的面向对象编程语言,所以函数也可以拥有方法。
apply方法可以让我们传入参数去调用函数,apply方法接收两个参数。第一个是将被绑定的this的值,第二个是参数数组。
//构造一个包含 status 成员的对象,
var statusObject={
status:"A-OK
}
//statusObject并没有继承自Quo.prototype,但我们可以在 statusObject 上调
//用get status 方法,尽管 statusObject并没有一个名为 get status 的方法。
var status =Quo.prototype.getstatus.apply(statusObject);
//stats 值为'A-OK'
参数
函数会附带一个参数arguments,通过他可以访问到所有传入函数的参数,这使得我们可以实现传入任意数量参数的函数
//构造一个将很多个值相加的函数
//注意该函数内部定义的变量 sum 不会与函数外部定义的 sum 产生冲突
//该函数只会看到内部的那个变量。
var sum=function(){var i,sum=0;for(i=0;i<arguments.length;i+=l){sum += arguments[i];}return sum;
document.writeln(sum(4,8,15,16,23,42));//108
后续将为数组添加一个相似的方法来达到同样的效果
arguments并不是一个真正的数组,他只是一个‘类似数组(array-like)’的对象。arguments拥有一个length属性,但他缺少所有的数组方法,这是语言设计的一个错误。
返回
return语句用来让函数返回。
一个函数总是会返回一个值,如果没有指定返回值,则会返回undefined。
如果函数以new前缀的方式进行调用,且返回值不是一个对象,则会返回this(该新对象)。
异常
当查出事故时,可以在try中通过throw抛出异常,throw会中断函数的执行,将控制权跳转到catch从句中
给类型增加方法
javascript允许为语言的基本类型增加方法,通过对基本类型的prototype添加方法来使得该方法对所有对象可用,这样的方法对函数、字符串、布尔值、数组、正则表达式、数字同样适用。
例如 我们可以为Function.prototype添加方法来使得该方法对所有的函数可用:
Function.prototype.method = function(name, func){this.prototype[name] = func;return this;
}
通过为Function的prototype添加一个方法,使得所有函数都可以直接调用该方法,为对应的函数的原型(不是Function)添加新的方法,并且添加时不再需要键入prototype这个属性名,这个缺点也就被掩盖了。
例如,为数字类型添加一个取整数的方法,如果没有method,我们需要这样定义:
Number.prototype.integer = function(){return Math[this<0?"ceiling":"floor"](this);
}
现在有了method方法,可以直接通过method方法来实现
Number.method('integer',function(){return Math[this<0?"ceiling":"floor"](this);
}):
// 所有对应的对象都可以使用这个方法了
document.writeln((-10/3).integer());//-3
在 JavaScript 中,Number
是一个内置的构造函数,它也是一个函数对象。因此,Number
也继承自 Function.prototype
。这意味着我们可以给 Function.prototype
添加方法,然后 Number
也可以使用这些方法。
通过给javascript基本类型增加方法,可以提高语言的表现力。并且javascript原型的继承是动态属性,新的方法添加后会被立即赋予到所有的值(对象实例)上,哪怕对象实例在方法创建前就已经建好了。
基本类型的原型是公共的结构,所以在类库混用时务必小心。一个保险的做法就是只在确定没有该方法时才添加它。
//有条件地增加一个方法
Function.prototype.method=function(name,func){if(!this.prototype[name]){this.prototype[name]= func;}
}
另一个要注意的就是for in语句用在原型上时表现很糟糕,他会沿着原型链遍历所有的属性名。可以使用 hasownProperty 方法去筛选出继承而来的属性,或者我们可以查找特定的类型。
递归
javascript也支持递归,去调用自身
一些语言提供了尾递归优化。这意味着如果一个函数返回自身递归调用的结果,那么调用的过程会被替换为一个循环,它可以显著提高速度。遗憾的是,JavaScript 当前并没有提供尾递归优化。深度递归的函数可能会因为返回堆栈溢出而运行失败。
作用域
在编程语言中,作用域控制着变量与参数的可见性及生命周期。对程序员来说这是一个重要的帮助,因为它减少了名称冲突,并且提供了自动内存管理。
然而javascript没有块级作用域,只有函数作用域。函数中的参数和变量在函数外部不可见,但在函数中的任何位置定义的变量在该函数中的任何地方都是可见的。
现代语言都推荐尽可能的推迟声明变量,但用在javascript上却不合适,因为他缺少块级作用域,最好的方法就是在函数体顶部声明函数中可能用到的所有变量。
闭包
闭包在于利用了内部函数拥有比外部函数更长的声明周期。
和以对象字面量形式去初始化 myobject不同,我们通过调用一个函数的形式去初始化myobject,该函数将返回一个对象字面量。此函数定义了一个value变量。该变量对increment和 getvalue方法总是可用的,但函数的作用域使得它对其他的程序来说是不可见的。
var myObject=function(){var value =0;return{increment:function(inc){value += typeof inc === 'number' ? inc :1;},getValue:function(){return value;}}
}();
注意最后的(),我们并没有对myObject返回一个函数,而是返回了一个对象字面量,包含两个方法,这些方法依旧保留对value访问的特权,而无法通过其他途径访问或非法更改。
理解内部函数能访问外部函数的实际变量而无须复制是很重要的
糟糕的例子
//构造一个函数,用错误的方式给一个数组中的节点设置事件处理程序
//当点击一个节点时,按照预想应该弹出一个对话框显示节点的序号
//但它总是会显示节点的数目。
var add_the_handlers=function(nodes){var i;for(i=0;i<nodes.length;i+=1){nodes[i].onclick =function(e){alert(i);}}
}
add_the_handlers 函数目的是给每个时间处理器一个唯一值(i)。它未能达到目的是因为事件处理器函数绑定了变量i,而不是函数在构造时的变量i的值。他会直接访问到实际变量而不是复制值。
因此我们需要做的就是在构造时传入的是复制值,通过在构造时运行一个函数,传入参数i,此时就成为了复制值。
var add_the_handlers =function(nodes){var i;for(i=0;i<nodes.length;i+=1){nodes[i].onclick=function(i){return function(e){alert(i);}}(i);}
}
现在,我们定义了一个函数并立即传递i进去执行,而不是把一个函数赋值给 onclick。那个函数将返回一个事件处理器函数。
这个事件处理器函数绑定的是传递进去的i的值,而不是定义在 add_the_handlers 函数里的i的值。那个被返回的函数被赋值给 onclick。
回调
函数可以让不连续事件的处理变得更容易。例如在像服务器发送请求时,同步很容易造成响应性降低,导致客户端进入假死状态。更好的方式是发起异步请求,提供一个当服务器的响应到达时将被调用的回调函数,异步的函数立即返回。
模块
可以使用函数和闭包来构造模块,模块是一个提供接口但是会隐藏状态和实现的函数或对象。通过使用函数去产生模块,几乎可以摒弃全局变量的使用。
模块函数利用了函数作用域和闭包来创建绑定对象与私有成员的关联。
模块模式的一般形式:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把他们保存到一个可以访问到的地方。
使用模块模式可以摒弃全局变量的使用。它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象,模块模式非常有效。
模块模式也可以用来产生安全的对象。
String.method('deentityify',function(){//字符实体表。它映射字符实体的名字到对应的字符。var entity={quot:'"',lt:'<',gt:'>'};// 返回 deentityify 方法。return function(){//这才是 deentityify 方法。它调用字符串的 replace 方法,// 查找'&'开头和';"结束的子字符串。如果这些字符可以在字符实体表中找到//那么就将该字符实体替换为映射表中的值。它用到了一个正则表达式(参见第7章)。return this.replace(/&([^&;]+);/g,function(a,b){var r=entity[b];return typeof r==='string"?r:a;});}
}());
请注意最后一行。我们用()运算法立刻调用我们刚刚构造出来的函数。这个调用所创建并返回的函数才是 deentityify方法。
document.writeln('slt;">'.deentityify());//<">
模块模式利用了函数作用域和闭包来创建绑定对象与私有成员的关联,在这个例子中,只有 deentityify方法有权访问字符实体表这个数据对象。
在函数中的this此时指向调用它的字符串,为方法调用模式。
级联
对于一些没有定义返回值的方法,我们可以让其返回this而不是undefined,就可以启用级联
getElement('myBoxDiv")
.move(350,150)
.width(100)
.height(100)
.color('red')
.border('10px outset')
.padding('4px')
.appendText("Please stand by").
级联可以产生具备很强表现力的接口。它也能帮助控制那种构造试图一次做太多事情的接口的趋势。
套用
套用允许我们将函数与传递给它的参数相结合去产生出一个新的函数
记忆
函数可以用对象去记住之前的操作,从而可以避免无谓的计算。这种优化被称为记忆。
比如计算斐波那契数列,将计算的结果存下来,后续再需要这个值时直接拿出来即可,无须重复计算
我们还可以将这种形式一般化,编写一个函数来帮助我们构造带记忆功能的函数。
var memoizer=function (memo,fundamental){var shell=function(n){var result=memo[n];if(typeof result!=='number'){result =fundamental(shell,n)memo(n]= result;}return result;}
};
return shell;
现在,我们可以使用memoizer来定义fibonacci数,提供其初始的memo数组和fundamental函数:
var fibonacci=memoizer([0,1],function(shell,n){return shell(n-1)+shell(n-2);
});
通过设计能产生出其他函数的函数,可以极大减少我们必须要做的工作。例如:要产生一个可记忆的阶乘函数,我们只须提供基本的阶乘公式即可:
var factorial =memoizer([1,1],function(shell,n){return n*shell(n-1);
});