面向对象的程序设计

面向对象的程序设计

创建对象的方法

工厂模式

1
2
3
4
5
6
7
function createPerson(name, age, job){
var o = new Object()
o.name = name
o.age = age
o.job = job
return o
}

缺点:无法识别对象类型,使用instanceof只能检测到Object

构造函数模式

1
2
3
4
5
6
7
8
9
function createPerson(name, age, job){
this.name = name
this.age = age
this.job = job
this.sayname = function(){
console.log(this.name)
}
}
var p1 = new createPerson('tom', 20, 'teacher')

这种方式的特点:

  1. 没有显式地创建对象
  2. 直接将属性与方法赋值给函数的this
  3. 没有return

new操作符的作用

  1. 创建一个新对象
  2. 将构造函数的this指向这个新对象
  3. 执行构造函数中的代码(为对象添加属性)
  4. 返回新对象

手写一个new

1
2
3
4
5
6
function myNew(fun, ...arg){
var obj = {}
obj.__proto__ = fun.prototype
fun.apply(obj, args)
return obj instanceof Object ? obj : {}
}
1
2
3
var p = new createPerson('tom', 20, 'teacher')
p instanceof createPerson //true
p instanceof Object //true

构造函数的使用

  1. 当做普通函数使用
1
2
createPerson('tom', 20, 'teacher')//window调用
window.sayname() //tom
  1. 当做构造函数使用
1
2
var p = new createPerson('tom', 20, 'teacher')
p.sayname() //tom
  1. 在其他对象的作用域内调用(new操作符就是用的这个形式)
1
2
3
var o = {}
createPerson.call(o, 'tom', 20, 'teacher')
p.sayname() // tom

构造函数模式的问题

不同实例的同名函数是不相等的
解决方案:

1
2
3
4
5
6
7
8
9
function sayname(){
console.log(this,name)
}
function createPerson(name, age, job){
this.name = name
this.age = age
this.job = job
this.sayname = sayname
}

缺点:无封装性可言

原型模式

构造函数,原型,实例的三角关系
在这里插入图片描述
使用原型对象的好处是可以让所有实例共享同一个属性和方法而不用重新开辟内存

1
2
3
4
5
6
7
function createPerson(){}
createPerson.prototype.name = 'tom'
createPerson.prototype.age = 20
//实际上此时的构造函数已经成为了一个空函数
var p = new createPerson()
var p1 = new createPerson()
//此时p和p1中的属性与方法是共享的

之所以可以这样写,是因为当访问实例的属性时,会进行搜索,首先搜索实例本身,如果找到就返回其值,否则搜索其原型对象。

注意:ES5中不可以通过实例更改原型中的属性

1
2
3
4
5
6
7
function p(){}
p.prototype.name = 'tom'
var p1 = new p()
var p2 = new p()
p1.name = 'jack'
p1.name // jack
p2.name //tom

上述代码中p1.name = ‘jack’这条语句相当于给p1这个实例添加了那么属性,这个属性覆盖了原型中的name属性,并不会影响到原型本身。
此时要想恢复访问原型中的属性,可以使用delete

1
2
delete(p1.name)
p1.name // tom

我们知道,hasOwnProperty()不检查原型,所以可以通过这个函数检测一个属性是来自于原型还是实例

in操作符

我们之前知道for…in循环可以遍历对象自身和继承的可遍历属性。但in操作符单独使用时,用于判断是否可以通过对象访问指定属性,该访问会搜索原型

1
2
3
4
5
6
7
8
9
10
function p(){}
p.prototype.name = 'tom'
var p1 = new p()
p1.age = 20

p1.hasOwnProperty(name) //false
'name' in p1 //true

p1.hasOwnProperty(age) //true
'age' in p1 //true

使用对象字面量的原型模式

1
2
3
4
5
6
7
function p(){}
p.prototype = {
name:'tom',
age:20,
job:'teacher'
}
var p1 = new p()

注意,这种方式会导致函数p的原型的构造函数,并不是p本身,也就是说破坏了三角关系中的constructor。虽然这并不影响instanceof操作符的使用,但也可以手动解决这个问题

1
2
3
4
5
6
7
8
function p(){}
p.prototype = {
constructor:p,
name:'tom',
age:20,
job:'teacher'
}
var p1 = new p()

当然也可以再严谨一点,因为默认的constructor是不可枚举的

1
2
3
4
Object.defineproperty(p.prototype, 'constructor', {
enumerable:false,
value: p
})

原型的动态性

对原型对象的任何修改会立即体现到实例上,不会考虑代码的先后因素

1
2
3
4
5
6
var f = new p()
p.prototype.sayname = function(){
console.log('tom')
}
f.sayname() //tom
//在p被实例化为f之后才为原型添加方法,但并不影响f使用它

原型和实例之间是通过指针连接的
原型的动态性也随之带来一个问题,那就是上面的对象字面量重写原型,会导致三角关系中的[[proto]]破裂。
之前是先重写原型,再new(没问题)
现在是先new,再重写原型(出现问题)

1
2
3
4
5
6
7
8
9
function p(){}
var f = new p()
p.prototype = {
constructor:p
name:'tom',
age:20,
job:'taecher'
}
f.name //找不到这个属性,报错

虽然重写了构造函数原型实例,但是原来的那个原型实例依然存在,并没有删除,而且new出来的对象依然引用这个最初的原型实例

原型模式的缺点

