聊聊javascript语言的设计思想

6,286 阅读11分钟

javascript语言是什么设计模式设计出来的?

作为前端工程师的你,相信对于javascript的掌握是必不可少的技能,那么你知道多少种设计模式呢?你有了解哪些设计模式呢。首先我们需要知道设计模式不是javascript独有模式、在众多语言中、都存在设计模式、所以、设计模式是一种编程思想、设计模式Design pattern)代表了最佳方案,这些模式通常被有经验的面向对象编程的程序员所使用、设计模式是软件开发人员在面临编程问题时候的一种解决方案,这些方案都会是经过许许多多的优秀程序员试验并改进而来的,是一种优秀思想的进化过程,到最终被绝大部分人所认可,从而诞生一种设计模式。

下面是维基百科对于设计模式的说明:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

设计模式分类

<设计模式>一书中,归纳总结了23种设计模式,从意图上区分,可以被划分为,创建型模式结构性模式行为型模式

创建型模式来说,要创建一个对象,是一种抽象的行为,而具体创建什么对象则是可以变化的,那么创建型模式的目的就是为了封装这些对象的变化。而结构性模式则是封装对象之间的组合关系。行为型模式封装的就是对象的变化了。

创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)、访问者模式。

原型模式与基于原型继承的javascript对象系统

要去了解javascript的设计思想,我们就应该去了解其历史,在Brendan Eich设计javascript的面向对象系统时,借鉴了SelfSmalltalk这两门基于原型语言。所以javascript语言的设计也采用了这种思想,使用了设计模式中的原型模式

  • 原型模式不单是一种模式,也被称为一种编程范式。

使用克隆的原型模式

从设计模式的角度来说,原型模式是用于创建对象的一种模式,和静态语言不同,当我们创建一个对象的时候,不是先去指定他的类型,再去创建这个对象,原型模式采用了另外一种方式,克隆的方式。我们不再需要去关注对象的类型,我们需要做到的是,只要我们需要,就去克隆一个一抹一样的对象,这就是原型模式。

所以,既然原型模式是通过克隆来创建对象的,那么如果我们需要一个跟某个对象一抹一样的对象,我们就可以采用原型模式。

也正是这样的特性决定了,原型模式实现的关键来自于语言是否支持clone,是否提供了clone方法。ECMAScript 5就提供了**Object.create()**方法,可以用于克隆对象,如下:

var Person = function(){
  this.name = 'Snine',
  this.age = 22,
  this.height = 173
} 
var person = new Person();
person.name = '小王';
person.age = 25
person.height = 175
let clonePerson = Object.create(person)
console.log(person.name)
console.log(person.age)
console.log(person.height)

克隆是创建对象的手段

我们知道了原型模式可以这样克隆出一个一抹一样的对象,但是原型模式的真正目的却不是为了得到一个这样的对象,而是提供了这样一种便捷的手段去处创建某个类,克隆只是创建这个类的过程和手段。在诸多静态语言中例如Java对于类型的要求非常严格,也就导致了我们进行new这样的操作会非常僵硬,不够灵活,想要去解决这个问题就会有额外的代码,相对之下javascript就变得异常灵活了,我们无需关心对象的具体类型,就好比一个小男孩指着飞机说”我要这个“,我不需要知道它是啥,却也能明白要干嘛,所以在javascript这类若语言中,创建对象非常容易,也不存在耦合问题,从设计模式的角度上看,原型模式的意义并不算大,但是javascript本身是一门基于原型的面向对象的编程语言,它的对象系统就是使用原型模式搭建而来,我们称之为原型编程范式也许更加合适

基于原型链的委托机制

我们设想这样一个场景,有这样一个类,动物类Aniam,首先要创建这个类,我们需要clone一个Object,然后在这个类下他们都拥有共同的能力,吃饭,于是Aniam就全部可以吃东西了,这时候我们再来创建一个对象Dog,它不仅仅可以东西,它还可以发出叫声,这样就有了三个类,我们用代码来表示:

var Aniam = new Object()
Aniam.eat = function(){
  this.Aniam = function(){ congsole.log('我可以吃东西了')}
}
var Dog = Object.create(Aniam)
Dog.say = function(){ console.log('我可以叫出声了') }

console.log(Dog.say())
console.log(Dog.eat())

上述代码就表示了这样一个过程,那么可以发现一个关系了么? Aniam来自于Object,Dog来自于Aniam这样的一个关系,他是一层层的,简单来看,我们理解他们为abc,b来自于a,c来自于b,反过来,a生了b,b生了c,c还可以继续下去,这样就形成了一个强大的动物王国了,这里我们是否能理解其中的关系呢?

这时候我们来设想下,Dog这个对象,小狗本身可以吃东西,他现在想发出声音,但是他自己不具备say()的能力,怎么办呢,我们就去找到生我们的对象Aniam,Aniam帮小狗执行这个操作就行,我拥有你的能力,我可以自己没有,但你可以帮我做这件事就可以了,那要是Aniam同样没有呢,那么继续向上洛,相信大家对于javascript的原型链并不陌生,但是我们这这里说的是原型模式的这种思想,所以你听明白了么?我们来总结一下好么?

