JS进阶 - 面向对象

面向对象、原型、class

面向对象解决的问题

并不是:封装、继承、多态

而是写代码的套路问题(减少思考)

对象与对象的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
name: 'xu',
age: 18,
isAdult: true,
say: function(){ console.log('person xu') }
}

var person2 = {
name: 'chen',
age: 17,
isAdult: false,
say: function(){ console.log('person chen') }
}

var person3 = {...}
var person4 = {...}

我们可以看到这些对象太类似了,于是我们发明了构造函数(返回对象的函数)

1
2
3
4
5
6
7
8
9
10
function createPerson(name, age){
var obj = {}
obj.name = name
obj.age = age
obj.isAdult = age >= 18 // 计算属性
obj.say = function(){
console.log('person' + $(this.name)) // this 可能是 obj
}
return obj
}

对象与函数的关系

JS 中对象与函数没有关系,JS 之父使用 this 强行绑定两者

封装

我现在想要两百块,那么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var hundred1 = {
id: 'AA00000000',
value: 100,
unit: '元',
color: '红',
form: '纸币',
issue: '2019-01',
兑换等值商品: function () {},
换两张50块: function () {}
}
var hundred2 = {
id: 'AA00000001',
value: 100,
unit: '元',
color: '红',
form: '纸币',
issue: '2019-01',
兑换等值商品: function () {},
换两张50块: function () {}
}

于是我就可以用这两百块钱去吃椰子鸡了。

构造函数 & 原型对象

哎呀,椰子鸡好吃,可是钱也花光了。不行,我得再造钱来用,但我不想让别人看到我是怎么造的!所以我要做一个百元印钞机,把这些过程放百元进印钞机里,我只要输入每张钞票的 ID ,再按印刷键,就可以刷刷刷出百元大钞 。同时,这个印钞机要智能一点,不要老是一个个重复得去刻面额、单位、喷颜色等,直接给它引用个百元钞模版,这样能省点墨水 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 百元印钞机——构造函数 Hundred()
function HundredYuan (id) {
var money = {}
money.__proto__ = HundredYuan.prototype // 实际开发中不能这么写,会严重影响性能,详见 MDN 解释:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/proto
money.id = id
return money
}
// 百元钞模版——原型对象 HundredYuan.prototype
HundredYuan.prototype = {
value: 100,
unit: '元',
color: '红',
form: '纸币',
issue: '2019-01',
兑换等值商品: function () {},
换两张50块: function () {}
}

// 给老子来五百,装进钱包里
var wallet = []
for (var i = 2; i < 7; i++) {
var id = 'AA0000000' + i
wallet.push(HundredYuan(id))
}

wallet // 5张100编号分别是 'AA00000002', 'AA00000003', 'AA00000004', 'AA00000005', 'AA00000006'
wallet[0].value // 100

这个百元印钞机,就是我们所说的「构造函数」,它能生成一个实例对象(也就是造出一张张百元钞),同时它还用 prototype 属性来引用一个存放共同属性的「原型对象」(百元钞模版)。把造钱的过程放进这个百元印钞机,就是「封装」。这样我们就能很好地理解了封装的意义了——隐藏某一方法的具体运行步骤。

new & this

从上面的例子我们可以知道,构造函数的套路就是:

  1. 创建一个空的新对象
  2. 为这个新对象添加公有属性,即 新对象.__proto__ 属性指向构造函数的原型( 构造函数.prototype引用的对象)
  3. 为这个新对象添加自有的属性
  4. 返回这个新对象。

既然套路都一样,那为什么不把这个套路变得贴心一点呢?于是有了 new 操作符,它可以帮我们省去 1、2、4 这三个动作,我们只需专注于添加对象的自有属性即可。同时, JS 之父还帮我们规定好,创建的空对象统一命名为 this 。于是,上面的函数我们可以写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function HundredYuan (id) {
// var this = {}
// this.__proto__ = HundredYuan.prototype
this.id = id
// return this
}
HundredYuan.prototype = {
value: 100,
unit: '元',
color: '红',
form: '纸币',
issue: '2019-01',
兑换等值商品: function () {},
换两张50块: function () {}
}

