2016,寒假前端学习总结

今天是2.25,寒假的最后一天。
一月九号我写下《寒假前端学习计划》一文作为寒假的开始,今天我同样写下《寒假前端学习总结》一文作为寒假的结束。

疯狂生长

给自己的寒假,定的总结词是“疯狂生长”。这也算是自己过的最为充实,最为努力的一个寒假。每天起床后,便
一头扎进编程的海洋中,从早到晚,乐此不疲。

读书

寒假带了三本书回家:

  1. 《JavaScript高级程序设计》
  2. 《JavaScript设计模式与开发实践》
  3. 《计算机科学导论》

颇为自豪的是,三本书我都看完了。
第一本《JavaScript高级程序设计》算是二战了,有种常看常新的感觉。感觉之前在做项目时,碰到的很多JavaScript问题,都可以在书上找到答案。难怪被人称之为红宝书。

第二本《JavaScript设计模式与开发实践》,给我带来了极大的震撼。书中每一个介绍的设计模式,都会有相应的实际案例相配套。从未有见过,代码还能以那样一种精巧的方式书写。算是在学习之余,极大的开拓了自己的眼界。从一个真正的程序员的视角,去审视前端与其性能。

第三本《计算机科学导论》,是我顺手带回家。结果在某一天的下午,翻开第一页后变便一口气连读十几个小时的书籍。要说《JavaScript高级程序设计》让我拥有了对JavaScript的大局观,那么《计算机科学导论》便是让我领略到计算机科学的万千精彩。

做项目

俗话说的好,光说不练假把式。在看书之余,自然就是不断的练习。把书上看到的设计模式,新方法运用到实际项目中,对自己之前的代码进行重构等。

移动端开发

之前一直做的是PC端,定宽的网页。对于移动端,可以算是一无所知。(用bootstrap等框架做出的网页虽然能适应移动端,但总觉得不算是真正的能力,逃)
于是把移动端开发的学习放进了寒假计划中。
先去网上看了看移动端开发的资料,被一大堆的dpi,px,ppi,dpr弄得晕头转向。随后便打开了慕课网,跟着视频学习。下面是两节学完的课程。
移动端开发
学习了一门《移动web入门》,又跟着慕课网,写了一个春节贺卡。顿时感觉有信心多了。
写完贺卡的我,开始思考该写点啥来巩固一下刚学的知识。恰巧看到了张秋怡学姐(学长?)的前端简历,顿时惊为天人。同时张秋怡学姐还做了响应式设计。
张秋怡学姐の简历

遂开始仿写学姐的简历。整个过程历时三天,没有去Github看源代码。素材也自己找。使用了Gulp+Sass等工具。可以说是像素级模仿。全面巩固了之前所学的移动端知识。
我仿写の简历

当然,网页底部带上了版权声明,证明是张秋怡学姐的作品。我只是仿写。
版权声明

简历目前挂在我的服务器上,使用Express为后台,Jade作为渲染引擎,Mongodb为数据库。因为想做一个简历生成页面,只需要填入信息,便可以自动生成张秋怡学姐那样的简历。同时呢也希望自己能在这个暑假,找到一份实习工作。

简历地址–刘子健,前端开发实习生

工作室项目

工作室的项目,是内网改造。使用了Vue+Vue-Router,来完成SPA的编写。寒假一个月,算是深入学习与使用了Vue这个框架,不得不感慨,Vue真的是太好用了。

工具学习

工具的学习,算是少的。因为在知乎上看到了这个问题:

前端深入到什么程度才可以本科就拿到bat google 的offer?

其中,一个我非常崇拜的前辈贺师俊回答了这个问题。

才大二,少做(react/angular/php)hello world级别的事情(除非你能作出点真的有点实际价值的产品),先打基础。

于是警醒了我,让我开始了补基础的旅程。
不过为了加速开发,还是学了一点点工具的。比如Gulp,Sass,Webpack,forever等
最具代表性算是Gulp了。
半年前想学习的时候,觉得怎么看都不理解Gulp的运作方式,遂放弃。结果寒假时候看了看Gulp的用法,最多半小时,便熟练上手使用。想来是这半年的前端与Node学习,让我在不知不觉中理解了Gulp的运转方式。自然上手就超级快。

写文章

这书也看了,项目也写了。自然就要写文章总结了。
整个寒假下来,一共写了十六篇文章。包括前端学习,管理学习,随笔杂文等。其中,前端学习10篇,主要发表于自己的博客、Segmentfault和慕课网中。
写博客两个月中,给我的博客网站带来了2.3k的浏览量。算是小有成就~
博客浏览量

同时,因为发表文章的缘故,在Segmentfault获得了1.2k的声望。这在我前两个月,是想都不敢想的事情。
Segmentfault

发表在慕课网的文章,获得了征文大赛的奖项,也得到了慕课网二月份优秀作者的称号~
慕课网

感想

要说寒假的感觉,就是自己更勇敢了。
之前还因为自己是个文科生,而惴惴不安,整日思考自己能否学好编程。

在前辈的帮助,指引与自己努力之下,一个寒假过后,感觉整个世界都是亮堂的。要说从哪儿开始的话,就是提笔写下第一篇博客之时,My life has changed.

前方路漫漫,编程的世界却又如此绚丽。一路前进,且行且歌!

寒假前端学习(10)——理解DOM事件流的三个阶段

本文主要解决两个问题:

  1. 什么是事件流
  2. DOM事件流的三个阶段

起因

在学习前端的大半年来,对DOM事件了解甚少。一般也只是用用onclick来绑定个点击事件。在寒假深入学习JavaScript时,愈发觉得自己对DOM事件了解不够,遂打开我的《JavaScript高级程序设计》,翻到DOM事件那一章,开始第二次学习之旅。
当然,DOM事件所囊括的知识较为庞杂,所以本文专注与自己学习时所碰到的难点,DOM事件流。

流的概念,在现今的JavaScript中随处可见。比如说React中的单向数据流,Node中的流,又或是今天本文所讲的DOM事件流。都是流的一种生动体现。
至于流的具体概念,我们采用下文的解释:

用术语说流是对输入输出设备的抽象。以程序的角度说,流是具有方向的数据。
通通连起来——无处不在的流 淘宝FED–愈之

事件流之事件冒泡与事件捕获

在浏览器发展的过程中,开发团队遇到了一个问题。那就是页面中的哪一部分拥有特定的事件?
可以想象画在一张纸上的一组同心圆,如果你把手指放在圆心上,那么你的手指指向的其实不是一个圆,而是纸上所有的圆。放到实际页面中就是,你点击一个按钮,事实上你还同时点击了按钮所有的父元素。
开发团队的问题就在于,当点击按钮时,是按钮最外层的父元素先收到事件并执行,还是具体元素先收到事件并执行?所以这儿引入了事件流的概念。

事件流所描述的就是从页面中接受事件的顺序。

因为有两种观点,所以事件流也有两种,分别是事件冒泡和事件捕获。现行的主流是事件冒泡。

事件冒泡

事件冒泡即事件开始时,由最具体的元素接收(也就是事件发生所在的节点),然后逐级传播到较为不具体的节点。
举个栗子,就很容易明白了。

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Event Bubbling</title>
</head>
<body>
<button id="clickMe">Click Me</button>
</body>
</html>

