《高程第六章---面向对象程序设计》小结---创建对象(该章节重
《JavaScript高级程序设计》这本书比较厚,之前刚学JS的时候挑战过一次,结果止步第三章。现在工作了一段时间想想还是得回过头来补充一下基础知识的。目前刚看完第六章,就从第六章开始总结吧,再逐步补充之前的好了。
本章小结
创建对象章节主要讲解如何解决一个问题:使用对象字面量或者Object构造函数创建对象时会因此产生的大量冗余代码。
解决该问题的思路有很多,本章介绍的有①工厂模式,②构造函数,③原型模式,④原型与构造函数的组合,⑤动态原型,⑥稳妥构造函数。
并且在介绍这些方法的同时也介绍了这些方法的试用问题和优缺点,并提出了解决方案。
该博客会采用介绍方法->提出该方法的问题->如何解决该方法产生的问题
这样的步骤来进行讲解,代码的展示有可能用截图的形式
工厂模式
- 解决的问题:该模式主要解决的的就是用对象字面量和Object构造函数创建对象时会产生大量冗余代码这个问题;
- 介绍和用法:
- 介绍:工厂模式顾名思义,就像一个工厂的功能一样可以批量创建产品,可以通过创建一个工厂函数,再通过传参的方式创建出我们想要的对象
- 用法示例:
// 例如我们要创建多个单车对象,先创建一个生产单车的工厂
function factory(brand, weight, height) {
let bike = new Object()
bike.brand = brand
bike.weight = weight
bike.height = height
bike.ride = function() {
console.log('ride')
}
return bike
}
// 然后通过设定单车的参数,让工厂生产出我们所需要的单车
let bikeA = factory('oxc', '3kg', '1.1m')
let bikeB = factory('mmp', '2kg', '1.2m')
// 这样我们就得到了两个单车
bikeA: {brand: "oxc", weight: "3kg", height: "1.1m", ride: ƒ}
bikeB: {brand: "mmp", weight: "2kg", height: "1.2m", ride: ƒ}
// 也可以通过无数次的调用factory函数去创建无数个你想要的型号的单车
- 产生的问题
- 对象内方法重复创建:在上面的单车工厂中,创建出来的单车都有
ride
这个方法,并且这个方法的功能是一模一样的,但是在bikeA和bikeB两个单车中的ride
方法却不是同一个,也就是说这个相同功能的方法被重复创建了
image.png - 无法识别创建出来的对象的类型:假设我们在一个工厂函数中去创建一个字符串对象,那么返回出来的这个数组的数据类型无法准确识别,如下例子:
- 对象内方法重复创建:在上面的单车工厂中,创建出来的单车都有
function factory() {
let str = new String('oxc')
return str
}
let oxc = factory()
console.log(typeof oxc) // object
// 当然,可以通过instanceof这个方法判定创建出来的对象是不是string类型,但是这样也会有一个问题,普通赋值方法创建出来的字符串用instanceof是无法判断该字符串是不是属于string类型的,而我们平时创建字符串时也极少使用Sting构造函数去创建一个字符串
let a = 'oxc'
a instanceof String // false
随后,又出现了使用构造函数模式创建对象的方法
构造函数模式
- 介绍:这个方法和工厂模式区别并不大,但是还是有区别的①不需要return对象出来,②使用了new操作符,③函数体内没有显式地去创建对象;
- 示例:以上面的单车例子来看
function Bike(brand, weight, height) {
this.brand = brand
this.weight = weight
this.height = height
this.ride = function() {
console.log('ride')
}
}
let oxc = new Bike('oxc', '1.2kg', '1.3m') // {brand: "oxc", weight: "1.2kg", height: "1.3m", ride: ƒ}
// 虽然该方法和工厂模式区别不大,但是还需要理解一下new操作符在这中间做了什么事情
- new操作符做的事情
- 创建了一个新的对象;
let o = new Object()
- 设置该对象的原型链,让其指向Bike构造函数的原型对象;
o.__proto__ = Bike.prototype
- 执行函数,并设置构造函数内this的指向为创建的新对象;
Bike.call(this)
- 返回这个对象; 'return obj'
- 创建了一个新的对象;
- 备注
- 使用这种方式创建的对象,可以使用
constructor
属性查看它的构造函数
oxc.constructor === Bike // true
- 构造函数的命名使用驼峰模式,不过首字母需要大写
- 使用new操作符进行调用(注意这里,如果不用new操作符而直接调用,那么函数内的属性和方法将会被设置到外层对象上,例如在全局环境window下调用这个函数,因为此时this指向的是window对象,所以其内属性与方法都会被放到window对象上,当然啦,你也可以自己创建一个对象,然后在构造函数调用时使用call或者apply强制将this指向这个对象,但这是多此一举)
- 使用这种方式创建的对象,可以使用
- 构造函数的问题
- 和工厂函数一样的重复创建对象内的方法
为了解决方法重复创建的问题,后来又出现了原型模式
原型模式
- 介绍:原型模式基于函数的
prototype
属性而产生,prototype
这个属性是一个指针,指向一个对象,这个对象包含所有由这个函数创建的实例都可以共享的方法和属性。那么在这个字面意思的理解上,其实就已经知道如何解决构造函数模式和工厂模式所产生的重复创建实例内方法的问题了,将在下面进行讲解: - 使用实例:以上面的单车为例
// 将属性和方法都设置到Bike函数的prototype属性中去
function Bike() {}
Bike.prototype.brand = 'oxc'
Bike.prototype.weight = '1.2kg'
Bike.prototype.height = '1.2m'
Bike.prototype.ride = function() {
console.log('ride')
}
let bikeA = new Bike()
let bikeB = new Bike()
bikeA.ride() // ride
bikeB.ride() // ride
console.log(bikeA.ride === bikeB.ride) // true bikeA和bikeB对象的ride方法变成了同一个
-
关于原型链
- 原型模式和原型链:提到原型模式,就不得不提及原型链这个东西了,这也是原型模式能解决重复创建函数这个问题的关键。
- 原型链介绍(以上面的Bike原型模式为例):
1 . 在JS中,无论何时何地,只要创建了一个函数,那么这个函数就肯定会有一个prototype
属性,prototype
属性是一个指针,指向这个函数的原型对象,该对象存储着所有由该函数的所创建的实例对象所共享的方法和属性,此外,该原型对象中还存在一个叫做constructor
的属性,这个属性指向这个原型函数,如下图所示:
image.png
2 . 这时候,我们使用new操作符创建了两个个Bike实例let bikeA = new Bike()
,在这个操作中,结合上面的new操作符所做的事,创建一个空对象并使这个对象的__proto__
指向Bike函数的prototype
:
image.png
image.png
3 . 然后,在我们调用bikeA或者bikeB的ride方法时,它会先在自身的属性中寻找是否存在ride方法,如果不存在,则通过__proto__
进入到上一层,也就是Bike构造函数的原型对象prototype
中去寻找ride方法,找到后进行调用,这相当于bikeA.__proto__.ride()
,bikeB调用的原理是一样的,他们调用的其实都是处于Bike.prototype对象中的ride方法。这个调用的查找链条就是原型链。
4 . 另外值得一提的是,所有的对象都是Object
构造函数的实例,bikeA和bikeB也可以通过原型链对Object
构造函数的prototype
里的方法和属性进行访问,而Object
的prototype
指向的就是null了(有木有一种道生一的感觉呢?),例如:bikeA.toString()
image.png
5 . 大家知道,在JS中,一切皆对象,那么如果将构造函数Bike当做一个对象来看的话,那么Bike对象(函数)的构造函数又是谁呢?答案是Function
构造函数,而Function
构造函数的prototype
对象因为是对象,所以Function.prototype.__proto__
指向的又是Object.prototype
,最终的效果图是下面这样的,因为画图工具的问题所以线条可能会有交织
image.png
-
原型模式的一些特点
- 在实例中重写原型中已经有的属性不会影响到原型中的属性,但是会通过该实例对这个属性进行访问的话,重写的属性会屏蔽, 例如
image.png
原因其实很简单,就是因为原型链由内向外查找这个特性造成的。
那么如何分辨一个对象的属性是它自己的还是属于它原型链上的呢?答案是使用hasOwnProperty
方法
image.png - 在实例上使用
delete
操作符无法删除原型对象上的属性,但是可以通过__proto__
去删除原型上的属性(尽量不要这么做,因为__proto__
不是在所有浏览器都能用,所以尽量少用__proto__
去进行操作)
image.png - in操作符可以通过实例获取到原型上的属性(in操作符特性是只要能在对象上访问到的属性,他都会返回true),另外,
for in
循环也是可以拿到原型上的属性的
image.png
- 在实例中重写原型中已经有的属性不会影响到原型中的属性,但是会通过该实例对这个属性进行访问的话,重写的属性会屏蔽, 例如
-
改良版原型构造函数(解决在创建原型函数的时候要写n次
prototype
的问题),注意:采用字面量的方法去创建构造函数的话需要将函数原型对象中的constructor
重写回指向构造函数本身, 否则函数原型对象上的constructor
指向Object
构造函数, 另外还要注意将这个重写的constructor
属性用Object.definedProperty
设置其不可进行遍历(enumerable
设置为false)
function Bike() {}
Bike.prototype = {
brand: 'oxc',
weight: '1.2kg',
height: '1.2m',
constructor: Bike,
ride() {
console.log('ride')
}
}
- 原型模式的产生的问题
- 如果将引用类型放到构造函数的原型对象上去,会导致由该构造函数创建出来的实例全部共享这个引用类型(就像函数被共享了一样),比如下面这个例子:
function A() {}
A.prototype.oxc = {
age: 123,
name: 'oxc'
}
let b = new A()
let c = new A()
// 这时候对b.oxc对象里面的属性进行修改,会影响到c.oxc对象,原因就在于oxc保存的是一个指向对象的地址,而b与c共享了这个地址,所以b与c持有同一个oxc对象
image.png
- 采用原型模式的话无法传递参数到构造函数中,导致所有创建出来的实例里面的属性都是一样的
- 总结:原型模式虽然解决了构造函数模式和工厂模式的重复创建函数的问题,但是又因为解决该问题的特性导致多个实例间的引用类型被共享,并且所有创建出来的实例都是一样的。
为了解决原型模式所导致的问题,又出现的原型模式和构造函数模式组合使用的方法。
原型模式和构造函数组合使用
- 介绍:该模式将原型模式的聚合性和构造函数模式的分离性(我自创的词)组合在一起使用,完美的解决了原型模式所带来的引用类型共享和无法传参,以及构造函数模式重复创建函数的问题。
- 原理:这种模式的原理很简单,无非是用构造函数的模式去设置属性,然后用原型模式去设置函数罢了
- 实例
function A(name) {
this.oxc = {
name: name,
age: 123
}
}
A.prototype.sayName = function() {
console.log(this.oxc.name)
}
let b = new A('oxc')
let c = new A('大春春')
image.png
动态原型模式
- 介绍:该方法是原型模式的一个变种,会有这种模式可能因为有人觉得把构造函数中函数定义的部分,也就是
Func.prototype.XXX = function(){}
写到函数体的外面不好看,所以想到这么个方法(纯属个人猜测) - 原理:既然觉得把构造函数中的函数定义部分写到外边不好,那就写到里面去呗,但是这样又会因为构造函数的多次调用而多次创建相同的函数,那就加上一个条件判断吧,看下面例子:
- 实例
function A(name) {
let mark = 0
this.oxc = {
name: name,
age: 123
}
if (typeof this.sayName !== 'function') {
mark++
console.log(`已经创建了${mark}次`)
A.prototype.sayName = function() {
console.log(this.oxc.name)
}
}
}
let b = new A('oxc')
let c = new A('大春春')
// 该函数在创建第一次执行A的时候因为sayName方法是undefined,所以会在构造函数A的原型对象上设置sayName函数,而在第二次执行A的时候因为this.sayName已经被创建,所以就不执行了
image.png
寄生构造函数模式
- 介绍和适用情况:该方法主要用于为JS的原声构造函数(
Array,String
等)添加自定义方法,在写法上和工厂模式除了会使用new
操作符之外几乎没有任何区别 - 实例:
function S() {
let value = new Array()
value.push.apply(value, arguments)
value.sum = function() {
console.log(this.reduce((p, n) => p + n))
}
return value
}
let newArr = new S(1, 2, 3)
newArr.sum() // 6
-
优势:
这个方法针对为JS原生构造函数添加自定义方法的开发者来说非常友好,因为如果直接在原生构造函数的原型链上去添加自定义方法有可能会出现覆盖原生对象原型链上的原生方法的情况。
而采用寄生构造函数模式,则是创建一个原生构造函数的实例,将其绑定到该实例本身上,对原生构造函数不会有任何影响;
image.png
稳妥构造函数模式
- 介绍:该方法只适用于特定的情况下(防止数据被改动),该模式特点是不适用this也不用new,但是由此创建的对象中的属性除了调用流出来的接口外,没有其它办法获取,因为场景见过的不多,所以这种模式本人在实际中没有用过也没见到有人用过;
- 实例:
function S(name) {
let o = new Object()
o.sayName = function() {
console.log(name)
}
return o
}
let newObj = new S('oxc')
image.png
后记
然并卵,上面的模式其实工作中用得都比较少了,但是这些只是还是非常有意思的,现在工作中创建对象用得多是ES6推出Class
了,当然了,Class
那又是另外一个话题了,将在以后的博客中进行记录和学习.