函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块. 函数对任何一门语言来说都是一个核心的概念, 在 JavaScript 中更是如此. 本文将深入介绍函数的 5 个高级技巧.
作用域安全的构造函数
构造函数其实就是一个使用 new 操作符调用的函数
- function Person(name,age,job){
- this.name=name;
- this.age=age;
- this.job=job;
- }
- var person=new Person('match',28,'Software Engineer');
- console.log(person.name);//match
如果没有使用 new 操作符, 原本针对 Person 对象的三个属性被添加到 Windows 对象
- function Person(name, age, job) {
- this.name = name;
- this.age = age;
- this.job = job;
- }
- var person = Person('match', 28, 'Software Engineer');
- console.log(person); //undefinedconsole.log(Windows.name);//match
Windows 的 name 属性是用来标识链接目标和框架的, 这里对该属性的偶然覆盖可能会导致页面上的其它错误, 这个问题的解决方法就是创建一个作用域安全的构造函数.
- function Person(name, age, job) {
- if (this instanceof Person) {
- this.name = name;
- this.age = age;
- this.job = job;
- } else {
- return new Person(name, age, job);
- }
- }
- var person = Person('match', 28, 'Software Engineer');
- console.log(Windows.name); // ""console.log(person.name); //'match'var person= new Person('match',28,'Software Engineer');
- console.log(Windows.name); // ""console.log(person.name); //'match'
但是, 对构造函数窃取模式的继承, 会带来副作用. 这是因为, 下列代码中, this 对象并非 Polygon 对象实例, 所以构造函数 Polygon() 会创建并返回一个新的实例.
- function Polygon(sides) {
- if (this instanceof Polygon) {
- this.sides = sides;
- this.getArea = function() {
- return 0;
- }
- } else {
- return new Polygon(sides);
- }
- }
- function Rectangle(wifth, height) {
- Polygon.call(this, 2);
- this.width = this.width;
- this.height = height;
- this.getArea = function() {
- return this.width * this.height;
- };
- }
- var rect = new Rectangle(5, 10);
- console.log(rect.sides); //undefined
如果要使用作用域安全的构造函数窃取模式的话, 需要结合原型链继承, 重写 Rectangle 的 prototype 属性, 使它的实例也变成 Polygon 的实例.
- function Polygon(sides) {
- if (this instanceof Polygon) {
- this.sides = sides;
- this.getArea = function() {
- return 0;
- }
- } else {
- return new Polygon(sides);
- }
- }
- function Rectangle(wifth, height) {
- Polygon.call(this, 2);
- this.width = this.width;
- this.height = height;
- this.getArea = function() {
- return this.width * this.height;
- };
- }
- Rectangle.prototype = new Polygo
惰性载入函数
因为各浏览器之间的行为的差异, 我们经常会在函数中包含了大量的 if 语句, 以检查浏览器特性, 解决不同浏览器的兼容问题. 比如, 我们最常见的为 dom 节点添加事件的函数
- function addEvent(type, element, fun) {
- if (element.addEventListener) {
- element.addEventListener(type, fun, false);
- } else if (element.attachEvent) {
- element.attachEvent('on' + type, fun);
- } else {
- element['on' + type] = fun;
- }
- }
每次调用 addEvent 函数的时候, 它都要对浏览器所支持的能力进行检查, 首先检查是否支持 addEventListener 方法, 如果不支持, 再检查是否支持 attachEvent 方法, 如果还不支持, 就用 dom0 级的方法添加事件.
这个过程, 在 addEvent 函数每次调用的时候都要走一遍, 其实, 如果浏览器支持其中的一种方法, 那么他就会一直支持了, 就没有必要再进行其他分支的检测了. 也就是说, if 语句不必每次都执行, 代码可以运行的更快一些.
解决方案就是惰性载入. 所谓惰性载入, 指函数执行的分支只会发生一次, 有两种实现惰性载入的方式
1, 第一种是在函数被调用时, 再处理函数. 函数在第一次调用时, 该函数会被覆盖为另外一个按合适方式执行的函数, 这样任何对原函数的调用都不用再经过执行的分支了
我们可以用下面的方式使用惰性载入重写 addEvent()
- function addEvent(type, element, fun) {
- if (element.addEventListener) {
- addEvent = function(type, element, fun) {
- element.addEventListener(type, fun, false);
- }
- } else if (element.attachEvent) {
- addEvent = function(type, element, fun) {
- element.attachEvent('on' + type, fun);
- }
- } else {
- addEvent = function(type, element, fun) {
- element['on' + type] = fun;
- }
- }
- return addEvent(type, element, fun);
- }
在这个惰性载入的 addEvent() 中, if 语句的每个分支都会为 addEvent 变量赋值, 有效覆盖了原函数. 最后一步便是调用了新赋函数. 下一次调用 addEvent() 时, 便会直接调用新赋值的函数, 这样就不用再执行 if 语句了.
但是, 这种方法有个缺点, 如果函数名称有所改变, 修改起来比较麻烦.
2, 第二种是声明函数时就指定适当的函数. 这样在第一次调用函数时就不会损失性能了, 只在代码加载时会损失一点性能. 以下就是按照这一思路重写的 addEvent(). 以下代码创建了一个匿名的自执行函数, 通过不同的分支以确定应该使用哪个函数实现.
- var addEvent = (function() {
- if (document.addEventListener) {
- return function(type, element, fun) {
- element.addEventListener(type, fun, false);
- }
- } else if (document.attachEvent) {
- return function(type, element, fun) {
- element.attachEvent('on' + type, fun);
- }
- } else {
- return function(type, element, fun) {
- element['on' + type] = fun;
- }
- }
- })();
函数绑定
在 JavaScript 与 DOM 交互中经常需要使用函数绑定, 定义一个函数然后将其绑定到特定 DOM 元素或集合的某个事件触发程序上, 绑定函数经常和回调函数及事件处理程序一起使用, 以便把函数作为变量传递的同时保留代码执行环境.
- <button id="btn"> 按钮 </button>
- <script>var handler = {
- message: "Event handled.",
- handlerFun: function() {
- alert(this.message);
- }
- };
- btn.onclick = handler.handlerFun;</script>
上面的代码创建了一个叫做 handler 的对象. handler.handlerFun() 方法被分配为一个 DOM 按钮的事件处理程序. 当按下该按钮时, 就调用该函数, 显示一个警告框.
虽然貌似警告框应该显示 Event handled, 然而实际上显示的是 undefiend. 这个问题在于没有保存 handler.handleClick() 的环境, 所以 this 对象最后是指向了 DOM 按钮而非 handler.
可以使用闭包来修正这个问题
- <button id="btn"> 按钮 </button>
- <script>var handler = {
- message: "Event handled.",
- handlerFun: function() {
- alert(this.message);
- }
- };
- btn.onclick = function() {
- handler.handlerFun();
- }</script>
当然这是特定于此场景的解决方案, 创建多个闭包可能会令代码难以理解和调试. 更好的办法是使用函数绑定.
一个简单的绑定函数 bind() 接受一个函数和一个环境, 并返回一个在给定环境中调用给定函数的函数, 并且将所有参数原封不动传递过去.
- function bind(fn, context) {
- return function() {
- return fn.apply(context, arguments);
- }
- }
这个函数似乎简单, 但其功能是非常强大的. 在 bind() 中创建了一个闭包, 闭包使用 apply() 调用传入的函数, 并给 apply() 传递 context 对象和参数. 当调用返回的函数时, 它会在给定环境中执行被传入的函数并给出所有参数.
- <button id="btn"> 按钮 </button>
- <script>function bind(fn, context) {
- return function() {
- return fn.apply(context, arguments);
- }
- }
- var handler = {
- message: "Event handled.",
- handlerFun: function() {
- alert(this.message);
- }
- };
- btn.onclick = bind(handler.handlerFun, handler);</script>
ECMAScript5 为所有函数定义了一个原生的 bind() 方法, 进一步简化了操作.
只要是将某个函数指针以值的形式进行传递, 同时该函数必须在特定环境中执行, 被绑定函数的效用就突显出来了. 它们主要用于事件处理程序以及 setTimeout() 和 setInterval().
然而, 被绑定函数与普通函数相比有更多的开销, 它们需要更多内存, 同时也因为多重函数调用稍微慢一点, 所以最好只在必要时使用.
函数柯里化
与函数绑定紧密相关的主题是函数柯里化 (function currying), 它用于创建已经设置好了一个或多个参数的函数. 函数柯里化的基本方法和函数绑定是一样的: 使用一个闭包返回一个函数. 两者的区别在于, 当函数被调用时, 返回的函数还需要设置一些传入的参数.
- function add(num1, num2) {
- return num1 + num2;
- }
- function curriedAdd(num2) {
- return add(5, num2);
- }
- console.log(add(2, 3)); //5
- console.log(curriedAdd(3));//8
这段代码定义了两个函数: add() 和 curriedAdd(). 后者本质上是在任何情况下第一个参数为 5 的 add() 版本. 尽管从技术来说 curriedAdd() 并非柯里化的函数, 但它很好地展示了其概念.
柯里化函数通常由以下步骤动态创建: 调用另一个函数并为它传入要柯里化的函数和必要参数.
下面是创建柯里化函数的通用方式:
- function curry(fn) {
- var args = Array.prototype.slice.call(arguments, 1);
- return function() {
- var innerArgs = Array.prototype.slice.call(arguments),
- finalArgs = args.concat(innerArgs);
- return fn.apply(null, finalArgs);
- };
- }
curry() 函数的主要工作就是将被返回函数的参数进行排序. curry() 的第一个参数是要进行柯里化的函数, 其他参数是要传入的值.
为了获取第一个参数之后的所有参数, 在 arguments 对象上调用了 slice() 方法, 并传入参数 1 表示被返回的数组包含从第二个参数开始的所有参数. 然后 args 数组包含了来自外部函数的参数. 在内部函数中, 创建了 innerArgs 数组用来存放所有传入的参数 (又一次用到了 slice()).
有了存放来自外部函数和内部函数的参数数组后, 就可以使用 concat() 方法将它们组合为 finalArgs, 然后使用 apply() 将结果传递给函数. 注意这个函数并没有考虑到执行环境, 所以调用 apply() 时第一个参数是 null.curry() 函数可以按以下方式应用.
- function add(num1, num2) {
- return num1 + num2;
- }
- var curriedAdd = curry(add, 5);
- alert(curriedAdd(3)); //8
在这个例子中, 创建了第一个参数绑定为 5 的 add() 的柯里化版本. 当调用 cuurriedAdd() 并传入 3 时, 3 会成为 add() 的第二个参数, 同时第一个参数依然是 5, 最后结果便是和 8. 也可以像下例这样给出所有的函数参数:
- function add(num1, num2) {
- return num1 + num2;
- }
- var curriedAdd2 = curry(add, 5, 12);
- alert(curriedAdd2()); //17
在这里, 柯里化的 add() 函数两个参数都提供了, 所以以后就无需再传递给它们了, 函数柯里化还常常作为函数绑定的一部分包含在其中, 构造出更为复杂的 bind() 函数.
- function bind(fn, context) {
- var args = Array.prototype.slice.call(arguments, 2);
- return function() {
- var innerArgs = Array.prototype.slice.call(arguments),
- finalArgs = args.concat(innerArgs);
- return fn.apply(context, finalArgs);
- };
- }
对 curry() 函数的主要更改在于传入的参数个数, 以及它如何影响代码的结果. curry() 仅仅接受一个要包裹的函数作为参数, 而 bind() 同时接受函数和一个 object 对象.
这表示给被绑定的函数的参数是从第三个开始而不是第二个, 这就要更改 slice() 的第一处调用. 另一处更改是在倒数第 3 行将 object 对象传给 apply(). 当使用 bind() 时, 它会返回绑定到给定环境的函数, 并且可能它其中某些函数参数已经被设好.
要想除了 event 对象再额外给事件处理程序传递参数时, 这非常有用.
- var handler = {
- message: "Event handled",
- handleClick: function(name, event){
- alert(this.message + ":" + name + ":" + event.type);
- }
- };var btn = document.getElementById("my-btn");
- EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn"));
handler.handleClick() 方法接受了两个参数: 要处理的元素的名字和 event 对象. 作为第三个参数传递给 bind() 函数的名字, 又被传递给了 handler.handleClick(), 而 handler.handleClick() 也会同时接收到 event 对象.
ECMAScript5 的 bind() 方法也实现函数柯里化, 只要在 this 的值之后再传入另一个参数即可.
- var handler = {
- message: "Event handled",
- handleClick: function(name, event) {
- alert(this.message + ":" + name + ":" + event.type);
- }
- };
- var btn = document.getElementById("my-btn");
- EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler, "my-btn"));
JavaScript 中的柯里化函数和绑定函数提供了强大的动态函数创建功能. 使用 bind() 还是 curry() 要根据是否需要 object 对象响应来决定. 它们都能用于创建复杂的算法和功能, 当然两者都不应滥用, 因为每个函数都会带来额外的开销.
函数重写
由于一个函数可以返回另一个函数, 因此可以用新的函数来覆盖旧的函数.
- function a(){
- console.log('a');
- a = function(){
- console.log('b');
- }
- }
这样一来, 当我们第一次调用该函数时会 console.log('a') 会被执行; 全局变量 a 被重定义, 并被赋予新的函数
当该函数再次被调用时, console.log('b') 会被执行.
再复杂一点的情况如下所示:
- var a = (function() {
- function someSetup() {
- var setup = 'done';
- }
- function actualWork() {
- console.log('work');
- }
- someSetup();
- return actualWork;
- })()
我们使用了私有函数 someSetup() 和 actualWork(), 当函数 a() 第一次被调用时, 它会调用 someSetup(), 并返回函数 actualWork() 的引用.
来源: http://www.css88.com/web/javascript/11580.html