然后,我们给button和它的父元素,加入点击事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var button = document.getElementById('clickMe');
button.onclick = function() {
console.log('1. You click Button');
};
document.body.onclick = function() {
console.log('2. You click body');
};
document.onclick = function() {
console.log('3. You click document');
};
window.onclick = function() {
console.log('4. You click window');
};

效果如图所示:
事件冒泡示例图

在代码所示的页面中,如果点击了button,那么这个点击事件会按如下的顺序传播(Chrome浏览器):

  1. button
  2. body
  3. document
  4. window

也就是说,click事件首先在<button>元素上发生,然后逐级向上传播。这就是事件冒泡。

事件捕获

事件捕获的概念,与事件冒泡正好相反。它认为当某个事件发生时,父元素应该更早接收到事件,具体元素则最后接收到事件。比如说刚才的demo,如果是事件捕获的话,事件发生顺序会是这样的:

  1. window
  2. document
  3. body
  4. button

事件捕获示例图
当然,由于时代更迭,事件冒泡方式更胜一筹。所以放心的使用事件冒泡,有特殊需要再使用事件捕获即可。

DOM事件流

DOM事件流包括三个阶段。

  1. 事件捕获阶段
  2. 处于目标阶段
  3. 事件冒泡阶段

如图所示(图片源于网络,若侵权请告知):
DOM事件流示例图

1. 事件捕获阶段

也就是说,当事件发生时,首先发生的是事件捕获,为父元素截获事件提供了机会。
例如,我把上面的Demo中,window点击事件更改为使用事件捕获模式。(addEventListener最后一个参数,为true则代表使用事件捕获模式,false则表示使用事件冒泡模式。不理解的可以去学习一下addEventListener函数的使用)

1
2
3
window.addEventListener('click', function() {
console.log('4. You click window');
}, true);

此时,点击button的效果是这样的。
DOM事件流中事件捕获示例图

可以看到,点击事件先被父元素截获了,且该函数只在事件捕获阶段起作用。

处于目标与事件冒泡阶段

事件到了具体元素时,在具体元素上发生,并且被看成冒泡阶段的一部分。
随后,冒泡阶段发生,事件开始冒泡。

阻止事件冒泡

事件冒泡过程,是可以被阻止的。防止事件冒泡而带来不必要的错误和困扰。
这个方法就是:stopPropagation()

stopPropagation() 方法
终止事件在传播过程的捕获、目标处理或起泡阶段进一步传播。调用该方法后,该节点上处理该事件的处理程序将被调用,事件不再被分派到其他节点。

我们对button的click事件做一些改造。

1
2
3
4
5
6
button.addEventListener('click', function(event) {
// event为事件对象
console.log('1. You click Button');
event.stopPropagation();
console.log('Stop Propagation!');
}, false);

点击后,效果如下图:
阻止冒泡示例图

不难看出,事件在到达具体元素后,停止了冒泡。但不影响父元素的事件捕获。

总结与感想

事件流:描述的就是从页面中接受事件的顺序。分有事件冒泡与事件捕获两种。
DOM事件流的三个阶段:

  1. 事件捕获阶段
  2. 处于目标阶段
  3. 事件冒泡阶段

在学习DOM事件的过程中,了解了DOM事件的三个阶段,也知道事件冒泡是干啥用的,又如何阻止。配合前期所学的二叉树的相关知识,受益匪浅。

前端路漫漫,且行且歌~

寒假前端学习(9)——理解CSS盒模型与宽高计算

起因

盒模型是CSS的核心知识,属于那种不掌握好,在实际工作中就容易犯迷糊的知识。
至于本篇文章,主要解决一个问题,那就是CSS盒模型的计算方法。至于别的知识,也算是自己回忆和复习一次。

盒模型的构成

关于盒模型的构成,算是前端的基础知识了。网络上关于这方面的知识也是多如牛毛。所以这儿我就用Chrome浏览器控制台的盒模型图。(毕竟最贴近实际开发环境)
盒模型构成图
可以看到盒模型由margin,border,padding,content(中心部分0x0的那个框)四部分组成。
如果要形象化的理解呢,我们举个栗子~

这儿有一个仓库,仓库里是各式各样的箱子。仓库代表网页,箱子代表独立的div。
两个箱子之间的空隙,就是margin。
箱子当然有自己边框了,每个箱子边框的厚度不一。这个边框,就是border。厚度呢就是border的大小。
箱子里面当然也装着各式各样的货物,箱子里面所有的货物,就是content。
但是货物也有可能没把箱子堆满,那么箱子内除去货物的空白部分,就是padding了。

这就是我对盒模型在现实中的理解。

盒模型的宽度计算

盒模型的宽度计算,不复杂但也不好玩。因为一个盒模型的宽度,不只是计算其content的宽度,还会加上元素的边框与内边距。

用个demo,就很好理解了。在demo中,两个div的宽度是一致的。(demo出处在底部)

1
2
3
4
5
6
7
8
9
10
11
.simple {
width: 500px;
margin: 20px auto;
}
.fancy {
width: 500px;
margin: 20px auto;
padding: 50px;
border-width: 10px;
}

但实际情况,却是这样的:
盒模型demo
这是因为盒模型计算宽度时,加上了padding和border的宽度。所以第二个元素看起来要比第一个元素大。

这样对于计算盒模型宽度是不利的,因为比较繁琐。于是后来人为了解决这个问题,在CSS3中给盒模型加入了新属性:box-sizing

CSS3的box-sizing

box-sizing共两个属性,一个是content-box,一个是border-box
设置为content-box则盒模型宽度计算方法同CSS2.1,计算内边距和边框。所以这儿我们着重讲解border-box

当设置一个盒模型为box-sizing: border-box时,这个盒子的内边距和边框都不会再增加它的宽度。

继续看第二个demo。在这儿,我们给所有盒模型统一设置box-sizing: border-box

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.simple {
width: 500px;
margin: 20px auto;
}
.fancy {
width: 500px;
margin: 20px auto;
padding: 50px;
border: solid blue 10px;
}

那么,出来的效果会是这样的。
box-sizing demo

可以看到两个盒模型的宽度一致了。
这是因为之前设置的宽度,是元素的宽度。而内边距和边框在元素宽度外绘制。
而设置border-box时,内边距和边框都在设定的宽度内进行绘制。元素宽度需要由设定宽度减去内边距和边框得到。

怎么样,是不是很容易理解呢?至于高度,计算原理同上。这儿就不赘述啦。

小tips

算是个自己写网页时经常碰到的问题,那就是如果给一个元素设置background时,背景颜色的范围将包括内边距。

总结

要说总结的话,这节应该是自己学的最轻松的一部分。之前都是盲点和难点,这里却只是似懂非懂。看了看文档就瞬间明白了。然后想了想,还是写篇博客出来,因为好记性不如烂笔头~

然后再放上参考链接,有兴趣多了解的同学,也可以点开看看。

前端路漫漫,且行且歌~

参考链接:

CSS - 盒模型(也是demo来源)
CSS - 盒模型 - box-sizing
CSS3 box-sizing 属性

