小程序变量后置自增无效

实现功能为:维护一个初始值count,每次按下按钮,count自增。

Page({
    data: {
        count: 0
    },
    onClick() {
        this.setData({count: this.data.count++});
    }
}

以上代码实现的结果是count值永远为0,原因是”后置自增是先用后加“, this.data.count++ 返回的结果是0,虽然this.data.count的值已经自增为1了,但是setData方法再次把count值重置为0,看起来的结果就是后置自增不起作用了。

解决办法:使用前置自增或者 + 1

Page({
    data: {
        count: 0
    },
    onClick() {
        this.setData({count: ++this.data.count});
        // or
        this.setData({count: this.data.count + 1});
    }
}

Git 学习笔记

名词介绍

Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。最早,肯定只有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。

其实一台电脑上也是可以克隆多个版本库的,只要不在同一个目录下。不过,现实生活中是不会有人这么傻的在一台电脑上搞几个远程库玩,因为一台电脑上搞几个远程库完全没有意义,而且硬盘挂了会导致所有库都挂掉。

工作区
简言之就是工作的区域。 对于git而言,就是的本地工作目录。 工作区的内容会包含提交到暂存区和版本库 (当前提交点)的内容,同时也包含自己的修改内容。 

版本库
工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库

暂存区
暂存区 (stage area, 又称为索引区index),是git中一个非常重要的概念。 是我们把修改提交版本库前的一个过渡阶段。Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区。

还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD

把文件往Git版本库里添加的时候,是分两步执行的:

  1. 第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;
  2. 第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。

与其他版本控制系统对比

  1. 和SVN不一样,Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示。
  2. Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念。

Git 常用命令

假如我们想恢复一个已经提交的版本怎么办。

为了消除一个旧版本,我们必须撤销旧版本里的所有更改然后提交一个新版本。这种操作叫做 reverse merge。

git config // 指定用户名和email地址

git init // 初始化仓库,将当前目录变为git可管理的仓库

git clone // 克隆远程库到本地库
git fork // 克隆到github账号

git add // 将文件添加到仓库

git commit // 把文件提交到仓库

git push // 把本地库的所有内容推送到远程库上

git pull // 更新本地库

git status // 查看仓库当前状态

git diff // 查看具体修改了哪些内容

git log // 查看历史记录,显示从最近到最远的提交日志,如果嫌输出信息太多,可以试试加上--pretty=oneline参数

git reset // 回退版本,回退到上一个:git reset HEAD^(HEAD指向的版本就是当前版本),也可以指定对应版本号

git reflog // 记录每一次命令

git checkout -- file // 把文件在工作区的修改全部撤销

git reset HEAD <file> // 可以把暂存区的修改撤销掉(unstage),重新放回工作区,git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本

git rm // 删除文件

git remote -v // 查看远程库信息

git remote rm <name> // 删除远程库

分支管理

git checkout -b 表示创建并切换,相当于以下两条命令

git branch dev // 创建分支
git checkout dev // 切换分支

git branch 命令查看当前分支,在master分支执行 git merge devdev分支合并到master分支上。

合并完成后,可以删除dev分支: git branch -d dev

切换分支使用git checkout <branch>,而前面讲过的撤销修改则是git checkout -- <file>,同一个命令,有两种作用,确实有点令人迷惑。

实际上,切换分支这个动作,用switch更科学。因此,最新版本的Git提供了新的git switch命令来切换分支。

git switch -c dev // 创建并切换到新的dev分支
git switch master // 切换到已有的master分支

解决冲突

当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。

解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。

git status可以告诉我们冲突的文件,用git log --graph命令可以看到分支合并图。

分支管理策略

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。

如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。合并dev分支,请注意--no-ff参数,表示禁用Fast forward

Bug分支

当手头工作没有完成时,需要先修复某个bug,Git提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作,刚才的工作现场存到哪去了?用git stash list命令看看,恢复工作现场有两个办法:

git stash apply // 恢复后,stash内容并不删除,你需要用git stash drop来删除
git stash pop // 恢复的同时把stash内容也删了

master分支上修复了bug后,因为dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。那怎么在dev分支上修复同样的bug

同样的bug,要在dev上修复,我们只需要把特定提交所做的修改“复制”到dev分支。Git专门提供了一个cherry-pick命令,让我们能复制一个特定的提交到当前分支。用git cherry-pick,我们就不需要在dev分支上手动再把修bug的过程重复一遍。

Feature分支

开发一个新feature,最好新建一个分支;

如果要丢弃一个没有被合并过的分支,可以通过git branch -D <name>强行删除。

多人协作

  • 查看远程库信息,使用git remote -v
  • 本地新建的分支如果不推送到远程,对其他人就是不可见的;
  • 从本地推送分支,使用git push origin branch-name,如果推送失败,先用git pull抓取远程的新提交;
  • 在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致;
  • 建立本地分支和远程分支的关联,使用git branch --set-upstream branch-name origin/branch-name
  • 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突。

Rebase(待完成)

标签管理

默认标签是打在最新提交的commit上的。 标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。

git tag <name>

也可以指定某次committag

git tag v0.1 f52c533 // (commit id)

可以使用git tag查看所有标签,标签不是按时间顺序列出,而是按字母排序的。可以用git show <tagname>查看标签信息。

可以创建带有说明的标签,用-a指定标签名,-m指定说明文字:

git tag -a v0.1 -m "version 0.1 released" 1094adb

删除标签:

git tag -d v0.1

因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。如果要推送某个标签到远程,使用命令git push origin <tagname>,或者,一次性推送全部尚未推送到远程的本地标签:

git push origin -tags

如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:

git tag -d v0.9

然后,从远程删除。删除命令也是push,但是格式如下:

git push origin :refs/tags/v0.9

JS 事件流和事件处理程序

事件流

当你点击一个按钮时,实际上不光点击了这个按钮,还点击了他的容器以及整个页面。事件流描述了页面接收事件的顺序。IE支持事件冒泡流,NetScape支持事件捕获流。

事件冒泡

事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)。

IE事件流被称为事件冒泡。所有现代浏览器都支持事件冒泡,现代浏览器中的事件会一直冒泡到window对象。IE5.5及早期版本会跳过html元素。

事件捕获

事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。

NetScape communicator团队提出了另一种名为事件捕获的事件流。事件捕获得到了所有现代浏览器的支持。实际上,所有浏览器都是从window对象开始捕获事件,而DOM2 Events规范规定的是从document开始。

由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情况下可以使用事件捕获。

DOM 事件流

DOM2 Events规范规定事件流分为3个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截提供了可能。然后,实际的目标元素接收到事件,最后一个阶段是冒泡,最迟要在这个阶段相应事件。

在DOM事件流中,实际的目标(<div>元素)在捕获阶段不会接收到事件,这是因为捕获阶段从document到<html>到<body>就结束了。下一阶段,即会在div元素上触发事件的到达目标阶段,通常在事件处理时被认为是冒泡阶段的一部分。然后,冒泡阶段开始,事件反向传播至文档。

大多数支持DOM事件流的浏览器实现了一个小小的扩展。虽然DOM2 Events规范明确捕获阶段不命中事件目标,但现代浏览器都会在捕获阶段在事件目标上触发事件。最终结果是在事件目标上有两个机会来处理事件。

注意 所有现代浏览器都支持DOM事件流,只有IE8及更早版本不支持。

事件处理程序

事件意味着用户或浏览器执行的某种动作,如单击、加载。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。

作为事件处理程序执行的代码可以访问全局作用域中的一切。

DOM0事件处理程序

像这样使用DOM0方式为事件处理程序赋值时,所附函数被视为元素的方法。

let btn = document.getElementById("myBtn");
btn.onclick = function() {
    console.log("Clicked");
};

通过将事件处理程序属性的值设置为null,可以移除通过DOM0方式添加的事件处理程序。

btn.onclick = null;

DOM2事件处理程序

DOM2 Events为事件处理程序的赋值和移除定义了两个方法:addEventListener()removeEventListener()。这两个方法暴露在所有DOM节点上,它们接收3个参数:事件名、事件处理程序和一个布尔值,true表示在捕获阶段调用事件处理程序,false表示在冒泡阶段调用事件处理程序。

let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
    console.log(this.id);
}, false);

