Javascript继承

| 字数 1839
  1. 1. 星级评分
  2. 2. 利用原型分别实现满星与半星
    1. 2.1. Dom结构
    2. 2.2. 满星&半星JS代码
  3. 3. 利用模版方法改写代码
    1. 3.1. 提取公共逻辑设置父类 Lighten
    2. 3.2. 子类 lightenFullStar & lightenHalfStar
  4. 4. 总结&改进
  5. 5. 资料
  1. 1. 星级评分
  2. 2. 利用原型分别实现满星与半星
    1. 2.1. Dom结构
    2. 2.2. 满星&半星JS代码
  3. 3. 利用模版方法改写代码
    1. 3.1. 提取公共逻辑设置父类 Lighten
    2. 3.2. 子类 lightenFullStar & lightenHalfStar
  4. 4. 总结&改进
  5. 5. 资料

      Javascript的继承是基于原型的,之前理解的比较简单,直到最近在用 es5 写星级评分组件的时候遇到了些问题,解决之后理解深了一些。总结归纳,以便后用。

星级评分

原本的需求有俩个:

  1. 根据后台返回的评价分数单纯进行5颗星的展示
  2. 用户可以在5颗星上为商品进行打分,单击后上传

开始用最笨的方法,从业务出发很快速的把俩个需求都实现了。最后想着能不能利用继承来优化,方便后期扩展可以进行半星或者四分之一星评分。

具体效果如下:
star

利用原型分别实现满星与半星

ps:这里的星星使用 iconfont 的字体图标

Dom结构

1
2
3
4
5
6
7
8
9
<section class="star">
<ul class="star-list">
<li class="item iconfont icon-star-off" title="wtf"></li>
<li class="item iconfont icon-star-off" title="bad"></li>
<li class="item iconfont icon-star-off" title="not bad"></li>
<li class="item iconfont icon-star-off" title="good"></li>
<li class="item iconfont icon-star-off" title="perfect"></li>
</ul>
</section>

满星&半星JS代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
var starRating = (function() {
// 整颗
var lightenFullStar = function(el, opts) {
this.$el = $(el)
this.$stars = this.$el.find('.item')
this.opts = opts
}
lightenFullStar.prototype.lighten = function(num) {
num = parseInt(num)
this.$stars.each(function(i) {
if(i < num ) {
$(this).removeClass('icon-star-off').addClass('icon-star-on')
}else{
$(this).removeClass('icon-star-on').addClass('icon-star-off')
}
})
}
lightenFullStar.prototype.bindEvent = function() {
var self = this
self.$el.on('click', '.item', function() {
var selectedStar = $(this).index() + 1
self.opts.num = selectedStar
typeof self.opts.select === 'function' && self.opts.select.call(this, selectedStar, self.$stars.length)
}).on('mouseover', '.item', function() {
self.lighten($(this).index() + 1)
}).on('mouseout', function() {
self.lighten(self.opts.num)
})
}
lightenFullStar.prototype.init = function() {
this.lighten(this.opts.num)
if(!this.opts.readOnly){
this.bindEvent()
}
}

// 半颗
var lightenHalfStar = function(el, opts) {
this.$el = $(el)
this.$stars = this.$el.find('.item')
this.opts = opts
this.patch = 1
}
lightenHalfStar.prototype.lighten = function(num) {
var integer = parseInt(num),
hasDecimals = integer !== num

this.$stars.each(function(i) {
if(i < num ) {
$(this).removeClass('icon-star-off icon-star-half').addClass('icon-star-on')
}else{
$(this).removeClass('icon-star-on icon-star-half').addClass('icon-star-off')
}
})
// 半颗的点亮
if(hasDecimals) {
this.$stars.eq(integer).removeClass('icon-star-off icon-star-on').addClass('icon-star-half')
}
}
lightenHalfStar.prototype.bindEvent = function() {
var self = this
self.$el.on('mousemove', '.item', function(e) {
// 这是理想状态如果设置padingleft/right 还没到目标dom星星就产生变化
// 星星的间距最好设置为margin
if(e.pageX - $(this).offset().left < $(this).width() / 2) {
self.patch = 0.5
}else{
self.patch = 1
}
var selectedStar = $(this).index() + self.patch
self.lighten(selectedStar)
}).on('click', '.item', function() {
self.opts.num = $(this).index() + self.patch
typeof self.opts.select === 'function' && self.opts.select.call(this, self.opts.num, self.$stars.length)
}).on('mouseout', function() {
self.lighten(self.opts.num)
})
}
lightenHalfStar.prototype.init = function() {
this.lighten(this.opts.num)
if(!this.opts.readOnly){
this.bindEvent()
}
}


// default
var defaults = {
mode: 'full',
num: 0,
readOnly: false,
select: function() {}
}

var reflect = {
'full': lightenFullStar,
'half': lightenHalfStar
}

var _init = function(el, opts) {
opts = $.extend({}, defaults, opts)

if(!reflect[opts.mode]) opts.mode = 'full'
new reflect[opts.mode](el, opts).init()
}

return {
init: _init
}

})()

starRating.init('.star-list', {
mode:'full',
num: 4.6,
readOnly: false,
select: function(selected, total) {
console.log(selected + '/' + total)
}
})