寒假前端学习(8)——理解CSS浮动与清除浮动

本文主要探讨两个问题:

  1. 为什么CSS设置浮动会引起父元素塌陷
  2. 为什么设置clear:both能清除浮动,并撑开父元素。

起因

CSS的浮动,算是我在写网页时用的最多的属性之一。但要说我对浮动的了解程度的话,只能说“知其然而不知其所以然”。虽然很多人都说浮动会用就行,但是要想成为一个优秀的前端,对这些常用属性得知根知底。

恰巧在慕课网,张鑫旭老师开了《CSS深入理解之float浮动》这门课。链接在文末,有兴趣的可以听听,老师讲课风格很风趣……

Float的历史

Float设计的初衷,是为了实现文字环绕效果。就像下图展示的一样(图片源于w3school):
Float效果图
嗯,就这么简单。

Float引起的父元素高度塌陷BUG?

在这儿,我们用一个例子来说明子元素设置浮动,从而引起父元素高度塌陷的问题。
首先写一个div,里面插入一张图片。

1
2
3
<div id="div">
<img src="./source/head.jpg">
</div>

我们再给div设置一个border,为了让大家看的清楚。
CSS设置如下:

1
2
3
4
#div {
border: 5px solid red;
width: 600px;
}

最后效果是这样的:
未Float的效果图
通过chrome控制台,可以看到此时div的高度为464px。
div高度

接下来,我们给那张图片添加浮动效果。

1
2
3
#div img{
float: left;
}

再看网页,发现父元素已经塌陷了,之前的边框也消失不见了,成为一条线了。
Float后的效果图
此时再去控制台查看div的高度,高度为0px。
div高度

不,不是BUG

很多人把这个现象称为浮动带来的BUG。但从一开始Float的用途来思考:

1
Float设计的初衷,是为了实现文字环绕效果。”

那么,在那远古蛮荒的互联网时代,要如何实现文字环绕图片的效果呢?机智的程序员加入了Float属性,也引入了子元素浮动,父元素高度塌陷的特性。
看到这句话的时候,我思考了很久。因为无法理解父元素高度塌陷为何能让文字环绕图片。于是把视频来来回回看了十多遍,又手写了个demo,总算理解了。

总结来说,核心要点在于一句话:

1
“浮动元素会脱离文档流。”

至于文档流是啥,我这儿就不介绍了。但浮动的元素脱离了文档流,所以是不计算高度的。
在此,我们加入一段话,看看div的高度。
段落高度
从图中可以看出,div因为段落的加入,高度被撑开了。

所以子元素浮动引起父元素高度塌陷的原因如下:
因为没有预先设置div高度,所以div高度由其包含的子元素高度决定。而浮动脱离文档流,所以图片并不会被计算高度。此时的div中,相当于div中子元素高度为0,所以发生了父元素高度塌陷现象。

文字环绕效果的实现

那么,文字环绕效果是如何实现的?
其实讲起来也很简单,因为父元素高度塌陷,所以文字会按正常顺序排列,无视图片高度。而图片宽带又还在,所以实现了文字环绕效果。

清除浮动

介绍完浮动,自然要介绍清除浮动。在此,我们不具体的去探讨各种清除浮动的方式。而是去探讨,为何设置clear:both能清除浮动,并撑开父元素。

clear:both的作用

clear:both的作用,对各位来说可以算是耳熟能详了。至于clear的left,right等属性,我们这儿就不一一列举了。

1
<div style="clear: both;"></div>

在父元素内的底部,加入这一行代码。看图易知,父元素因为子元素设置浮动而高度塌陷的问题,已经被解决了。
加入clear: both后的效果

然后如果只是会用clear:both,又怎么能满足我的求知欲呢?相比与这行代码产生的作用,我更关心为什么这行代码能清除浮动。
对此,我继续翻阅文档。
在w3school中,clear的定义如下:

1
clear 属性规定元素的哪一侧不允许其他浮动元素。

当然,这样看,还是很难理解为什么clear能清除浮动并撑开父元素高度。那我们举个栗子!
当先声明一个元素A向左浮动时,由于脱离文档流,这个元素的右边就会空出一片空间,空间的长宽与浮动元素长宽相同。
然后我们再声明另外一个元素B,如果元素A右侧空出的空间内,还能放下元素B的话,那么元素B就会自动补上去。
下面我写一个demo,应该就很好理解了。
HTML部分如下:

1
2
3
4
5
6
7
8
<div id="div">
<div id="a">
<p>I'm divA</p> //此处用p
</div>
<div id="b">
<span>I'm divB</span> //用span,防止两个都是p,不能展现父元素塌陷效果。
</div>
</div>

CSS部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#div {
border:5px solid red;
width:400px;
}
#a {
background:rgba(149, 149, 149, 0.42); // 为了方便演示,A的背景色设置成半透明。
width:200px;
float: left;
}
#b {
background: #6EEBC1;
width:300px;
}

效果图长这样:
demo效果

从图上可以看到,元素B的一部分是在元素A空出的空间内的。并且I’m divB这句话在元素A的右侧。且父元素高度塌陷,父元素现在的高度就是元素B的高度。
那么如果元素A右侧空出的空间内,放不下元素B呢?
我们把元素B宽度调整为200px。

放不下时的效果
可以看出,元素B就自成一行了。

给元素B加入clear:both后,元素B将忽略左边浮动所产生的空间,不去补空缺。
元素B设置clear:both后
如图所示,元素B会另起一行。而不是缩到浮动产生的空间内。

撑开父元素空间的奥秘

在w3school中,clear的定义中还有这么一句话:

1
“在 CSS2.1 中,会在设置清除浮动的元素上外边距之上增加清除空间,而外边距本身并不改变。”

也就是说,因为浮动而产生的空白空间,会被填充为实际存在的空间。。那么,自然就能撑开父元素。

总结

总结下来,浮动与清除浮动的顺序关系如下:

  1. 设置元素A浮动,元素脱离文档流,不计算高度。父元素出现高度塌陷。
  2. 浮动元素A产生空白空间。空间长宽等于元素A的长宽。后面元素会自动补空缺。
  3. 给浮动元素之后的元素B设置clear:both,元素B将不去补空缺。
  4. 元素B不仅不补空缺,还会把元素A因浮动而产生的空白空间填充为实际空间。
  5. 实际空间被计算高度,父元素被撑开。

这么一番走下来,花了很多时间去思考,去写demo。但对CSS浮动的理解也加深了。浮动为何引起父元素塌陷和清除浮动为何能撑开父元素这两个问题,一直是我的盲点。所以这次寒假,集中了两天时间去攻克它(除夕和正月初一,大过年的写代码,感觉有点怪但效率却出奇的高……)。

参考链接:

《CSS深入理解之float浮动》– 张鑫旭(也是课程地址)
CSS clear 属性
clear:both 为什么不起作用?–知乎,田雅文的回答

《计算机科学导论》读书笔记(一):浅析面向过程与面向对象编程

《计算机科学导论》的来源

仍记得那是15年的5月底,面临着毕业的学长学姐看着自己大学四年的诸多书籍,纷纷在校内开始了摆摊生活。当时才大一的我,看着一长条的书摊,仿佛看到了宝藏。才逛了一会儿,我心满意足的抱着好几本书回寝室了。其中就包括这两天看到入迷的《计算机科学导论》。