与DOM0方式类似,这个事件处理程序同样在被附加到的元素的作用域中运行。使用DOM2方式的主要优势是可以为同一个事件添加多个事件处理程序。多个事件处理程序以添加顺序来触发。

通过addEventListener()添加的事件处理程序只能使用removeEventListener()并传入与添加时同样的参数来移除。这意味着使用addEventListener()添加的匿名函数无法移除

大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。把事件处理程序注册到捕获阶段通常用于在事件到达其制定目标前拦截事件,如不需要,则不要使用事件捕获。

IE事件处理程序

IE实现了与DOM类似的方法,即attachEvent()detachEvent()。这两个方法接收相同的参数:事件处理程序的名字和事件处理函数。因为IE8及更早版本只支持事件冒泡,所以使用attachEvent添加的事件处理程序会添加到冒泡阶段。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
    console.log("Clicked");
});

IE中使用attachEvent与使用DOM0方式的主要区别是事件处理程序的作用域。使用DOM0方式时,事件处理程序中的this值等于目标元素。而使用attachEvent时,事件处理程序是在全局作用域中运行的,因此this等于window

使用attachEvent也可以给一个元素添加多个事件处理程序。与DOM方式不同的是,这里的事件处理程序会以添加他们的顺序反向触发

JS 原型链

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性([[Prototype]])指回构造函数,而实例有一个内部指针指向原型。

