面向对象是一种编程思想,像C#、java完全是面向对象语言。面向对象三大特性:封装、继承和多态,使程序易维护、易扩展、高效率和更安全,当然面向对象的思想javascript同样也应该支持,开发过程中学会用面向对象思想编程,你的代码质量会有质的飞跃。

声明一个对象

创建一个object对象,如下:

此时我们又需要一个对象,同理再new一个object对象即可,但是这么写要产生大量的重复代码,我们可以用工厂模式来创建对象。

工厂模式

如此我们就解决了创建对象时需要些大量重复代码的情况,难道这么写就没有问题吗?这么写问题也是有问题的,最重要的一个问题是识别问题,无法判断people1和people2到底是哪个对象的实例。

所有的实例都是Object类型,无法区分实例属于哪个对象。

ECMAScript中可以采用构造函数的方式来创建特定的对象。

构造函数模式

  1. 构造函数没有new Object,后台会自动var obj = new Object
  2. this就相当于obj
  3. 构造函数方式没有return语句,自动返回
  4. 构造函数也是函数,但第一个字母大写,普通函数首字母无需大写。
  5. 实例化必须使用new运算符

构造函数方式创建对象,可以解决写重复大量代码的问题,能不能解决实例所属对象问题呢?

从上面例子我们可以看到,构造函数方式创建对象也解决了区分实例所属对象的问题。实例化过程中如果不new,直接调用People(‘wyd’,20),这样this指向的是全局对象window,其次构造函数没有返回值。如果你强行return,就失去了面向对象的意义,能否让它强制性new呢?可以用下面这种写法:

两次实例化People,其实是开辟了两份内存空间,把地址的引用传给了people1和people2。name 和 age都是比较的值所以返回true,而show方法则是对两个地址引用进行的比较,故返回false。如果想保持引用地址一致,可以采用下面的写法,虽然此方法可用,但是我们有更好的方法实现。

原型

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,它的作用是包含可以由特定类型的所有实例共享的属性和方法。使用原型的好处可以让所有对象实例共享它所包含的属性和方法。也就是说不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。

通过实例可以看出原型方法中,show方法的引用地址是相同的,也就是共享的,所有通过原型方法创建的实例共享对象里面所有的属性和方法。下面两张图加深理解:

在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的,_proto_属性是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性constructor。people1.constructor可以获取到构造函数本身,起到实例对象和原型对象之间的连接作用。

字面量

为了让属性和方法更好的体现封装的效果,减少不必要的输入,原型的创建可以使用字面量的方式,如下:

构造函数和字面量方式创建对象在使用上基本相同,但是还是有一些区别,字面量创建的方式使用constructor属性不会指向实例,而是指向Object,构造函数创建的方式则相反。

为什么字面量方式constructor会指向Object?因为People.prototype={};这种写法其实就是创建一个新对象,创建新对象的同时创建peototype,这个对象也会自动获取constructor属性,所以,新对象的constructor重写了People原来的constructor,因此会指向新对象,新对象没有指定构造函数,那么默认为Object。所以我们可以进行强制指向到People即可,如下:

如果对原型对象进行重写,那么会对之前的原型对象进行覆盖。如:

ECMAScript内置的引用类型本身也使用了原型,所以我们可以对其进行方法扩展,如下:

扩展方法虽好,但是也不能乱用,和C#扩展一样,有可能会引起命名冲突或者其他不必要麻烦,不利于代码维护,不推荐使用。

原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它的最大优点:”共享”。

此时people2的country属性也被修改了,原因是:people1和people2的country属性是一个引用类型,在原型对象中指向相同的地址,其中一个实例修改后,另一个实例同样得到了修改。如何解决这个问题呢?采用构造函数模式+原型模式=组合模式。

组合模式

这种混合模式很好的解决了传参和引用共享的大难题,是创建对象比较好的方法。

上面代码看下来还是感觉比较怪异,因为构造函数和原型不是一个整体,最好两者可以封装到一起,也体现面向对象的封装性。解决这个问题我们可以采用动态原型模式。

这么写,也会存在一个问题,那就是每次实例化都需要进行原型初始化,每次原型初始化是没有必要的,只需要第一次初始化就可以了,所以稍作修改:

寄生构造函数模式

上面的写法已经满足大部分需求,为了满足小部分需求,又发明了一种寄生构造函数。

下面看一个具体的实例,为内置的Array对象增加一个toPipedString方法。

看到这你估计会对寄生构造函数有两点疑问

  1. 寄生构造函数的作用:上面例子中,返回的对象values是一个新的对象,这个values和内置的Array多了一个自定义的方法,这么做的好处是如果直接在Array中定义新的方法,会污染内置的Array类型,可能会造成不必要的麻烦,在讲原型的时候也有一个扩展方法,那个就是在内置对象中直接扩展,不推荐。
  2. 和工厂模式有什么区别:除了调用的时候多了一个New,没有实质性的区别,只是调用方式上更像使用内置Array一样使用SpecialArray,如果使用工厂模式,显得不优雅。

寄生构造函数模式创建的对象与构造函数之间没有什么关系,因此instanceof操作符对这种对象没有意义。

稳妥构造函数模式

道格拉斯.克罗克福德发了JavasScript中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序发动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。代码如下:

注意:以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境提供的环境下使用。

与寄生构造函数模式类似,稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义。

call方法

方法构造:call([thisObj[,arg1[, arg2[, [,.argN]]]]])
thisObj:可选项。将被用作当前对象的对象。
arg1, arg2, , argN:可选项。将被传递方法参数序列。
说明:call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj
下面举例:

上面例子我们可以看到,此时o实例也具备了show方法,同时也说明访问到了name和age属性,是不是嗅到一丝继承的味道。

如果转载,请给出原文链接,谢谢!


微信支付宝

如果本文帮助到您,请随意打赏作者