Git 中的后悔药

前言:

  • git fsck & git stash apply <commit_id> :针对 stash 存储的误删
  • git checkout -- <file> :针对工作区的修改
  • git reset :针对暂存区的修改

💊fsck —— 针对 stash 存储的误删

在不同的公司,技术团队在代码仓库中的协同工作流程可能会有不同。我公司就是在 dev 分支上进行开发,合并更新的流程基本就是下面:

  1. git stash 存储自己在 dev 上做的修改(新建的文件是不会被存储起来的)
  2. git pull 拉取远程仓库中的 dev 的更新
  3. git stash pop 弹出存储的修改(有冲突的话就解决冲突)
  4. 然后就是添加文件并提交修改那些操作。

那么我在工作中,遇到最让我窒息、心痛如绞的事情就是:我亲手把自己 stash 的修改给 clear 了……….

神操作如下:

  1. 啊,终于可以提交了,我先 git stash 存起来先

  2. 那么接下来查查看我的存储仓记录 git stash clear (一失足成千古恨)

  3. 嗯?为什么没有显示存储记录?

  4. ???我执行了 git stash clear ??? CLEAR????我不是要 git stash list 的吗???
    快叫120!

  5. 我不信,一定没有清空掉的, git stash list ………
    (Sorry, the list you checked is empty. Du… Du… Du…)

  6. 我还是不信,可能我没有 stash 到,还在工作区里。 git status ……..
    (Sorry, the list you checked is empty. Du… Du… Du…)
    我要冷静,冷静!

哇的一声哭出来
这不是真的
我的心好痛

不哭,一定有解决方法的!
一定有解决方法的!

掉坑里的果然不止我一个人,各路大佬介绍了自己的解决方法,我找到符合自己的方法:

  1. git fsck 查询仓库中所有未被其他对象引用的对象,这密密麻麻地列出了一摞(我记得当时不是时间顺序排序的,但是今天一看好像又是时间顺序的)。
  2. 于是我只能 git show <commit_id> 一个个打开来看。
  3. 经过漫长的版本查找后,我终于找到了离上一次修改最近的记录!最后 git stash apply <commit_id> 。谢天谢地,回来了!

千里寻子终得你

💊checkout —— 针对工作区的修改

对于在工作区的修改,还没执行 git add 等操作,此时若是想放弃工作区的全部修改,只需要:

1
git checkout -- <需要撤销修改的文件名>

注意:这个不针对 Untracked 的文件哦~

💊reset —— 针对暂存区的修改

对于刚执行完 git add 把文件添加到暂存区的修改,此时若是想放弃暂存区某个文件的修改,只需要:

1
git reset HEAD <需要撤销修改的文件名>

而如果你已经执行了 git commit 将这些暂存区的文件提交,那你只能:

1
2
3
4
5
6
git reset --hard HEAD^                ## 将 HEAD 回退到上一个版本
git reset --hard HEAD@{<index>} ## 将 HEAD 回退到第 index 个版本
git reset --hard <commit_id> ## 将 HEAD 指向指定的 commit_id 版本

git log ## 查看提交的历史
git reflog ## 查看 HEAD 移动的历史记录,从而回到任意版本

注意:这个操作非常危险!⚠️如果你的工作区中还有已跟踪的修改文件未提交,执行这个操作将会丢失你的这些文件!

Vue 双向绑定的实现原理

我们都知道, Vue 最大的特点之一就是响应式的双向绑定。那么它的实现原理是怎么样的呢?无论是深入学习 Vue 框架也好,还是作为一个面试常考题也好,这都是前端必须了解的一个问题。那么我们今天就来探索一下它的实现方法。

模拟实现

Vue 在它的官网中就已经有对它的这个「双向绑定」特性进行说明。详情请戳传送门。我们可以看到, Vue 会遍历实例的 data 对象的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter / setter。根据 MDN 的介绍,我尝试着使用这个方法来做一个简单的模拟实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var $person1 = {},   // 模拟 vm.$data
person1 = { name: 'caaa' } // 模拟我们在 Vue 中的 data 选项

Object.defineProperty($person1, 'name', {
get() { return person1.name },
set(val) {
person1.name = val
// 执行某些操作实现 DOM 局部更新
console.log('发生了更新')
}
})

$person1.name // 'caaa'
$person1.age // undefined

/* 改变 $person1.name */
$person1.name = 'jack' // '发生了更新'
$person1.name // 'jack'
person1.name // 'jack'

局限

通过上面的例子,我们知道了 Vue 是如何实现响应式原理的,但是「受现代 JavaScript 的限制,Vue 不能检测到对象属性的添加或删除。」例如说,我们对上面的 $person1 添加一个属性:

1
2
3
4
/* 为 $person1 添加 age 属性 */
$person1.age = 18
$person1.age // 18
person1.age // undefined

我们可以看到,即使已经改变了 $person1.ageperson1.age 也依旧没有变化。这是因为 $person1.age 是一个非响应的属性,它并没有 setter 来对它进行追踪。也就是说 $person1.age = 18 其实等同于:

1
2
3
4
5
Object.defineProperty($person1, 'age', {
value: 18,
enumerable: true,
writable: true
})

那么要如何在 Vue 中为已创建的实例动态添加新的根级响应式属性呢? Vue 提供的一个方法是 Vue.set(object, key, value) 。这里不详细举例说明,有兴趣的可以自己去尝试一下。

我想展开讲的是, Vue 3.0 中对于响应式数据的更新。

⬇️

⬇️

⬇️

Proxy

尤雨溪老师在 VueConf TO 大会上发表了「Vue 3.0 Updates」的演讲,提到了 3.0 版本将使用原生 Proxy 来取代 Object.defineProperty ,这使得 Vue 可以跳出无法添加或删除响应式属性的局限。那么废话少说,我们立马来用用这聪明的 Proxy

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
var person2 = { name: 'caaa' }

var $person2 = new Proxy(person2, {
get(target, key) {
return target[key]
},
set(target, key, newVal) {
console.log(`更新了${key}`)
target[key] = newVal
}
})

$person2.name // 'caaa'
$person2.age // undefined

/* 改变 $person2.name */
$person2.name = 'jack' // '更新了name'
$person2.name // 'jack'
person2.name // 'jack'

/* 为 $person2 添加 age 属性 */
$person2.age = 18 // '更新了age'
$person2.age // 18
person2.age // 18

/* 为 $person2 删除 age 属性 */
delete $person2.age // true
person2.age // undefined

看, person2.age 随着 $person2.age 的改变而作出相同的改变了!这说明 $person2 直接新增的属性也可以是响应式的了~

有点期待 Vue 3.0 的问世了呢🤤 当然 3.0 还有很多其他更新呢,给你传送门,带你去看「Vue 3.0 Updates」。

本文的演示请点击传送门查看 AND 捣鼓。


本文完。

若文中有错误还请指正与包涵!