如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

function SuperType() {
    this.property = true;
}
SuperType.protype.getSuperValue = function() {
    return this.property;
}
function SubType() {
    this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
    return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true;

JS this

this关键字是JS中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。

为什么要使用this

this提供了一种更优雅的方式来隐式 ”传递“ 一个对象引用,因此可以把API设计得更加简洁且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用this则不会这样。

this是在运行时进行绑定的,他的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

this既不指向函数自身也不指向函数的词法作用域,this实际上是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

调用位置

function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    console.log("baz");
    bar();
}
function bar() {
    // 当前调用栈是:baz - bar
    // 因此,当前调用位置是baz
    console.log("bar");
    foo();
}
function foo() {
    // 当前调用栈是:baz - bar - foo
    // 因此,当前调用位置是bar
    console.log("foo");
}
baz();

绑定规则

函数的执行过程中调用位置如何决定this的绑定对象:你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。

默认绑定

最常见的函数调用类型:独立函数调用。

function foo() {
    console.log(this.a);
}
var a = 2;
foo(); // 2

当调用foo时,this.a被解析成全局变量a。为什么?因为在本例中,函数调用时应用了this的默认绑定,因此this指向全局对象。

怎么知道是应用默认绑定?可以通过分析调用位置。在代码中,foo是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

如果使用严格模式,则不能将全局对象用于默认绑定,this会绑定到undefined

function foo() {
    "use strict"
    console.log(this.a);
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined

这里有一个微妙但非常重要的细节,虽然this的绑定规则完全取决于调用位置,但只有foo运行在非严格模式下时默认绑定才能绑定到全局对象,在严格模式下调用foo不影响绑定。

function foo() {
    console.log(this.a);
}
var a = 2;
"use strict"
foo(); // 2

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说被某个对象拥有或者包含。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

foo 被调用时,他的前面加上了对 obj 的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因此,this.aobj.a 是一样的。

对象属性引用链中只有最后一层在调用位置中起作用。

function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 2,
    foo: foo
};
var obj1 = {
    a: 1,
    obj2: obj2
};
obj1.obj2.foo(); // 2

隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定,从而把this绑定到全局对象或者undefined上。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo;
var a = 3;
bar(); // 3

一种更微妙、更常见、更出乎意料的情况发生在传入回调函数时:

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    fn();
}
var obj = {
    a: 2,
    foo: foo
}
var a = "global";
doFoo(obj.foo); // "global" , 无论全局变量a是在obj之前还是之后声明并赋值

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。如果把函数传入语言内置函数而不是传入自己声明的函数,结果也是一样的。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
var a = "global";
setTimeout(obj.foo, 100); // "global"