原型模式对于基本数据类型很适用,对于引用类型来说,其中函数由于本身就需要共享,因此也很适用,但对于数组、对象等不共享的引用类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
function p(){}
p.prototype = {
constructor:p,
name:'tom',
age:20,
arr:[1, 2]
}
var f1 = new p()
var f2 = new p()
f1.arr.push(3)
f1.arr //[1, 2, 3]
f2.arr //[1, 2, 3]
//f1和f2共享同一个原型实例,因此对于引用类型,也是指向同一个属性,一个实例的更改会影响另一个实例

还有一个致命缺陷:不能通过构造函数传参

组合使用构造函数模式和原型模式

了解了构造函数模式和原型模式的优缺点,我们就可以取长补短

  • 构造函数模式:定义实例属性
  • 原型模式:定义函数方法以及一些需要共享的属性

好处:最大限度节约了内存,还能支持构造函数传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function p(name, age, job){
this.name = name
this.age = age
this.job = job
this.arr = [1, 2, 3]
}
p.prototype = {
constructor:p,
sayname:function(){
console.log(this.name)
}
}
var p1 = new p('tom', 20, 'teacher')
var p2 = new p('jack', 20, 'doctor')
p1.arr.push(4)
p1.arr //[1, 2, 3, 4]
p2.arr//[1, 2, 3]
p1.sayname === p2.sayname // true

基本上这是ES5最广泛最受认可的一种自定义类型的方法

动态原型模式

1
2
3
4
5
6
7
8
9
10
function p(name, age, job){
this.name = name
this.age = age
this.job = job
if(typeof this.sayname !== function){
p.prototype.sayname = function(){
console.log(this.name)
}
}
}

寄生构造函数模式

1
2
3
4
5
6
7
8
9
10
function p(name, age, job){
var o = {}
o.name = name
o.age = age
o.job = job
o.sayname = function(){
console.log(this.name)
}
}
var p1 = new p()

这种方式和工厂模式的区别就是用new操作符创建对象

稳妥构造函数模式

原型链

利用原型让一个引用类型继承另一个引用类型,三角关系层层递进,就构成了实例与原型的链条,这就是原型链
读取实例的属性时,搜索的过程就是沿着原型链网上搜索

默认原型

所有实例的默认原型就是Object的实例,因此默认原型内部指针指向Object.prototype

instanceof操作符

只要原型链中存在构造函数的prototype,就认为这个是该构造函数的实例
在这里插入图片描述

1
2
3
b instanceof B //true
b instanceof A//true
b instanceof Object//true

覆盖父类的方法

无论是给原型添加方法还是重写原型中的方法,都要放在替换原型的语句之后

1
2
3
4
5
6
function Parent(){}
Parent.prototype.fun1 = function(){...}
function Son(){}
Son.prototype = new Parent()
Son.prototype.fun2 = function(){...}//添加方法
Son.prototype.fun1 = function(){...}//重写方法

注意:在继承时不可以使用对象字面量创建原型,这样会破坏原型链

1
2
3
4
5
Son.prototype = {
fun1:function(){...},
fun2:function(){...}
}
//错误

原型链存在的问题

  1. 引用类型的属性值会被原型链上所有实例共享,一个实例对一个引用类型值的修改 ,会在另一个实例上表现出来,参考原型模式创建对象

  2. 子类型无法在不影响所有实例的前提下给超类传递参数

继承方法

经典继承(借用构造函数).

1
2
3
4
5
6
function parent(){
arr:[1, 2, 3]
}
function son(){
parent.call(this)
}

这种方法实际上是在son环境下调用parent,这是son的每一个实例都会有一份arr的副本,而不用担心引用类型的问题
优点:可以传递参数
缺点:方法在构造函数中国定义,不便于函数复用
超类原型中的方法对于子类不可见(无法继承方法)

组合继承(伪经典继承)

1
2
3
4
5
6
7
8
9
10
11
function parent(name){
this.name = name
this.arr = [1, 2, 3]
}
parent.prototype.sayname = function(){console.log(this.name)}
function son(name, age){
parent.call(this,name) //继承属性(第一次调用构造函数)
this.age = age
}
//
son.prototype = new parent()//继承方法(第二次调用构造函数)

优点:避免了原型链和借用构造函数的缺陷,是js最常见的继承模式

缺点:超类构造函数被调用了两次,导致子类的原型上多了不需要的父类属性,存在内存上的浪费

原型继承

ES5提供了Object.create函数,它接收两个参数

  1. 用作原型的对象
  2. 新对象额外属性组成的对象
1
2
3
4
5
6
7
8
9
10
var person = {
name:'tom',
arr:[1, 2, 3]
}
var obj = Object.create(person, {
age:{
value:20
}
})
obj.age //20

注意:这种方式会把原型链的缺陷再次暴露,父类的引用类型会被所有实例共享

因为这个函数内部的运行原理是这样的:

1
2
3
4
5
function create(o){
function F(){}
F.prototype = o
return new F()
}

寄生式继承

封装一个原型式继承的过程

1
2
3
4
5
function create(o){
var obj = Object.create(o)
obj.fun = function(){...}
return obj
}

寄生组合式继承

解决组合式继承中调用两次超类构造函数的问题
与组合式继承唯一的不同,就是用原型继承的步骤,代替了son.prototype = new parent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function parent(name){
this.name = name
}
parent.saynamne = function(){...}
function son(name, age){
parent.call(this, name)
this.age = age
}
//下面这一步代替了son.prototype = new parent()来继承方法,不会调用超类构造函数
function inherit(son, parent){
var o = Object.create(parent.prototype)
o.constructor = son
son.prototype = o
]
inherit(son, parent)
son.sayage = function(){...}
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2015-2021 AURORA_ZXH
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信