什么是设计模式呢? 就是指对于类似的问题,我们可以用大致相同的思想、方法去解决之,而这种通用的思想、方法就是设计模式。学习设计模式可以帮助我们在遇到问题时迅速地搜索出一种清晰的思路来实现之。
第一部分: 面向对象的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。
常见错误