当初买下它的时候,只是模糊的觉得,自己可能会用到。既然又是书,所以就爽快的买了下来。反正也才5元(逃)。回去一翻开,更是惊喜,书如全新的一般,连名字都没有。然后……放在书柜上,就束之高阁了。直到寒假回家时,顺手把它带回了家。

就是下图的这本书。

计算机科学导论

结果昨天下午看了一眼,便一发不可收拾,一口气读到深夜两点。读完之后,感觉之前所有零散的知识点被串成了一串,有种拨云见月的感觉。之前很多无法理解的概念,也都迎刃而解了。

看来无论是学习社会科学或计算机科学,都如杨绛先生所说的那句话一样:

“你的问题主要在于读书不多而想得太多。”

或者又如《荀子》一书中劝学篇所言:

“吾尝终日而思矣,不如须臾之所学也;吾尝跂而望矣,不如登高之博见也。”

总而言之,万分庆幸自己回家时候带了这本《计算机专业导论》,让我领略到计算机科学的万千精彩。

关于面向过程与面向对象的疑惑

第一次学编程时,学习的是C语言。在刚开始学的时候,就知道C语言是一门面向过程的编程语言,除此之外还有面向对象的编程语言。当时的我,并没有想这么多。只是慢慢的看视频然后学习。

面向过程与面向对象是学编程过程中不可避免的问题。果然,这个问题在15年3月份,我学习Java后开始出现了。

当时在图书馆借阅了李刚老师的《疯狂Java 第二版》,在学习到面向对象部分,彻底晕头转向了。究竟什么是面向对象,什么又是面向过程?为什么说面向对象是一种良好编程方法?封装、继承、多态到底是什么?(这应该算自己第一次尝到计算机基础不牢的苦果,只是当时没有意识到)。

在15年6月学前端,到现在已有大半年。期间也看过诸如《JavaScript面向对象编程》等书,也去谷歌过相关文档。但总感觉似懂非懂。

还好,我碰见了《计算机专业导论》这本书,一本让我有“拨云见月”之感的书。

面向过程

在这儿,我们先介绍面向过程。
在面向过程的程序中,我们把程序看成是 操纵被动对象的活动主体。其中,被动对象本身不能开始一个动作,但能从活动主体(程序)接收动作。
被动对象的数据储存在内存中,程序为了操纵它们,会发布动作。称之过程

例如打印一个文件,文件就是被动对象。同时为了能被打印,文件会存储在内存中。而程序为了打印文件,会调用一个print过程,print过程中包含了计算机打印所需的步骤。

在过程式模式中,对象(文件)和过程(打印)是完全分开的实体。对象(文件)是能接收print动作的实体。而过程print是被编写的一个独立的实体,程序只是触发它。

看到上面这一大串,是不是有点晕了?简单来说,面向过程模式的程序由三部分组成:

  1. 对象创建部分
  2. 一组过程调用
  3. 每个过程的一组代码

结合上面的例子,这样就比较好理解了。

面向对象

面向对象模式与面向过程模式区别在于:面向对象模式处理活动对象,而非被动对象。如日常生活中的洗衣机,汽车等。 在这些对象上执行的动作都包含在这些对象中,对象只需要接收合适的外部刺激即可。

还是拿打印文件做例子,在面向对象模式中的文件能把所有被文件执行的过程(面向对象中成为方法)(打印,复制粘贴等)打包在一起。在这种模式下,程序只需要向文件发出打印或者复制的请求,文件就会被打印或复制。而这些方法,也被从这些对象继承的其它对象共享。
比较面向过程与面向对象,可以看出面向过程编程中的过程是独立的实体,但面向对象模式中的方法是属于对象的。

面向对象的核心要点,在于类。因为相同类型的对象需要一组方法,为了创建这些方法,C++或者Java都选择使用成为类的单元。

继承性

在面向对象模式中,作为本质,一个对象能从另外一个对象继承。这个概念称为继承性。例如,当一个几何形状类被定义后,我们就可以定义矩形类。矩形是拥有额外特性的几何形状。

多态性

面向对象的多态性是指我们可以定义一些具有相同名字操作的方法,但这些操作在不同类中会产生不同结果。
例如我们给几何图形类定义一个算面积的方法,同时定义圆形类和方形类继承几何图形类。那么同样是算面积,圆形类的结果和方形类的结果会不一样。因为两者计算公式不一样。
这就是多态。

感想

本以为只是简单的描述一下自己对面向过程和面向对象的理解。结果写的时候却是磕磕绊绊。看来和老师说的一样,自己学的好和教别人教的好是两回事。还是得努力去加强这一方面。

接下来可能会写好几篇《计算机专业导论》的读书笔记,因为解决了我颇多难点,所以算是值得一写。至于看的速度,我觉得算是较快的,因为计算机系统组成,计算机网络,算法等章节,我在之前就有过专门学习。只是没有一本书把知识点给串起来而已。

前端路漫漫,且行且歌~

计科之路--Start!

起因

按照之前的学习进度表,现在的我应该在钻研CSS的float等属性。但是我在这两天却选择了读一本书。
书的全名是:《如何高效学习:1年完成MIT4年33门课程的整体性学习法》
就是下面图片这本:
《如何高效学习》
哈哈,看到这名字,是不是很像鸡汤?
曾经的我也是这么认为的。
我第一次接触到这本书时,是在大一下学期,也就是大约一年前。当初看这本书的初衷,只是因为名字听起来很流弊,学习后能功力大涨的样子。(心态大概和学习闭包一样,以为学习后便能习得不世神功,称霸武林。)
但是当时年轻气躁,这本书只翻了两页,便觉得不适合自己,学习还是得一板一眼的学等等。这本书就放在书架上吃灰了。

计科之路

白驹过隙,不知不觉已是一年。过年回家时,捎上了《JavaScript设计模式》、《数学与生活》与这本书。因为虽然人还是那个人,但是体验和感悟已经完全不一样了。
一年前的我,还在思考要考研还是考公务员。那时候对于编程,只是觉得很新奇而已。
一年后的我,不在思考考研或是公务员,而是一心想着编程,想着怎么让代码写的更漂亮,怎么让代码跑的更快。
但一个很严峻的问题也摆在我的眼前。那就是我的计算机基础怎么办?
虽然一直有人说前端不需要啥基础,但是我依然坚信并践行着那句话:

1
“编程中,业务能力决定下限,而计算机基础决定其上限。”

这句话给我的感觉,就像这学期《领导科学》课程中老师所说的“天花板现象”一样。只是“天花板现象”的原因有天然的和人为的之分。但是计算机基础的学习,人为因素占了绝大部分。同时也是因为热爱,决定开始从零开始学计算机基础。
嗯……其实也不算从0开始学,之前自己也有学过一些计算机科学的课程。比如我的选修课《计算机网络》,在Coursea上了一半的《计算机系统》,自学的《学习JavaScript数据结构与算法》(逃)等。
不过一直觉得知识学的过于零散,然后想系统学习一番。

于是轻车熟路的找到了网易云课堂的大学计算机专业体系:
网易云课堂的大学计算机专业体系

