JavaScript设计模式(一)
什么是设计模式呢? 就是指对于类似的问题,我们可以用大致相同的思想、方法去解决之,而这种通用的思想、方法就是设计模式。学习设计模式可以帮助我们在遇到问题时迅速地搜索出一种清晰的思路来实现之。
第一部分: 面向对象的JavaScript
1. JavaScript是动态类型语言。
静态类型语言即强迫规定程序员在使用某个变量时先定义它的类型。而动态类型语言是在程序运行的时候,才会具有某个类型,不需要严格定义。显然,静态类型语言要求更严格,而动态类型语言却无法保证变量的类型。JavaScript就是这样的动态类型语言,它的好处是我们可以花更多的时间关注在逻辑上,而不是变量的定义上,代码会更加简洁。 静、动我们就可以理解为变量类型的静与动。
2. 面向接口编程和鸭子模型
JavaScript王国需要100只鸭子来组成合唱团,但最后只能找到99只, 而正巧发现一只鸡的叫声也是嘎嘎嘎,于是我们就把鸡也拉近了鸭子合唱团。 这就是说,我们只关注对象的行为,而不关注对象本身。
基于这种思想,比如一个对象若有push和pop方法,就可以把它当作栈来使用等等,这种思想的编程就是面向接口编程。
3.多态
多态的实际含义是: 同一个操作作用在不同的对象上面,可以产生不同的解释和不同的执行结果。 如下所示:
var makeSound = function(animal) { if (animal instanceof Duck) { console.log("嘎嘎嘎"); } if (animal instanceof Chicken) { console.log("咯咯咯"); } } var Duck = function() {}; var Chicken = function() {}; makeSound(new Duck()); makeSound(new Chicken());
可以看出makeSound函数对于不同的输入就有不同的输出,这就是多态。
但是如果再添加一只狗呢? 我们不仅要在创建一个狗的构造函数,还要改变makeSound函数,即这样的可扩展性是十分糟糕的。
解决方法: 多态背后最重要的思想是将“做什么”和“谁去做以及怎么样去做”分离开来,也就是将不变的事物和可变的事物分离开来。
如下所示:
var makeSound = function(animal) { animal.sound(); // 做什么 } var Duck = function() {}; // 谁去做 Duck.prototype.sound = function() { console.log("嘎嘎嘎"); // 怎么做 }; var Chicken = function() {}; Chicken.prototype.sound = function() { console.log("咯咯咯"); }; var Dog = function() {}; Dog.prototype.sound = function() { console.log("汪汪汪"); }; makeSound(new Duck()); makeSound(new Chicken());
可以看出在上面的例子中不变的部分就是animal.sound(),我们将之分离出来(做什么)。 然后再将谁去做,怎么做分离出来,这样函数的可扩展性就非常好了。
4. 封装
封装的思想在于隐藏内部的实现、 提高代码的可重用性、 封装变化。
5.原型模式
在以类为中心的面向对象变成语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建。
但是在原型编程的思想中,类不是必须的,对象未必必须从类中创建而来,一个对象是通过克隆另外一个对象而得到的。
第二部分:this、 call、 apply
我在《JavaScript函数之美~》中详尽的介绍了this的用法。这里还是要提及一些重点。
我们知道:this总是指向一个对象,也许是window对象,也许是调用它所在的方法的对象,也有可能是新创建的一个对象,具体执行的对象是运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
情况一:作为对象的方法调用,this指向的是该对象。
情况二:作为普通函数调用,而非对象的方法,this指向的全局对象window。
情况三:构造器调用,那么就指向这个新创建的函数。
情况四:Function.prototype.call或Function.prototype.apply调用。
注意点一: 在一个对象中,如果存在回调函数,我们还想this存在,该怎么办?
可以把 this 在对象的环境中赋值给 that,然后使用that,这时就有正确的指向了。
注意点二: 构造器调用的过程中,如果最后返回了对象,那么this就会指向这个返回的对象。如下:
var MyClass = function() { this.name = "zzw"; return { name: "htt" }; } var person = new MyClass(); console.log(person.name); // htt
如果说这里构造函数并没有返回一个对象,那么最终的结果一定是zzw,但是如果返回了对象,那么这个this指向的一定是这个返回的对象。
注意点三: docuemnt.getElementById()方法需要用到this
var getId = function(id) { return document.getElementById(id); } getId("div").style.color = "red";
这样我们就可以成功获取到id为div的元素。但是:
var getId = document.getElementById; getId("div").style.color = "red";
这样就会报错。
这时因为用getId来引用document.getElementById()之后,再调用getId,此时就变成额普通函数调用,内部的this就指向了window而不是document。(许多引擎的document.getElementById方法的内部实现需要用到this)。
在JavaScript版本的设计模式中,call和apply方法都是很常用的,能熟练应用这两个方法是我们真正成为一名JavaScript程序员的重要一步。
Function.prototype.apply 和 Function.prototype.call 显然都是被一个函数(Function)调用的。
其中apply接受两个参数,第一个参数指定了函数体内this对象的指向(之前说过,this总是指向一个对象),第二个参数是一个到右下标的集合,这个集合可以是数组,也可以是类数组,apply把这个集合中的元素作为参数传递给被调用的函数。 而call也接收两个参数,同样的,第一个参数指定了函数体内的this对象的指向,第二个参数是这个函数需要接受的参数(没有call和apply函数也要接收参数啊!)。举例如下:
var myObject = { c: 66 }; var anotherObject = { c: 88 } var c = 233; function outputNum(a, b) { var c = 10; console.log([a, b, this.c]); } function outputNumSecond(a, b) { c = 10; console.log([a, b, this.c]); } outputNum(1, 2); // [1, 2, 233] outputNum.apply(null, [1,2]); // [1, 2, 233] outputNum.apply(window, [1,2]); // [1, 2, 233] outputNum.call(null, 1, 2); // [1, 2, 233] outputNum.call(window, 1, 2); // [1, 2, 233] outputNum.apply(myObject, [1, 2]); // [1, 2, 66] outputNum.apply(anotherObject, [1, 2]); // [1, 2, 88] outputNumSecond.apply(null, [1, 2]); // [1, 2, 10]
如果第一个参数是null,那么this就会指向默认的宿主对象,在浏览器中就是window。 但是在严格模式下, 函数体内的this还是为null。
常见错误
document.getElementById("div").onclick = function() { alert(this.id); // div var func = function() { alert(this.id); //undefined } func(); }
这是因为func函数中的this指向的是window,而window是没有id的。 如果想要得到正确的结果,我们只需要使用apply或call改变this的指向。
document.getElementById("div").onclick = function() { alert(this.id); // div var func = function() { alert(this.id); // 两次都是div } func.call(this); func.apply(this); }
这样,我们就可以得到想要的结果了。
第三部分:闭包和高阶函数
因为在JavaScript版本的设计模式中,许多模式都可以使用闭包和高阶函数来实现,所以这里重点强调闭包和高阶函数的使用。
一:闭包
闭包对于JavaScript程序员来说,是一个难点但又是必须征服的知识点。下面介绍一些与闭包相关的知识点和闭包的应用
1. 变量的作用域
变量的作用域,就是指变量的有效范围,我们最常谈到的就是在函数中声明的变量作用域。 在JavaScript中,函数可以用来创造函数作用域,此时的函数像一层半透明的玻璃,在函数里面可以看到外面的变量,在函数外面却无法看到函数里面的变量。
2.变量的声明周期
除了变量的作用域外,另一个跟闭包有关的就是变量的声明周期,对于全局变量而言,它的声明周期当然是永久的,除非我们主动销毁这个全局变量,而对于在函数内使用var关键字声明的局部变量而言,当退出函数时,这些局部变量就失去了他们的价值,他们都会随着函数调用的结束而被销毁。
举例如下:
var func = function() { var a = 1; console.log(a); // 1 } func();
函数func在被调用之后, a 就没有存在的价值了,所以a会随着函数调用的结束而被销毁。
var func = function() { var a = 1; return function() { a++; console.log(a); } } var f = func(); // func函数被调用,并将返回的匿名函数赋值给f。 f(); // 2 f(); // 3 f(); // 4 f(); // 5
在这里我们可以看出,虽然func()已经被调用,将道理其中的变量a也应该被销毁了,但是我们后面调用f函数时,发现还是可以引用上a的,说明a并没有被销毁,也就是说,闭包的存在使得局部变量在调用结束之后有了不被销毁的理由!!!
实用的例子!
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>this</title> </head> <body> <div class="div">我是一个div</div> <div class="div">我是一个div</div> <div class="div">我是一个div</div> <div class="div">我是一个div</div> <div class="div">我是一个div</div> <script> var divs = document.querySelectorAll(".div"), len = divs.length, i; for (i = 0; i < len; i++ ) { divs[i].onclick = function() { console.log(i); // 全部是5 } } </script> </body> </html>
上述代码中,我的本意是当我点击不同的div时,得到其相应的index,但是最后发现我们得到的永远都是5, 这是因为最后点击时,for循环早已结束,而这时的i就已经变成 5 了。
解决方法: 形成闭包
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>this</title> </head> <body> <div class="div">我是一个div</div> <div class="div">我是一个div</div> <div class="div">我是一个div</div> <div class="div">我是一个div</div> <div class="div">我是一个div</div> <script> var divs = document.querySelectorAll(".div"), len = divs.length, i; for (i = 0; i < len; i++ ) { (function(i) { divs[i].onclick = function() { console.log(i); // 分别得到 0 1 2 3 4 } })(i) } </script> </body> </html>
这时我们发现得到的就都是相应的index值了。
原理: 每次循环,都形成了一个匿名函数, 给匿名函数传递参数i,并立即调用,这样就形成了块级作用域,而里面的onclick后面的函数就是闭包了,i值就是这个块级作用域的私有变量。
第一个i是传递进去的参数,这样才能分别保留每一个i值,必须要有; 最后的i是调用函数, 也必须有。
3. 闭包的更多用途
(1). 封装变量
(2). 延续局部变量的寿命
二、 高阶函数
高阶函数是指至少满足下列条件之一的函数:
-
函数可以作为参数被传递
-
函数可以作为返回值输出
在JS中的函数显然是满足高阶函数的特点的。
1. 函数作为参数被传递
回调函数
在ajax异步请求中,回调函数的使用就非常的频繁,不做过多说明
还有就是sort方法。
console.log([15,58,3,33].sort(function(a, b){ return a - b; })); //[3,15,33,58]
2. 函数作为返回值输出
3. 高阶函数的其他应用 --- 函数柯里化
即currying,又称为部分求值。 一个currying的函数会首先接收一些参数,在接收了参数之后并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来,等到真正需要求值的时候,之前传入的所有参数就会被一次性用于求值。举例如下:
var monthlyCost = 0; var cost = function(money) { monthlyCost += money; }; cost(100); // 第一天的开销 cost(200); // 第二天的开销 cost(300); // 第三天的开销 // cost(700); // 第三十天开销 alert( monthlyCost );
这里它每天都记录花了多少钱,并计算一共花了多少,但是我们其实并不太关心截至今日一共花了多少,而是关心30天一共花了多少, 所以每天都进行一次计算是浪费的。
如果我们可以在每个月的前29天只是保存当天的开销,知道第30天才开始计算一共的开销不就可以减少计算量了吗? 这样的思路就是currying的实现思路。
var currying = function( fn ) { var args = []; return function() { if ( arguments.length === 0 ) { return fn.apply( this,args ); } else { [].push.apply( args, arguments ); return arguments.callee; } } }; var cost = (function() { var money = 0; return function() { for ( var i = 0, l = arguments.length; i < l; i++ ) { money += arguments[i]; } return money; } })(); var cost = currying( cost ); cost(100); cost(200); cost(300); alert(cost());
上面的过程即完成了一个currying函数的编写,当调用cost()时,如果明确带上了一些参数,表示此时并不进行真正的求值计算,而是把这些参数保存起来, 此时让cost函数返回另一个函数,只有不带参数的形式执行cost()时,才利用前面保存的所有参数,真正开始进行求值计算。