原文链接:https://caijialinxx.github.io/2019/02/17/vue-reactivity-implement/

转载请注明出处。

参考资料:

JavaScript 中的面向对象(继承)

面向对象,是作为一个程序员经常能够碰到的一个概念。那么到底什么是面向对象编程呢?

有人可能会回答:

把一组数据结构和处理它们的方法组成对象(object),把相同行为的对象归纳为类(class),通过类的封装(encapsulation)隐藏内部细节,通过继承(inheritance)实现类的特化(specialization)/泛化(generalization),通过多态(polymorphism)实现基于对象类型的动态分派(dynamic dispatch)。
如何用一句话说明什么是面向对象思想? - Milo Yip的回答 - 知乎
https://www.zhihu.com/question/19854505/answer/23421930

总结确实是已经很精辟了,但对于初学者而言,可能看完就懵逼了,我只是想搞懂面向对象是什么意思,怎么给了我更多的概念出来😭

这大概就是所谓的「从入门到放弃」吧… 哈哈哈哈哈

于是这时有人跑出来回答说:

  • 无论是面向对象编程还是函数式编程,这些都只是一种编程思想。就像是不同的宗教,有些不允许养猪吃猪肉,有些不允许吃牛肉,有些要求男性戴帽女性盖头…… 你可以信某个教,也可以不信教或者汲取多个教的精华。也就是说,我们只需要关注面向对象的意义,而不必纠结于一个语言或者程序设计到底是遵循或应该遵循什么编程典范。

这让我想到了咱南北方的饮食差异:北方的粽子是甜的,南方的粽子是咸的;北方人吃完12个饺子后“老板其它菜咋还没上呢”,南方人吃完12个饺子后“老板买单”…😂

那么接下来,我们来详细聊聊 JavaScript 是如何体现面向对象思想的,以及继承该如何实现。

封装

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

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 中的面向对象思想。


本文完。

若文中有错误还请指正与包涵!

原文链接:https://caijialinxx.github.io/2019/01/04/oop-in-javascript/

转载请注明出处。

参考资料:

拓展阅读:

誓死讲清 prototype 、 __proto__ 、 原型链与继承

尽管看过很多解释这些概念的文章,每次看完都觉得自己理解了。但一遇到这类问题的时候,我还是不免会答错。这次博客的诞生原因,就是今天又做错了题😭,我这破记性,恐怕得写一篇自己的博客梳理一下,才能真正记住并理解。

先给你们看看这篇博客的“罪魁祸首”:

现有如下代码:

1
2
3
4
var F = function() {}
Object.prototype.a = function() {}
Function.prototype.b = function() {}
var f = new F()

问:f.a 和 f.b 都能取到吗?