如上,回调函数丢失this绑定是非常常见的,此外,调用回调函数还可能会修改this

显式绑定

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

具体来说,可以使用callapply方法。这两个方法如何工作呢,他们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。

function foo() {
    console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 2

可惜,显示绑定仍然无法解决丢失绑定的问题。

硬绑定

硬绑定可以解决丢失绑定的问题。

我们创建函数bar并在内部手动调用foo.call(obj),因此强制把foothis绑定到了obj,之后无论如何调用函数bar,他总会在obj上调用foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

function foo() {
    console.log(this.a);
}
var obj = { a: 2 };
var bar = function() {
    foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的bar不可能再修改this
bar.call(window) // 2

硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = { a: 2 };
var bar = function() {
    return foo.apply(obj, arguments);
}
var b = bar(3); // 2 3 
console.log(b); // 5

另一种是创建一个可以重复使用的辅助函数

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    };
}
var obj = { a: 2 };
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); //5

由于硬绑定是一种非常常用的模式,所有ES5提供了内置的方法Function.protoprototype.bind

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = { a: 2};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

new 绑定

JS中,构造函数只是一些使用new操作符时被调用的函数,他们并不会属于某个类,也不会实例化一个类。实际上,他们甚至不能说是一种特殊的函数类型,他们只是被new操作符调用的普通函数而已。

function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

使用new来调用foo时,我们会构造一个新对象并把它绑定到foo调用中的this上。

优先级

如果某个调用位置应用了多条规则怎么办?为了解决这个问题就必须给这些规则设定优先级。

毫无疑问,默认绑定的优先级是四条规则中最低的,new绑定比显式绑定优先级高。显示绑定的优先级比隐式绑定高。

JS 闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); // 2 —— 这就是闭包的效果

函数bar的词法作用域可以访问foo的词法作用域。在foo执行后,通常会期待foo的整个内部作用域被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。而闭包可以阻止这件事情的发生。

bar所声明的位置所赐,它拥有涵盖foo内部作用域的闭包使得该作用域能够一直存活,以供bar在之后任何时间进行引用。

bar依然持有对该作用域的引用,这个引用就叫做闭包

本质上无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

function wait(message) {
    setTimeout(() => console.log(message), 1000);
}
wait("Hello Closure"); 

将一个匿名函数传递给setTimeout,内部函数具有涵盖wait作用域的闭包,因此保留对变量message的引用。

循环与闭包

for (var i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), i * 1000);
}

正常情况下,我们对这段代码的预期是分别输出0-4,每秒一次,一次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次5。

5从哪里来?循环终止条件是i < 5, 条件首次成立时i的值为5,因此,输出显示的是循环结束时i的最终值。

仔细想一下,延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(…, 0),所有的回调函数依然是在循环结束后才会被执行。

为什么代码的行为跟语义所暗示的不一致?

我们试图假设循环中的每个迭代在运行时都会给自己捕获一个i的副本,但根据作用域的工作原理,实际情况是尽管循环中五个函数是在各个迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i

缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

IIFE通过声明并立即执行一个函数来创建作用域。

for (var i = 0; i < 5; ++i) {
    (function(j) { // IIFE需要有自己的变量在每次迭代中存储i的值
        setTimeout(function timer() { console.log(j); }, j * 1000); 
    })(i);
}

在迭代中使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

块作用域

我们使用IIFE在每个迭代都生成一个新的作用域,换句话说,每次迭代都需要一个块作用域。let声明可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

for (var i = 0; i < 5; ++i) {
    let j = i;
    setTimeout(function timer() {
        console.log(j);
    }, j * 1000);
}

for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不只被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

for (let i = 0; i < 5; ++i) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

图解HTTP笔记(1 – 4章)

HTTP协议用于客户端和服务器端之间的通信

在两台计算机之间使用HTTP协议通信时,在一条通信线路上必定有一端是客户端,另一端是服务器端。有时候,两台计算机作为客户端和服务器端的角色有可能互换,但仅从一条通信路线来说,服务器端和客户端的角色是确定的,而用HTTP协议能够明确区分哪段是客户端,哪段是服务器端。