决定按知识路线图,彻底给自己补一补基础。

最后的最后

当然,前端的学习也不会落下。因为两者不会冲突,前端的学习与应用,在一定程度上是对所学的计算机技术的实践。

而且点开网易云课堂计算机体系的大一课程时,惊喜的发现计算机专业导论课所学习的内容,自己从前两年就有过接触,上个学期更是在Coursea有过专门的学习。只是名称不一致罢了。

看来课业会轻松很多,这两年的兴趣式上课,还是帮助了我非常多的。

无论是人文社科的经史哲政,或者是理工科的计算机、医学常识等。都切实的让我了解到另一个世界,一个文科生从未见过的理科世界。

前端路漫漫,且行且歌。
(本篇博客略凌乱,因为写这篇文章的两天,发生了很多事情,心境变化较大。所以前后对接会有些不协调。)

寒假前端学习(7)——学习JavaScript之this,call,apply

学习起因:

在之前的JavaScript学习中,this,call,apply总是让我感到迷惑,但是他们的运用又非常的广泛。遂专门花了一天,来弄懂JavaScript的this,call,apply。
中途参考的书籍也很多,以《JavaScript设计模式与开发实践》为主,《JavaScript高级程序设计》、《你不知道的JavaScript》为辅。这三本书对我理解this,call,apply都起了很大的帮助。

this

首先,我们先讲述this。

在《JavaScript设计模式与开发实践》关于this的描述中,我认为有一句话切中了this的核心要点。那就是:

JavaScript的this总是指向一个对象

具体到实际应用中,this的指向又可以分为以下四种:

  1. 作为对象的方法调用
  2. 作为普通函数调用
  3. 构造器调用
  4. apply和call调用

接下来我们去剖析前3点,至于第4点的apply和call调用,会在call和apply部分详细讲解。

1.作为对象的方法调用

说明:作为对象方法调用时,this指向该对象。
举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 1.作为对象的方法调用
*
* 作为对象方法调用时,this指向该对象。
*/
var obj = {
a: 1,
getA: function() {
console.log(this === obj);
console.log(this.a);
}
};
obj.getA(); // true , 1

2.作为普通函数调用

说明:作为普通函数调用时,this总是指向全局对象(浏览器中是window)。
举例:

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
/**
* 2.作为普通函数调用
*
* 不作为对象属性调用时,this必须指向一个对象。那就是全局对象。
*/
window.name = 'globalName';
var getName = function() {
console.log(this.name);
};
getName(); // 'globalName'
var myObject = {
name: "ObjectName",
getName: function() {
console.log(this.name)
}
};
myObject.getName(); // 'ObjectName'
// 这里实质上是把function() {console.log(this.name)}
// 这句话赋值给了theName。thisName在全局对象中调用,自然读取的是全局对象的name值
var theName = myObject.getName;
theName(); // 'globalName'

3.构造器调用

说明:作为构造器调用时,this指向返回的这个对象。
举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 3.作为构造器调用
*
* 作为构造器调用时,this指向返回的这个对象。
*/
var myClass = function() {
this.name = "Lxxyx";
};
var obj = new myClass();
console.log(obj.name); // Lxxyx
console.log(obj) // myClass {name: "Lxxyx"}

但是如果构造函数中手动指定了return其它对象,那么this将不起作用。
如果return的是别的数据类型,则没有问题。

1
2
3
4
5
6
7
8
9
10
var myClass = function() {
this.name = "Lxxyx";
// 加入return时,则返回的是别的对象。this不起作用。
return {
name:"ReturnOthers"
}
};
var obj = new myClass();
console.log(obj.name); // ReturnOthers

Call和Apply

Call和Apply的用途一样。都是用来指定函数体内this的指向。

Call和Apply的区别

Call:第一个参数为this的指向,要传给函数的参数得一个一个的输入。
Apply:第一个参数为this的指向,第二个参数为数组,一次性把所有参数传入。

如果第一个参数为null,则this指向宿主环境,浏览器中是window。

1.改变this指向

说明:这是call和apply最常用的用途了。用于改变函数体内this的指向。
举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = "GlobalName"
var func = function() {
console.log(this.name)
};
func(); // "GlobalName"
var obj = {
name: "Lxxyx",
getName: function() {
console.log(this.name)
}
};
obj.getName.apply(window) // "GlobalName" 将this指向window
func.apply(obj) // "Lxxyx" 将this指向obj

2.借用其它对象的方法

这儿,我们先以一个立即执行匿名函数做开头:

1
2
3
4
5
6
(function(a, b) {
console.log(arguments) // 1,2
// 调用Array的原型方法
Array.prototype.push.call(arguments, 3);
console.log(arguments) // 1,2,3
})(1,2)

函数具有arguments属性,而arguments是一个类数组。
但是arguments是不能直接调用数组的方法的,所以我们要用call或者apply来调用Array对象的原型方法。
原理也很容易理解,比如刚才调用的是push方法,而push方法在谷歌的v8引擎中,源代码是这样的:

1
2
3
4
5
6
7
8
9
function ArrayPush() {
var n = TO_UINT32(this.length); // 被push对象的长度
var m = % _ArgumentsLength(); // push的参数个数
for (var i = 0; i < m; i++) {
this[i + n] = % _Arguments(i); // 复制元素
}
this.length = n + m; //修正length属性
return this.length;
}

他只与this有关,所以只要是类数组对象,都可以调用相关方法去处理。

这部分内容比较复杂,再加上自己水平也不太够。所以推荐有条件的同学去购买相关书籍,或者等我的后续博客文章。

感想

通过对这部分的学习,算是加深了对JavaScript的理解。最直观的表现就是,去看一些优秀框架的源代码时,不再是被this,call,apply,bind绕的晕乎乎的。还是很开心的~

下一段时间,准备深入探索一下日常学习和使用的CSS。毕竟JavaScript学了,HTML和CSS也不能落下。

前端路漫漫,且行且歌。

迁移至Ubuntu的折腾之旅

前因

之前一直有装Win10和Deepin的双系统,本打算装了Deepin15后,就安心的在Linux下学习和工作。
但无奈的是,Deepin15基于Debian而非Ubuntu,所以为了装一个shadowsocks的Gui客户端,还得专门去编译。这也就算了,在装上SS后,又安装了Sublime,但Sublime在Linux下有不能输入中文的BUG。 这个问题算是致命问题了,因为我平常90%的开发,都只使用Sublime Text。
于是去找解决方法。一番谷歌之下,告诉我得去新建个c文件啥的。或者去Github上使用一键包。
我当然用一键包啊,于是很开心的打开了修复包的Github地址,开头第一句话就让我懵逼了。
There still some problems with Debian.
翻译成中文就是:“Debian用户还是洗洗睡吧。”
再加上Deepin15桌面经常会冒出别的文件夹的新文件,于是心灰意冷的我,开始继续使用Windows做开发。

向Ubuntu转移

本来Windows用的也还可以,但是由于自身CPU性能跟不太上(i5-4200u),用webpack编译个文件要花上40多秒,运行过程中也各种降频,简直不能忍。于是决定上Liunx,同时又因为有Deepin的前车之鉴,所以决定上Ubuntu Kylin。
嗯,不是Ubuntu,是中国的麒麟版。原因很简单:壁纸好看+内置Chrome和搜狗输入法。 于是在折腾中,开始了Ubuntu Kylin的安装。