好的,你们可以先做做这个题。答案我一会儿告诉你们。现在,我要先来讲讲这题涉及到的知识点(其实叫概念更合适?):

  • 构造函数、实例对象与继承
  • 原型对象(如 Object.prototype
  • (不知道中文名怎么叫或者根本没有名字的) __proto__
  • 原型链(prototype chain)

构造函数、实例对象与继承

构造函数就是用来构造(创建)一个实例对象的函数。举个例子就是:

1
2
3
4
5
6
7
8
9
10
11
var 你妈妈 = function() {
this.带不带把 = '不带把'
this.笑 = function() { return '咯咯咯' }
this.撒娇 = function() { return '喵喵喵' }
}
你妈妈.prototype = {
长相: '美丽动人',
说话: function() { return '叽里呱啦' },
笑: function() { return '嘻嘻嘻' }
}
var 你 = new 你妈妈()

你就是一个实例对象,你妈妈就是一个构造函数,她把你造出来就是 new 了你(不要问我你爸是什么)。你遗传(继承)了她的特征(**你妈妈.prototype**),例如你妈美丽动人的长相、说话的方法、笑的方法。此外,她期望你是个女孩,笑得可爱,还会撒娇。
于是,你就成了下面这个样子,你需要通过 __proto__ 这根生命线连接你妈妈的原型特征:
如你妈愿的你

然而,老天嫉妒你妈脱俗的美貌,决定报复在你的身上,让你变成一个又丑又讨人厌的小孩:

1
2
你.长相 = '奇丑无比'
你.嚎啕大哭 = function() { return '哇哇哇' }

太可怕了,这真是个悲伤的故事:
变了的你

举了个不是非常恰当的例子,戏精该下场了🙂。不过相信聪明的你们应该懂构造函数和实例对象是什么了吧。目前你只需要记住:

实例对象被构造(new)了之后能够自动引用(__proto__)其构造函数的原型对象(prototype),同时还可以设置自己的属性或方法,如果与原型对象有同名的属性或方法,则会覆盖原型对象上的属性或方法,优先读取自身的属性或方法。

好了,看完这节,忘记上面只为帮助理解的例子吧…

原型对象

原型对象,顾名思义,就是一个对象,它包含着所有实例对象需要共享的属性和方法。下面是一些原型对象包含的属性和方法的举例:
Object.prototype
Function.prototype
Array.prototype

这些原型对象都有一个 constructor 属性,它们都指向了自己的构造函数,即

1
2
3
Function.prototype.constructor === Function         // true
Array.prototype.constructor === Array // true
Object.prototype.constructor === Object // true
原型对象的constructor指向各自的构造函数

有没有被绕晕😄,与第一节结合起来看,构造函数里的 prototype 是它的原型对象,而这个原型对象里有一个属性 constructor 指向构造函数…(??? hahaha~)

如果你还不理解原型对象是什么,那么请先记住:

原型对象是一个包含着所有实例对象需要共享的属性和方法的对象

__proto__

我们在上一节可以看到,这三个原型对象包含的属性和方法不尽相同。例如 Function.prototypecall() 方法,而 Array.prototypeObject.prototype 没有。但 Function.prototypeArray.prototype 都有 __proto__ 属性,且它们的值指向相同的 Object.prototype
Function.prototype和Array.prototype的__proto__属性指向Object.prototype

在第一节中,你._proto__ === 你妈妈.prototype ,被创建的实例对象的 __proto__ 指向构造函数的原型对象中。整成一个公式就是:

被构造的对象.__proto__ === 构造函数.prototype

那么也就是说 Function.prototypeArray.prototype 都是被 Object 构造函数构造出来的。你现在可能已经晕了,没办法理解它们是如何被构造出来的,那么接下来,我又要举造人的例子了(放心,这次保证高级一点),希望能帮助你理解:

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
// 充当 Object
var 好神仙 = function(性别) {
this.性别 = 性别
}
好神仙.prototype.constructor = 好神仙
好神仙.prototype.品质 = '善良'
好神仙.prototype.修仙 = function() {}
/* 假装这是控制台:
* 好神仙.prototype
* {
* + 修仙: ƒ ()
* 品质: "善良"
* + constructor: ƒ (性别)
* ...
* }
*/

// 充当 Function/Array
var 葫芦籽 = function(排行, 颜色, 技能, 弱点) {
this.排行 = 排行
this.颜色 = 颜色
this.技能 = 技能
this.弱点 = 弱点
}
葫芦籽.prototype = new 好神仙('男') // 即 Function.prototype = new Object(...)
葫芦籽.prototype.constructor = 葫芦籽
葫芦籽.prototype.叫爷爷 = function() { return '爷爷' }
/* 假装这是控制台:
* 葫芦籽.prototype
* {
* + constructor: ƒ (排行, 颜色, 技能, 弱点)
* + 叫爷爷: ƒ ()
* 性别: "男"
* - __proto__: <-- 指向 好神仙.prototype,相当于 Function.prototype.__proto__ 指向 Object.prototype
* + 修仙: ƒ ()
* 品质: "善良"
* + constructor: ƒ (性别)
* ...
* }
*/

葫芦籽.prototype.__proto__ === 好神仙.prototype // true

这里例子中,即使葫芦籽自己有个原型对象 葫芦籽.prototype ,这个原型对象也可以通过 __proto__ 属性指向上一层原型对象 好神仙.prototype 。同理 Function.prototypeArray.prototype

上面的例子只是为了解释【Function.prototype和Array.prototype的__proto__属性指向Object.prototype】这个是如何实现的,名词多了看起来确实复杂了,好吧看来第一节的例子又要拿出来用了… 我尝试把 __proto__ 删掉你就能知道它的作用了。

1
2
3
你.说话()                 // "叽里呱啦"
你.__proto__ = null
你.说话() // Uncaught TypeError: 你.说话 is not a function

看!删了之后你就说不了话了!因为你自身没有说话这个技能,只好靠妈妈助攻,结果你跟她断绝母女关系了!你妈妈伤心欲绝,挥一挥衣袖,带走所有云彩。所以别随便作死😀

说了那么多,整理一下这一节的内容:

  1. __proto__ 是实例对象的一个属性,这个属性使得实例对象可以使用其共用的属性和方法。
  2. 每当一个实例被构造,其 __proto__ 属性就会自动指向构造它的函数的原型对象。记住这个公式: 被构造的对象.__proto__ === 构造函数.prototype
  3. __proto__ 会一层层向上指,直到遇到 null

原型链

相信看到这里,你应该知道原型链是个啥子东西了。但操心的我还是会举个栗子的(毕竟我的葫芦娃还没放出来呢😀)

1
2
3
4
5
6
var 大娃 = new 葫芦籽(1, '红', {
力壮术: function() {},
巨大化: function() {}
}, ['鲁莽', '不懂随机应变'])

大娃.品质 // "善良"

为什么大娃自身明明没有“品质”这个属性,还是可以读到有值呢?

  • 因为有原型链呀~!

大娃自身没有,它就是去 大娃.__proto__ 里找,发现也没有,就继续走到 大娃.__proto__.__proto__ 里找,大娃长舒一口气:“好家伙总算找到了,原来我的品质是善良呀✌”。所以 大娃.品质 实际上是 大娃.__proto__.__proto__.品质 。那么大娃走过来的这一路,就是我们所说的“原型链”。
大娃的原型链

好吧,又得拿出第一节的例子… 我们尝试取你的长相是什么值,结果返回是“奇丑无比”,为什么不是“美丽动人”呢!!?(自己心里没点 B tree 吗)。请看下图,不记得老天报复把你变丑了吗?你在自身属性中找到了你的长相,你看到是“奇丑无比”,天空突然电闪雷鸣、雷雨交加,你哭倒在厕所,无力去走原型链找你的最初的样子,于是一蹶不振接受了这个事实(苍天绕过谁)。
变了的你

心情是坏的,但总结还是要做的:

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。 —— 继承与原型链 | MDN

大总结

实例对象被构造了之后,能够自动引用其构造函数的原型对象,即 实例对象.__proto__ === 构造函数.prototype 。当试图访问这个实例的一个属性时,它不仅仅会在自身上搜寻,还会沿着它的原型链向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

好了,戏总有演完的时候。小可爱们是不是都要忘了开头的问题了。

现有如下代码:

1
2
3
4
var F = function() {}
Object.prototype.a = function() {}
Function.prototype.b = function() {}
var f = new F()

问:f.a 和 f.b 都能取到吗?

答案是:不说。自己画原型链分析。不然我这篇博客你就白看了!就是这么善变这么傲娇~

f.__proto__ === F.prototype => f.__proto__.__proto__ === F.prototype.__proto__ === Object.prototype => f.__proto__.__proto__.a

f.constructor === f.__proto__.constructor === F => f.constructor.__proto__ === F.__proto__ === Function.prototype => f.constructor.__proto__.b


本文完。

若文中有错误还请指正与包涵!

原文链接:https://caijialinxx.github.io/2018/10/22/prototype-proto-and-inheritance/

转载请注明出处。

今天的参考资料我想这么写:

BFC(块格式化上下文)的理解及其应用

理解 BFC

BFC(Block Formatting Context, 块格式化上下文),根据 MDN 的解释,它是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。看这个定义是真的很让人摸不着头脑,简单来说, BFC 就是页面中的一个独立容器,一个页面可以有很多的这样的容器,容器里的子元素与外界互不干扰。先看以下例子,你或许就能知道 BFC 到底是何方神圣了:

  • 例一、现有一个 div.parent 包裹着它的儿子 div.child

    1
    2
    3
    <div class="parent">
    <div class="child"></div>
    </div>
    1
    2
    3
    4
    5
    6
    7
    8
    .parent {
    outline: 1px solid red;
    }
    .child {
    width: 150px;
    height: 150px;
    background-color: #ccc;
    }

    此时 div.parent 的高度即为其儿子 div.child 的高度 150px 。如果为 div.child 添加左浮动,那么 div.parent 就无法包住它的儿子 div.child ,其高度变为 0 (如下图所示)。
    div.child左浮动

    但是,如果为 div.parent 创建一个 BFC ,就可以使它包住其儿子。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .parent {
    outline: 1px solid red;
    display: flow-root;
    }
    .child {
    float: left;
    width: 150px;
    height: 150px;
    background-color: #ccc;
    }

    例一

  • 例二、现有两个兄弟 div.leftdiv.right ,我为 div.left 添加左浮动,使得他们能够并排排列:

    1
    2
    <div class="left">div.left</div>
    <div class="right">div.right</div>
    1
    2
    3
    4
    .right {
    height: 150px;
    outline: 1px solid red;
    }

    结果发现,即使 div.left 设置了右边距,这两兄弟还是重叠在了一起,如下图所示:
    div.left与div.right重叠

    若想将两者分离开来,我们可以为 div.right 创建一个 BFC ,即:

    1
    2
    3
    4
    5
    .right {
    height: 150px;
    outline: 1px solid red;
    display: flow-root;
    }

    div.right的BFC作用

通过以上两个例子,我们初步了解了 BFC 可以解决浮动元素对布局的影响,如浮动元素的父元素无法包住自身的问题、浮动元素与其他兄弟元素重叠的问题。此外,它还可以阻止 margin collapse(坍塌?不知道该如何翻译..看例子吧)的问题。例如:

1
2
3
4
<div class="outer">
<p>first paragraph</p>
<p>second paragraphs</p>
</div>
1
2
3
4
5
6
7
div.outer {
outline: 1px solid red;
}
p {
outline: 2px solid #000;
margin: 20px 0 20px 0;
}

我们可以看到,在 p 元素和其父元素 div.outer 的边缘之间没有任何东西的情况下, div.outer 像是塌了一样顶部直接了第一个 p 元素的顶部齐平,同理底部。
margin collapse

如果我们为 div.outer 创建 BFC ,那么就可以解决这个问题:

1
2
3
4
div.outer {
outline: 1px solid red;
display: flow-root;
}

创建BFC解决margin collapse问题

细心的同学可以发现,尽管 div.outer 被撑起来了,但相邻的两个 p 元素还是发生了 margin 的重叠。这就是 BFC 的布局规则之一:同一 BFC 内的两个相邻块元素上下 margin 会重合。若想解决这个问题,只需在其中一个 p 元素外创建一个 BFC 包裹器即可:

1
2
3
4
5
6
<div class="outer">
<p>first paragraph</p>
<div class="wrapper">
<p>second paragraphs</p>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
div.outer {
outline: 1px solid red;
display: flow-root;
}
p {
outline: 2px solid #000;
margin: 20px 0 20px 0;
}
div.wrapper {
display: flow-root;
}

BFC中的BFC

创建 BFC 的方法

在上面的所有例子中,我都使用 display: flow-root; 来创建 BFC ,当然还有其他的方法,例如:

  • 根元素
  • float 属性不为 none
  • overflow 属性不为 visible 的块元素
  • position: absolute;position: fixed;
  • display: flex;display: inline-flex;
  • display: grid;display: inline-grid;
  • display: inline-block;, display: table-cell;display: table-caption;
  • column-span: all;
  • view more on MDN

这些大法虽好,但很容易产生副作用。例如,如果我们使用 overflow: auto; 来创建一个 BFC ,在某些情况下,你可能会发现有一个多余显示的滚动条或者像我们上述例子中的 outline 会被剪切掉。而 flow-root 是 CSS2 中专门设计为创建 BFC 的属性,它不会产生多余的副作用,所以建议使用这个属性来代替其他方法。

不使用 BFC 清除浮动(题外扩展)

若要清除浮动,我们可以使用 clearfix hack ,只需将 clearfix 加到浮动元素的父元素的 class 属性中即可,而不一定非得创建 BFC 来达到清除浮动的效果。例如例一可以通过以下方式达到同样的效果:

1
2
3
<div class="parent clearfix">
<div class="child"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.parent {
outline: 1px solid red;
}
.child {
float: left;
width: 150px;
height: 150px;
background-color: #ccc;
}
.clearfix::after {
content: '';
display: block;
clear: both;
}

本文完。

若文中有错误还请指正与包涵!

原文链接:https://caijialinxx.github.io/2018/10/15/block-formatting-context/

转载请注明出处。

参考资料:

HTTP 的请求和响应

HTTP 协议是用来指导服务器和浏览器之间如何进行沟通的,一般来说端口默认是 80 。浏览器通过向服务器发送 HTTP 请求,来请求服务器中的资源。服务器根据这个请求,返回相应的响应。最后浏览器下载这个响应的实体内容。

HTTP请求

根据 RFC 2616 的第五章 Request ,一个请求基本包括请求行、请求头、 CRLF 至少这三部分内容,有时还有第四部分请求实体。
RFC 2616规范中HTTP请求的结构

例如,我们使用下列命令向 https://www.baidu.com 发送 POST 请求(点击查看命令的解释),可以看到它的请求报文如下图所示:

1
curl -X POST -d '{"name":"caijialinxx"}' -H 'Content-Type: application/json' -s -v https://www.baidu.com

向 https://www.baidu.com 发送POST请求的请求报文

  • 第一部分即为请求行,分别包括 请求方法请求URIHTTP版本 ,它们之间使用英文空格 SP 分隔,此外,最后是一个换行符 CRLF
  • 第二部分是以 key: value 形式组成的请求头。其中包括 request-header (如 Host User-Agent Accept)和 entity-header (如 Content-Type Content-Length)。每一对 key: value 请求头后也都有一个 CRLF
  • 第三部分是固定不变的单独一行 CRLF ,用于分隔第二部分和第四部分。
  • 第四部分是发送请求时带上的请求实体,即 {"name":"caijialinxx"} 。命令行在第二部分中添加的 Content-Type: application/json 请求头,标示着第四部分是 JSON 格式的数据。

我们发送请求的方法有以下几种:

  • GET
    GET 方法是 HTTP 请求中最常用的方法,通常用于请求服务器中的某个资源。此方法应只用来获取数据。
  • HEAD
    HEAD 方法与 GET 方法类似,但是服务器在响应中只返回头部,而不返回响应实体。
  • POST
    此方法最初是用来向服务器输入数据的。实际上经常与 HTML 表单结合使用,表单被提交时,数据通常被附加到请求实体中发送给服务器。
  • PUT
    此方法让服务器用请求的实体部分来创建一个由请求中的 URL 命名的新文档,如果这个 URL 已存在,那么此次请求的主体会替代原有的主体。
  • PATCH
    用于请求服务器修改指定资源的某个部分。
  • DELETE
    向服务器请求删除指定 URL 对应的资源。
  • OPTIONS
    请求 Web 服务器告知其支持的各种功能,如支持的方法或对请求某些资源支持的方法。
  • CONNECT
    建立一个到由目标资源标识的服务器的隧道。
  • TRACE
    沿着到目标资源的路径执行一个消息环回测试。

HTTP响应

根据 RFC 2616 的第六章 Response ,一个响应包括状态行、响应头、 CRLF 和响应实体。
RFC 2616规范中HTTP响应的结构

在上一部分中(HTTP 请求),我们向 https://www.baidu.com 发送了一个 POST 请求,它紧接着返回了响应给我们,让我们来看看它的响应报文:
服务器返回的响应报文

  • 第一部分即为状态行,分别包括 HTTP版本状态码状态解释 ,它们之间使用英文空格 SP 分隔,此外,最后是一个换行符 CRLF 。在此报文中我们可以看到状态码是 302 ,表示页面暂时被另一个 URI 所替代,但客户端在未来的请求中仍应使用当前请求的 URI 。
  • 第二部分是以 key: value 形式组成的响应头。其中包括 general-header (Connection , Date) 、 response-header (ETag , Server) 和 entity-header (Content-Type , Content-Length) 。每一对 key: value 请求头后也都有一个 CRLF
  • 第三部分是固定不变的单独一行 CRLF ,同请求报文的第三部分。
  • 第四部分是响应实体。在此报文第二部分中我们可以看到 Content-Type: text/html 响应头,表明第四部分是一个 HTML 文档,它有 3824 字节长度的数据。

关于状态码的详解,请参考我的另一篇文章 一些常见的 HTTP 状态码

如何使用开发者工具查看报文

作为一个前端开发人员,学会用开发者工具分析请求是一件很有帮助且不费时间的事情。下面,我将举例如何在 Chrome 中利用开发者工具查看请求报文和响应报文:

  1. 按下 F12 即可打开开发者工具。

  2. 点击 Network ,当访问一个网页时,这里将会出现很多请求结果。我以阮一峰老师的个人网站为例,在地址栏中输入 www.ruanyifeng.com ,然后可以看到如下请求结果:
    在Chrome中访问www.ruanyifeng.com

  3. 我们点开第一个 www.ruanyifeng.com ,点击 Request Header 中的 view source,这才是我们该看的格式。于是它的请求报文如下:
    www.ruanyifeng.com的请求报文

    • 浏览器使用 GET 方法向 http://www.ruanyifeng.com 发送一个请求,并告诉服务器它接受的响应实体类型、 Cookie 等请求头信息。
  4. 然后同样的,点开 Response Header 中的 view source ,看到服务器发回的响应报文如下所示:
    www.ruanyifeng.com的响应报文

    • 服务器返回状态码 302 ,告诉我们这个域名被响应头 Location 指向的 URI 所暂时替代,并自动完成了重定向,于是得到了第二个请求结果(箭头指向的红框)。
  5. 查看重定向的请求结果:
    home.html的报文

    • 重定向使得浏览器重新向服务器请求 http://www.ruanyifeng.com/home.html 这个资源,在 Response Headers 中我们可以看到此次请求成功(状态码为 200 ),并且返回的响应实体是一个 HTML 文档,也就是我们看到的这个网页,一共有 4346 字节的内容。这个网页最后一次更新是在格林尼治的 2018-5-24 星期四 05:56:54 等信息。

现在应该知道要如何利用开发者工具查看我们的网络请求了吧~ 好处你用过就知道~ 当然开发者工具还不只有这些功能,在这里我就不做深入研究。


天色已晚,本文完。

若文中有错误还请指正与包涵!

原文链接:https://caijialinxx.github.io/2018/09/09/http-request-and-response/

转载请注明出处。

参考资料:

前端搜索引擎优化(SEO)的技巧

我们常常可以看到一些面试题中有问【 HTML 语义化的好处是什么】,很多人都可以溜溜地说其中一个好处就是【有利于 SEO 】。那么,什么是 SEO ,大家真的都清楚吗?

搜索引擎优化(SEO)

搜索引擎优化(英语: Search Engine Optimization ,缩写为 SEO ),根据维基百科的解释是:

它是一种透过了解搜索引擎的运作规则来调整网站,以及提高目的网站在有关搜索引擎内排名的方式。由于不少研究发现,搜索引擎的用户往往只会留意搜索结果最前面的几个条目,所以不少网站都希望透过各种形式来影响搜索引擎的排序,让自己的网站可以有优秀的搜索排名。当中尤以各种依靠广告维生的网站为甚。

也就是说,通过搜索引擎优化,我们的网站可以在搜索结果的排名中靠前。

那么,作为一个前端,我们该如何为搜索引擎优化做出一点贡献呢?

优化的技巧

在学习如何优化之前,我们最好了解一下当我们在“百度一下”或者“ Google 搜索”时,搜索引擎都做了一些什么。

这个视频 How Search Works 解释了搜索引擎是如何工作的。需要科学上网才能看得到,当然在这里我也会总结一下这个视频的大意:

当搜索进行时,它会从“蜘蛛”事先抓取到的大量有关联性的索引中,搜索所有包含用户输入的关键字的网页,然后根据算法给出每个网页一个总评分,再根据评分的高低给用户返回搜索结果。而这些包含关键字的内容,可以是文章标题、描述、关键字、内容、甚至是链接等。

由上可以得知,要想优化,我们可以从这些方面入手。

创建唯一且准确的网页标题 <title>

<title> 标记可告诉用户和搜索引擎特定网页的主题是什么。它应放置在 HTML 文档的 <head> 元素中。我们应该为网站上的每个网页创建一个唯一标题,并且尽量避免与网页内容无关或使用默认或模糊的标题。如:

1
2
3
4
5
<!-- 正确示范 -->
<title>前端搜索引擎优化的技巧</title>

<!-- 错误示范 -->
<title>我的文档</title>

使用 meta description

我们可以使用 meta description 标签来准确概括总结网页内容。我们应避免内容中出现关键词的堆砌、描述过长、描述过于笼统简单,如直接拷贝关键词或正文内容、或“这是一个网页”这种没有实际性意义的描述等现象。正确示范如下:

1
<meta name='description' content='本文主要介绍搜索引擎优化(SEO)的技巧,如使用title、description、keywords、语义化标签、img的alt属性等。'>

使用 meta keywords

我们可以使用 meta keywords 来提炼网页重要关键字,如:

1
<meta name='keywords' content='SEO,搜索引擎优化,网页排名优化'>

但有些建站者为了网页能有较好的排名,故意在这个标签中大量堆砌关键字,也就是所谓的“黑帽方法”之一。于是搜索引擎为了为用户提供优质的搜索结果,优化了它们的爬取算法——当出现大量关键字堆砌时,搜索引擎可能会降低这个网站的排名甚至将其列入黑名单。所以我们需慎用或者不用这个标签,使用的话一般设置3-4个关键词即可。

注意: <title>meta descriptionmeta keywords 三者的权重依次减小,我们要想网页有好的排名,必须合理使用这三个标签。

使用语义化元素

在合适的位置使用合适的元素表达合适的内容,让用户和“蜘蛛”能一目了然文档结构。例如使用 <h1> 可以让“蜘蛛”知道这是很重要的内容。然而,值得注意的是,例如在想要表达强调时,我们不应该滥用标题元素或者 <b><i> 这种没有实际意义的标签,换而可以使用 <em><strong> 来表示强调。此外, <h1> 的权重比 <h2> 的大,我们不应该为了增大权重而去滥用 <h1> ,一般来说 <h1> 用于正文的标题。

利用 <img> 中的 alt 属性

alt 属性可以在图片未成功显示时候,使用文本来代替图片的呈现,使“蜘蛛”可以抓取到这个信息。此外它还可以解决浏览器禁用图像或屏幕阅读器解析等问题。

设置 rel='nofollow' 忽略跟踪

如果某个 <a> 的链接不需要跟踪,那么添加 rel='nofollow' 即可通知“蜘蛛”忽略跟踪。因为“蜘蛛”分配到每个页面的权重是一定的,为了集中网页权重并将权重分给其他必要的链接,为不必跟踪的链接添加这个属性就显得很必要了。

提高加载速度

我们应尽量让结构(HTML)、表现(CSS)及行为(JavaScript)三者分离。如果在一个 HTML 页面中,编写大量的 CSS 样式或脚本,会拖慢其加载速度,此外,如果不为 <img> 定义宽高,那么会引起页面重新渲染,同样也会影响加载速度。一旦加载超时,“蜘蛛”就会放弃爬取。

扁平化网站结构

一般来说,一个网站的结构层次越少,越有利于“蜘蛛”的爬取。所以目录结构一般不多于 3 级,否则“蜘蛛”很容易不愿意继续往下爬。就像用户在操作一个网页一样,层级大于 3 就很影响用户体验了,“蜘蛛”就是模仿用户的心理。

合理安排重要内容的位置

我们应该将含重要内容的 HTML 代码放在最前面,因为“蜘蛛”爬取 HTML 的顺序是从上到下,有的搜索引擎对抓取长度有限制,所以要保证重要内容被优先爬取。并且,重要内容不应该由 JavaScript 输出,因为“蜘蛛”没有办法读取 JavaScript ,同时也要少用 iframe ,因为“蜘蛛”一般不会去读取它里面的内容。


本文完。

若文中有错误还请指正与包涵!

原文链接:https://caijialinxx.github.io/2018/07/30/seo-in-front-end/

转载请注明出处。

参考资料:

Windows 系统下的 Heroku 部署

本篇教程是在 Windows 的 Node.js 环境下使用的~ 如果需要其他语言或平台,请戳 这里 看看有没有你要的。

这是官网的 教程(Node.js) ,全英的,英文阅读能力 OK 的同学可以戳戳,但是!竟然来都来了,那就看一下我写的吧~ 欢迎各位同学指正!

安装

第一步肯定是安装。这里没有多余的文案,直接甩安装地址给你,请各位同学根据自己的系统选择 64位32位 的。

假设你阅读本篇文章的时候已安装了 Node v8+ 、 对应版本的 npm 和 git 。接下来让我们继续往下走。

本地创建 Heroku 账户

在 Windows 的命令提示符上操作

安装完成之后,你可以直接使用 Windows 的命令提示符(Win + R),输入 heroku login ,即可创建你的 Heroku 账户。
在命令提示符使用heroku.png

但是, Windows 的命令行这么难用(且丑),用习惯 Git Bash 的我简直没法忍!我是在 Git Bash 上操作的,所以下面介绍一下 Heroku 在 Git Bash 上的使用(本节的命令都可以在命令提示符里使用)。

在 Git Bash 上操作

我在 Git Bash 执行 heroku 的命令时,

1
2
$ heroku login
bash: heroku: command not found

发现它竟然给我错误提示说找不到该命令!后来我找到它的安装路径(G:\heroku\bin),并尝试运行 /g/heroku/bin/heroku -v ,嘿嘿,成了!

在Git Bash中使用heroku失败.png

原来是我没有给 Git Bash 添加环境变量的关系,那么二话不说立马为它添加环境变量吧~ 步骤如下:

  1. 如果你没有 .bashrc 这个文件,就先创建再编辑。
    1
    $ touch ~/.bashrc && vi ~/.bashrc
  2. 在文件中添加 heroku.cmd 的路径作为其环境变量后(如下所示),保存并退出 :wq 即可。
    1
    export PATH="/g/heroku/bin/:$PATH"
    此时,我们就可以在 Git Bash 上愉快地使用 heroku [COMMAND] 了~

在Git Bash中成功使用heroku.png

部署项目到 Heroku

首先,我们直接拿 Heroku 提供的 Demo 来学习如何部署。

1
2
3
$ git clone https://github.com/heroku/node-js-getting-started.git heroku-demo

$ npm install

这样我们就在 heroku-demo 目录下有了一个 Node.js 的项目,然后我们就可以将项目部署到 Heroku 了。

  1. 在 Heroku 上创建一个应用程序,准备让 Heroku 接收我们的代码。

    1
    2
    3
    $ heroku create
    Creating app... done, tranquil-crag-99140
    https://tranquil-crag-99140.herokuapp.com/ | https://git.heroku.com/tranquil-crag-99140.git

    这时, Heroku 给我们返回这个 app 的编号 tranquil-crag-99140 及地址 https://tranquil-crag-99140.herokuapp.com/ ,即可说明我们已创建成功。

    c93fe62e0ba6d6daa7b3416566929c9.png

  2. 接下来我们终于可以部署代码到 Heroku 了。

    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
    $ git push heroku master
    Counting objects: 490, done.
    Delta compression using up to 4 threads.
    Compressing objects: 100% (368/368), done.
    Writing objects: 100% (490/490), 230.24 KiB | 20.93 MiB/s, done.
    Total 490 (delta 87), reused 490 (delta 87)
    remote: Compressing source files... done.
    remote: Building source:
    remote:
    remote: -----> Node.js app detected
    remote:
    remote: -----> Creating runtime environment
    ...
    remote: -----> Installing binaries
    ...
    remote: -----> Restoring cache
    ...
    remote: -----> Building dependencies
    ...
    remote: -----> Caching build
    ...
    remote: -----> Pruning devDependencies
    ...
    remote: -----> Build succeeded!
    remote: -----> Discovering process types
    ...
    remote: -----> Compressing...
    remote: Done: 18.9M
    remote: -----> Launching...
    remote: Released v3
    remote: https://tranquil-crag-99140.herokuapp.com/ deployed to Heroku
    remote:
    remote: Verifying deploy... done.
    To https://git.heroku.com/tranquil-crag-99140.git
    * [new branch] master -> master

    说明太长了所以我只截取了一些重要的过程,最后的 remote: Verifying deploy... done. 说明项目已被成功部署,我们刷新 https://tranquil-crag-99140.herokuapp.com/ ,即可看到下图所示的页面。

    项目成功部署到heroku.png
    当然我们也可以用 heroku open 来快速打开这个网页。

好啦,你应该学会了吧~ 那么何不尝试做一个小项目并将其部署上去了~ 温故知新哦!


使用 SSH Git 传输(扩展)

本节是额外补充,上节是使用默认的 HTTP Git 传输,不过 Heroku 也支持使用 SSH Git 传输。所以喜欢用 SSH Git 传输的可以了解一下~

要使用 SSH Git 传输,需要先配置 SSH 密钥。如果你没有生成过 SSH 密钥,那么需要先输入 ssh-keygen -t rsa 生成,期间有三次询问的过程(如下所示),直接三次回车即可,它将会自动取其默认值(即括号中显示的内容)。

1
2
3
4
5
6
7
8
$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\ASUS-NB\.ssh\id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in C:\Users\ASUS-NB\.ssh\id_rsa.
Your public key has been saved in C:\Users\ASUS-NB\.ssh\id_rsa.pub.
...

然后, heroku keys:add 将公钥上传到 Heroku ,它会询问你是否确定上传,输入 y 即可:

1
2
3
4
$ heroku keys:add
Found an SSH public key at C:\Users\ASUS-NB\.ssh\id_rsa.pub
Would you like to upload it to Heroku? [Y/n]: y
Uploading C:\Users\ASUS-NB\.ssh\id_rsa.pub SSH key... done

此时,你就可以输入 heroku keys 查看 Heroku 的 keys 了。下图说明我们已成功上传 SSH key。
查看heroku密钥.png

你可以点击这里可以了解更多关于 Heroku 管理 SSH 密钥 的 API 。

SSH Git 传输跟 HTTP Git 传输,在命令上只有一小点区别。我们只需要添加 --ssh-githeroku createheroku git:remoteheroku git:clone 命令中即可。例如:

1
$ heroku create --ssh-git

如果你始终使用 SSH Git 传输,那么可以为其全局配置:

1
git config --global url.ssh://git@heroku.com/.insteadOf https://git.heroku.com/

本文完。

参考资料总结:

面试常考题之 HTML

HTML 语义化的理解

A semantic element clearly describes its meaning to both the browser and the developer.
一个语义化的元素能够清晰地向浏览者和开发者描述它的意义。

常见的语义化标签有: <p><h1><button><article><aside><header><footer><main><nav><section> 等。

  • 「语义化」概念的出现
    • 最开始的 HTML 页面是由 PHP 后端写的,他们不会 CSS ,于是只能使用 table 来布局。然而 table 是用来展示表格的,这种投机取巧的使用方法严重违反了 HTML 语义化原则。
    • 后来,有了会写 CSS 的前端之后,他们就使用 DIV + CSS 来实现页面布局,主要是用 floatposition 属性。看起来好了一点,但整个页面的结构都是 DIV ,也非常的不可观。
    • 随着发展,前端渐渐变得专业,为了改善这种杂乱的结构,「语义化」这一概念被提出,前端尽量使用 <p><h1> 这种有意义的标签来展示内容。此外, HTML5 为我们提供了更多的语义化标签,例如 <header><footer><main><nav><section> 等。

总的来说

HTML 语义化就是使用有意义的标签来展示内容,例如 <p> 表示段落, <aside> 表示侧边栏, <nav> 表示导航。这使得页面结构易于阅读、方便理解,此外,还能有益于 SEO (Search Engine Optimization,搜索引擎优化)、其他设备的解析(如屏幕阅读器、盲人阅读器、移动设备)。

meta viewport 的原理

1
<meta name='viewport' content='width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0'>

最初,网页只会在 PC 端展现,所以页面宽度是适应大多数 PC 端的。后来,随着智能手机(iPhone 3GS)的出现,原本为 PC 端设计的网页在小屏的手机上就“胖到挤不下”,用户只能不断移动这些网页来达到浏览的目的。后来乔布斯的工程师想了一个办法:他们让手机宽度模拟成 980px ,这样页面就能缩小了,用户想要浏览某个部分只需要手动放大。
但是随着智能手机的普及,这种不方便用户的功能显然不够贴心。 <meta name='viewport'> 的出现就是为了解决这个问题:

属性名 属性值 描述
width [number] / device-width 控制 viewport 的大小,可以指定的一个数字,如 600,或者特殊的值,如 device-width 为设备的宽度(单位为缩放为 100% 时的 CSS 的像素)。
height [number] / device-height 和 width 相对应,指定高度。
initial-scale [number] 初始缩放比例,也即是当页面第一次加载时的缩放比例。
maximum-scale [number] 允许用户缩放到的最大比例。
minimum-scale [number] 允许用户缩放到的最小比例。
user-scalable yes / no 用户是否可以手动缩放。

总的来说

我们可以通过设置 <meta name='viewport' content='width=device-width, initial-scale=1.0'> 来使得页面自适应屏幕的宽度,并且不被缩小。

canvas 的使用

这里有一个我自己做的项目示例 Canvas 画板,去玩一下呗~

这里总结一下 <canvas> 元素中常用的 API 及用法。

首先我们要在 HTML 中添加一个 canvas 元素:

1
<canvas id="canvas"></canvas>

然后在 JavaScript 中取到这个元素,就可以执行一些操作:

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
// 获取 canvas 元素
var canvas = document.getElementById('canvas')
// 「常用 API」 1. getContext() 访问绘画上下文
var ctx = canvas.getContext('2d')

// 「常用 API」 2. fillStyle 设置图形的填充颜色
ctx.fillStyle = 'orange'
// 「常用 API」 3. strokeStyle 设置图形的轮廓颜色
ctx.strokeStyle = 'red'
// 「常用 API」 4. strokeStyle 设置图形的轮廓颜色
ctx.lineWidth = 2

/* 绘制矩形 */
// 「常用 API」 5. fillRect(x, y, width, height) 绘制一个填充的矩形
ctx.fillRect(25,25,100,100)
// 「常用 API」 6. clearRect(x, y, width, height) 清除指定矩形区域,让清除部分完全透明
ctx.clearRect(45,45,60,60)
// 「常用 API」 7. strokeRect(x, y, width, height) 绘制一个矩形的边框
ctx.strokeRect(50,50,50,50)

/* 绘制路径 */
// 「常用 API」 8. beginPath() 新建一条路径
ctx.beginPath()
// 「常用 API」 9. moveTo(x, y) 将笔触移动到指定的坐标x以及y上
ctx.moveTo(160,75)
// 「常用 API」 10. lineTo(x, y) 绘制一条从当前位置到指定x以及y位置的直线
ctx.lineTo(220,125)
ctx.lineTo(220,25)
// 「常用 API」 11. closePath() 闭合路径,不是必须的
ctx.closePath()
// 「常用 API」 12. stroke() 通过线条来绘制图形轮廓。如果不调用 closePath() 则 stroke() 不会自动闭合
ctx.stroke()
// 「常用 API」 13. fill() 填充路径的内容区域生成实心的图形。即使不调用 closePath() 所有没有闭合的形状仍会自动闭合填充
ctx.fill()

/* 绘制圆形 */
ctx.beginPath()
/*
* 「常用 API」 14. arc(x, y, radius, startAngle, endAngle, anticlockwise)
* 画一个以(x,y)为圆心, radius 为半径的圆,
* 从 startAngle 开始到 endAngle 结束,
* 按照 anticlockwise 给定的方向(默认为顺时针)来生成
*/
ctx.arc(300, 75, 50, 6, 2*Math.PI, true)
ctx.fill()
ctx.stroke()

你可以查看这个小 Demo 的演示效果 ,并且可以尝试改动一下。

  • 总结:
    • getContext()
    • fillStyle
    • strokeStyle
    • beginPath()
    • closePath()
    • moveTo()
    • lineTo()
    • stroke()
    • fill()

你还可以学习一下 MDN 中的 canvas 教程 ,里面还有绘制文本变形等很有意思的教程。

细节常识题

  1. 浏览器默认的字体大小
    • 16px
  2. <strong><b><em><i> 的区别
    • <strong><b> 都可以使字体变粗,但前者表示强调,它可以标明重点内容,而后者没有什么实际含义。

    • <em><i> 都可以使字体变斜体,但前者表示也是用来强调文本,经常使用在表示重要术语或引用等,而后者没有什么实际含义。

      1
      2
      3
      4
      我是这个世界上<strong>最美的</strong>人。
      我想让这个字变<b></b>
      我喜欢 <em>Professional Javascript for Web Developer 3rd</em> 这本书。
      我想让这个字变<i></i>

      我是这个世界上最美的人。
      我想让这个字变
      我喜欢 Professional Javascript for Web Developer 3rd 这本书。
      我想让这个字变

    • 我们应尽量遵循 HTML 语义化,表示重要的内容时使用 <strong><em> ,避免单纯为了让字体变粗而使用 <b> ,或者单纯为了让字体变斜而使用 <i> 的情况,这些样式应该由 CSS 去控制。

    • 这里有一个来自 MDN 的比较,可以参考一下。

  3. hrefsrc 的区别
    • href 是 Hypertext Reference 的缩写,表示超文本引用,用来建立文档和外部资源之间的联系。常用的有 <a><link> 标签。
    • src 是 source 的缩写,是页面种必不可少的一部分,其指向的内容会嵌入到文档中当前元素所在的位置。浏览器在下载、解析这个元素的时候会暂停页面的加载和处理。常用的有 <script><img><iframe> 标签。
    • 总的来说就是 src 用于替换当前元素,而 href 用于建立文档与引用资源的联系。
  4. readonlydisabled 的区别
    • readonly 表示这个控件是只读的,无法在页面上修改,但可以通过 JavaScript 修改。在提交表单时此控件的值会被传出去。
    • disabled 表示这个控件是被禁用的,同样无法在页面上修改内容(可以通过 JS 修改),在提交表单时该控件的内容不会被提交。
  5. 块级元素与行内元素的区别
    • 默认情况下,块级元素会另起一行,而行内元素不会以新行开始。
    • 块级元素可以设置宽高,而行内元素的宽高由内容物决定。
    • 块级元素里面可以有行内元素或其他块级元素,而行内元素只能包含数据或其他行内元素。
    • 块级元素有 <div><p><ul><header><form> 等,而行内元素有 <span><select><strong><input><button><textarea> 等。
  6. doctype 的作用
    • 声明文档类型,告诉浏览器要用什么文档类型规范来解析这个文档。

Cookie, Session, LocalStorage & SessionStorage

由于刚刚写过一篇介绍 Cookie 的博客,所以我在这里不打算再次介绍多一遍,直接总结如下:

  1. Cookie 是一段由服务器发送给浏览器并保存到本地的一段数据,大小一般为 4KB 。
  2. 它存储着用户的一些信息,在浏览器再次访问同一个 URL 时会将这段 Cookie 附加到 HTTP 请求中发送给服务器。因此,这会增加流量的消耗。
  3. 一般在浏览器关闭(会话结束)时就被删除,但也可以通过 ExpireMax-Age 来设置过期时间。
  4. 浏览器可以通过 document.cookie 读写 Cookie ,若要阻止此行为,可以在 Set-Cookie 头中添加 HttpOnly 标记。

详细的还请移步到 HTTP 牌的 Cookie 参阅。

Session

Session 的实质是存储在服务器里的一小块内存,一般来说是基于 Cookie 实现的。实现过程举例如下:

1
2
3
4
5
6
7
8
9
10
/* 
* 1. 当第一次使用 Session 时,服务器要创建一个 SessionID
* 来作为 Session 中存放用户信息的唯一标识
*/
let session = {}
let sessionID = Math.random().toString().slice(2) // 假设为 '1234567890'
session[sessionID] = 用户信息 // 此时服务器中已存含有用户信息的 SessionID 对应的内存

/* 2. 服务器通过 Cookie 给用户返回一个 SessionID */
response.setHeader('Set-Cookie', sessionID)
1
2
3
4
5
6
7
8
/* 3. 当用户访问同一个 URL 时,向服务器发送这个内容为其 SessionID 的 Cookie 头 */
Request Header
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 580
Cookie: 1234567890
...

1
2
3
4
5
6
7
8
/* 
* 4. 服务器读取 Cookie 中的 SessionID ,
* 然后去 Session 中读取对应的内存,最终得到用户信息。
*/
if (request.headers.cookie) {
let sessionID = request.headers.cookie
let userInfo = session[sessionID] // 读取用户信息
}

值得一提的是, SessionID 是随机生成的。

LocalStorage

window.localStorage 是 HTML5 新增的一个 API ,它遵循同源政策,属于本地存储。它有以下特点:

  1. 大小一般为 5MB ,永久有效;
  2. 本地存储和读取,不会被附加到 HTTP 请求中。

window.localStorage 的使用如下例所示:

1
2
3
4
5
6
7
8
9
10
11
// 保存数据到 LocalStorage
localStorage.setItem('key', 'value') // {"key": "value"}

// 从 LocalStorage 获取数据
let key = localStorage.getItem('key') // "value"

// 从 LocalStorage 删除保存的数据
localStorage.removeItem('key') // {}

// 从 LocalStorage 删除所有保存的数据
sessionStorage.clear();

SessionStorage

window.sessionStorage 同样也遵循同源政策,它与 window.localStorage 不同的是,它在页面或浏览器关闭(会话结束)时就会被清除。
window.sessionStorage 的使用如下例所示:

1
2
3
4
5
6
7
8
9
10
11
// 保存数据到 SessionStorage
sessionStorage.setItem('key', 'value')

// 从 SessionStorage 获取数据
var data = sessionStorage.getItem('key')

// 从 SessionStorage 删除保存的数据
sessionStorage.removeItem('key')

// 从 SessionStorage 删除所有保存的数据
sessionStorage.clear()

总结

四者之间的比较如下:

  • Cookie 保存在客户端的硬盘里;而 Session 是保存在服务端的内存里。
  • Cookie 每次都随着 HTTP 请求发送给服务器;而 Session 只需要通过发送保存在 Cookie 头中的 SessionID 即可从服务器中读取对应内存。
  • Cookie 以明文的形式存储,内容可以被用户查看或篡改;而 Session 因为只提供一个随机的 SessionID ,所以无法被用户直接查看。
  • Cookie 的大小一般为 4KB ;而 LocalStorage 一般为 5MB 。
  • Cookie 一般在浏览器关闭(会话结束)时就失效,后端可以设置 ExpiresMax-Age 来改变 Cookie 的过期时间;而 LocalStorage 是永久有效,除非用户手动清除。
  • Cookie 会被附加到 HTTP 请求中;而 LocalStorage 不会。

LocalStorage 和 SessionStorage

  • 前者永久有效,除非用户删除;后者在浏览器关闭(会话结束)后就被清空。