HTTP协议规定,请求从客户端发出,最后服务器端响应请求并返回。换句话说,肯定是先从客户端开始建立通信,服务器端在没有收到请求之前不会发送响应。

请求报文是由请求方法、请求URI,协议版本、可选请求首部字段和内容实体构成的。

HTTP是不保存状态的协议

HTTP是一种不保存状态,即无状态(stateless)协议。HTTP自身不对请求和响应之间的通信状态进行保存,也就是说,在HTTP这个级别,协议对于发送过的请求或响应不做持久化处理。这是为了更快地处理大量事务,确保协议的可伸缩性。

为了实现保持状态的功能,引入了cookie技术。

HTTP请求方法

  • GET:获取资源
  • POST:传输实体主体
    GET也可以传输实体主体,但一般不用。POSTGET相似,但POST的主要目的不是获取响应的主体内容
  • PUT:传输文件
    就像FTP的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求URI指定位置
  • HEAD:获得报文首部
    用于确认URI有效性及资源更新的日期时间等
  • DELETE:删除文件
  • OPTIONS:询问支持的方法
  • TRACE:追踪路径
    Web服务器端将之前的请求通信返回给客户端的方法。容易引发XSTCross-Site Tracing,跨站追踪)攻击,通常不会用到
  • CONNECT:要求用隧道协议连接代理
    主要使用SSLSecure Sockets Layer,安全套接层)和TLSTransport Layer Security,传输层安全)协议将通信内容加密后经网络隧道传输

持久连接节省通信量

HTTP协议的初始版本中,每进行一次HTTP通信就要断开一次TCP连接。发送请求一份包含多张图片的HTML文档对应的Web页面,会产生大量的通信开销。

持久连接

持久连接(HTTP Persistent Connections,也称为HTTP keep-aliveHTTP connection reuse)特点是:只要任意一端没有明确提出断开连接,则保持TCP连接状态。

HTTP1.1中,所有连接默认都是持久连接。服务器和客户端都需要支持持久连接。

管线化

从前发送请求后需等待并受到响应才能发送下一个请求。管线化技术出现后,不用等待响应亦可直接发送下一个请求。

管线化技术比持久连接更快,请求数越多,时间差就越明显。

使用Cookie的状态管理

Cookie技术通过在请求和响应报文中写入Cookie信息来控制客户端的状态。

Cookie会根据从服务器发送的响应报文中的set-cookie首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入Cookie值后发送出去。

服务器端发现客户端发送过来的Cookie后,会去检查究竟是从哪一个客户端发来的连接请求,然后比对服务器上的记录,最后得到之前的状态信息。

HTTP报文

用于HTTP协议交互的信息称为HTTP报文。HTTP报文大致可分为报文首部和报文主体两块。通常,并不一定要有报文主体。

请求报文和响应报文的首部内容由以下数据组成。

  • 请求行
    包含请求的方法,请求URIHTTP版本。
  • 状态行
    包含表明响应结果的状态码,原因短语和HTTP版本。
  • 首部字段
    包含请求和响应的各种条件和属性的各类首部,一般有4种首部:通用首部、请求首部、响应首部和实体首部。
  • 其他
    可能包含HTTPRFC里未定义的首部(Cookie等)。

编码提升传输速率

HTTP在传输数据时可以按照数据原貌直接传输,也可以在传输过程中通过编码提升传输速率。通过在传输时编码,能有效处理大量的访问请求。但是,编码的操作需要计算机来完成,因此会消耗更多的CPU等资源。

报文主体和实体主体的差异

  • 报文(message)
    HTTP通信中的基本单位,由8位组字节流(octet sequence,其中octet为8个比特)组成,通过HTTP通信传输。
  • 实体(entity)
    作为请求或响应的有效载荷数据(补充项)被传输,其内容由实体首部和实体主体组成。

HTTP报文的主体用于传输请求或响应的实体主体。

通常,报文主体等于实体主体。只有当传输中进行编码操作时,实体主体的内容发生变化,才导致它和报文主体产生差异。

分割发送的分块传输编码