安装与配置

安装过程倒是稀疏平常,一路next就开始了安装。安装速度很快。不一会儿就提示我安装完成了。
至于分区,当然是用Ubuntu安装界面的一键分区啦。(逃) 配置过程就一帆风顺的多了。

开机,安装更新,git和Shadowsocks,然后就是安装oh-my-zsh。

装上新款的oh-my-zsh后,继续安装nvm用于管理node版本。如果先装nvm再装zsh的话,zsh启动时会读取不到nvm。虽然可以自己去配置文件改,但终究比较麻烦。 接下来用nvm安装node 5.5(版本号更新的好快……),安装国内源的cnpm。也都一下子就过去了。

最难配置的地方,还是在Sublime,下了修复文件修复Sublime不能输入中文的问题后,发现只有在命令行输入subl才能启动输入中文的Sublime,于是又深入/usr/share/applications去修改sublime_text.desktop,把exec改为subl。顺带还在default.list中把默认编辑软件从gedit换成了Sublime。于是耗时好几个小时的Ubuntu,终于完成了。

感想

果然做开发,还是Linux好,不说别的。感觉Linux的幺蛾子比windows要少上很多。用的时候也没用怎么卡顿,编译速度超过windows n+1倍。 不管咋样,寒假就靠ubuntu过啦~

寒假前端学习(6)——学习JavaScript数据结构与算法(四):二叉搜索树

本系列的第一篇文章: 学习JavaScript数据结构与算法(一):栈与队列
第二篇文章:学习JavaScript数据结构与算法(二):链表
第三篇文章: 学习JavaScript数据结构与算法(三):集合
第四篇文章: 学习JavaScript数据结构与算法(四):二叉搜索树

我与二叉树的前尘往事

在刚学编程时,就知道有一种数据结构叫“树”,树中的翘楚是“二叉树”,“红黑树”等。
据说“树”构在编程界呼风唤雨无所不能。让无数程序员闻风丧胆。甚至在面试时,更是有“手写二叉树”,“翻转二叉树”等题目坐镇。

好吧,我承认这些在当时都把我吓住了。

但是当我颤抖着打开《学习JavaScript数据结构与算法》,开始敲下关于“树”的代码时,突然觉得,好像也没有那么难呢。
于是心怀激动,一口气敲完了书上的例子,中途也思考了很久,不断的在纸上演算等。但总的来说,还是学的很开心的。

树の简介

之前学的栈、队列、链表等数据结构,都是顺序数据结构。而树,将会是我们学的第一种非顺序数据结构。

放在现实里呢,有个很生动的例子,公司组织架构图。长这样:
公司组织架构图

而我们要学的树,长这样:
树の图示

节点简介

其中,树中的每个元素,都叫做节点。从节点延伸而下的,叫子节点
树顶部的节点叫根节点。每棵树只有一个根节点。(图中15就是根节点)
在节点中,有子节点的节点也称为内部节点,没有的话则被称为外部节点或者叶节点。
同时在节点中是有祖先和后代关系的,比如节点9的祖先就有13,7,6,15四个。

节点属性

深度: 节点的深度取决于其祖先的数量,节点9的深度就是4。
树的高度,树的高度体现为节点深度的最大值。
比如上图,节点深度最大值为4,则树的高度为4。

二叉树与二叉搜索树

二叉树的最大特点就在于,它的节点最多只有两个子节点:左侧子节点和右侧子节点。
二叉搜索树则是二叉树的一种,但它只允许你在左侧节点储存比父节点小的值,右侧只允许储存比父节点大的值。
像刚才的这幅图,就是二叉搜索树。
二叉搜索树

而我们本文要学习的内容,就是如何写一个二叉搜索树。

JavaScipt中二叉搜索树的实现

首先,创建一个构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 二叉搜索树的构造函数
*/
function BinarySearchTree() {
/**
* 二叉搜索树键的构造函数
* @param {Number} key 要生成的键值
*/
var Node = function(key) {
// 键值
this.key = key;
// 左子节点
this.left = null;
// 右子节点
this.right = null;
}
/**
* 二叉树的根节点,不存在时表示为Null
* @type {Null or Number}
*/
var root = null;
}

在之前提到过的双向链表中,每个节点包含两个指针,一个指向左侧节点,一个指向右侧节点。在二叉搜索树中,每个节点也有两个指针,一个指向左侧子节点,一个指向右侧子节点。但在二叉搜索树中,我们把节点成为,这是术语。

二叉搜索树需要有如下的方法:

  • insert(key): 向树中插入一个新的键
  • inOrderTraverse(): 通过中序遍历方式,遍历所有节点
  • preOrderTranverse(): 通过先序遍历方式,遍历所有节点
  • postOrderTranverse(): 通过后序遍历方式,遍历所有节点
  • min(): 返回树中最小的值
  • max(): 返回树中最大的值
  • search(key): 搜索某个值,在树中则返回true
  • remove(key): 从树中移除某个键

二叉搜索树的实现,基本都与递归有关(对我来说递归很绕,花了很久才理解)。如果不清楚递归相关概念,可以看看下面的参考链接。

什么是递归

insert方法:

说明:向树中插入一个新的键
实现:

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
/**
* 插入某个键到二叉树中
* @param {Number} key 要插入的键值
*/
this.insert = function(key) {
// 用传入的值生成二叉树的键
var newNode = new Node(key);
// 根节点为Null时,传入的键则为根节点
// 否则调用insertNode函数来插入子节点
if (root === null) {
root = newNode;
} else {
insertNode(root, newNode)
}
};
/**
* 用于插入子节点。
* @param {Node} node 根节点
* @param {Node} newNode 要插入的节点
*/
var insertNode = function(node, newNode) {
//由于二叉搜索树的性质,所以当键值小于当前所在节点的键值
//则使得左子结点成为新的要比较的节点,进行递归调用
//如果左子结点为null,则将键值赋值给左子结点。
//如果键值大于当前所在节点的键值,原理同上。
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
insertNode(node.left, newNode)
}
} else {
if (node.right === null) {
node.right = newNode
} else {
insertNode(node.right, newNode)
}
}
};

inOrderTraverse方法:

说明:通过中序遍历方式,遍历所有节点
实现:

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
/**
* 中序遍历操作,常用于排序。会把树中元素从小到大的打印出来。
* 因为在javascript的递归中,遇到递归是,会优先调用递归的函数。直到递归不再进行。
* 然后会在递归调用的最后一个函数中执行其它语句。再一层层的升上去。
* 所以中序遍历会有从小到大的输出结果。
* 后续的先序和后续遍历和这个原理差不多,取决于callback放在哪儿。
*
* @param {Function} callback 获取到节点后的回调函数
*/
this.inOrderTraverse = function(callback) {
inOrderTraverseNode(root, callback);
};
/**
* 中序遍历的辅助函数,用于遍历节点
* @param {Node} node 遍历开始的节点,默认为root
* @param {Function} callback 获取到节点后的回调函数
* @return {[type]} [description]
*/
var inOrderTraverseNode = function(node, callback) {
// 当前节点不为NULL则继续递归调用
if (node != null) {
inOrderTraverseNode(node.left, callback);
// 获取到节点后,调用的函数
callback(node.key);
inOrderTraverseNode(node.right, callback);
}
};