原型模式的基本范式

  • 所有的数据都是对象。
  • 要得到一个对象,不是通过实例化类,而是找到一个对象,并作为原型克隆他。
  • 对象会记住他的原则
  • 如果对象无法响应,他会把这个请求委托给自己的原型

JavaScript中的原型继承

好了上面我们已经总结了原型模式的基本范式,那你们觉得JavaScript遵循了么,下面来分别讨论JavaScript是如何在这些规则的基础上来构建属于他自己的对象系统

​ 1.所有数据都是对象

JavaScript在设计的时候,模仿Java引入了两套机制:基本数据类型引用数据类型(对象类型) ,从这里来看,他并不算是一个很好的想法,按照JavaScript最初的设计思想,除了Undefined之外的所有都应该是对象,为了实现这一目标,number、boolean、string这几种基本数据类型也可以通过包装类包装成对象,

​ 所以,我们不能说JavaScript全是对象,但我们可以说其绝大部分都是对象,那么相信在JavaScript中也同样存在原型中的一个根对象,就想裂变,克隆的母体一样有一个源头,事实上,在JavaScript中的根对象是一个空的对象Object.prototype,我们在任何地方所看到的JavaScript皆是由此而来,

​ 2.要得到一个对象,不是通过实例化类,而是找到一个对象,并且克隆它

​ 在JavaScript中我们并没有看到明显的clone动作,是它没有遵循么?并不是,因为这是引擎内部负责实现的,我们不需要关心克隆的细节,我们需要的就是显示的调用 var obj = new Object() 或者var obj2 = {},此时,引擎内部回去克隆一个Object.prototype而不需要我们关心,我们来看看下面这段代码:

function Person( name ) {
  this.name = name
};
Person.prototype.getName = function () {
  return this.name
}
var people = new Person('Snine')
console.log(people.name); 
console.log(people.getName())

首先,我们需要说明的是,在JavaScript中没有类的概念,不同于java这种面相对象的语言,他没有类或者接口的概念,即不能定义抽象的类,也不能实现继承。不过,为了编程的方便,我们模拟了类和继承的行为,也就是构造函数,构造函数就是具有一系列属性和行为作为函数体的函数,可以通过函数参数传入值。它就相当于 Java 中类的构造函数,需要时可以通过new来实现模拟创建对象。

​ 3:对象会记住他的原型

​ 所以,在这里Person并不是一个类,而是一个构造函数,首先在JavaScript中,函数可以被直接使用,也可以被New,被new的时候他就是一个构造器,而当使用new运算符来调用函数的过程,实际上也就是先clone了Objec.prototype对象,再进行一些额外操作这样的过程。在chromefirefox等浏览器中向外暴露了对象的_proto _ 属性,我们可以通过这个属性找到对象的原型,我们知道一旦某个对象不能执行某个方式的时候,就会委托他的原型去进行执行,那么他如何把自己的请求上传给自己的原型呢,就是这个___proto__ _属性,通过这个隐藏属性就可以找到父级了。

​ 4:如果对象无法响应某个请求,他会把这个请求委托给他的原型

​ 这条规则是原型链继承的精髓所在,JavaScript不同于其他面向对象语言,他不是从别的对象克隆而来,实际上,他的每个对象都是从Object.prototype上克隆而来的,如果是这样,我们就只能得到单一的继承关系,即每个对象的原型都是Object.prototype,这显然不合理,所以,虽然所有的对象都是自根对象克隆而来,但是我们的原型却不仅限于Object.prototype,我们可以动态指向其他对象,当我a对象需要借用b对象的能力时,可以选择性把自己的原型指向b,就可以达到继承效果,例如:

var A = function(){}
A.prototype = {name:'Snine'}

var B = function(){}
B.prototype = new A();

var b = new B();
console.log(b.name)   //输出 Snine

我们定义一个A,一个b,给A的原型加了一个name属性, 再把B的原型指向A,再去new一个b, 那么b要去输入name的时候回去找其原型B,B原型指向了A,所以我们再去A找到name属性输出,这就是一条完整的原型链,也是一个借用关系。

总结

  • JavaScript是有原型模式这一模式设计出来的语言
  • JavaScript的任何对象都是从Object.prototype克隆而来的
  • JavaScript的原型链之间的关系指向指针属性是隐藏属性_ _proto _ _ 。
  • JavaScript的原型链并不是无限长的,只会到对象的根起始点Object.prototype

扩展

Peter Novig曾说,设计模式是对语言不足的补充,如果要使用设计模式,不如去找一门更好的语言。

设计模式很多时候都体现了语言的不足之处,但是作为web前端开发者,相信JavaScript依然是我们很长一段时间的唯一选择,虽然我们没有办法随便更换一门语言,但是语言本身也是在发展的。说不定某天就会发现某个模式就天然存在于语言之中,就像Object.create就是原型模式的天然形成的一样。使用Object.create更能体现原型模式的精髓所在,但是通过这个方法来创建对象的效率并不高,通常要比构造函数创建的对象要慢。同时**Object.create(null)**可以创建出一个没有原型的对象。

ES6的类

ECMAScript 6拥有新的Class的语法,这让JavaScript看起来更像是一门基于类的语言,但是其背后依然是通过原型机制来创建对象的,通过class创建的类和之前一样,本文采用es5的写法来为家解析JavaScript的设计思想,希望你可以更加理解这门语言设计