HTTP通信过程中,请求的编号实体资源尚未传输完成之前,浏览器无法显示请求页面。在传输大容量数据时,通过把数据分割成多块,能够让浏览器逐步显示页面。这种把实体主体分块的功能称为分块传输编码(Chunked Transfer Coding)。

分块传输编码会将实体主体分成多个部分(块),每一块都会用十六进制来标记块的大小,而实体主体的最后一块会使用“0(CR+LF)”来标记。

常见的内容编码有以下几种:

  • gzip(GNU zip)
  • compress(UNIX 系统的标准压缩)
  • deflate(zlib)
  • identity(不进行编码)

发送多种数据的多部分对象集合

发送邮件时,我们可以在邮件里写入文字并添加多份附件,这是因为采用了MIME(Multipurpose Internet Mail Extensions, 多用途因特网邮件扩展)机制,它允许邮件处理文本、图片、视频等多个不同类型的数据。

MIME扩展中会使用一种称为多部份对象集合(Multipart)的方法,来容纳多份不同类型的数据。相应地,HTTP协议中也采纳了多部份对象集合,发送的一份报文主体内可含有多类型实体。通常是在图片或文本文件等上传时使用。

多部分对象集合包含的对象如下:

  • multipart/form-data
    Web表单文件上传时使用。
  • multipart/byteranges
    状态码206(Partial Content,部分内容)响应报文包含了多个范围的内容时使用。

获取部分内容的范围请求

指定范围发送的请求叫做范围请求(Range Request),如对一份10 000字节大小的资源,可以只请求5001 – 10 000字节内的资源。

执行范围请求时,会用到首部字段Range来指定资源的byte范围。

// 指定范围5001-10000
Range: bytes=5001-10000

// 从5001字节之后全部
Range: bytes=5001-

// 多个部分
Range: bytes=0-5000, 5000-7000

针对范围请求,响应会返回状态码206 Partial Content 的响应报文。另外,对于多重范围的范围请求,响应会在首部字段Content-Type标明multipart/byteranges后返回响应报文。

如果服务器端无法响应范围请求,则会返回状态码200OK 和完整的实体内容。

内容协商返回最合适的内容

当浏览器的默认语言为英语或中文,访问相同URIWeb页面时,则会显示对应的语言版本的页面,这样的机制称为内容协商(Content Negotiation)。

内容协商机制是指客户端和服务器端就响应的资源内容进行交涉,然后提供给客户端最为适合的资源。内容协商会以语言、字符集、编码方式等为基准判断响应的资源。

包含在请求报文中的某些首部字段(如下)就是判断的基准:

  • Accept
  • Accept-Charset
  • Accept-Encoding
  • Accept-Language
  • Content-Language

状态码告知从服务器端返回的请求结果

状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果。

类别原因短语
1XXInformational(信息性状态码)接收的请求正在处理
2XXSuccess(成功状态码)请求正常处理完毕
3XXRedirection(重定向状态码)需要进行附加操作以完成请求
4XXClient Error(客户端错误状态码)服务器无法处理请求
5XXServer Error(服务器错误状态码)服务器处理请求出错
状态码的类别

2XX 成功

  • 200 OK
  • 204 No Content
  • 206 Partial Content

3XX 重定向

  • 301 Moved Permanently
    永久性重定向,该状态码表示请求的资源已被分配了新的URI,以后应使用资源现在所指的URI
  • 302 Found
    临时性重定向,该状态码表示请求的资源已被分配了新的URI,希望用户本次能使用新的URI访问。与301相似,但302状态码代表的资源不是被永久移动,只是临时性质的。
  • 303 See Other
    该状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源。
  • 304 Not Modified
    该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但因发生请求未满足条件的情况后,直接返回304Not Modified(服务器资源未改变,可直接使用客户端未过期的缓存)。304虽然被划分在3XX类别中,但和重定向没关系。
    附带条件的请求是指采用GET方法的请求报文中包含If-MatchIf-Modified-SinceIf-None-MatchIf-RangeIf-UnModified-Since中任一首部。
  • 307 Temporary Redirect
    临时重定向,与302有着相同的含义,尽管302禁止POST变换成GET,但实际使用时大家并不遵守。307会遵守浏览器标准,不会从POST变成GET