假如我们这儿加入打印节点值的函数:

1
2
3
4
5
var printNode = function(value) {
console.log(value);
};
inOrderTraverse(printNode) // 输出排序后树的值

preOrderTranverse方法:

说明:通过先序遍历方式,遍历所有节点
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 前序遍历操作,常用于打印一个结构化的文档
* @param {Function} callback 获取到节点后的回调函数
*/
this.preOrderTranverse = function(callback) {
preOrderTranverseNode(root, callback);
};
/**
* 前序遍历的辅助函数,用于遍历节点
* @param {Node} node 遍历开始的节点,默认为root
* @param {Function} callback 获取到节点后的回调函数
*/
var preOrderTranverseNode = function(node, callback) {
if (node != null) {
callback(node.key);
preOrderTranverseNode(node.left, callback);
preOrderTranverseNode(node.right, callback);
}
};

postOrderTranverse方法:

说明:通过后序遍历方式,遍历所有节点
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 后序遍历操作,常用于计算所占空间
* @param {Function} callback 获取到节点后的回调函数
*/
this.postOrderTranverse = function(callback) {
postOrderTranverseNode(root, callback);
};
/**
* 后序遍历的辅助函数,用于遍历节点
* @param {Node} node 遍历开始的节点,默认为root
* @param {Function} callback 获取到节点后的回调函数
*/
var postOrderTranverseNode = function(node, callback) {
postOrderTranverseNode(node.left, callback);
postOrderTranverseNode(node.right, callback);
callback(node.key);
};

min方法:

说明:返回树中最小的值,由二叉搜索树的性质易知,最左侧的为最小值。则只需取得最左侧的值即可。
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 返回树中最小的值
* @return {Function} min函数的辅助函数
*/
this.min = function() {
return minNode(root);
};
/**
* min函数的辅助函数
* @param {Node} node 查找开始的节点,默认为root
*/
var minNode = function(node) {
// 如果node存在,则开始搜索。能避免树的根节点为Null的情况
if (node) {
// 只要树的左侧子节点不为null,则把左子节点赋值给当前节点。
// 若左子节点为null,则该节点肯定为最小值。
while (node && node.left !== null) {
node = node.left;
}
return node.key;
}
return null;
};

max方法:

说明:返回树中最大的值,由min函数易知,最大值在最右侧。
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 返回树中最大的值
* @return {Function} max函数的辅助函数
*/
this.max = function() {
return maxNode(root);
};
/**
* max函数的辅助函数
* @param {Node} node 查找开始的节点,默认为root
* @return {Key} 节点的值
*/
var maxNode = function(node) {
if (node) {
while (node && node.right !== null) {
node = node.right;
}
return node.key;
}
return null;
};

search方法:

说明: 搜索某个值,在树中则返回true
实现:

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
/**
* 搜索某个值是否存在于树中
* @param {Node} key 搜索开始的节点,默认为root
* @return {Function} search函数的辅助函数
*/
this.search = function(key) {
return searchNode(root, key);
};
/**
* search函数的辅助函数
* @param {Node} node 搜索开始的节点,默认为root
* @param {Key} key 要搜索的键值
* @return {Boolean} 找到节点则返回true,否则返回false
*/
var searchNode = function(node, key) {
// 如果根节点不存在,则直接返回null
if (node === null) {
return false;
} else if (key < node.key) {
searchNode(node.left, key)
} else if (key > node.key) {
searchNode(node.right, key)
} else {
// 如果该节点值等于传入的值,返回true
return true;
}
};

remove方法:

说明:从树中移除某个键,要应对的场景:

  1. 只是一个叶节点
  2. 有一个子节点
  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
    /**
    * 从树中移除某个键
    * @param {Key} key 要移除的键值
    * @return {Function} remove函数的辅助函数
    */
    this.remove = function(key) {
    root = removeNode(root, key);
    };
    /**
    * remove函数的辅助函数
    * @param {Node} node 搜索开始的节点,默认为root
    * @param {Key} key 要移除的键值
    * @return {Boolean} 移除成功则返回true,否则返回false
    */
    var removeNode = function(node, key) {
    // 如果根节点不存在,则直接返回null
    if (node === root) {
    return null;
    }
    // 未找到节点前,继续递归调用。
    if (key < node.key) {
    node.left = removeNode(node.left, key)
    return node;
    } else if (key > node.key) {
    node.right = removeNode(node.right, key)
    return node;
    } else {
    // 第一种场景:只是一个叶节点
    // 这种情况只需要直接把节点赋值为null即可
    if (node.left === null && node.right === null) {
    node = null;
    // 处理完直接return节点
    return node;
    }
    // 第二种场景:有一个子节点
    // 如果左节点为null,则代表右节点存在。
    // 于是把当前节点赋值为存在的那个子节点
    if (node.left === null) {
    node = node.right;
    // 处理完直接return节点
    return node;
    } else if (node.right == null) {
    node = node.left;
    // 处理完直接return节点
    return node;
    }
    // 第三种场景:有两个子节点
    // 首先加入辅助节点,同时找寻右子节点中的最小节点
    // 并把当前节点替换为右子节点中的最小节点
    // 同时为了避免节点重复,移除右子节点中的最小节点
    var aux = findMinNode(node.right);
    node.key = aux.key;
    node.right = removeNode(node.right, aux.key);
    // 处理完直接return节点
    return node;
    }
    };
    /**
    * remove函数的辅助函数
    * @param {Node} node 查找开始的节点,默认为root
    * @return {Node} 最小的节点
    */
    var findMinNode = function(node) {
    // 如果node存在,则开始搜索。能避免树的根节点为Null的情况
    if (node) {
    // 只要树的左侧子节点不为null,则把左子节点赋值给当前节点。
    // 若左子节点为null,则该节点肯定为最小值。
    while (node && node.left !== null) {
    node = node.left;
    }
    return node;
    }
    return null;
    };

源代码:

源代码在此~

二叉搜索树-源代码

感想

写文章的时候,人有点感冒,晕晕乎乎的。不过写完之后就好多了,脑子清醒了许多。
二叉树这一章,就我而言感慨万分,也算是暂时满足了自己对数据结构中“树”的向往与愿望,也不是之前看数据结构中那种迷茫的感觉。

能用JavaScript亲手实现,还是非常开心的。

前端路漫漫,且行且歌~

寒假前端学习(5)——学习JavaScript数据结构与算法(三):集合

本系列的第一篇文章: 学习JavaScript数据结构与算法(一):栈与队列
第二篇文章:学习JavaScript数据结构与算法(二):链表
第三篇文章: 学习JavaScript数据结构与算法(三):集合
第四篇文章: 学习JavaScript数据结构与算法(四):二叉搜索树

集合(Set)

