JavaScript设计模式与开发实践读书笔记1
首先感谢牛客网的这次活动,能够给我这样一次机会去读书和学习。在这次活动中,作为一个即将踏入工作岗位的前端工程师,我选择了去学习JavaScript设计模式与开发实践这本书。之所以选择这本书,主要是考虑到了在写JS代码的过程中,要解决一个问题往往有多种解决方案,而面临这些方案时,常常手足无措,不知道选择哪种方案。比如我们知道在JS中,实现一个类的继承,就可以通过原型链继承,构造继承,实例继承,拷贝继承等方法,如何在这些方法中选择简洁、高效、可靠的方法一直是困扰我的问题。希望通过学习此书,治好我写代码时的“选择困难症”,帮助我写出简洁、高效、可靠的代码。下面开始正文部分。
这篇博客是我们读书笔记的第一部分,主要对应书中的前言和第一部分:基础知识。
前言
首先什么是设计模式?
面向对象软件设计中针对特定问题的简洁而优雅的解决方案称之为设计模式
设计模式就是软件设计中的模板。我们学习设计模式,就好比高中时背作文模板。有了模板,第一,在写代码的过程中,我们可以少一些纠结,多一份速度;第二,我们写出来的东西遵照某种规则,方便阅读和维护,“也容易得到一个好的分数”。因此设计模式对于一个程序员而言是一样很重要的东西。
第一部分:基础知识
第1章面向对象的JavaScript
设计模式是针对面向对象语言而提出的,在学习设计模式之前,我们首先要了解JS作为面向对象语言的一些特性。
JS是一门动态类型语言
动态类型语言是相对静态类型语言而说的,所谓动态类型语言,指的是声明变量的时候无法确定变量的数据类型,而只有在赋值后,变量的类型才能确定下来,并且变量的类型是可以改变的。而静态类型语言,在声明的时候就必须规定变量的类型,并且在后序的过程中无法改变。JS作为一种动态类型语言,有许多优势,比如它可以做到函数和参数类型以及返回值类型的解耦。在使用某个函数时,只要参数满足某种条件能够让这个函数执行,那我们可以不考虑参数的类型。这就是所谓的“鸭子类型”,
像鸭子一样走路并且“嘎嘎”叫的就是鸭子
举个例子,
function iAmDuck(animal) { animal.duckWalk(); animal.duckSing(); } var duck = { name: 'Duck', duckWalk: function() {}, duckSing: function() {}, }; var chicken = { name: 'Chicken', duckWalk: function() {}; duckSing: function() {}; }; iAmDuck(duck); //不会报错 iAmDuck(chicken); //不会报错
我们可以看到尽管duck和chicken是两个不同的对象,但是他们都有duckWalk()和duckSing()的方法,因此它们两个作为iAmDuck()参数时,函数都能执行,不会报错。但是对于Java这样的静态类型语言,函数是和参数的类型耦合在一起的,如果我们声明了iAmDuck()的形参是Duck类型的,即使chicken和duck都有函数所需的特定方法,chicken也不能作为iAmDuck()的实参。我们把上面的代码用Java重新写一遍。
public class Duck { String name; public void duckWalk() {}; public void duckSing() {}; } public class Chicken { String name; public void duckWalk() {}; public void duckSing() {}; } public class Test { public static void iAmDuck(Duck duck) { //只接受Duck类型的参数 duck.duckWalk(); duck.duckSing(); } public static void main(String args[]) { Duck duck = new Duck(); Chicken chicken = new Chicken(); duck.name = "Duck"; chicken.name = "Chicken"; iAmDuck(duck); //不会报错 iAMDuck(chicken); //会报错 } }
上面的那段Java代码,要想不报错,必须借助抽象类或者接口将对象进行向上转型,代码如下,
public abstract Animal { abstract String name; abstract void duckWalk() {}; abstract void duckSing() {}; } public class Duck { String name; public void duckWalk() {}; public void duckSing() {}; } public class Chicken { String name; public void duckWalk() {}; public void duckSing() {}; } public class Test { public static void iAmDuck(Animal animal) { //只接受Duck类型的参数 animal.duckWalk(); animal.duckSing(); } public static void main(String args[]) { Animal duck = new Duck(); Animal chicken = new Chicken(); duck.name = "Duck"; chicken.name = "Chicken"; iAmDuck(duck); //不会报错 iAMDuck(chicken); //不会报错 } }
我们通过抽象类,将duck和chicken的真实类型隐藏在Animal下,使得它们可以在类型监视系统的监督下,相互替换使用。JS则简单许多,
“我们不必借助超类型的帮助,就能轻松地在在JS中实现一个原则:‘面向接口编程,而不是面向实现编程’”。
下面我们介绍一下面向对象语言的三个重要概念,多态、封装和继承。
多态
多态在面向对象编程中指的是,
相同的操作作用于不同的对象会产生不同的结果。
下面举例说明一下,
假设我们有一只狗和一只猫,我们发出叫的命令,狗会“汪汪汪”的叫,猫则会“喵喵喵”的叫,这就是多态。用JS代码实现这个过程,
function makeSound(animal) { if(animal instanceof Dog) { console.log('汪汪汪'); }esle if(animal instanceof cat) { console.log('喵喵喵'); } } function Cat() {} function Dog() {} makeSound(new Cat()); //喵喵喵 makeSound(new Dog()); //汪汪汪
上面的代码就体现了多态,但是上面的代码有一个问题,就是可扩展性比较差,如果我想让这个函数对其他动物也有效,我得不断往里面添加if语句,因此这在实际编程过程中是不可取的。上文我们提到了一个原则面向接口编程,而不是面向实现编程。设想如果所有的动物都有一个共同的接口sound,但是它们各自的实现是不一样,比如狗执行sound会发出“汪汪汪”的叫声,猫则会发出“喵喵喵”的叫声。我们发出“叫”的命令后,不管是什么动物,都会执行自己的sound函数。下面我们用代码表示这个过程,
function makeSound(animal) { animal.sound(); } function Dog() { sound: function() { console.log('汪汪汪'); } } function Cat() { sound: function() { console.log('喵喵喵'); } } makeSound(new Cat()); //喵喵喵 makeSound(new Dog()); //汪汪汪
上面的代码可扩展性显然提高了许多。面向接口编程是多态的一个重要原则,而实现面向接口编程最重要的是解除函数与类型之间的耦合,这对于JS而言是再简单不过的事情了,因为JS是动态类型的语言。
封装
面向接口编程就是将具体的实现隐藏在对象的内部,外界只对对象提供的接口进行操作,并且这个操作往往是固定格式的,不会因为对象中的代码发生变化而改变。在这里“将具体的实现隐藏在对象内部”的操作就成为封装。除了封装实现外,封装还包括封装数据、封装类型以及封装变化。
继承
继承是面向对象编程中一个重要的概念,在谈JS的继承之前,我们谈一谈JS是如何创建对象的。JS是基于原型模式创建对象的,所谓的原型模式,就是每一个对象都存在一个原型,我们通过克隆这个原型来创建对象。在JS中,每个对象都有一个proto的属性,这个属性就是这个对象的原型。这样在JS中,对象通过原型链关联起来,当我们调用对象的某个属性或者方法的时候,如果对象自身不存在,其会沿着原型链向上查找。我们通过下面的代码解释原型链,
function Animal() { this.legs = 4; } function Cat() { this.sound = function() { console.log('喵喵喵'); } } Cat.prototype = new Animal(); var cat = new Cat() cat.sound(); //喵喵喵 console.log(cat.legs); //4 console.log(cat.__proto__ === Cat.prototype); //true cosole.log(Cat.prototype.__proto__ === Animal.prototype); //true console.log(Animal.prototype === Object.prototype); //true
通过上面的代码,我们可以看出,通过new关键字创建对象可以分为五个步骤,
1.声明一个空对象object;
2.将构造函数的this指向object;
3.执行构造函数中的内容;
4.将构造函数的prototype属性赋值给对object.proto;
5.返回object。
对象的原型存贮在两个地方,一个是对象的proto属性中,一个是构造函数的prototype属性中,因此我们运行“console.log(cat.proto === Cat.prototype);”返回的是true。对象的原型可以是另一个对象,比如“Cat.prototype = new Animal();”,这样就会形成一个原型链,而这个原型链的顶端就是Object.prototype。
第2章this、call和apply
this
在JS中this指向某个对象,this具体指向哪个对象,与this函数的执行环境有关,而与声明环境无关,或者我们可以说
那个对象调用这个函数,this就指向哪个对象。
下面我们看一组代码,
function Animal() { this.legs = 2; this.legCount = function() { console.log(this.legs); } } function Cat() {}; var animal = new Animal(); Cat.prototype = animal; var cat = new Cat(); cat.legs = 4; animal.legCount(); //2 cat.legCount(); //4
“cat.legCount”的结果之所以是4,而不是2,是因为cat调用了legCount,legCount中的this应该指向cat。还有很多情况下是谁调用的函数不太明现,但是我们得切记这个函数是否是某个对象的方法,如果不是某个具体对象的方法,this就指向window。
call和apply
call和apply是所有函数类型共有的一组方法,它们都可以改变函数this指针的指向,不同的是apply接受两个参数,第一个参数是this指向的对象,第二个参数是一个数组或类数组对象,数组中的每一个元素都将作为参数传递给函数。而call则接受多个参数,除了第一个参数是this指向的对象外,其他参数或作为参数传递给函数。注意call和apply的返回值是调用它们的函数的返回值,这一点和bind区别很大,bind的返回值是一个函数。
闭包和高阶函数
闭包的定义如下,
在一个永久存在的函数中,使用了在另一个函数中声明的局部变量,这中结构称之为闭包。
这里“永久存在的函数”,指的是一个全局函数,或者某个全局对象的方法,它不会被垃圾回收器回收。具体我们可以看下面的代码,
function f1() { var a = 0; return function() { console.log(a++); } } var f2 = f1(); f3(); //0 f3(); //1 f3(); //2
分析上面的代码,我们在函数f1中定义了一个局部变量a,并且在返回值中使用了这个局部变量,我们执行函数f1,把它的返回值赋值给f2,此时f2是一个全局的函数,它内部使用了一个在f1中声明的变局部量a,这样就形成了一个闭包。把a包裹在闭包中之后,a永远保留在内存中,不会消失。我们还可以通过下面的方法实现闭包,
function f1() { var a = 0; return { m1: function() { console.log(a++); } } } var obj1 = f1(); obj1.m1(); //0 obj1.m1(); //1 obj1.m1(); //2
我们在f1的返回值中的m1属性中使用了局部变量a,并且返回值赋值给了全局变量obj1,这样也实现了闭包。从上面的代码我们还可以看出,
闭包的作用是提供一个永不消失的局部变量。
这个局部变量可以帮助我们实现很多很强大的功能。
高阶函数
高阶函数是指满足以下任意一个条件的函数,
1.可以作为参数传递给函数;
2.可以作为函数的返回值。
在JS中,函数被当做变量来对待,因此其既可以作为函数的参数,也可以作为函数的返回值。比如addEventListener函数的第二个参数就是一个回调函数。这种特性给编程提供了极大的方便,书的作者举了很多例子来说明这种编程的强大性,这里不再举例说明。
总结
这一部分主要是总结了书中的前言和基础知识部分。作者在前言中强调了学习设计模式的重要性,它能为我们进行软件开发提供一个“高分模板
”。在基础知识部分,作者为我们讲述了JS几个重要的语言特色,比如,动态类型、原型链、闭包、高阶函数等,作者强调了后面的许多设计模式是依赖JS的这些语言特性来展开的,理解这部分是学好后面设计模式的关键。
水平有限,对书中知识的理解难免有不足的地方,还请大家批评指正!
#前端#