面向对象案例
一、放大镜
1、效果演示

2、布局分析
通过观察京东等电商网站的放大镜,看到右边的大图会将原本空白区域的内容覆盖显示,所以右边大图,选择从左边的盒子中定位到右边。
2.1、结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <div class="enlarge"> <div class="middle"> <img src="./images/middle1.jpg" alt=""> <div class="mask"></div> <div class="big"></div> </div> <div class="small"> <img src="./images/small1.jpg" alt=""> <img src="./images/small2.jpg" alt=""> </div> </div>
|
右边盒子中的图片,有两种做法:
- 给大盒子添加背景图片
- 给大盒子放图片标签
2.2、样式
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
| .enlarge .middle{ width: 400px; height: 400px; border: 3px solid #aaa; position: relative; } .enlarge .middle .mask{ width: 100px; height: 100px; background-color: rgba(255,255,0,0.7); position: absolute; left: 0; top: 0; display: none; } .enlarge .middle .mask{ cursor: move; } .enlarge .middle .big{ width: 400px; height: 400px; display: none; position: absolute; left: 105%; top: 0; background-image: url('./images/big1.jpg'); background-size: 1600px 1600px; background-position: 0 0; } .small img{ margin: 5px; border: 5px solid #000; } .small img:first-child{ border-color: #f00; }
|
注意:布局时必须遵循一个比例:
$$
\frac{中等图宽度}{遮罩宽度} = \frac{大图宽度}{大盒子宽度}
$$
$$
\frac{中等图高度}{遮罩高度} = \frac{大图高度}{大盒子高度}
$$
3、效果分析
3.1、点击小图
每个小图都需要绑定单击事件,点击小图的时候,要将中等图和大图进行更换,要根据小图的路径更换中等图的大图的路径。
3.2、移入移出中等盒子
鼠标移入中等盒子,要让遮罩和大盒子显示,移出隐藏。
3.3、鼠标移动
鼠标移入中等盒子后,在中等盒子中移动,要设置大图的定位,让大图也跟随遮罩的比例进行移动。
4、代码
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
| function Enlarge(cname) { this.box = document.querySelector('.'+ cname) this.middleBox = this.box.querySelector('.middle') this.middleImg = this.box.querySelector('.middle img') this.mask = this.box.querySelector('.middle .mask') this.bigBox = this.box.querySelector('.middle .big') this.smallImgs = this.box.querySelectorAll('.small img') }
Enlarge.prototype.init = function() { for(let a=0; a<this.smallImgs.length; a++) { this.smallImgs[a].onclick = () => { for(var b=0; b<this.smallImgs.length; b++) { this.smallImgs[b].style.borderColor = '#000' } this.smallImgs[a].style.borderColor = '#f00' var smallImgPath = this.smallImgs[a].getAttribute('src') var middleImgPath = smallImgPath.replace('small', 'middle') this.middleImg.setAttribute('src', middleImgPath) var bigImgPath = this.smallImgs[a].getAttribute('src') this.bigBox.style.backgroundImage = `url('${bigImgPath}')` } } this.middleBox.onmouseover = () => { this.mask.style.display = 'block' this.bigBox.style.display = 'block' this.middleBox.onmousemove = (e) => { var x = e.pageX var y = e.pageY var l = x - this.middleBox.offsetLeft - this.middleBox.clientLeft - this.mask.offsetWidth / 2 var t = y - this.middleBox.offsetTop - this.middleBox.clientTop - this.mask.offsetHeight / 2 if(l < 0) { l = 0 } if(t < 0) { t = 0 } if(l > this.middleBox.clientWidth - this.mask.offsetWidth) { l = this.middleBox.clientWidth - this.mask.offsetWidth } if(t > this.middleBox.clientHeight - this.mask.offsetHeight) { t = this.middleBox.clientHeight - this.mask.offsetHeight } this.mask.left = l + 'px' this.mask.top = t + 'px' var percentX = l / this.middleBox.clientWidth var percentY = t / this.middleBox.clientHeight var bigLeft = percentX * parseInt(getComputedStyle(this.bigBox).backgroundSize.split(' '))[0] var bigTop = percentY * parseInt(getComputedStyle(this.bigBox).backgroundSize.split(' '))[1] this.bigBox.style.backgroundPosition = `-${bigLeft}px -${bigTop}px` } } }
var elg = new Enlarge('enlarge')
elg.init()
|
二、烟花
1、效果演示

2、效果分析
- 创建夜空
- 在夜空大盒子中点击鼠标,记录鼠标在夜空中的坐标,根据坐标创建小盒子放在夜空底部
- 给小盒子添加动画上升到鼠标点击位置
- 在鼠标位置创建多个小div
- 让每个小div移动到随机位置,到达目标位置后移除小div
3、代码
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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
| function FireWorks() { this.nightSky = document.createElement('div') this.setStyle(this.nightSky, { width:"1000px", height:"600px", background:"#000", border:"10px solid pink", position:"relative" }) document.body.appendChild(this.nightSky) }
FireWorks.prototype.init = function() { this.nightSky.onclick= e => { var x = e.offsetX; var y = e.offsetY; this.createFire(x, y) } }
FireWorks.prototype.createFire = function(x, y) { var div = document.createElement("div"); this.setStyle(div,{ width: "10px", height: "10px", position: "absolute", left: x + 'px', bottom: 0, background: this.getColor() }); this.nightSky.appendChild(div); this.toUp(div, x, y) }
FireWorks.prototype.toUp = function(ele, x, y) { this.animate(ele, { top: x }, () => { ele.remove() this.createFires(x, y) }) }
FireWorks.prototype.createFires = function(x, y) { var num = this.getRandom(20, 50) for(var a=0; a<num; a++) { var div = document.createElement('div') this.setStyle(div,{ width: "10px", height: "10px", position: "absolute", left: x + 'px', top: y + 'px', borderRadius: '50%', background: this.getColor() }); this.nightSky.appendChild(div) this.boom(div) } }
FireWorks.prototype.boom = function(ele) { this.animate(ele, { left: this.getRandom(this.nightSky.clientWidth - 10), top: this.getRandom(this.nightSky.clientHeight - 10) }, () => { ele.remove() }) }
FireWorks.prototype.animate = function(ele, animateObj, cb = function() {}) { var k = 0 for(let key in animateObj) { k++ let cssValue = getComputedStyle(ele)[key] let target = key === 'opacity' ? animateObj[key] * 100 : animateObj[key] cssValue = key === 'opacity' ? parseInt(cssValue * 100) : parseInt(cssValue) let timer = setInterval(function() { let speed = (target - cssValue) / 20 speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed) cssValue += speed ele.style[key] = key === 'opacity' ? cssValue / 100 : cssValue + 'px' if(target === cssValue) { clearInterval(timer) k-- k === 0 ? cb() : '' } }, 16) } }
FireWorks.prototype.getColor = function() { var color = '#' for(var a=0; a<3; a++) { var num = this.getRandom(256).toString(16) num = num.length === 1 ? '0' + num : num color += num } return color }
FireWorks.prototype.getRandom = function(a, b = 0) { return Math.floor(Math.random() * Math.abs(a - b)) + Math.min(a, b) }
FireWorks.prototype.setStyle = function(ele, styleObj) { var cssText = '' for(var key in styleObj) { var reg = /[A-Z]/ var value = styleObj[key] if(reg.test(key)) { var reg = /([a-z]+)([A-Z])([a-z]+)/g key = key.replace(reg, '$1-$2$3') key = key.toLowerCase() } cssText += key + ':' + value + ';' } ele.style.cssText = cssText }
var fire = new FireWorks()
fire.init()
|
三、贪吃蛇
1、效果分析
1.1、地图
一个大div,控制食物和蛇的范围。
1.2、食物
一个随机出现的小div,被吃掉后重新创建。
1.3、蛇
有3个小div组成。需要有移动方向,被键盘控制。
3个小div可以移动,移动过程中,可以吃食物和撞墙或撞身体死亡。
2、代码
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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
| function Map() { this.map = document.createElement('div') this.setStyle(this.map, { width: '1000px', height: '600px', backgroundColor: '#aaa', border: '10px solid #666', position: 'relative' }) document.body.appendChild(this.map) }
Map.prototype.setStyle = function(ele, styleObj) { var cssText = '' for(var key in styleObj) { var cssValue = styleObj[key] var reg = /[A-Z]/ if(reg.test(key)) { var word = key.match(reg)[0] key = key.replace(word, '-' + word.toLowerCase()) } cssText += key + ':' + cssValue + ';' } ele.style.cssText = cssText }
var m = new Map()
function Food() { this.food = document.createElement('div') m.setStyle(this.food, { width: '10px', height: '10px', backgroundColor: '#f00', position: 'absolute', left: this.getRandom(m.map.clientWidth - 10) + 'px', top: this.getRandom(m.map.clientHeight - 10) + 'px' }) m.map.appendChild(this.food) }
Food.prototype.getRandom = function(a, b = 0) { return Math.floor(Math.random() * Math.abs(a - b)) + Math.min(a, b) }
var f = new Food()
function Snake() { this.body = [ { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 20, y: 0 } ] this.direction = 'right' this.timer = 0 }
Snake.prototype.init = function() { this.changeDirection() this.move() }
Snake.prototype.changeDirection = function() { document.onkeyup = e => { var word = String.fromCharCode(e.keyCode).toLowerCase() switch(word) { case 'w': this.direction = 'up' break case 'd': this.direction = 'right' break case 'a': this.direction = 'left' break case 's': this.direction = 'down' break } } }
Snake.prototype.show = function() { var snakes = m.map.querySelectorAll('.snake') if(snakes) { for(var a=0; a<snakes.length; a++) { snakes[a].remove() } } for(var a=0; a<this.body.length; a++) { var div = document.createElement('div') m.setStyle(div, { width: '10px', height: '10px', backgroundColor: '#0ff', position: 'absolute', left: this.body[a].x + 'px', top: this.body[a].y + 'px' }) m.map.appendChild(div) if(a === this.body.length - 1) { div.style.borderRadius = '50%' div.style.backgroundColor = '#00f' } div.className = 'snake' } }
Snake.prototype.move = function() { this.timer = setInterval(() => { for(var a = 0; a < this.body.length - 1; a++) { this.body[a].x = this.body[a + 1].x this.body[a].y = this.body[a + 1].y } var snakeHead = this.body[this.body.length - 1] switch(this.direction) { case 'up': snakeHead.y -= 10 break case 'down': snakeHead.y += 10 break case 'left': snakeHead.x -= 10 break case 'right': snakeHead.x += 10 break } this.show() this.eat() this.die() }, 200) }
Snake.prototype.eat = function() { var snakeHead = this.body[this.body.length - 1] if(snakeHead.x === f.food.offsetLeft && snakeHead.y === f.food.offsetTop) { this.body.unshift({ x: this.body[0].x, y: this.body[0].y }) f.food.remove() f = new Food() } }
Snake.prototype.die = function() { var snakeHead = this.body[this.body.length - 1] if(snakeHead.x < 0 || snakeHead.y < 0 || snakeHead.x > m.map.clientWidth - 10 || snakeHead.y > m.map.clientHeight - 10) { alert('GAME OVER!!!') clearInterval(this.timer) this.show() } for(var a=0; a<this.body.length-1; a++) { if(snakeHead.x === this.body[a].x && snakeHead.y === this.body[a].y) { alert('GAME OVER!!!') clearInterval(this.timer) this.show() break } } }
var s = new Snake()
s.init()
|
四、深拷贝和浅拷贝
1、需求
贪吃蛇案例中,我们发现一个问题,蛇在死亡后,最后一个动作是将蛇头走出地图。这个效果的用户体验感不好。
为了能让蛇在死亡后,蛇头不走出地图,我们需要一个新数组,用来记录蛇上一次走动后的坐标,当蛇死亡后,蛇头走出地图,我们可以通过记录的蛇上次走动的坐标,将蛇的位置还原到上一次的位置。
在蛇的构造函数中定义属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function Snake() { this.body = [ { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 20, y: 0 } ] this.direction = 'right' this.timer = 0 this.oldBody = [] }
|
蛇每次走动都需要先记录上次的坐标,然后再走动:
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
| Snake.prototype.move = function() { this.timer = setInterval(() => { this.oldBody = this.body for(var a = 0; a < this.body.length - 1; a++) { this.body[a].x = this.body[a + 1].x this.body[a].y = this.body[a + 1].y } var snakeHead = this.body[this.body.length - 1] switch(this.direction) { case 'up': snakeHead.y -= 10 break case 'down': snakeHead.y += 10 break case 'left': snakeHead.x -= 10 break case 'right': snakeHead.x += 10 break } this.show() this.eat() this.die() }, 200) }
|
当赋值完成后,我们发现蛇的走动出了bug,因为这次的赋值属于引用数据类型的赋值,改变body其实也是在改变oldBody,因为两个属性共享同一个数据空间了。这就相当于没有记录。
此时,我们需要一个跟body一模一样的数据,但不要跟body共享同一个数据空间。
这就需要使用到深拷贝和浅拷贝了。
深拷贝和浅拷贝都是根据源数据拷贝一个新数据,新数据和源数据不共享同一个数据空间。
2、浅拷贝
浅拷贝:顾名思义,拷贝的比较浅。源数据和目标数据不共享同一个数据空间,如果源数据中包含引用数据类型,源数据中的引用数据和新数据中的引用数据还在共享同一个数据空间。
2.1、数组浅拷贝
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var arr = [1, false, {name: '张三', age: 12}, function() {}] var brr = []
arr.forEach(item => brr.push(item))
var brr = [...arr]
var brr = arr.slice() var brr = arr.concat()
cosnole.log(arr === brr)
console.log(arr[2] === brr[2])
|
如果用到其中具体的引用数据,还是会造成两个数据一起改变,更严谨的做法是要进行深拷贝。
2.2、对象浅拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var obj = { name: '张三', age: 12, wife: { name: '翠花', age: 13 } } var pbj = {}
for(var key in obj) { pbj[key] = obj[key] }
pbj = {...obj}
pbj = Object.assign({}, obj)
console.log(obj === pbj)
console.log(obj['wife'] === pbj['wife'])
|
assign方法用于将一个对象中的属性拷贝到另一个目标对象中,返回目标对象。
3、深拷贝
深拷贝:深层次的拷贝,新数据和源数据不共享同一个数据空间,其中包含的引用数据也不共享同一个数据空间。
3.1、json方法
例:
1 2 3 4 5 6 7 8
| var arr = [1, false, {name: '张三', age: 12}]
var brr = JSON.parse(JSON.stringify(arr))
console.log(arr === brr) console.log(arr[2] === brr[2])
|
json:一种数据格式,类似于数组和对象的数据格式。
我们在项目中存储数据,通常会使用json文件进行存储,一种后缀为.json
的文件。
json文件格式要求:
例:
1 2 3 4
| { "name": "zhangsan", "age": 12 }
|
或:
这种格式的数据叫做json。这种格式的字符串叫json字符串。
json方法:
1 2 3
| JSON.stringify(对象/数组)
JSON.parse(json字符串)
|
例:
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
| var obj = { name: '张三', age: 12, wife: { name: '翠花', age: 13 }, children: ['大毛', '二毛', '小明'], eat: function() { console.log('吃东西') } } var objStr = JSON.stringify(obj) console.log(objStr)
var arr = [1, false, {name: '张三', age: 12}, function() {console.log('函数')}] var arrStr = JSON.stringify(arr) console.log(arrStr)
var pbj = JSON.parse(objStr) console.log(pbj)
var brr = JSON.parse(arrStr) console.log(brr)
|
每次转换后,会得到一个新数据,所以可以利用json方法实现深拷贝。
但json方法的转换,会丢失函数。数组或对象中如果包含有函数,转换后的结果中是没有函数的。
为了在深拷贝过程中不丢失函数,我们需要自己手动封装一个递归函数,用于实现深拷贝。
3.2、递归实现深拷贝
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
| function deepCopy(data) { var newData if(Object.prototype.toString.call(data) === '[object Object]') { newData = {} } else if(Object.prototype.toString.call(data) === '[object Array]') { newData = [] } else { return data } for(var key in data) { if(typeof data[key] === 'object') { newData[key] = deepCopy(data[key]) } else { newData[key] = data[key] } } return newData } var obj = { name: '张三', age: 12, wife: { name: '翠花', age: 13 }, children: ['大毛', '二毛', '小明'], eat: function() { console.log('吃东西') } } var pbj = deepCopy(obj) console.log(pbj); console.log(pbj === obj); console.log(pbj['wife'] === obj['wife']);
var arr = [1, false, {name: '张三', age: 12}, function() {console.log('函数')}] var brr = deepCopy(arr) console.log(brr); console.log(arr === brr); console.log(arr[2] === brr[2]);
|
4、深拷贝完善贪吃蛇
在构造函数中,新定义一个数据用于存储蛇上一次的坐标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function Snake() { this.body = [ { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 20, y: 0 } ] this.direction = 'right' this.timer = 0 this.oldBody = [] }
|
蛇每次移动,将蛇上次的坐标进行记录:
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
| Snake.prototype.move = function() { this.timer = setInterval(() => { this.oldBody = JSON.parse(JSON.stringify(this.body)) for(var a = 0; a < this.body.length - 1; a++) { this.body[a].x = this.body[a + 1].x this.body[a].y = this.body[a + 1].y } var snakeHead = this.body[this.body.length - 1] switch(this.direction) { case 'up': snakeHead.y -= 10 break case 'down': snakeHead.y += 10 break case 'left': snakeHead.x -= 10 break case 'right': snakeHead.x += 10 break } this.show() this.eat() this.die() }, 200) }
|
蛇死亡后,让蛇的身体坐标变成上次死亡之前的坐标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Snake.prototype.die = function() { var snakeHead = this.body[this.body.length - 1] if(snakeHead.x < 0 || snakeHead.y < 0 || snakeHead.x > m.map.clientWidth - 10 || snakeHead.y > m.map.clientHeight - 10) { alert('GAME OVER!!!') clearInterval(this.timer) this.body = JSON.parse(JSON.stringify(this.oldBody)) this.show() } for(var a=0; a<this.body.length-1; a++) { if(snakeHead.x === this.body[a].x && snakeHead.y === this.body[a].y) { alert('GAME OVER!!!') clearInterval(this.timer) this.body = JSON.parse(JSON.stringify(this.oldBody)) this.show() break } } }
|