说起集合,就想起刚进高中时,数学第一课讲的就是集合。因此在学习集合这种数据结构时,倍感亲切。
集合的基本性质有一条: 集合中元素是不重复的。因为这种性质,所以我们选用了对象来作为集合的容器,而非数组。
虽然数组也能做到所有不重复,但终究过于繁琐,不如集合。

集合的操作

集合的基本操作有交集、并集、差集等。这儿我们介绍JavaScipt集合中交集、并集、差集的实现。至于这三个的具体概念,可以看图:
交集、并集、差集

JavaScipt中集合的实现

首先,创建一个构造函数。

1
2
3
4
5
6
7
8
9
10
/**
* 集合的构造函数
*/
function Set方法 {
/**
* 集合元素的容器,以对象来表示
* @type {Object}
*/
var items = {};
}

集合需要有如下方法:

  • has(value): 检测集合内是否有某个元素
  • add(value): 给集合内添加某个元素
  • remove(value): 移除集合中某个元素
  • clear(value): 清空集合
  • size(): 返回集合长度
  • values(): 返回集合转换的数组
  • union(otherSet): 返回两个集合的并集
  • intersection(otherSet): 返回两个集合的交集
  • difference(otherSet): 返回两个集合的差集
  • subset(otherSet): 判断该集合是否为传入集合的子集

has方法:

说明:集合中元素是不重复的。所以在其它任何操作前,必须用has方法确认集合是否有某个元素。这儿使用了hasOwnProperty方法来检测。
实现:

1
2
3
4
5
6
7
8
9
10
/**
* 检测集合内是否有某个元素
* @param {Any} value 要检测的元素
* @return {Boolean} 如果有,返回true
*/
this.has = function(value) {
// hasOwnProperty的问题在于
// 它是一个方法,所以可能会被覆写
return items.hasOwnProperty(value)
};

add方法:

说明: 给集合内添加某个元素。
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 给集合内添加某个元素
* @param {Any} value 要被添加的元素
* @return {Boolean} 添加成功返回True。
*/
this.add = function(value) {
//先检测元素是否存在。
if (!this.has(value)) {
items[value] = value;
return true;
}
//如果元素已存在则返回false
return false;
};

remove方法:

说明: 移除集合中某个元素
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 移除集合中某个元素
* @param {Any} value 要移除的元素
* @return {Boolean} 移除成功返回True。
*/
this.remove = function(value) {
//先检测元素是否存在。
if (this.has(value)) {
delete items[value];
return true;
}
//如果元素不存在,则删除失败返回false
return false;
};

####clear方法:
说明: 清空集合
实现:

1
2
3
4
5
6
/**
* 清空集合
*/
this.clear = function() {
this.items = {};
};

size方法

说明: 返回集合长度,这儿有两种方法。第一种方法使用了Object.keys这个Api,但只支持IE9及以上。第二种则适用于所有浏览器。
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 返回集合长度,只可用于IE9及以上
* @return {Number} 集合长度
*/
this.size = function() {
// Object.keys方法能将对象转化为数组
// 只可用于IE9及以上,但很方便
return Object.keys(items).length;
}
/**
* 返回集合长度,可用于所有浏览器
* @return {Number} 集合长度
*/
this.sizeLegacy = function() {
var count = 0;
for (var prop in items) {
if (items.hasOwnProperty(prop)) {
++count;
}
}
return count;
}

values方法

说明: 返回集合转换的数组,这儿也有两种方法。理由同上。使用了Object.keys,只能支持IE9及以上。
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 返回集合转换的数组,只可用于IE9及以上
* @return {Array} 转换后的数组
*/
this.values = function() {
return Object.keys(items);
};
/**
* 返回集合转换的数组,可用于所有浏览器
* @return {Array} 转换后的数组
*/
this.valuesLegacy = function() {
var keys = [];
for (var key in items) {
keys.push(key)
};
return keys;
};

union方法

说明: 返回两个集合的并集
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 返回两个集合的并集
* @param {Set} otherSet 要进行并集操作的集合
* @return {Set} 两个集合的并集
*/
this.union = function(otherSet) {
//初始化一个新集合,用于表示并集。
var unionSet = new Set();
//将当前集合转换为数组,并依次添加进unionSet
var values = this.values();
for (var i = 0; i < values.length; i++) {
unionSet.add(values[i]);
}
//将其它集合转换为数组,依次添加进unionSet。
//循环中的add方法保证了不会有重复元素的出现
values = otherSet.values();
for (var i = 0; i < values.length; i++) {
unionSet.add(values[i]);
}
return unionSet;
};

intersection方法

说明: 返回两个集合的交集
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 返回两个集合的交集
* @param {Set} otherSet 要进行交集操作的集合
* @return {Set} 两个集合的交集
*/
this.intersection = function(otherSet) {
//初始化一个新集合,用于表示交集。
var interSectionSet = new Set();
//将当前集合转换为数组
var values = this.values();
//遍历数组,如果另外一个集合也有该元素,则interSectionSet加入该元素。
for (var i = 0; i < values.length; i++) {
if (otherSet.has(values[i])) {
interSectionSet.add(values[i])
}
}
return interSectionSet;
};

difference方法

说明: 返回两个集合的差集
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 返回两个集合的差集
* @param {Set} otherSet 要进行差集操作的集合
* @return {Set} 两个集合的差集
*/
this.difference = function(otherSet) {
//初始化一个新集合,用于表示差集。
var differenceSet = new Set();
//将当前集合转换为数组
var values = this.values();
//遍历数组,如果另外一个集合没有该元素,则differenceSet加入该元素。
for (var i = 0; i < values.length; i++) {
if (!otherSet.has(values[i])) {
differenceSet.add(values[i])
}
}
return differenceSet;
};

subset方法

说明: 判断该集合是否为传入集合的子集。这段代码在我自己写完后与书上一比对,觉得自己超级low。我写的要遍历数组三次,书上的只需要一次,算法复杂度远远低于我的。
实现:

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
/**
* 判断该集合是否为传入集合的子集
* @param {Set} otherSet 传入的集合
* @return {Boolean} 是则返回True
*/
this.subset = function(otherSet) {
// 第一个判定,如果该集合长度大于otherSet的长度
// 则直接返回false
if (this.size() > otherSet.size()) {
return false;
} else {
// 将当前集合转换为数组
var values = this.values();
for (var i = 0; i < values.length; i++) {
if (!otherSet.has(values[i])) {
// 第二个判定。只要有一个元素不在otherSet中
// 那么则可以直接判定不是子集,返回false
return false;
}
}
return true;
}
};

源代码

源代码在此~

集合-源代码

ES6中的集合

ES6也提供了集合,但之前看ES6的集合操作一直迷迷糊糊的。实现一遍后再去看,感觉概念清晰了很多。
具体的我掌握的不是很好,还在学习中,就不写出来啦~推荐看阮一峰老师的《ECMAScript 6入门》中对ES6 Set的介绍。

《ECMAScript 6入门》– Set和Map数据结构

感想

到了这儿,已经掌握了一些基本的数据结构。剩下的都是难啃的骨头了(对我而言)。

字典的散列表、图、树、排序算法。算是四大金刚,所以近期关于数据结构与算法系列的文章,可能会更新的很慢。对我来说,也算是一个坎。希望这个寒假,能跨过这个坎。

前端路漫漫,且行且歌~