4XX 客户端错误

  • 400 Bad Request
    表示请求报文中存在语法错误。
  • 401 Unauthorized
    表示发送的请求需要有通过HTTP认证的认证信息。
  • 403 Forbidden
    表示请求资源的访问被服务器拒绝了。
  • 404 Not Found
    服务器上无法找到请求的资源,也可以在服务器拒绝请求且不想说明理由时使用。

5XX 服务器错误

  • 500 Internal Server Error
    表明服务器端在执行请求时发生了错误,也可能是Web应用存在的bug或某些临时的故障。
  • 503 Service Unavailable
    表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

CSS定位(position属性)

元素的定位类型:固定定位、相对定位以及绝对定位。

position属性的初始值是static,如果把它改成其他值,我们就说元素被定位了,如果使用了静态定位,那么就说它未被定位。

布局方法(flex等)是用各种操作来控制文档流的行为,定位则不同:它将元素彻底从文档流中移走,它允许你将元素放在屏幕的任意位置,还可以将一个元素放在另一个元素的前面或后面,彼此重叠。

固定定位

固定定位让元素相对视口定位,给一个元素设置position: fixed 就能将元素放在视口的任意位置,这需要搭配四种属性一起使用:toprightbottomleft。比如,top: 3em表示元素的上边缘距离视口顶部3em

设置这四个值还隐式地定义了元素的宽高。比如指定left: 2em; right: 2em 表示元素的左边缘距离视口左边2em,右边缘距离视口2em。因此元素的宽度等于视口总宽度减去4em

绝对定位

绝对定位(position: absolute)是相对最近的祖先定位元素,属性toprightbottomleft决定了元素的边缘在包含块里的位置。通常情况下,包含块是元素的父元素。如果父元素未被定位,那么浏览器会沿着DOM树往下找它的祖父、曾祖父,直到找到一个定位元素,用它作为包含块。

如果祖先元素都没有定位,那么绝对定位的元素会基于初始包含块来定位,初始包含块和视口一样大,固定在网页顶部。

相对定位

相对定位依赖文档流。当第一次给元素加上position: relative的时候,通常看不到页面上有任何视觉变化。如果加上toprightbottomleft属性,元素就会从原来的位置移走,但不会改变周围任何元素的位置,其他元素还是围绕着被移走元素的初始位置,跟随着正常的文档流。

relative该关键字下,元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白)。

跟固定或者绝对定位不同的是,不能用toprightbottomleft改变相对定位元素的大小,这些值只能让元素在上下左右方向移动,可以用top或者bottom,但不能一起用(bottom会忽略,同理,right会被忽略)。

有时可以用这些属性调整相对元素的位置,但这只是相对定位的一个冷门用法,更常用的是使用position: relative给它里面的绝对定位元素创建一个包含块,比如创建一个下拉菜单。

渲染过程和层叠顺序

通常情况下(使用定位前),元素在HTML里出现的顺序决定了绘制的顺序。

定位元素时,浏览器会先绘制所有非定位的元素,然后绘制定位元素。默认情况下,所有定位元素会出现在非定位元素里面。

用z-index控制层叠顺序

z表示的是笛卡尔x-y-z坐标系中的深度方向,拥有较高的z-index的元素出现在拥有较低z-index的元素前面,拥有负数z-index的元素出现在静态元素后面。

z-index只在定位元素上生效,不能用它控制静态元素,给一个定位元素加上z-index可以创建层叠上下文,层叠上下文负责决定哪些元素出现在另一些元素前面。

所有层叠上下文内的元素会按照以下顺序,从后往前叠放:

  • 层叠上下文的根
  • z-index为负的定位元素(及其子元素)
  • 非定位元素
  • z-indexauto的定位元素(及其子元素)
  • z-index为正的定位元素(及其子元素)

粘性定位

浏览器提供了一种新的定位类型: 粘性定位(sticky positioning),它是相对定位和固定定位的结合体:正常情况下,元素会随着页面滚动,当到达屏幕的特定位置时,如果用户继续滚动,他就会锁定在这个位置。常见用例是侧边栏导航。