简单来说一下,这里主要运用的事件有 mouseover(mousemove)、mouseout、click。为什么不用 mouseenter&mouseleave 具体可以参考mdn。满星利用 mouseover 监听鼠标移入当前 dom 的操作,半星则将 mouseover 替换为 mousemove ,因为此时我们的鼠标移入时在星星的右侧也就是整颗星,此时在星星内部鼠标从右向左移动 mouseover 是无法触发的,所以不能选择半星。其他的实现就是按照业务一步步来即可,这俩段代码写完之后,发现其实很像。ok,下来利用继承与多态来改写。

利用模版方法改写代码

模版方法是什么?简单来说,抽离出公共逻辑作为父类,子类继承父类,对于特有的逻辑或方法对父类中的逻辑或方法进行重写。具体可以参考设计模式。

提取公共逻辑设置父类 Lighten

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var Lighten = function(el, opts) {
this.$el = $(el)
this.$stars = this.$el.find('.item')
this.opts = opts
this.patch = 1
this.selectEvent = 'mouseover'
}
Lighten.prototype.lighten = function(num) {
num = parseInt(num)

this.$stars.each(function(i) {
if(i < num ) {
$(this).removeClass('icon-star-off icon-star-half').addClass('icon-star-on')
}else{
$(this).removeClass('icon-star-on icon-star-half').addClass('icon-star-off')
}
})
}
Lighten.prototype.bindEvent = function() {
var self = this
self.$el.on(self.selectEvent, '.item', function(e) {
self.calculateRange(e, $(this))
self.lighten($(this).index() + self.patch)
}).on('click', '.item', function() {
var selectedStar = $(this).index() + self.patch
self.opts.num = selectedStar
typeof self.opts.select === 'function' && self.opts.select.call(this, selectedStar, self.$stars.length)
}).on('mouseout', function() {
self.lighten(self.opts.num)
})
}
Lighten.prototype.calculateRange = function() {
// 子类必须重写该方法
console.warn('Subclasses must be overridden')
}
Lighten.prototype.init = function() {
this.lighten(this.opts.num)
if(!this.opts.readOnly){
this.bindEvent()
}
}

子类 lightenFullStar & lightenHalfStar

1
2
3
4
5
6
7
8
9
10
var lightenFullStar = function(el, opts) {
Lighten.call(this, el, opts)
this.selectEvent = 'mouseover'
}
lightenFullStar.prototype.lighten = function(num) {
Lighten.prototype.lighten.call(this, num)
}
lightenFullStar.prototype.calculateRange = function() {}
lightenFullStar.prototype = new Lighten()
lightenFullStar.prototype.construtor = lightenFullStar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var lightenHalfStar = function(el, opts) {
Lighten.call(this, el, opts)
this.selectEvent = 'mousemove'
}
lightenHalfStar.prototype.lighten = function(num) {
var integer = parseInt(num),
hasDecimals = integer !== num

Lighten.prototype.lighten.call(this, integer)
// 半颗的点亮
if(hasDecimals) {
this.$stars.eq(integer).removeClass('icon-star-off icon-star-on').addClass('icon-star-half')
}
}
lightenHalfStar.prototype.calculateRange = function(e, $this) {
if(e.pageX - $this.offset().left < $this.width() / 2) {
this.patch = 0.5
}else{
this.patch = 1
}
}
lightenHalfStar.prototype = new Lighten()
lightenHalfStar.prototype.construtor = lightenHalfStar

alright!看起来不错,感觉万事大吉了。跑起来试试,发现报错

wtf?

看起来还不错的代码竟然跑挂了,根据报错信息很明显可以分析出没有执行子类重写的 lighten 方法。这是什么原因导致的呢?

lightenHalfStar.prototype = new Lighten()

这行代码是将构造函数 Lighten 的原型对象赋值给 lightenHalfStar 的原型对象。这本身是没什么错的,原型继承的关键也是这句话。将此时的lightenHalfStar.prototype打印出来如下:

result-1

在执行lightenHalfStar.prototype = new Lighten()之前lightenHalfStar.prototype中的 lighten确实是自己重写的(可以看 FunctionLocation:120)。让我再看看执行之后的结果。如下:

result-2

可以看到 FunctionLocation:41 (即指向了父类的 lighten 方法所在文件中的位置)为什么会这样?答案其实很简单,将 lightenHalfStar.prototype = new Lighten() 的顺序放在声明 lightenHalfStar 原型方法之前即可。

总结&改进

对于实现重写方法来说,继承语句的位置是相当关键的。一不注意就进了坑。

注意原型继承中子类的构造器需要手动置回。如下:

1
2
lightenHalfStar.prototype = new Lighten()
lightenHalfStar.prototype.constructor = lightenHalfStar

原型继承若子类与父类构造器中变量同名则子类的变量会被覆盖,要想重写,就得在构造器中改变 this 指向。如代码中的:Lighten.call(this, el, opts)

通过图 result-2 我们也可以看到实例化之后父类构造函数的值也会被带进来。如果值很多的话也会浪费资源,这并不是比较好的结果。优化方法:

1
2
3
4
5
6
var extend = function(sub, sup) {
var F = function() {}
F.prototype = sup.prototype
sub.prototype = new F()
sub.prototype.construtor = sub
}

利用空的构造函数做中间人(代理)即可解决。

资料

  1. 完整代码:星级评分组件

  2. Javascript面向对象编程(二):构造函数的继承