new HundredYuan('AA00000007')

继承

嘿嘿,有了百元印钞机,富得流油不再是梦 。
但是新的问题来了,整天带着一大沓百元大钞出门,要么可能会被乞丐围着,要么可能会有生命危险!不行不行,还是低调点,我需要点小钞。例如五角硬币机:

1
2
3
4
5
6
7
8
9
function FiveJiao () {}
FiveJiao.prototype = {
value: 5,
unit: '角',
color: '金',
form: '硬币',
issue: '2019-01',
兑换等值商品: function () {}
}

哇,跟百元印钞机好像哦。说不定把它们整一下,就可以弄出一元印钞机、十元印钞机…… 那么我就弄个造钱机,它造的钱都有 兑换等值商品 的功能,我要让 N 元印钞机和 N 角硬币机在不需要自己定义的情况下就能直接用到它爸爸造钱机的 兑换等值商品 功能以及一些属性。这就是我们所谓的「继承」——让子类实例能够拥有父类实例的所有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 父类 Money
function Money(options) {
this.form = options.form
this.issue = options.issue
}
Money.prototype.兑换等值商品 = function () { }

// 子类 HundredYuan
function HundredYuan(id) {
Money.call(this, { form: '纸币', issue: '2019-01' }) // 调用父类 Money 使得子类 HundredYuan 的实例具有父类实例的属性
this.id = id
}
function tempMoney () { } // 1. 为了使 HundredYuan 的原型能够继承父类 Money 的原型,我们造一个空构造函数
tempMoney.prototype = Money.prototype // 2. 父类 Money 的原型赋给空构造函数的原型
HundredYuan.prototype = new tempMoney() // 3. 于是 HundredYuan 的原型间接继承父类 Money 的原型
HundredYuan.prototype.value = 100
HundredYuan.prototype.unit = '元'
HundredYuan.prototype.color = '红'
HundredYuan.prototype.换两张50元 = function () { }

// ……以此类推其他印钞机

new HundredYuan('AA00000001')

Object.create()

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 proto
让子类原型继承父类原型的操作看起来太麻烦了,更人性化及正确的写法如下:

1
2
3
4
5
6
7
8
9
// ...
function HundredYuan(id) {
Money.call(this, { form: '纸币', issue: '2019-01' })
this.id = id
}
HundredYuan.prototype = Object.create(Money.prototype) // 相当于 HundredYuan.prototype.__proto__ = Money.prototype
HundredYuan.prototype.constructor = HundredYuan // 重新指定原型的 constructor
HundredYuan.prototype.value = 100
// ...

ES6 引入的「类」是一个特殊的函数,它可以帮助我们进一步简化继承的操作,但它依旧是「基于原型」的而不是引入新的面向对象继承模型,类语法只不过是一种语法糖。
上面的例子用类改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 父类 Money
class Money {
constructor (options) {
this.form = options.form
this.issue = options.issue
}
兑换等值商品 () { }
}

// 子类 HundredYuan
class HundredYuan extends Money {
constructor (id) {
super({ form: '纸币', issue: '2019-01' }) // 调用父类 Money 使得子类 HundredYuan 的实例具有父类实例的属性
this.id = id
// 在构造函数中的公有属性可以是简单类型,但在类中不可以,所以需要把公有属性放在 constructor 中,作为子类的自有属性。
this.value = 100
this.unit = '元'
this.color = '红'
}
换两张50元 () { }
}

new HundredYuan('AA00000001')

使用类方法确实简洁明了了许多,但是有个问题就是,它明明是个函数,但却不能被调用:

1
2
typeof Money        // "function"
Money() // Uncaught TypeError: Class constructor Money cannot be invoked without 'new'

要用类还是用构造函数,你来决定,这里只是提出这一个现象。
希望看完本文的你,能理解到 JavaScript 中的面向对象思想。

编辑