CSS响应式布局

em和rem

em是最常见的相对长度单位,适合基于特定的字号进行排版。在CSS中,1em等于当前元素的字号,其准确值取决于作用的元素。

.padded {
    font-size: 16px;
    padding: 1em; //浏览器将其乘以字号,最终渲染为16px
}

浏览器会根据相对单位的值计算出绝对值,称作计算值。当设置paddingheightwidthborder-radius等属性时,使用em会很方便。这是当元素继承了不相同的字号,这些属性会跟着元素均匀的缩放。

使用em定义字号

前面说到,当前元素的字号决定了em,如果声明font-size: 1.2em,一个字号怎么等于自己的1.2倍?——实际上,font-size是根据继承的字号来计算的。

// html
<body>
    Body text
    <p class="text">paragraph text</p>
</body>
// css
body {
    font-size: 16px;
}
.text {
    font-size: 1.2em; // 计算结果为元素继承的字号的1.2倍
}

使用rem设置字号

在文档中,根节点是所有其他元素的祖先节点。根节点有一个伪类选择类(:root),可以用来选中自己。

remroot em的缩写,rem是相对于根元素的单位

vh、vw、vmin和vmax

em和rem都是相对于font-size定义的,但CSS中不只有这一种相对单位,还有相对于浏览器视口定义长度的视口的相对单位。
视口:浏览器中窗口里网页可见部分的边框区域,不包括地址栏、工具栏和状态栏。

  1. vh:视口高度的1/100
  2. vw:视口宽度的1/100
  3. vmin:视口宽、高中较小一方的1/100(IE9中叫vm,而不是vmin
  4. vmax:视口宽、高中较大一方的1/100

比如,50vw等于视口宽度的一半。

// 定义正方形
.square {
   width: 90vmin;
   height: 90vmin;
}

使用vw定义字号——如果给一个元素加上font-size: 2vw, 在一个1200px的显示器上,计算值为24px(1200*2%)。这么做的好处在于元素能够在两种大小之间平滑的过渡,不会在某个断点突然改变。当视口大小改变时,元素会逐渐过渡。

使用calc()

calc()函数内可以对两个及其以上的值进行基本运算(加减乘除),加号和减号两边必须有空白。

:root {
    font-size: calc(0.5em + 1vw);
}

0.5em保证了最小字号,1vw确保了字体随着视口缩放。

媒体查询 @media

我们可以根据屏幕尺寸,用媒体查询改变根元素的字号

:root {
    font-size: 0.75em; // 作用到所有屏幕上
}

@media (min-width: 800px) {
    :root {
        font-size: 0.875em; // 仅作用于宽度800px及以上的屏幕,覆盖以前的值
    }
}

@media (min-width: 1200px) {
    :root {
        font-size: 0.875em; // 仅作用于宽度1200px及以上的屏幕,覆盖前面两个的值
    }
}

JS 相等

等于和不等于

ECMAScript的等于操作符使用两个等于号(==),不等于则使用(!=),这两个操作符都会先进行类型转换(强制类型转换)再确定操作数是否相等。

类型转换规则:

  1. 任一操作数为布尔值,先转换为数值再进行比较。false转换为0,true转换为1
  2. 一个操作数为字符串,另一个为数值,则将字符串转换为数值,再进行比较
  3. 一个操作数为对象,另一个不是,则调用对象的valueOf()方法取得原始值,再按照前面的规则进行比较

进行比较时遵循规则:

  1. nullundefined相等
  2. nullundefined不能转换为其他类型的值进行比较
  3. 任一操作数为NaN,相等操作符返回false,不等操作符返回true,因此按照规则,NaN不等于NaN
  4. 两个操作符都是对象,则比较他们是不是同一个对象,如果两个操作符指向同一个对象,则相等操作符返回true

全等与不全等

全等与不全等操作符在比较相等时不转换操作数。

null == undefined; // true
null === undefined; // false

示例:

内容相等的对象不相等, 不是同一个对象
两个不同对象的方法相等,其方法指向同一个对象,ECMAScript中函数是对象
相当于sayName每次都初始化一个新的对象,所以不相等
与上个例子等价