0%

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

在 JS 中另一个基本概念是函数, 它允许你在一个代码块中存储一段用于处理单任务的代码,然后在任何你需要的时候用一个简短的命令来调用,而不是把相同的代码写很多次。在本文中,我们将探索函数的基本概念,如基本语法、如何定义和调用、范围和参数。

函数的引入

现有有一个大于等于 2 的正整数 a ,现在我们需要判断这个数字是不是质数。质数是只有 1 和它自己两个因数的数。对于所有的大于 2 的数,我们需要枚举那些大于 2 小于等于根号 a 的所有数字,一一确定它们是不是 a 的因数,如果都找不到那么它肯定是质数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let a = 114514; // 很明显这是个合数
let isPrime = false; // 一个 boolen ,记录是否为质数

if (a === 2) {
isPrime = true;
} else {
isPrime = true; // 先暂时赋值,如果后面没被覆盖肯定是质数
for (let i = 2; i <= Math.sqrt(a); i++) {
if (a % i === 0) {
isPrime = false;
}
}
}

if (isPrime) {
console.log("是质数。");
} else {
console.log("不是质数。");
}

看起来很合理,但是如果有三个数,分别是 a, b, c。或者有三十个数,你是不是要把上面那个 if else 和循环都复制一下?这么做的话太麻烦了。所以我们有更好的方法:函数 (function) 。这就是上面写成函数的例子:

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
function judgePrime(a) {
let isPrime = false;
if (a === 2) {
isPrime = true;
} else {
isPrime = true; // 先暂时赋值,如果后面没被覆盖肯定是质数
for (let i = 2; i <= Math.sqrt(a); i++) {
if (a % i === 0) {
isPrime = false;
}
}
}
return isPrime;
}

let a = 114514, b = 17;

if (judgePrime(a)) {
console.log(a + " 是质数。");
} else {
console.log(a + " 不是质数。");
}

if (judgePrime(b)) {
console.log(b + " 是质数。");
} else {
console.log(b + " 不是质数。");
}

我们可以看出函数的结构:

1
2
3
4
function 函数名(参数) {
// 主体代码块
return;
}

一个函数定义(也称为函数声明,或函数语句)由一系列的 function 关键字组成,依次为:

  • function 关键字。
  • 函数的名称。
  • 函数参数列表,包围在括号中并由逗号分隔。
  • 定义函数的 JavaScript 语句,用大括号{}括起来。

比如我们从定义一个简单的乘方运算函数开始,这个函数可以得到给定数字的平方:

1
2
3
function square(number) {
return number * number;
};

函数 square 使用了一个参数,叫作 number 。这个函数只有一个语句,它说明该函数将函数的参数 number 自乘(就是平方)后返回。函数的 return 语句确定了函数的返回值。返回值就像普通的数值一样。比如下面这个代码:

1
2
3
4
function square(number) {
return number * number;
}
let x = square(3);

它执行了这样的操作:

  • 声明一个 square 函数。
  • 调用函数, 将 3 作为参数传给函数,此时函数的 number 变量的值变成了 3 。
  • 程序继续向下运行,计算 number * number 的值。
  • 将计算出来的值返回回来。
  • 然后 let x = square(3) 就相当于 let x = 9,x 就变成了 9 了。

可以非常清楚的非常清楚地看到,调用函数的方式是在函数名后加上一对括号,括号中包含参数。如果有多个参数就可以用逗号分隔:

1
2
3
4
function add(a, b) {
return a + b;
}
let x = add(1, 3); // x = 1 + 3

有些函数可以没有返回值,比如我有一个输出的函数:

1
2
3
4
5
function print(x) {
console.log("你的参数是 " + x);
}
print(12);
print('aaa');

这种情况下面,函数将会返回默认值 undefined

函数的特性

赋值

好像有点跑题,不过这个也是有关联的。

我们都知道那些基本数据是在赋值的时候,是可以被复制的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> let val1 = 23;
undefined
> let val2 = val1; // val2 是 val1 的一个拷贝
undefined
> val1 = 11; // 修改 val1
11
> val2; // val2 没变
23
> let str1 = "abc"; // 下面同理
undefined
> let str2 = str1;
undefined
> str2 = "aaa";
'aaa'
> str1;
'abc'
>

但是,不同于之前的那些基本数据,比如字符串和数字,数组和对象在使用 = 进行赋值的时候,其实没有进行复制,而是让两者指向同一个数组或者对象,就像是同一个数组或者对象有了两个名字,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> let arr1 = [12, 22];
undefined
> let arr2 = arr1; // 两个其实是同一个
undefined
> arr1;
[ 12, 22 ]
> arr2;
[ 12, 22 ]
> arr2[1] = 1; // 直接修改,结果两个变量都变了,说明是指向同一个数组的
1
> arr1;
[ 12, 1 ]
> arr2;
[ 12, 1 ]

类似的,对象也是一样。如果要复制的话,可以使用 Array.from() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> let arr1 = [12, 22];
undefined
> let arr2 = Array.from(arr1);
undefined
> arr1;
[ 12, 22 ]
> arr2;
[ 12, 22 ]
> arr2[1] = 1;
1
> arr1; // 现在这个值没有变了
[ 12, 22 ]
> arr2;
[ 12, 1 ]
>

参数

函数的参数传入之后,如果我们修改了参数,会发生什么?

直接跑一遍代码,清楚明了:

1
2
3
4
5
6
7
8
9
10
function test(x) {
console.log("In function test, x = " + x);
x = 3;
console.log("After you changed, x = " + x);
}

let a = 1;
console.log("Before called the function, a = " + a);
test(a);
console.log("After called the function, a = " + a);

输出:

1
2
3
4
Before called the function, a = 1
In function test, x = 1
After you changed, x = 3
After called the function, a = 1

是不是非常清楚,函数 test 改了一个寂寞,最后 a 的值还是没变,其实函数的参数传递,就类似对每个参数进行了一次赋值,函数里面的变量和外面的不是同一个变量。类似的,如果传入的是数组对象,情况就不太一样了:

1
2
3
4
5
6
7
8
9
10
function test(x) {
console.log("In function test, x = " + x.join(','));
x[0] = 3;
console.log("After you changed, x = " + x.join(','));
}

let a = [1, 3, 5];
console.log("Before called the function, a = " + a.join(','));
test(a);
console.log("After called the function, a = " + a.join(','));

输出:

1
2
3
4
Before called the function, a = 1,3,5
In function test, x = 1,3,5
After you changed, x = 3,3,5
After called the function, a = 3,3,5

定义域

函数里面的定义的值不会占用掉外面的名称,更不会占用掉其他函数里面定义的变量名。比如说这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let x = "x global";

function test1() {
let x = "x in test1()";
console.log(x);
}

function test2() {
let x = "x in test2()";
console.log(x);
}

test1();
test2();
console.log(x);

输出:

1
2
3
x in test1()
x in test2()
x global

函数和对象

我们之前谈过了对象,现在是进一步扩充它的时候了。

函数作为值

函数可以被作为一个值赋值给一个变量:

1
2
3
4
5
function add(a, b) {
return a + b;
}
let myadd = add;
let x = myadd(1, 3);

更加常用的是这么写:

1
2
3
let myadd = function (a, b) {
return a + b;
}

这种情况下面会创造一个匿名函数,并且将这个函数赋值给 myadd 变量,使它的值成为一个函数,并且可以使用函数的方式调用。

方法

那么,既然函数可以赋值给普通变量,那么也可以赋值给一个对象成员了。一个函数成员叫做方法。,比如我们举个例子:

1
2
3
4
5
6
7
let cat = {
name: "Ket",
say: function() {
console.log("My name is " + this.name);
}
}
cat.say();

你也许在我们的方法里注意到了一些奇怪的地方, this 到底是什么?

this 指的是正在运行这个函数的对象。你可能会很奇怪为什么不能直接使用 cat 来代指而是用这个奇怪的关键字。这是因为对象是可以被复制 (更准确地说是继承) 的,这可以构造一个新的对象。如果直接使用 cat 就会导致这个代码不能指向被复制后的对象。我们以后会在进阶的 JS 里面提到。下面举个简单的例子, Object.create() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let cat1 = {
name: "Ket",
say: function() {
console.log("My name is " + this.name);
}
}
let cat2 = Object.create(cat1);
cat2.name = "Bill";
cat1.say();
cat2.say();

let badCat1 = { // 一个不好的例子
name: "Steve",
say: function() {
console.log("My name is " + badCat1.name);
}
};
let badCat2 = Object.create(badCat1);
badCat2.name = "重干是老鼠";
badCat1.say();
badCat2.say();

输出:

1
2
3
4
My name is Ket
My name is Bill
My name is Steve
My name is Steve

可以看到最后那个函数并没有指向正确的对象,导致输出了错误的值。对象之间的继承是一件很复杂的事情。事实上最常用的是构造器函数。推荐阅读 https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects 当中列出的文章。

大家都是函数

那么,我们可以了解了,之前那些不明不白的什么 console.log() 什么 join() 什么 split() 什么 Math.floor(),通通都是函数或者方法,就和我们写出来的一样。

下一步

到了现在 JS 的基本语法你都已经掌握了,下面到了要拿这个新的工具去做点什么的时候了。

下一章: DOM!我们将会正式用 JS 去对网页做点事情。

练习

没啥好些的,接下来就直接是 DOM 了。我也累了,改天再更点啥吧。

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

有时候,我们需要完成一些重复的任务。比方说,现在我要按顺序输出 1 到 32 这些数字,要怎么做?曾经有个伟大的 GCVillager 手动写下了 32 行:

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
console.log(1);
console.log(2);
console.log(3);
console.log(4);
console.log(5);
console.log(6);
console.log(7);
console.log(8);
console.log(9);
console.log(10);
console.log(11);
console.log(12);
console.log(13);
console.log(14);
console.log(15);
console.log(16);
console.log(17);
console.log(18);
console.log(19);
console.log(20);
console.log(21);
console.log(22);
console.log(23);
console.log(24);
console.log(25);
console.log(26);
console.log(27);
console.log(28);
console.log(29);
console.log(30);
console.log(31);
console.log(32);

我们肯定不能学他,毕竟要输出 1145141919810 行怎么办?所以就有了今天的主角——循环结构。

for 循环

下面这个代码就可以解决开头的问题。

1
2
3
for (let i = 0; i < 32; i++) { // 还记得自增运算符吧
console.log(i+1); // 想想为什么加一
}

我们可以这样看到循环的结构是这样的

1
2
3
for (初始化语句; 退出条件; 更新语句) {
一些需要执行的代码;
}

可以看到这里的基本结构

  1. 关键字for,后跟一些括号。
  2. 在括号内,我们有三个项目,以分号分隔:
    • 初始化语句 - 通常我们会在这里声明一个数字变量,这个变量可以配合后面的更新语句来统计循环当前执行了的次数。它也有时被称为计数变量。
    • 退出条件 - 用来定义怎么退出循环。这个语句在每轮循环之前都会判断,如果满足条件就进入循环,如果不满足这个条件就退出循环。
    • 更新语句 - 在每轮循环结束的时候执行。它通常用于增加(或在某些情况下递减)计数器变量,使其更接近退出条件值。
  3. 包含一些代码的花括号 - 每次循环时都会运行花括号里面的代码。

那么我们看下下面的代码:

1
2
3
4
5
6
7
8
let cats = ['Bill', 'Jeff', 'Pete', 'Biggles', 'Jasmin'];
let info = 'My cats are called ';

for (let i = 0; i < cats.length; i++) {
info += cats[i] + ', ';
}

console.log(info);

这里每次都把 cats 里面的元素加到 info 的末尾,输出就是这样:

1
My cats are called Bill, Jeff, Pete, Biggles, Jasmin,
  • 迭代器i从0开始(let i = 0)。
  • 循环将会一直运行直到 i 不再小于 cats 数组的长度(cats.length)。
  • 在循环中,我们将当前的 cats[i] 以及逗号和空格拼接 (+=)到 info 变量的末尾。 所以:
    • 在第一次运行中,i 为 0,所以 cats[0] +',' 将被拼接到 info 上。
    • 在第二次运行中,i 为 1,所以 cats[1] +',' 将被拼接到 info 上。
    • 每次循环运行后,i 都会加上 1,然后进程将再次启动,直到退出条件。
  • 当等于 cats.length 时,循环将停止,浏览器将移动到循环下面的下一个代码位。

使用 break 退出循环

在循环当中,我们有时候需要在中途退出。还是上面的例子,如果我们需要找到 cats 数组里面的 Jeff 这个名字所在的下标,找到就退出,那可能就要写成这样

1
2
3
4
5
6
7
8
9
10
11
let cats = ['Bill', 'Jeff', 'Pete', 'Biggles', 'Jasmin'];
let cur;

for (let i = 0; i < cats.length; i++) {
if ('Jeff' === cats[i] ) {
cur = i;
break;
}
}

console.log(cur);

输出就是

1
2
3
i = 0
i = 1
1

其中循环执行到 i = 1 的时候,循环就因为满足了 if 的条件,然后被 break 语句退出了,没有执行到 i < cats.length 这个条件。

使用 continue 跳过迭代

continue语句以类似的方式工作,而不是完全跳出循环,而是跳过当前循环的下面部分,直接进入执行下一个循环。

比如说我们需要在遇到名字 Jeff 的时候不输出,就可以这么实现:

1
2
3
4
5
6
for (let i = 0; i < cats.length; i++) {
if ('Jeff' === cats[i] ) {
continue;
}
console.log(cats[i]);
}

输出

1
2
3
4
Bill
Pete
Biggles
Jasmin

这里在名称是 Jeff 的时候,continue 语句被执行,导致了后面的语句跳过去了,所以只有 Jeff 没有输出。

while 和 do while 循环

除了 for 循环之外, JS 还有两种循环:while 和 do while 循环。while 循环只有在入口处包含一个条件判断语句,如果满足就会进入循环。

比如之前的那个打印 cats 数组内容的程序,使用 while 循环我们就可以写成这个样子:

1
2
3
4
5
6
7
8
9
10
let cats = ['Bill', 'Jeff', 'Pete', 'Biggles', 'Jasmin'];
let info = 'My cats are called ';

let i = 0;// 初始化语句被放在外面了,成了一个普通的声明变量
while (i < cats.length) { // 这里只有一个条件判断
info += cats[i] + ', ';
i++; // 更新变成了普通代码
}

console.log(info);

还有一个 do while 循环和它类似,不过条件判断是放在结尾的,也就是循环节结束之后判断是否继续循环。

1
2
3
4
5
let i = 0;
do {
info += cats[i] + ', ';
i++;
} while (i < cats.length);

小结

这里我们了解了程序的循环结构,这是用来执行一些重复性工作的好办法。

编程练习

现在回到开头的问题:请你帮助 GCVillager 把这个程序分别用 for, while, do while 循环实现。

把一个斐波那契数组算到第 45 个,这里我们给出了前两个,你需要自己计算每个数,并且存到数组里面,最后输出整个数组。(下标从 0 开始!)

1
let fibonacci = [1, 1];

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

基础的数据结构,我们了解不少了。现在到了去用它们做点什么的时候了。

加速!这里我们要开始讲一个基础结构——分支结构。

开始之前

人类(以及其他的动物)无时无刻不在做决定,这些决定都影响着他们的生活,从小事(“我应该吃一片还是两片饼干”)到重要的大事(“我应该留在我的祖国,在我父亲的农场工作;还是应该去美国学习天体物理学”)。

条件语句结构允许我们来描述在 JS 中这样的选择,从不得不作出的选择(例如:“一片还是两片”)到产生的结果或这些选择(也许是“吃一片饼干”可能会“仍然感觉饿”,或者是“吃两片饼干”可能会“感觉饱了,但妈妈会因为我吃掉了所有的饼干而骂我”。)

cookie

布尔值

在正式开始分支结构之前,我们要讲下**布尔值(Boolean)**,这是进行条件判断的基础,分支和循环结构都要用到。

布尔值也是一种类型,和我们之前讲过的数字和字符串一样。不同的是它只有两个值: truefalse。分别对应 “真” 和 “假”。

1
2
3
4
5
> true;
true
> false;
false
>

一般情况下我们也不会这样用布尔值,而是采用一些比较运算和逻辑运算来得到,就像下面这样。

1
2
3
4
5
6
7
8
9
> 1 === 1; //1 和 1 相等,正确,是 true
true
> 1 === 0; //1 和 0 不相等
false
> true && true; // And 运算符
true
> true && false; // 必须两个都是 true 才是 true
false
>

比较运算

比较运算符是一个二元运算符,意思是左右两边分别需要一个运算数,就像之前我们讲过的四则运算一样。不过比较运算的结果不是普通的数字,而是一个布尔值,即 truefalse

1
2
3
4
5
6
7
> 1 + 2; // 四则运算,结果是数字
3
> 1 < 2; // 比较运算的结果是布尔值
true
> 1 > 2; // 1 小于 2 是假命题,所以 false
false
>

通过这里,我们可以很明显的看出,比较运算有这两个特点:

  • 如果表达式成立,那么值为 true
  • 如果表达式不成立,那么值为 false

和数学类似,我们有大于,小于,等于这些比较运算符。不同的是,我们的大于等于和小于等于是 >=<=,因为键盘打不出来数学里那个。而等于一般使用 ===,这样不会和赋值 = 混淆。常用的比较运算符有这些:

比较运算符
运算符 描述
等于 Equal (==)

如果两边操作数相等时返回true。

不等于 Not equal (!=) 如果两边操作数不相等时返回true
全等 Strict equal (===) 两边操作数相等且类型相同时返回true。
不全等 Strict not equal (!==) 两边操作数不相等或类型不同时返回true。
大于 Greater than (>) 左边的操作数大于右边的操作数返回true
大于等于 Greater than or equal (>=) 左边的操作数大于或等于右边的操作数返回true
小于 Less than (<) 左边的操作数小于右边的操作数返回true
小于等于 Less than or equal (<=) 左边的操作数小于或等于右边的操作数返回true

下面是一些示例,可以看到它们和预期的结果是符合的:

1
2
3
4
5
6
7
8
9
10
11
> 1 + 2 >= 3;
true
> 569 < 123;
false
> 123 === 123;
true
> 2 * 3 === 3 + 3;
true
> 2 * 3 !== 3 + 3;
false
>

字符串也可以使用比较运算,它们之间的比较使用的是字典序,由于日常应用的时候基本只需要判断字符串之间是否相等,所以这里不详细提及字典序大小的比较。

1
2
3
4
5
6
7
8
9
> "aaa" === 'aaa'; // 相等
true
> "aaa" !== "bbb"; // 不相等
true
> 'aaa' > 'bbb'; // 字典序
false
> 'aaa' <= 'bbb';
true
>

你可能会注意到,上面我给出的示例只用了全等号(===)和不全等号(!==),而没有使用普通的等于和不等于。事实上我们提倡尽可能使用三个等号组成的全等号,因为它可以避免 JS 过于宽松的自动类型转换带来的一些容易造成错误的问题。看看下面的代码:

1
2
3
4
5
6
7
8
> "0" == 0; // 自动类型转换
true
> "0" === 0; // 禁止了自动类型转换
false
> 123 != "123";
false
> 123 !== "123";
true

这里等于号和不等号遇到了一些问题:它把一个字符串和一个数字进行了转换,然后认为它们是相等的。这可能会导致你的程序出现一些难以排查的错误,所以尽可能避免它们。

逻辑运算

在进行比较运算的时候,我们可能会需要将多个条件连起来,我们假设有这三个变量需要判断:

  • num1 大于 0 ,小于 100 。
  • num2 可以被 2 整除或者可以被 3 整除。
  • num3 既不被 5 整除,又不被 4 整除。

这就需要使用逻辑运算,将多个表达式组合起来,然后得到想要的结果。逻辑运算符有三种:

运算符 描述 示例
与 ( && ) 当两边表达式值都为 true 时为 true,否则为 false true && true === true
true && false === false
或 ( || ) 当两边表达式值有一个为 true 时为 true,否则为 false true || false === true
false || flase === false
非 ( ! ) 当后面的值是 true 是为 false,反之为 true !true === false
!false === true

因此上面我们提到的例子可以这么写:

1
2
3
num1 > 0 && num1 < 100;
num2 % 2 === 0 || num2 % 3 === 0;
!(num3 % 5 === 0 || num3 % 4 === 0);

前面两个应该比较好理解,第三个为什么要带个括号呢?答案是运算符优先级问题。括号的作用是让括号里面的东西先算,这个学过数学都知道,放在这里也是一样的。如果我们去掉这个括号,就相当于下面这样:

1
2
!num3 % 5 === 0 || num3 % 4 === 0);
(!num3 % 5 === 0) || num3 % 4 === 0;

可以看到是左边的那个非运算符先算,再到后边的或运算符,就出错了。运算符优先级是这样:先算 !,再算 && ,最后算 ||,同级别从左到右。如果写程序的时候个别优先级不了解,那就多打几个括号,最多丑点。

if 语句

前面讲了这么多逻辑,现在有请我们的主角出场吧:就是 if 语句 !有了它,你就可以在程序里面根据不同的条件,去做出一系列的决定。以下是它的伪代码表示:

1
2
3
4
5
6
7
if (一个表达式或者值) {
如果这个表达式结果是 true ,那么执行这里的内容;
可以有很多句;
} else {
如果值是 false ,那么执行这里的内容;
也可以有很多句;
}

关键字 if,并且后面跟随括号。
要测试的条件,放到括号里(通常是“这个值大于另一个值吗”或者“这个值存在吗”)。这个条件会利用比较运算符(我们会在最后的模块中讨论)进行比较,并且返回 true 或者 false 。
一组花括号,在里面我们有一些代码——可以是任何我们喜欢的代码,并且只会在条件语句返回 true 的时候运行。一般情况下前一个大括号和 if 在同一行,后一个单独一行,这样比较美观,也可以避免 JS 自动加入分号的机制造成的问题。
关键字 else 。
另一组花括号,在里面我们有一些代码——可以是任何我们喜欢的代码,并且当条件语句返回值不是 true 的话,它才会运行。

这段代码真的非常易懂——它说“如果(if)条件(condition)返回true,运行代码A,否则(else)运行代码B”

注意:你不一定需要else和第二个花括号——下面的代码也是符合语法规则的:

1
2
3
if (一个表达式) {
如果这个表达式结果是 true ,那么执行这里的内容;
}

多个条件代码可以使用 else if,你可以用它来进行多个条件的判断。

1
2
3
4
5
6
7
8
9
if (一个表达式) {
如果这个表达式结果是 true ,那么执行这里的内容;
} else if (一个表达式) {
如果前一个表达式是 false ,这个表达式结果是 true ,那么执行这里的内容;
} else if (一个表达式) {
如果前面的表达式都是 false ,这个表达式结果是 true ,那么执行这里的内容;
} else {
如果前面都是 false ,那么执行这里的内容;
}

if 语句也可以嵌套:

1
2
3
4
5
6
7
if (一个表达式) {
一些语句;
if (一个表达式){
一些语句;
}
一些语句;
}

有时候你可能会看到 if…else 语句没有写花括号,像下面的速记风格:

1
2
if (一个表达式) 一行代码;
else 一行代码;

这是完全有效的代码,但不建议这样使用——因为如果有花括号进行代码切割的话,整体代码被切割为多行代码,更易读和易用。

switch 语句

在需要多重分支时可以使用基于一个数字或字符串的 switch 语句:

1
2
3
4
5
6
7
8
9
10
11
let x = 1;
switch (x) {
case 0:
console.log(0);
break;
case 1:
console.log(1);
break;
default:
console.log("default");
}

因为 switch 语句本质是跳到指定的标签开始执行,所以每个 case 结尾需要加上 break 语句用来退出,否则它会一直按顺序执行,运行到后面的代码,比如这样。

1
2
3
4
5
6
7
8
9
let x = 1;
switch (x) {
case 0:
console.log(0);
case 1:
console.log(1);
default:
console.log("default");
}

输出就变成了

1
2
1
default

一些例子

现在已经到了比较复杂的程序,所以我们就不再直接使用交互式解释器来运行我们的代码了。接下来的所有例子都会是一个单独的 JS 文件,你可以加到 HTML 里面通过浏览器运行,然后在控制台查看输出。

假设有一个年份,变量名叫做 year,现在要你判断它是否是闰年。

1582年以来的置闰规则:

  • 普通闰年:公历年份是4的倍数,且不是100的倍数的,为闰年(如2004年、2020年等就是闰年)。
  • 世纪闰年:公历年份是整百数的,必须是400的倍数才是闰年(如1900年不是闰年,2000年是闰年)。

写成程序就是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 实际情况需要配合输入,这个以后在讲 DOM 的时候会了解
let year = 2021;

// 先判断最特殊的
if (year % 4 !== 0) {
console.log("不是闰年");
} else if (year % 400 === 0) {
console.log("是闰年");
} else if (year % 100 === 0) {
console.log("不是闰年")
} else {
// 现在只能是被 4 整除的了
console.log("是闰年");
}

当然你也可以按照别的顺序进行判断。就像这个例子。只要答案是对的就可以。

1
2
3
4
5
6
7
8
9
if (year % 4 === 0) {
if (year % 100 === 0 && year % 400 !== 0) {
console.log("不是闰年");
} else {
console.log("是闰年");
}
} else {
console.log("不是闰年");
}

给定一个数组,然后从小到大排序数组里面的数字并输出,用空格分隔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let myArray = [3, 1, 2];

if (myArray[0] > myArray[1]) {
let temp = myArray[0];
myArray[0] = myArray[1];
myArray[1] = temp;
}
if (myArray[1] > myArray[2]) {
let temp = myArray[1];
myArray[1] = myArray[2];
myArray[2] = temp;
}
if (myArray[0] > myArray[2]) {
let temp = myArray[0];
myArray[0] = myArray[2];
myArray[2] = temp;
}
// 最后还要比较一下 (自己想想为什么)
if (myArray[0] > myArray[1]) {
let temp = myArray[0];
myArray[0] = myArray[1];
myArray[1] = temp;
}
console.log(myArray);

这里写成这样是为了演示分支,下章循环结构我们会有更简便的方法。实际上 JS 的数组还有一个自带的方法 .sort(),可以从小到大排序数组里面的内容。

1
2
3
let myArray = [3, 1, 2];
myArray.sort();
console.log(myArray);

码风

貌似我们之前的 if 语句都是这样的:

1
2
3
4
5
6
7
if () {
//do something
} else if () {
//do something
} else {
//do something
}

其实 if 还可以写而成这样

1
2
3
4
5
6
7
8
9
10
11
12
if ()
{
//do something
}
else if ()
{
//do something
}
else
{
//do something
}

我们可以看到这里的开头大括号放在了下一行。其实其他的语言有很多也是这么写的。但是 JS 里面不推荐这么写,因为 JS 有个自动插入分号的机制,这本来是为了你万一忘记打分号也能正常运行程序用的,但是有时候会带来一点不愉快的事情:

1
2
3
4
return 
{
statues : true
};

看起来你是想要返回一个对象,然而这种机制导致它实际上是这样的意思:

1
2
3
4
return; // BAD!!!
{
statues : true;
};

你可以看到它直接返回了,而后面那个对象就没有了。所以还是要这么写:

1
2
3
return {
statues : true
};

为了统一风格,我们就把所有的开头大括号不换行了。

练习

还记得 Tux 么?之前它让你写了一个计算 BMI 的程序,现在它要你去把这个程序改一下,判断 Tux 的 BMI 是不是正常的。

下面是程序的开头,你要用给出的体重 (变量 weight ) 和身高 (变量 height ) 来计算 BMI,同时判断 BMI 水平,输出“偏瘦”(小于18.5) “正常” (在 18.5-23.9 之间)和 “偏胖” (大于 23.9)。(BMI=体重/身高^2)

1
2
let weight = 82;
let height = 1.61;

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

我们上次讲了变量。这是一种用来存单个值的容器。但是如果我们要存多个值呢?比如我们现在需要统计一个班级的人名,或者统计每天的气温,这显然就不能用反复声明大量的变量进行存储。所以我们就要引出本章的主角:数组和对象。

数组

数组是一系列按顺序排列的值的集合。它可以一下子存下一系列的值,就相当于一些按顺序排列的变量。比如我们现在要去超市买东西,依次买了很多物品,并且记录了它们的价格。

数组包裹在一对中括号内 ( [] ) ,里面的值按顺序排列,并且使用英文逗号进行分隔 ( , ) 。可以通过在数组名后面加上一个 ( [] ) ,然后在括号内使用通过从零开始的下标来找到对应的值。下面我们给出了一系列的例子。

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
> let shopping = ['bread', 'milk', 'cheese', 'hummus', 'noodles'];
undefined
> shopping; // 打印 shopping 数组
[ 'bread', 'milk', 'cheese', 'hummus', 'noodles' ]
> let price = [ 5.00, 3.50, 12.50, 15.00, 7.50];
undefined
> price; // 打印 price 数组
[ 5, 3.5, 12.5, 15, 7.5 ]
> shopping[0]; // 使用下标进行查找,从 0 开始到比长度小一的值(这里是 4)结束
'bread'
> price[0]; // price 的第一个元素
5
> price[100]; // 越界之后值会变成 undefined
undefined
> shopping[3];
'hummus'
> price[2] = 114; // 可以给对应的下标赋值,这里是 2,就是第三个
114
> price; // 可以看到第三个发生变化了
[ 5, 3.5, 114, 15, 7.5 ]
> shopping[2] = 1234; // 还可以给原来的赋值不同类型,就和普通变量一样
1234
> shopping;
[ 'bread', 'milk', 1234, 'hummus', 'noodles' ]
> let anEmptyArray = []; // 可以先声明一个空的数组,以后再往里面加东西
undefined
> anEmptyArray; // 现在它是空的
[]
>

数组里的值甚至可以是另一个数组,这叫做数组嵌套,下面演示了嵌套数组的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> let random = ['tree', 795, [0, 1, 2]]; // 声明一个嵌套的数组
undefined
> random[2][1]; // 访问嵌套数组,第一个[]导航到数组,第二个继续导航
1
> random[0] = ['acacia', 'birch', 'spruce', 'oak']; // 赋值为一个数组
[ 'acacia', 'birch', 'spruce', 'oak' ]
> random; // 现在里面有两个嵌套数组了
[ [ 'acacia', 'birch', 'spruce', 'oak' ], 795, [ 0, 1, 2 ] ]
> random[0][2]; // 同样可以访问
'spruce'
> random[0][3] = ['oak leaves', 'oak log']; // 数组套了三层了
[ 'oak leaves', 'oak log' ]
> random[0][3][1]; // 访问到第三层的数组
'oak log'
>

数组方法

length

最常用的数组方法,可能就是获取数组长度了。数组长度可以通过 length 方法来获取,就是在数组名之后加上一个 .length,例如 sequence.length 就可以了解我们下面定义的数组的长度。

1
2
3
4
5
> let sequence = [1, 1, 2, 3, 5, 8, 13]; // 声明一个数组
undefined
> sequence.length; // 打印数组的长度
7
>

length 属性最常用的时候,就是循环遍历一个数组中的所有项目。比如说下面的这个代码:

1
2
3
4
let sequence = [1, 1, 2, 3, 5, 8, 13];
for (let i = 0; i < sequence.length; i = i + 1) {
console.log(sequence[i]);
}

我们以后会详细了解循环,但是这里先稍微提下这里主要干的事情:

  • 在数组中的元素编号 0 开始循环。
  • 在元素编号等于数组长度的时候停止循环。 这适用于任何长度的数组,但在这种情况下,它将在编号 7 的时候终止循环(还记得数组的编号是从 0 开始的吗?0 到 6 就是 7)。
  • 对于每个元素,使用 console.log() 将其打印到浏览器控制台。

字符串和数组之间的转换

有时候你会需要把一个有规律的字符串转化成数组来处理数据,比方说这样的字符串

1
let myData = 'Manchester,London,Liverpool,Birmingham,Leeds,Carlisle';

显然,这里是用 , 来分割的一系列单词。如果我们要把它转成一个数组,就可以用 split() 方法,它会对字符串进行处理,然后返回一个数组。方法的用法是在字符串之后加上一个 .split(),在括号内指定分隔符(默认是空格)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> let myData = 'Manchester,London,Liverpool,Birmingham,Leeds,Carlisle';
undefined
> let myArray = myData.split(','); // 转换成数组
undefined
> myArray; // 现在已经转成数组了
[
'Manchester',
'London',
'Liverpool',
'Birmingham',
'Leeds',
'Carlisle'
]
> myArray[2]; // 第三个
'Liverpool'
> myArray[myArray.length - 1]; // 最后一个
'Carlisle'
>

如果要把数组转成对应的字符串,可以用相反的方法 .join() ,用法和前面的相同。(假设我们这里是紧跟着上面执行的)

1
2
3
4
5
> let myNewString = myArray.join(',');
undefined
> myNewString;
'Manchester,London,Liverpool,Birmingham,Leeds,Carlisle'
>

添加和删除数组项

这里我们可以使用 push()pop() 方法在数组尾部进行添加和删除元素。使用 push() 方法之后会返回新数组的长度,使用 pop() 方法之后会返回被删除的那个值。也许你可以用变量存下它们。

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
> let myArray = ['Manchester', 'London', 'Liverpool', 'Birmingham', 'Leeds', 'Carlisle'];
undefined
> myArray.push('Cardiff'); // 添加一个元素
7
> myArray;
[
'Manchester',
'London',
'Liverpool',
'Birmingham',
'Leeds',
'Carlisle',
'Cardiff'
]
> myArray.push('Bradford', 'Brighton'); // 添加两个元素
9
> myArray;
[
'Manchester', 'London',
'Liverpool', 'Birmingham',
'Leeds', 'Carlisle',
'Cardiff', 'Bradford',
'Brighton'
]
> let newLength = myArray.push('Bristol'); // 存下新的长度
undefined
> myArray;
[
'Manchester', 'London',
'Liverpool', 'Birmingham',
'Leeds', 'Carlisle',
'Cardiff', 'Bradford',
'Brighton', 'Bristol'
]
> newLength; // 现在的新长度
10
> myArray.pop(); // 删除最后一个元素
'Bristol'
> let removedItem = myArray.pop(); // 存下删掉的元素
undefined
> myArray;
[
'Manchester',
'London',
'Liverpool',
'Birmingham',
'Leeds',
'Carlisle',
'Cardiff',
'Bradford'
]
> removedItem; // 删掉的元素
'Brighton'
>

采取这个方法我们可以模拟一个栈操作。栈是一种 LIFO(先进先出)的数据结构。通俗的讲起来就像是叠盘子,后面的叠在上面,拿的时候就拿走最上面的。最后叠的最先拿出来,就是一个先进先出了。以后我们会单独开章节讲数据结构,当作拓展的内容。

对象

我们刚刚提到了数组,这是一种按顺序存值的方法。它用途非常广泛,但是因为只能按下标找值,在有些时候可能会比较难用。比如现在我们要存一个人的相关信息,比如名字,性别,年龄,简介这些,如果用数组就会遇到一个难题:必须给这些信息确定一个顺序,规定第一个是名字,第二个是性别等等。这样写会带来理解上面的困难(一堆乱七八糟的数字可不是很好阅读的)。如果使用一系列的单一变量去存,会搞出一大堆变量。那么有没有更好的方法呢?答案是使用对象。

对象 (Object) 是包含一系列相关数据和方法的集合(通常由一些变量和函数组成,我们称之为对象里面的属性和方法)。由于我们目前还没有讲函数,我们暂时先讲对象的数值相关的东西,以后在函数部分再提对象的方法。

对象使用一对大括号 ( {} ) 来表示,里面包含着很多用逗号分开的成员,每一个成员都拥有一个名字(下面的 name, age 这些),和一个(如[“Linus”, “Torvalds”], 52 都是值)。每一个名字和值(Name and Value)之间由冒号(:)分隔,然后就可以通过成员的名字来查找对应的值。下面是一个例子。为了看得更清楚,我们在其中插入了换行,一行一个成员。我们推荐在以后编程实践当中适当使用换行,因为可以让代码更加整洁。

1
2
3
4
5
6
let person = {
name: ["Linus", "Torvalds"],
age: 52,
gender: 'male',
saying: 'NVIDIA, **** YOU!'
};

可以使用点表示法来查找对应的值,就是在对象标识符之后加上一个点,在点后面加上键名称就可以了,比如 person.age 就可以对应 personage 对应的值,即 52

还有另一种表示法是括号表示法,用法类似数组,是在一个中括号 [] 内加上键名称来对应的,例如 person["saying"] 就可以对应到那个 saying

我们建议使用点表示法,因为更加简洁,方便打字。

下面是在 NodeJS 里面运行的结果,换行之后那三个点是 node 加上的,表示上一句话还没结束。这不是代码的一部分,所以自己写代码的时候不要加上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> let person = {
... name: ["Linus", "Torvalds"],
... age: 52,
... gender: 'male',
... saying: 'NVIDIA, **** YOU!'
... };
undefined
> person;
{
name: [ 'Linus', 'Torvalds' ],
age: 52,
gender: 'male',
saying: 'NVIDIA, **** YOU!'
}
> person.age; // 点表示法
52
> person['saying']; // 括号表示法
'NVIDIA, **** YOU!'
>

子命名空间

可以用一个对象来做另一个对象成员的值。比如我们现在可以把 personname 从一个数组换成一个对象。就像是下面这样。

1
2
3
4
5
6
7
8
9
let person = {
name: {
firstName: 'Linus',
lastName: 'Torvalds'
},
age: 52,
gender: 'male',
saying: 'NVIDIA, **** YOU!'
};

这样就可以用点表示法进行多重的查找,比如 person.name.firstName 这样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> let person = {
... name: {
..... firstName: 'Linus',
..... lastName: 'Torvalds'
..... },
... age: 52,
... gender: 'male',
... saying: 'NVIDIA, **** YOU!'
... };
undefined
> person.name.firstName;
'Linus'
> person.name.lastName;
'Torvalds'
>

这样非常有用,因为你可以在对象里面嵌套另一个对象,从而有结构地保存一系列的数据,起到方便阅读和维护的作用。

设置对象成员

有时候我们需要对对象进行一系列的操作。比方说我们可能会修改一个成员,可能会加上一个还没有的成员,或者一个成员不要了我们可以删除它。这些都是可以做到的。

现在我们再创建一次上文中的 person 对象,然后对这个变量进行一系列操作:

1
2
3
4
5
6
let person = {
name: ["Linus", "Torvalds"],
age: 52,
gender: 'male',
saying: 'NVIDIA, **** YOU!'
};

现在你可以试着修改其中的成员:

1
2
3
4
5
6
7
8
9
10
> person.name = 'Tux'; // 设置名字
'Tux'
> person.age = 25; // 设置年龄
25
> person; // 现在已经变化了
{ name: 'Tux',
age: 25,
gender: 'male',
saying: 'NVIDIA, **** YOU!' }
>

你可以删除其中的成员

1
2
3
4
5
> delete person.saying;
true
> person; // 现在已经成功删除了
{ name: 'Tux', age: 25, gender: 'male' }
>

还可以直接添加原来没有的成员

1
2
3
4
5
> person.newValue = 123;
123
> person; // 可以看到直接加上了一个值
{ name: 'Tux', age: 25, gender: 'male', newValue: 123 }
>

方法

其实对象远远比我们现在讲的要复杂多了,比如对象的成员可以不只是一个普通的值,还可以是函数(事实上这个才是对象里面用的最多的)。关于函数的定义比较复杂,现在你可以认为是一组执行任务或计算值的语句。在对象里面作为的函数叫做这个对象的方法

下面我们看下这个例子,我们给它添加了一个用来在控制台进行输出的方法。

1
2
3
person.say = function (){
console.log("Hello, I am " + this.name + ".");
};

在 NodeJS 中运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> person.say = function (){
... console.log("Hello, I am " + this.name + ".");
... // 这里的 this 指的是当前的对象,相当于外面用 person.name
... };
[Function]
> person.say; // 这里不是调用函数,而是会指向函数本身
[Function]let cat = {
name: "Ket",
say: function() {
console.log("My name is " + this.name);
}
}
cat.say();
> person.say(); // 要加上括号才能调用,现在输出了内容
Hello, I am Tux.
undefined
>

可以看到我们让 person 对象输出了自己的名字。简单地说我们干了这几件事情:

  • 使用 function 关键字表明下面是一个函数
  • 在函数里定义了一条 console.log() 语句,向控制台输出。
  • 用 this 定位到自己本身,然后把自己的名字拼接到字符串内
  • 使用 person.say() 来进行输出。

所以我们之前遇到的很多东西 (例如本章讲的 push() pop() 或者刚刚用到的 console.log() ) 其实都是方法。有关于函数的内容我们会在以后详细地了解。

小结

我们了解了数组和对象。

数组是一种按顺序保存一系列值的数据结构,声明的时候用一对的中括号 [] 来表示,其中可以包含用逗号分隔的值。
数组的每个元素可以采用下标进行查找,下标范围从 0 开始到比长度小一的值结束,你可以用 length 来获取长度。
数组和字符串之间可以使用 split()join() 方法进行相互转换。
可以用 push() 在数组末尾加入值,也可以用 pop() 来删除末尾的值。

对象通过一对大括号 {} 来声明,里面用逗号分隔每一个成员。成员是由 名字: 值 的配对组成的。
对象的成员可以用点表示法查找,也可以使用括号表示法。
对象的成员就像一个普通的变量,可以进行修改。如果给一个原本没有的成员进行赋值,会添加一个新的成员。
对象的成员可以是函数,这也叫做方法。

练习

罗辑经常会忘记自己在谈的对象的名字,因此要你去用对象去记录她们的信息。现在他给你了一些信息,要你马上拿一个对象存起来。记住,只能用一个对象存。(名字都是书里抄的)

要求声明一个 girls 对象,将变量存到同名的成员内,然后使用 console.log() 输出对象。

1
2
let name1 = "张珊"
let name2 = "陈晶晶"

下面是一个数组,你需要把其中的第二项和第四项交换,然后输出数组。(注意下标从零开始)

1
let myArray = [ 1, 4, 3, 2, 5, 6, 7];

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

本文基于 CC-BY-SA 4.0 协议发布。

语句和注释

在开始变量之前我们讲一下语句。JS 的语句是以英文分号 ; 结尾的,而不是换行,因为同一行内可以用分号隔开多条语句。虽然 JS 对于一个没有分号直接换行的语句会自动补上分号,但是最好不要这么做。为了防止自己出现这些问题,你可以给自己的 VSCode 装上相关的 Lint 插件。

注释可以写在代码里面,解释器会自动忽略它们。你可以用这些说明你的程序干了什么。JS 里面有着两种注释,一种是用 /* */ 包裹的注释,一种是用 // 开头到一行结尾的注释。下面是两种注释的使用。后面我们会用到的。

1
2
3
4
5
6
7
8
/* 我是一行注释 */
// 我也是一行注释

/*
我可以跨行
*/
// 我只能
// 这么跨行

数据类型与基本运算

JS 里有七种基本数据类型,但是很多类型现在都用不到。所以我们就先讲下最常用的数字和字符串,以及一些基础的运算。

数字

在 JS 里面只有一个数字类型。不论这些数字是像30(也叫整数)这样,或者像2.456这样的小数(也叫做浮点数),在内部都是用浮点数来表示的。这个和别的编程语言(比如 python)不同。这意味着 11.0 是同一个数字(而其他语言就是两个类型的数字)。

数字可以进行四则运算,优先级和数学里一样,你也可以加上括号改变优先级,就像在用计算器。乘号使用星号(*)代替,除号使用斜杠(/)代替。运算符两边可以加上空格,这样排版会更美观一点(也许是)。

你可以打开浏览器的控制台运行你的 JS,也可以在本地安装 Nodejs ,使用 Node 来运行。我演示的时候使用的是 Node,因为可以直接在 VSCode 的控制台内输入 node 指令来打开。

下面是在 Node 中运行的结果,其中 > 表示这行是输入,输入紧跟着的下一行就是控制台的提示输出。注意实际的 JS 脚本在浏览器运行的时候是没有给每行加上一个输出的,你要用 console.log() 来进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> 1; // 1 就是 1
1
> -1; // -1 就是 -1
-1
> 1+1; // 就是一加一
2
> 1 + 1; // 加上空格之后运行结果也一样
2
> 1*2 + 2*3; // 运算符优先级和数学一样
8
> (1+2)*3; // 使用了括号
9
> 6 / 3; // 除法
2
> 1.1 + 1.5; // 小数加法
2.6
> 0.1 + 0.2; // 这是浮点数误差导致的
0.30000000000000004
>

最后一个 0.1 + 0.2 算出来结果是 0.30000000000000004 (15 个 0 ,不用数了),这是怎么回事?这不是 bug,是语言的特性,叫做浮点数误差,是因为计算机在转换小数到二进制的时候丢失精度导致的(可以看 https://0.30000000000000004.com/ 了解)。所以以后判断小数相等的时候可能不能直接用等号来进行。

除了四则运算之外,还有一种取余运算,使用百分号(%)。在两者都是正数的时候,可以取得前一个数字除以后一个数字的余数。如果出现负数可能就不太一样,不过一般不会遇到,所以就先略过。

1
2
3
4
5
6
7
> 5 % 2;
1
> 5 % 3;
2
> 25 % 5;
0
>

有时候我们需要舍弃小数部分,这可以用 Math 的一些方法来进行。其中 Math.round() 四舍五入,Math.floor() 向下取整, Math.ceil() 向上取整。

1
2
3
4
5
6
7
8
9
> Math.round("3.1415926")
3
> Math.round("3.999")
4
> Math.ceil("3.1415926")
4
> Math.floor("3.999")
3
>

数字的范围是有限制的,超出限制的就无法精确表示。JS 能够准确表示的整数范围在 -2^532^53 之间(不含两个端点),超过这个范围,就无法精确表示这个整数。

如果一个式子不能被计算,那么它的值就会变成 NaN ( Not a Number,不是一个数字,最典型的就是给 -1 开平方,或者尝试给一个不全是数字的字符串转成数字。

1
2
3
4
5
6
7
> Math.sqrt(-1); // 给 -1 开平方 
NaN
> parseInt("123"); // 正常的转换
123
> parseInt("Your life is which you loved"); //给字符转数字
NaN
>

字符串

另一个非常常用的就是字符串。顾名思义,这就是一串的字符。它可以用一对英文单引号('')或者一对英文双引号("")包裹一串字符来表示,比如 '我是一个字符串' "我是另一个字符串"

同样和别的语言不一样的是,JS 只有字符串类型,而没有单个字符类型。你可以用只有一个字符的字符串代替。

如果要在字符串里面包含特殊字符可以在字符前加上反斜杠( \ )进行转义。比方说要包含引号的时候,就可以用 \" 或者 \' 来表示引号本身而不是字符串结尾。

你可以使用加号 + 来对字符串进行拼接操作,这可以得到这些字符串首尾相连的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> "I am a str"; // 使用一对双引号
'I am a str'
> 'Another str'; // 一对单引号
'Another str'
> 'a'; // 单个字符的字符串
'a'
> '一个有引号\"的字符串'; // 转义
'一个有引号"的字符串'
> '也可以这样带上单引号:\'. '; // 也是转义
"也可以这样带上单引号:'. "
> "aa" + "bb"; // 拼接
'aabb'
> 'abc' + ' ' + 'def'; // 多个一起拼接
'abc def'
>

转换

字符串和数字之间可以相互转换。这些转换通过一些 JS 内置的函数进行。有些是显式进行的(比如调用一个函数),有些是隐式进行的(比如让字符串和数字相加)。

一般情况下,涉及数字转字符串的我们采取隐式的方式。字符串转数字的时候我们使用 parseFloat()parseInt() 函数来显式转换,用法是在括号内放一个字符串。parseFloat() 的结果如果有小数会带上小数部分, parseInt() 的结果会直接去掉小数部分(而不是四舍五入)有关函数更为具体的内容我们以后会探讨,这里可以简单看作是一些封装起来的代码,执行之后返回一个结果。

下面我们来演示这些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> "The answer is " + 42; // 一个拼接
'The answer is 42'
> 42 + " is the answer"; // 另一个拼接
'42 is the answer'
> "37" + 7; // 加法会把数字转成字符串
'377'
> "37" - 7; // 除了加法都会将字符串转成数字
30
> parseFloat("37") - 7; // parseFloat 的结果也一样
30
> parseFloat("123"); // 这里也是整数
123
> parseFloat("123.45"); // 小数部分也正常
123.45
> parseFloat("10e5"); // 还可以用科学计数法
1000000
> parseInt("123"); // parseInt 的整数转换
123
> parseInt("123.45"); // 现在去掉小数了
123
>

变量

变量本质上是值(例如数字或字符串)的容器,比如你可以用它去存一个数字 123,也可以存一个 Hello world! 字符串。

变量可以通过这三个关键字声明:

  • let: 这是在 ECMAScript 2015 里面添加的。我们建议如果没有支持旧浏览器的需求就使用它。
  • var: 这是旧的声明变量方式,现在也可以用,但是不推荐,因为 var 可以被重复声明,这会导致很多问题。
  • const: 这是常量,声明之后不能修改。

最好不要使用 var,因为它可以被重复声明,如果不小心重复声明了一个变量,会导致问题问题。同时用 var 声明的变量会发生变量提升,产生更多难以预料的问题。

变量名在 JS 里面又叫做标识符。标识符是有规则的,必须以字母、下划线(_)或者美元符号($)开头,而不能是数字开头;但是后续的字符也可以是数字(0-9)。同时 JS 支持 Unicode 字符集(也就是你可以声明一个中文变量,虽然没啥用)。JS 是大小写敏感的,比如 aAaa 就是两个变量。

除了上面的规则之外,你也不能使用 JS 的保留字给变量命名。保留字,即是组成JavaScript 的实际语法的单词。比方说什么 if, while, for 这些。一般变量命名是不会和保留字重复的。

我们建议使用大小写驼峰命名法,也就是包含多个单词的变量名称的第一个单词开头小写,其余单词开头大写。比如我要命名一个 “My first name” 来存名字,变量名就可以写成 myFirstName

变量声明之后,默认的值是 undefined 。你可以通过比较一个变量是否等于 undefined 来判断变量是否初始化(使用双等号 == 或三等号 === 来比较,例如 x === undefined; )。直接在控制台里面打一个变量名可以输出变量的值,如果调用没有声明的变量会弹出一个错误。

const 变量在声明之后可以重新赋值。赋值使用单个等号,格式为 变量名 = 新的值; 。新的值可以是单个值,比如 x = 1; 也可以是一个表达式 x = 1 + 2;, 或者 x = x + 1; (给 x 加上 1)。

声明和赋值可以写在一起,比如 let x = 1; 就可以声明一个变量,值为 1

我们现在可以这样试着声明变量,下面是在 Nodejs 上运行的结果,当然浏览器也一样。(语句一定要加上分号,虽然没有分号也能运行,但是最好不要养成坏习惯):

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
> let myFirstName; /*声明了一个 myFirstName*/
undefined
> myFirstName; // 显示 myFirstName 的值
undefined
> var myValue; // 声明了一个 myValue
undefined
> myFirstName = "Steve"; /*给 myFirstName 赋值了 "Steve" */
'Steve'
> myFirstName;
'Steve'
> myValue = 1 + 1; /*给 myValue 赋值了 1 + 1,值是 2 */
2
> myValue;
2
> myValue = "aaa"; /*重新给 myValue 赋值了一个字符串 */
'aaa'
> myValue; /*现在 myValue 是一个字符串*/
'aaa'
> let myFirstName;// 这里重复声明没有通过,而是报错了
Uncaught SyntaxError: Identifier 'myFirstName' has already been declared
> var myValue; /*这里重复声明,而且通过了*/
undefined
> myFirstName;
'Steve'
> myValue; /*还保留原来的值*/
1
> const myConst; /*声明 const 一定要先初始化!*/
const myConst;
^^^^^^^

Uncaught SyntaxError: Missing initializer in const declaration
> const myConst = 2; /*声明了一个 const 变量*/
undefined
> myConst; /*打印 myConst 的值*/
2
> myConst = 3; /*尝试给 myConst 重新赋值,不被允许*/
Uncaught TypeError: Assignment to constant variable.
>

你也可以现在就打开浏览器的控制台输入这些,来进行尝试。上机实践对学习编程非常重要,不上机很多问题在纸上看看是没有结果的。

由于 JS 的一些奇怪问题,我们建议不能使用下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
> bad1 = "This is bad"; /*没有加上关键字,自动变成全局变量 */
'This is bad'
> bad1;
'This is bad'
> bad2 === undefined; /*这是一个没有声明的变量的正常行为*/
Uncaught ReferenceError: bad2 is not defined
> bad2 === undefined; var bad2 = 0; /* 先使用后声明,发生变量提升*/
true
> bad2;
0
> bad3 = 0; let bad3; /*所以要用 let*/
Uncaught ReferenceError: Cannot access 'bad3' before initialization
>

var 声明的变量会发生变量提升,相当于把声明语句往上挪到作用域的开头了。这没什么用,反而会造成一些问题,所以你应该尽量只用 let 来声明。

变量赋值之后就可以用来进行一系列运算,就和普通的值一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> let x = 2; // 声明+初始化
undefined
> let y = 3;
undefined
> x + y; // 进行四则运算
5
> x - y;
-1
> x + 10;
12
> let str1 = 'AAA'; // 声明了两个字符串
undefined
> let str2 = 'BBB';
undefined
> str1 + str2; // 尝试一些拼接操作
'AAABBB'
> x + ' ' + str1;
'2 AAA'
> str1 + " is Str1";
'AAA is Str1'
>

输入和输出

不像其他语言,JS 最初是给浏览器设计的,直到现在这也是它的最主要用途,所以它和 DOM 深度绑定,信息几乎都是从 HTML 页面读取,输出也基本都是写在HTML 当中,甚至没有标准化的输入和输出。由于我们现在还没学过对象(Object),所以我们采用一些其他方式进行输入输出。

我们使用 console.log() 函数进行输出,这个函数会在控制台上留下一条信息。不仅可以给它一个字符串,也可以给它一个语句,比如 console.log(1 + 1); 输出 2。

对于输入,我们选择手动更改代码中的相关变量赋值来进行替代。这可能有点麻烦,不过在学了 DOM 之后就可以用更标准的方法进行。

最后——顺序结构

我们了解了 JS 里面的数字和字符串类型,以及它们的基本运算和转换关系。

我们还了解了变量,它是一种容器,可以用来存放变量。变量在赋值之后可以进行一系列的操作,就像普通的值一样。

现在,你可以写一些小程序来进行一些简单的运算了。比如一个最简单的 A+B Problem,输出 a+b 的结果:

1
2
3
4
let a = 1;
let b = 1;
let c = a + b;
console.log(c);

试试写出类似的 A-B,A*B,然后按照我们之前 Hello world 的方式加到浏览器里面运行。

这里的程序就是一个典型的顺序结构,代码从上到下一步步运行,最后得出结果。这是程序的三大基本结构之一,另外两个分别是分支循环,我们会在不久之后提到。了解三大基本结构之后,理论上你就可以写出一些真正能用的程序了。

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

JavaScript(以下简称 JS)是一种脚本,一门编程语言,它可以在网页上实现复杂的功能,网页展现给你的不再是简单的静态信息,而是实时的内容更新,交互式的地图,2D/3D 动画,滚动播放的视频等等。

它是标准 Web 技术蛋糕的第三层,其中 HTML 和 CSS 我们已经在其他部分进行了详细的讲解。

JS 的应用场合极其广泛,简单到幻灯片、照片库、浮动布局和响应按钮点击,复杂到游戏、2D/3D 动画、大型数据库驱动程序等等。这里我们讲的主要是浏览器里面的 JS 。当然配合 Nodejs 你也可以让 JS 成为一个应用程序。

JS 所做的,就是让网页“动”起来,让网页不再是一些静态的文本和样式,而拥有实际的功能(比如登录帐号,上传文件之类)。加上了这个之后,我们总算是可以做出一个具有完整的功能的网站了。

API

JS 的可以使用应用程序接口(Application Programming Interfaces(API)),它们可以用来操作很多的。API 是已经建立好的一套代码组件,可以让开发者实现原本很难甚至无法实现的程序。

就像现成的家具套件之于家居建设,用一些已经切好的木板组装一个书柜,显然比自己设计,寻找合适的木材,裁切至合适的尺寸和形状,找到正确尺寸的螺钉,再组装成书柜要简单得多。而且使用 API 还可以方便别人理解你的代码。

我们最常用的 API 就是 DOM(Document Object Model——文档对象模型),它用来对 HTML 进行一些读取和修改操作,这也是很多网站的动态效果的实现形式(不过能用 CSS 实现的还是尽量选择 CSS 比较好)。以后我们会做详细介绍。

当然你也可以使用其他的 API,比方说你可以调用 Canvas API 来绘制图形,或者调用地图的 API 来了解当前位置之类。我们的教程暂时不会涉及这些,你以后可以自己查阅它们的文档进行使用。

算法

编程语言里面,很重要的一点就是算法。算法相当于是下一系列的指令给计算机,让计算机执行得到结果并且输出。这和让人去做一些事情是一样的。比如导航指路的时候:

100 米后右转
直行 500 米
掉头
……

这里一系列的指令就可以看成是算法。只是为了让电脑看懂,我们使用的语言从中文变成了编程语言。

Hello world

按照所有编程语言教程的惯例,我们会实现一个输出 Hello world! 的程序,用来演示如何运行 JS 。

Web 控制台

运行 JS 代码最好的地方,当然是浏览器。

打开浏览器,在一个标签页下面打开控制台(通常是按下 F12 来打开),你会看到浏览器的调试窗口,转到 控制台(Console),就可以在这里直接运行一些简单的 JS 代码了(别问我为啥无痕)。

在这个控制台当中输入下面的内容,就可以输出一个 Hello world! 了。

1
console.log("Hello world!");

运行起来大概是这样。

下面那个 undefined 我们以后再解释。我们这个例子写成纯文字版本就是这样子( >>> 不是代码,只是用来表示这是个输入)。

1
2
3
>>> console.log("Hello world!");
Hello world!
>>>

导入你的 JS

一般情况下面,我们会在 HTML 里面链接 JS(你总没有见过啥网站要你打开控制台输入代码的吧)。我们使用 <script></script> 标签来在 HTML 的 <body> 当中最末尾的地方插入 JS ,这是因为 HTML 是从上到下解析的,如果需要操作的东西排在 JS 后面,就不能被正常解析。下面是一些方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Example</title>
</head>
<body>
<!-- 内部 JS -->
<script>
console.log("Hello, I am a JS CODE!");
console.log("Hello, I am the second line!");
</script>

<!-- 导入同目录下的 hello.js -->
<script src="hello.js"></script>

<!-- 导入其他的外链 JS -->
<script src="http://一个脚本的 URL"></script>
</body>
</html>

然后浏览器就会自动执行这些嵌入的 JS。

比如我们可以新建一个 hello.js 内容如下:

1
console.log("Hello world!");

然后我们在同目录新建 index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Example</title>
</head>
<body>
<p>js...</p>
<!-- 导入同目录下的 hello.js -->
<script src="hello.js"></script>
</body>
</html>

接着在浏览器里面打开,就可以在控制台里面看到 Hello world! 了。

运行原理概述

JS 是一种解释型语言。浏览器得到一段 JS 代码之后,就可以从上到下逐行读取代码,然后按照自己的方式去运行它们。比如我们刚刚的那个 Hello world 的示例程序,被浏览器读取后,浏览器在控制台里面留下了一行 Hello world! 。这就相当于一行一行告诉浏览器应该做什么,然后浏览器解析它得到结果。

当浏览器执行到一段 JS 代码时,通常会按从上往下的顺序执行这段代码。也就是你有时候需要注意一些顺序问题。如果你后面的代码需要用到前面的东西,比方说声明一个变量,你就要保证这两者的顺序。

最后

学习编程,语法本身并不难,真正困难的是如何应用它来解决现实世界的问题。 你要开始像程序员那样思考。一般来讲,这种思考包括了解你程序运行的目的,为达到该目的应选定的代码类型,以及如何使这些代码协同运行。想办法用一些数学模型去概括一些实际问题,这会非常有用。

现在 JS 或许还有些令人生畏,但不用担心。在课程中我们会循序渐进。下一课我们要开始介绍变量。

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

https://developer.mozilla.org/zh-CN/docs/Learn/CSS/CSS_layout/Fundamental_Layout_Comprehension

这里我们找了 MDN 上面的一个例子,给大家来讲解一下这个具体的网站。

我们要做一个这样的网页:

你可以从 https://github.com/mdn/learning-area/tree/master/css/css-layout/fundamental-layout-comprehension 来下载基础的 HTML 和 CSS 资源。为了方便大家我们这里直接列出下面的基础 HTML 和 CSS,图片就先不放了。图片是下列 HTML 和 CSS 显示出来的样子。

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Layout Task</title>
<link href="styles.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="logo">My exciting website! </div>
<nav>
<ul>
<li> <a href="">Home</a> </li>
<li> <a href="">Blog</a> </li>
<li> <a href="">About us</a> </li>
<li> <a href="">Our history</a> </li>
<li> <a href="">Contacts</a> </li>
</ul>
</nav>
<main class="grid">
<article>
<h1>An Exciting Blog Post</h1>
<img src="images/balloon-sq6.jpg" alt="placeholder" class="feature">
<p>Veggies es bonus vobis, proinde vos postulo essum magis kohlrabi welsh onion daikon amaranth tatsoi tomatillo melon azuki bean garlic.</p>
<p>Gumbo beet greens corn soko endive gumbo gourd. Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato. Dandelion cucumber earthnut pea peanut soko zucchini.</p>
<p>Turnip greens yarrow ricebean rutabaga endive cauliflower sea lettuce kohlrabi amaranth water spinach avocado daikon napa cabbage asparagus winter purslane kale. Celery potato scallion desert raisin horseradish spinach carrot soko. Lotus root water spinach fennel kombu maize bamboo shoot green bean swiss chard seakale pumpkin onion chickpea gram corn pea. Brussels sprout coriander water chestnut gourd swiss chard wakame kohlrabi beetroot carrot watercress. Corn amaranth salsify bunya nuts nori azuki bean chickweed potato bell pepper artichoke.</p>
<p>Nori grape silver beet broccoli kombu beet greens fava bean potato quandong celery. Bunya nuts black-eyed pea prairie turnip leek lentil turnip greens parsnip. Sea lettuce lettuce water chestnut eggplant winter purslane fennel azuki bean earthnut pea sierra leone bologi leek soko chicory celtuce parsley jícama salsify.</p>
<p>Celery quandong swiss chard chicory earthnut pea potato. Salsify taro catsear garlic gram celery bitterleaf wattle seed collard greens nori. Grape wattle seed kombu beetroot horseradish carrot squash brussels sprout chard.</p>
</article>
<aside>
<h2>Photography</h2>
<ul class="photos">
<li> <img src="images/balloon-sq1.jpg" alt="placeholder"> </li>
<li> <img src="images/balloon-sq2.jpg" alt="placeholder"> </li>
<li> <img src="images/balloon-sq3.jpg" alt="placeholder"> </li>
<li> <img src="images/balloon-sq4.jpg" alt="placeholder"> </li>
<li> <img src="images/balloon-sq5.jpg" alt="placeholder"> </li>
</ul>
</aside>
</main>
</body>
</html>
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
body {
background-color: #fff;
color: #333;
margin: 0;
font: 1.2em / 1.2 Arial, Helvetica, sans-serif;
}
img {
max-width: 100%;
display: block;
}
.logo {
font-size: 200%;
padding: 50px 20px;
margin: 0 auto;
max-width: 980px;
}
.grid {
margin: 0 auto;
padding: 0 20px;
max-width: 980px;
}
nav {
background-color: #000;
padding: .5em;
}
nav ul {
margin: 0;
padding: 0;
list-style: none;
}
nav a {
color: #fff;
text-decoration: none;
padding: .5em 1em;
}
.photos {
list-style: none;
margin: 0;
padding: 0;
}
.feature {
width: 200px;
}

将以上的 HTML 和 CSS 分别复制到 index.html 和 styles.css 里面,在浏览器里打开 index.html 就可以看到下面的内容。

现在有这些要求:

  1. 在一行中显示导航选项,并且选项之间拥有相同的空间。
  2. 导航条应随着内容一起滚动并且在触碰到视口顶部之后于顶部固定。
  3. 文章内的图片应该被文本包围。
  4. <article><aside> 元素应该为双列布局。它们的列尺寸应该是弹性的,以便在浏览器窗口收缩得更小的时候能够变窄。
  5. 照片应该以有 1px 间隔的两列网格显示出来。

在实现布局的过程中你不需要修改 HTML,而是通过 CSS 相关的技术来解决。如果忘记了可以回头看布局和定位篇的内容。

那么我们下面给出对每个要求逐个给出步骤。

对与第一点,我们用到了 Flexbox,也就是弹性盒子。我们应该将顶栏 <nav> 里面的 <ul> 加上一个 display: flex; 声明,表示 <ul> 子元素使用弹性盒子来进行布局。再给 <ul> 里面的 <li> 加上 flex: 1; ,让它们都占用相同大小的空间。

第二点,我们采用粘性定位技术,给顶栏 <nav> 加上 position: sticky; 声明,采用粘性定位。加上 top: 0px;left: 0px; 指定固定的位置为页面最上方 0px 处(也就是贴着边框)。

第三点要用到浮动(Float),这里我们给和文字在一起的那张图片,也就是类名为 .feature 的那张,指定浮动。使用声明 float: left; 来让元素向左浮动,为了不和文字贴在一起,我们设置了 margin-right: 30px; 排开右边的文字 30 像素。

第四点和第五点都要用到 Grid 来布局。

<article><aside> 元素都被一个类名为 .grid<main> 包裹在内,所以我们要先把 .main 指定成一个 Grid 。添加 display: grid; 指定使用 Grid 布局,再用 grid-template-columns: 2fr 1fr; 指定一共有两列,两列宽度比例是 2:1 ,grid-gap: 10px; 表示两列之间间隔 10px

同理,对于 .photos 这个容器,我们也要使用 display: grid;,使用 grid-template-columns: 1fr 1fr; 表示有 1:1 的两列,再用 grid-gap: 1px; 指定间隔为 1px

合起来的 CSS 如下。只要将其复制到原来的 CSS 下面就行,无需改动原先 CSS 。

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
/*第一点 使用的是子代选择器*/
nav > ul {
display: flex;
}
nav > ul > li {
flex: 1;
}
/*第二点*/
nav {
position: sticky;
top: 0px;
left: 0px;
}
/*第三点*/
.feature {
float: left;
margin-right: 30px;
}
/*第四点*/
.grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-gap: 10px;
}
/*第五点*/
.photos{
display: grid;
grid-gap: 1px;
grid-template-columns: 1fr 1fr;
}

这里只是我个人的例子,如果你们有什么别的方式可以达成,也可以自己试一下,毕竟没有什么标准答案,只要显示出来是符合要求的,就可以算作答案的一种。

小结

通过这个例子我们应该理解了一些基础的 CSS 的布局用法,接下来我们会找更多的例子去让大家了解更多的网站例子。

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

本文基于 CC-BY-SA 4.0 协议发布。

再谈字体

正如你已经在你使用 HTML 和 CSS 完成工作时所经历的一样,元素中的文本是布置在元素的内容框中。以内容区域的左上角作为起点,一直延续到行的结束部分。一旦达到行的尽头,它就会进到下一行,然后继续,再接着下一行,直到所有内容都放入了盒子中。文本内容表现地像一些内联元素,被布置到相邻的行上,除非到达了行的尽头,否则不会换行,或者你想强制地,手动地造成换行的话,你可以使用 <br> 元素。

用于样式文本的 CSS 属性通常可以分为两类,我们将在本文中分别观察。

  • 字体样式: 作用于字体的属性,会直接应用到文本中,比如使用哪种字体,字体的大小是怎样的,字体是粗体还是斜体,等等。
  • 文本布局风格: 作用于文本的间距以及其他布局功能的属性,比如,允许操纵行与字之间的空间,以及在内容框中,文本如何对齐。

字体颜色

在 CSS 里面,指定字体颜色的是 color 属性。严格来讲,color 设置的是前景内容,设置元素背景颜色要用 background-color

1
2
3
p {
color: red;
}

这段用来设置<p>中的字体颜色为红色。

字体种类

要在你的文本上设置一个不同的字体,你可以使用 font-family 属性,这个允许你为浏览器指定一个字体 (或者一个字体的列表),然后浏览器可以将这种字体应用到选中的元素上。浏览器只会把在当前机器上可用的字体应用到当前正在访问的网站上;如果字体不可用,那么就会用浏览器默认的字体代替 default font. 下面是一个简单的例子:

1
2
3
p {
font-family: arial;
}

这段语句使所有在页面上的段落都采用 arial 字体,这个字体可在任何电脑上找到。

网页安全字体

网页安全字体,就是保证绝大多数设备都可以正常显示的字体。这样可以防止你的网页在别人电脑上显示的字体出问题。

实际的 Web 安全字体列表将随着操作系统的发展而改变,但是可以认为下面的字体是网页安全的。所以建议可以尽可能使用这些。

字体名称 泛型 注意
Arial sans-serif 通常认为最佳做法还是添加 Helvetica 作为 Arial 的首选替代品,尽管它们的字体面几乎相同,但 Helvetica 被认为具有更好的形状,即使Arial更广泛地可用。
Courier New monospace 某些操作系统有一个 Courier New 字体的替代(可能较旧的)版本叫Courier。使用Courier New作为Courier的首选替代方案,被认为是最佳做法。
Georgia serif
Times New Roman serif 某些操作系统有一个 Times New Roman 字体的替代(可能较旧的)版本叫 Times。使用Times作为Times New Roman的首选替代方案,被认为是最佳做法。
Trebuchet MS sans-serif 您应该小心使用这种字体——它在移动操作系统上并不广泛。
Verdana sans-serif

默认字体

CSS 定义了 5 个常用的字体名称: serif, sans-serif, monospace, cursive,和 fantasy. 这些都是非常通用的,当使用这些通用名称时,使用的字体完全取决于每个浏览器,而且它们所运行的每个操作系统也会有所不同。这是一种糟糕的情况,浏览器会尽力提供一个看上去合适的字体。 serif, sans-serifmonospace 是比较好预测的,默认的情况应该比较合理,另一方面,cursivefantasy 是不太好预测的,我们建议使用它们的时候应该稍微注意一些,多多测试。

五个名称定义如下:

名称 定义
serif 有衬线的字体 (衬线一词是指字体笔画尾端的小装饰,存在于某些印刷体字体中)
sans-serif 没有衬线的字体。
monospace 每个字符具有相同宽度的字体,通常用于代码列表。
cursive 用于模拟笔迹的字体,具有流动的连接笔画。
fantasy 用来装饰的字体

字体栈

我们也常常使用逗号来分割字体,这样当第一个字体找不到的时候,会依次找下一个字体,直到找到为止。这叫字体栈。

一般情况下,我们会采取直接给 <html> 指定字体的方式来网指定字体,这样页面上的所有文字都继这个属性,然后整个页面的字体都会同时设置。

1
2
3
4
html {
font-family: "Noto Sans SC", "Source Han Sans CN", "MicroSoft YaHei", sans-serif;
/*前两个思源黑体,第三个微软雅黑,最后是无衬线类。*/
}

字体样式

字体大小

我们之前就已经讲过调整字体大小了,不过在这里我们详细讲清楚,理清一些误区。设置字体大小通常有这些方法:

  • px 将像素的值赋予给你的文本。这是一个绝对单位, 它导致了在任何情况下,页面上的文本所计算出来的像素值都是一样的。一般不推荐使用。
  • em 设置相对父元素的字节大小,推荐使用。注意这不是直接相对浏览器本身的字节大小,而是自己的父元素。也就是如果你给一个段落设置成了 1.2em ,它的子元素设置 1em 就是相对 1.2em 而言的。
  • rem 相对根元素大小。这个可以避免上面提到的嵌套问题,而是直接相对浏览器本身的字节大小。

文字样式

font-style: 用来打开和关闭文本 italic (斜体)。 可能的值如下: normal : 将文本设置为普通字体 (将存在的斜体关闭) ,italic: 如果当前字体的斜体版本可用,那么文本设置为斜体版本;如果不可用,那么会将文字倾斜来模拟 italics 。你很少会用到这个属性,除非你因为一些理由想将斜体文字关闭斜体状态。

font-weight: 设置文字的粗体大小。这里有很多值可选 (比如 light, normal, bold, extrabold, black, 等等), 不过事实上你很少会用到 normalbold 以外的值。其中 normal 设置正常字体,bold 设置粗体。

text-decoration: 设置/取消字体上的文本装饰 (你将主要使用此方法在设置链接时取消设置链接上的默认下划线。) 可用值为: none (取消已经存在的任何文本装饰),underline(文本下划线),overline(文本上划线) ,line-through(删除线)。

文本布局

有了基本的字体属性,我们来看看我们可以用来影响文本布局的属性。

文本对齐

text-align 属性用来控制文本如何和它所在的内容盒子对齐。可用值如下,并且在与常规文字处理器应用程序中的工作方式几乎相同:

  • left: 左对齐文本。
  • right: 右对齐文本。
  • center: 居中文字
  • justify: 使文本展开,改变单词之间的差距,使所有文本行的宽度相同。你需要仔细使用,它可以看起来很可怕。特别是当应用于其中有很多长单词的段落时。如果你要使用这个,你也应该考虑一起使用别的东西,比如 hyphens ,打破一些更长的词语。

如果我们应用 text-align: center; 到一个页面的 <h1> 元素中,结果如下:

行高

line-height 属性设置文本每行之间的高,可以接受大多数单位,不过也可以设置一个无单位的值,作为乘数,通常这种是比较好的做法。无单位的值乘以 font-size (字体大小)来获得 line-height 行高)。当行与行之间拉开空间,正文文本通常看起来更好更容易阅读。推荐的行高大约是 1.5–2 (双倍间距。) 所以要把我们的文本行高设置为字体高度的 1.5 倍,你可以使用这个:

1
line-height: 1.5;

字母和单词间距

letter-spacingword-spacing 属性允许你设置你的文本中的字母与字母之间的间距、或是单词与单词之间的间距。你不会经常使用它们,尤其是中文的网站下面,但是可能可以通过它们,来获得一个特定的外观,或者让较为密集的文字更加可读。它们可以接受大多数单位。

所以作为例子,如果我们把这个样式应用到我们的示例中的 <p> 段落的第一行:

1
2
3
4
p::first-line {
letter-spacing: 2px;
word-spacing: 4px;
}

More

以上属性让你了解如何开始在网页上设置文本,但是你可以使用更多的属性。我们只是想介绍最重要的。你也可以通过 MDN 文档去了解其他更多的属性,然后自己试着用上。

再谈颜色和背景

我们之前已经了解过颜色了,不过都没有单独拎出来讲过,所以这里给大家单独讲下。

我们在 CSS 里面用的最多的就是前景色(color)和背景色( background-color )。前景色通常用来指定文本的颜色,背景色通常用来指定块元素的背景颜色。下面是一个例子,形成了黑底灰字。

1
2
3
4
p {
color: grey;
background-color: black;
}
1
<p>Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean.</p>

颜色设置方式

下面列举了一系列的可用的值。至于具体要设置什么颜色,那还是自己去调色盘调着上面看下有啥好看的吧,毕竟这是美术范畴,超出计算机方面了,我也无能为力。讲的还是会比较简单,有些内容会省略不讲,有兴趣可以自己看下 MDN 的文档:(https://developer.mozilla.org/zh-CN/docs/Web/CSS/color

颜色名

这个最简单了,具体有多少种颜色自己可以去找文档。

1
2
color: red;
color: orange;

16 进制颜色

由一个 # 打头的十六进制数字设置,符号由红色、绿色和蓝色的值组成,就是把 RGB 直接写成一个数字。

1
2
color: #090;
color: #009900;

rgb() 和 rgba()

rgb() 和 rgba() 这里两个函数在 CSS 最新标准里面是同一个东西可以指定三个 RGB 颜色值和一个可选的透明度值。不过旧的实现可能只有 rgb() 函数,而且没有透明度值。

1
2
3
color: rgb(34, 12, 64);
color: rgb(34, 12, 64, 0.6);
color: rgba(34, 12, 64, 0.6);

hsl()

这个好像不是特别常用,不过也是可以设置的。具体可以搜索下啥是 HSL,这里懒得讲

1
2
color: hsl(30, 100%, 50%, 0.6);
color: hsla(30, 100%, 50%, 0.6);

图片背景

虽然图片背景好像和这里没啥关系,不过既然提到背景了那也一起讲了,防止以后又要单独开章节补。

background-image 属性用于为一个元素设置一个或者多个背景图像。接受一个图片的 URL 作为参数。例如下面:

1
2
background-image:
url("https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png");

多列布局

多列布局,通常也简写为 multicol,我们将会给出多列布局的具体例子。

在下面的 HTML 里面,我们会把内容放在一个 <div> 里面,它带有一个
container 类。我们会将它作为一个放置文本的容器,来进行一个展示。

比方说我们举个最简单的例子,这里的 column-count 被设置成 3 意思是三列。

1
2
3
.container {
column-count: 3;
}
1
2
3
4
5
<div class="container">
<h1>Simple multicol example</h1>
<p>节约纸张</p>
<p>同上</p>
</div>

除了列数,还可以通过指定每列的宽度。之前为了方便都是用 px 当作单位。这里也可以换成 em (一般情况下,1em 等于 16px,具体换算由浏览器决定),两者之中 em 更适合移植。

1
2
3
.container {
column-width: 12.5em; /*相当于 200px*/
}

浏览器将按照你指定的宽度尽可能多的创建列;任何剩余的空间之后会被现有的列平分。 这意味着你可能无法期望得到你指定宽度,除非容器的宽度刚好可以被你指定的宽度除尽。

给多列增加样式

Multicol 创建的列无法单独的设定样式。 不存在让单独某一列比其他列更大的方法,同样无法为某一特定的列设置独特的背景色、文本颜色。不过你也可以给列之间采用这样的样式:

  • 使用 column-gap 改变列间间隙。
  • column-rule 在列间加入一条分割线。

以上面的代码为例,增加 column-gap 属性可以更改列间间隙:

1
2
3
4
.container {
column-width: 200px;
column-gap: 20px;
}

你可以尝试不同的值 — 该属性接受任何长度单位。现在再加入 column-rule。和你之前遇到的 border 属性类似, column-rulecolumn-rule-colorcolumn-rule-style 的缩写,接受同 border 一样的单位。然后就得到了下面的效果

1
2
3
4
5
.container {
column-count: 3;
column-gap: 20px;
column-rule: 4px dotted rgb(79, 185, 227);
}

列与内容折断

多列布局的内容被拆成碎块。 和多页媒体上的内容表现大致一样 — 比如打印网页的时候。 当你把内容放入多列布局容器内,内容被拆成碎块放进列中,内容折断(比如断词断句)使得这一效果可以实现。

这里因为篇幅原因暂且不讲这么多,只要知道由这样的东西存在,以后设计网站的时候注意就行。

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

本文基于 CC-BY-SA 4.0 协议发布。

前言

接下来,我们就要开始正经的网页实战了。我们将会把 HTML 和 CSS 综合起来,用来构建一些实际的网站,这样就可以真正做出能够看的网页了(要”能用”还少个 JavaScript)。

正常布局流

在搭建网页之前,先要了解的是布局。有这个概念,以后才可以统筹全局。

我们建议始终使用 CSS 方式进行布局,虽然看起来可能会有点麻烦,但是绝对比用 HTML 的 table 来进行所谓的排版好得多,因为表格会破坏网站的语义,导致维护困难。

正常布局流(normal flow)是指在不对页面进行任何布局控制时,浏览器默认的HTML布局方式。让我们快速地看一个HTML的例子:

1
2
3
4
5
6
7
8
9
<p>I love my cat.</p>

<ul>
<li>Buy cat food</li>
<li>Exercise</li>
<li>Cheer up friend</li>
</ul>

<p>The end!</p>

显示起来应该是这样的:

NP

一个朴实无华的网页。

在这里 HTML 元素完全按照源码中出现的先后次序显示——第一个段落、无序列表、第二个段落。

出现在另一个元素下面的元素被描述为块元素,与出现在另一个元素旁边的内联元素不同,内联元素就像段落中的单个单词一样。(你应该还记得之前的盒模型吧)。

注意:块元素内容的布局方向被描述为块方向。块方向在英语等具有水平书写模式(writing mode)的语言中垂直运行。它可以在任何垂直书写模式的语言中水平运行。对应的内联方向是内联内容(如句子)的运行方向。

当你使用 css 创建一个布局时,你正在离开正常布局流,但是对于页面上的多数元素,正常布局流将完全可以创建你所需要的布局。这也是为什么要保持 HTML 的良好结构的原因,不然就容易出现一些不好修复的问题。

下列布局技术会覆盖默认的布局行为:

  • display 属性——标准的 value, 比如 block, inline 或者 inline-block 元素在正常布局流中的表现形式 (见 Types of CSS boxes). 接着是全新的布局方式,通过设置 display 的值, 比如 CSS Grid 和 Flexbox.
  • 浮动——应用 float 值,诸如 left 能够让块级元素互相并排成一行,而不是一个堆叠在另一个上面。
  • position 属性 — 允许你精准设置盒子中的盒子的位置,正常布局流中,默认为 static ,使用其它值会引起元素不同的布局方式,例如将元素固定到浏览器视口的左上角。
  • 表格布局——表格的布局方式可以用在非表格内容上,可以使用 display: table 和相关属性在非表元素上使用。
  • 是多列布局——这个 Multi-column layout 属性 可以让块按列布局,比如报纸的内容就是一列一列排布的。

看起来有点复杂,不过没有关系,下面我们会讲得更详细的。

display 属性

在 css 中实现页面布局的主要方法是设定 display 属性的值。此属性允许我们更改默认的显示方式。正常流中的所有内容都有一个 display 的值,用作元素的默认行为方式。例如,英文段落显示在一个段落的下面,这是因为它们的样式是 display:block (也就是块级元素)。如果在段落中的某个文本周围创建链接,则该链接将与文本的其余部分保持内联,并且不会打断到新行。这是因为 <a> 元素默认为 display:inline,表现为内联元素。

您可以更改此默认显示行为。例如,<li> 元素默认为display:block,这意味着在我们的英文文档中,列表项显示为一个在另一个之下。如果我们将显示值更改为inline,它们现在将显示在彼此旁边,就像单词在句子中所做的那样。事实上,您可以更改任何元素的display值,这意味着您可以根据它们的语义选择 html 元素,而不必关心它们的外观。他们的样子是你可以改变的。

除了可以通过将一些内容从 block 转换为 inline(反之亦然)来更改默认表示形式之外,还有一些更大的布局方法以 display 值开始。但是,在使用这些属性时,通常需要调用其他属性。在讨论布局时,对我们来说最重要的两个值是 display:flexdisplay:grid

弹性盒子(Flexbox)

Flexbox 是CSS 弹性盒子布局模块(Flexible Box Layout Module)的缩写。它被专门设计出来用于创建横向或是纵向的一维页面布局。要使用flexbox,你只需要在想要进行flex布局的父元素上应用display: flex ,所有直接子元素都将会按照flex进行布局。我们来看一个例子。

下面这些HTML标记描述了一个 classwrapper 的容器元素,它的内部有三个

元素。它们在我们的英文文档当中,会默认地作为块元素从上到下进行显示。

现在,当我们把 display: flex 添加到它的父元素时,这三个元素就自动按列进行排列。这是由于它们变成了 flex 项(flex items),按照 flex 容器(也就是它们的父元素)的一些 flex 相关的初值进行 flex 布局:它们整整齐齐排成一行,是因为父元素上 flex-direction 的初值是 row 。它们全都被拉伸至和最高的元素高度相同,是因为父元素上 align-items 属性的初值是 stretch 。这就意味着所有的子元素都会被拉伸到它们的 flex 容器的高度,在这个案例里就是所有 flex 项中最高的一项。所有项目都从容器的开始位置进行排列,排列成一行后,在尾部留下一片空白。

从 MDN 语言翻译成人话就是,给这个 wrapper 设置 display: flex 之后,它里面的子元素就会变成弹性盒子。根据弹性盒子的默认属性,子元素会被排成横着的一行,按照自己的大小自动排列,在后面留下空白。

1
2
3
.wrapper {
display: flex;
}
1
2
3
4
5
<div class="wrapper">
<div class="box1">One</div>
<div class="box2">Two</div>
<div class="box3">Three</div>
</div>

Example1

除了刚刚的属性,还有很多属性可以用在 flex items 上面。他们可以改变 flex 项在 flex 布局中占用宽/高的方式,允许它们通过伸缩来适应可用空间。

作为一个简单的例子,我们可以在我们的所有子元素上添加 flex 属性,并赋值为 1 ,这会使得所有的子元素都伸展并填充容器,而不是在尾部留下空白。它们会调整自己的大小,直到占用相同宽度的空间。比如你的容器太小,它们就会把自己变小,刚好把容器填满。

1
2
3
4
5
6
.wrapper {
display: flex;
}
.wrapper > div {
flex: 1;
}
1
2
3
4
5
<div class="wrapper">
<div class="box1">One</div>
<div class="box2">Two</div>
<div class="box3">Three</div>
</div>

Example1

Grid 布局

Flex 排列单列的时候,固然是很不错的,但是你要排很多东西的时候,一列排不下,就需要用到 Grid。Grid 可以用来按行和列排列你的 HTML 元素。

同flex一样,你可以通过指定 display 的值来转到 grid 布局: display: grid 。下面的例子使用了与flex例子类似的HTML标记,描述了一个容器和若干子元素。除了使用 display:grid ,我们还分别使用 grid-template-rowsgrid-template-columns 两个属性定义了一些行和列的轨道。定义了三个1fr的列,还有两个100px的行之后,无需再在子元素上指定任何规则,它们自动地排列到了我们创建的格子当中。

1
2
3
4
5
6
.wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 100px 100px;
grid-gap: 10px;
}
1
2
3
4
5
6
7
8
<div class="wrapper">
<div class="box1">One</div>
<div class="box2">Two</div>
<div class="box3">Three</div>
<div class="box4">Four</div>
<div class="box5">Five</div>
<div class="box6">Six</div>
</div>

grid

有了一个 grid 之后,你也可以显式地将元素摆放在里面,而不是依赖于浏览器进行自动排列。在下面的第二个例子里,我们定义了一个和上面一样的 grid ,但是这一次我们只有三个子元素。我们利用 grid-columngrid-row 两个属性来指定每一个子元素应该从哪一行/列开始,并在哪一行/列结束。这就能够让子元素在多个行/列上展开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 100px 100px;
grid-gap: 10px;
}
.box1 {
grid-column: 2 / 4;
grid-row: 1;
}
.box2 {
grid-column: 1;
grid-row: 1 / 3;
}
.box3 {
grid-row: 2;
grid-column: 3;
}
1
2
3
4
5
<div class="wrapper">
<div class="box1">One</div>
<div class="box2">Two</div>
<div class="box3">Three</div>
</div>

Grid2

这篇指南的其余部分介绍了其他的布局方式,它们与你的页面的主要布局结构关系不大,但是却能够帮助你实现特殊的操作。同时,只要你理解了每一个布局任务的初衷,你就能够马上意识到哪一种布局更适合你的组件。

Float

把一个元素“浮动”(float)起来,会改变该元素本身和在正常布局流(normal flow)中跟随它的其他元素的行为。这一元素会浮动到左侧或右侧,并且从正常布局流(normal flow)中移除,这时候其他的周围内容就会在这个被设置浮动(float)的元素周围环绕。

float 属性有四个可选的值:

  • left — 将元素浮动到左侧。
  • right — 将元素浮动到右侧。
  • none — 默认值, 不浮动。
  • inherit — 继承父元素的浮动属性。

听起来有点绕,不过看个例子就懂了。下面 Show Code 。在下面这个例子当中,我们把一个 <div> 元素浮动到左侧,并且给了他一个右侧的 margin,把文字推开。这给了我们文字环绕着这个 <div> 元素的效果。

1
2
3
4
5
6
.box {
float: left;
width: 150px;
height: 150px;
margin-right: 30px;
}
1
2
3
<h1>Simple float example</h1>
<div class="box">Float</div>
<p> Lorem ipsum dolor sit amet, consectet(以下为了节省纸张保护树木省略)……</p>

Float

表格布局

首先声明,对于一个现代的网站,不要表格对整个网页进行布局,除非想给某些上古时代的伟大遗产做支持,因为表格会破坏网站的语义。出于历史遗留原因,我们也略微带过这样的形式。

一个 <table> 标签之所以能够像表格那样展示,是由于 CSS 默认给 <table> 标签设置了一组 table 布局属性。当这些属性被应用于排列非 <table> 元素时,这种用法被称为“使用 CSS 表格”。

让我们来看一个例子。首先,创建 HTML 表单的一些简单标记。每个输入元素都有一个标签,我们还在一个段落中包含了一个标题。为了进行布局,每个标签/输入对都封装在 <div> 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form>
<p>First of all, tell us your name and age.</p>
<div>
<label for="fname">First name:</label>
<input type="text" id="fname">
</div>
<div>
<label for="lname">Last name:</label>
<input type="text" id="lname">
</div>
<div>
<label for="age">Age:</label>
<input type="text" id="age">
</div>
</form>
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
html {
font-family: sans-serif;
}
form {
display: table;
margin: 0 auto;
}
form div {
display: table-row;
}
form label, form input {
display: table-cell;
margin-bottom: 10px;
}
form label {
width: 200px;
padding-right: 5%;
text-align: right;
}
form input {
width: 300px;
}
form p {
display: table-caption;
caption-side: bottom;
width: 300px;
color: #999;
font-style: italic;
}

多列布局

多列布局模组给了我们 一种把内容按列排序的方式,就像文本在报纸上排列那样。由于在web内容里让你的用户在一个列上通过上下滚动来阅读两篇相关的文本是一种非常低效的方式,那么把内容排列成多列可能是一种有用的技术。

要把一个块转变成多列容器(multicol container),我们可以使用 column-count 属性来告诉浏览器我们需要多少列,也可以使用 column-width 来告诉浏览器以至少某个宽度的尽可能多的列来填充容器。

在下面这个例子中,我们从一个 classcontainer<div> 容器元素里边的一块 HTML 开始。

我们指定了该容器的 column-width 为200像素,这让浏览器创建了尽可能多的200像素的列来填充这一容器。接着他们共同使用剩余的空间来伸展自己的宽度。

1
2
3
4
<div class="container">
<h1>Multi-column layout</h1>
<p>节约纸张人人有责</p>
</div>
1
2
3
.container {
column-width: 200px;
}

最后

布局总算是讲完了,这么多内容要一下子掌握是不可能的,有个大概印象,知道以后搞网页要查什么资料就行。接下来我们会给出更具体的例子给大家,希望能够起到作用。

本文按照 Mozilla 贡献者基于 CC-BY-SA 2.5 协议发布的以下文章改编:

定位

定位(positioning)能够让我们把一个元素从它原本在正常布局流(normal flow)中应该在的位置移动到另一个位置。定位(positioning)并不是一种用来给你做主要页面布局的方式,它更像是让你去管理和微调页面中的一个特殊项的位置。

有一些非常有用的技术在特定的布局下依赖于 position 属性。同时,理解定位(positioning)也能够帮助你理解正常布局流(normal flow),理解把一个元素移出正常布局流(normal flow)是怎么一回事。

  • 静态定位(Static positioning)是每个元素默认的属性——它表示“将元素放在文档布局流的默认位置——没有什么特殊的地方”。
  • 相对定位(Relative positioning)允许我们相对于元素在正常的文档流中的位置移动它——包括将两个元素叠放在页面上。这对于微调和精准设计(design pinpointing)非常有用。
  • 绝对定位(Absolute positioning)将元素完全从页面的正常布局流(normal layout flow)中移出,类似将它单独放在一个图层中。我们可以将元素相对于页面的 <html> 元素边缘固定,或者相对于该元素的最近被定位祖先元素(nearest positioned ancestor element)。绝对定位在创建复杂布局效果时非常有用,例如通过标签显示和隐藏的内容面板或者通过按钮控制滑动到屏幕中的信息面板。
  • 固定定位(Fixed positioning)与绝对定位非常类似,但是它是将一个元素相对浏览器视口固定,而不是相对另外一个元素。 这在创建类似在整个页面滚动过程中总是处于屏幕的某个位置的导航菜单时非常有用。
  • 粘性定位(Sticky positioning)是一种新的定位方式,它会让元素先保持和 position: static 一样的定位,当它的相对视口位置(offset from the viewport)达到某一个预设值时,他就会像 position: fixed一样定位。

如果看不明白,就了解下我们下面的例子吧。

简单定位示例

我们将展示一些示例代码来熟悉这些布局技术. 这些示例代码都作用在下面这一个相同的HTML上:

1
2
3
4
<h1>Positioning</h1>
<p>I am a basic block level element.</p>
<p class="positioned">I am a basic block level element.</p>
<p>I am a basic block level element.</p>

该 HTML 将使用以下 CSS 样式(下面为了节省纸张,统一忽略这些,希望能够理解):

1
2
3
4
5
6
7
8
9
10
11
body {
width: 500px;
margin: 0 auto;
}
p {
background-color: rgb(207,232,220);
border: 2px solid rgb(79,185,227);
padding: 10px;
margin: 10px;
border-radius: 5px;
}

渲染效果如下,就是一个很正常的 HTML:

相对定位

相对定位(relative positioning)让你能够把一个正常布局流(normal flow)中的元素从它的默认位置按坐标进行相对移动。比如将一个图标往下调一点,以便放置文字. 我们可以通过下面的规则添加相对定位来实现效果:

1
2
3
4
5
.positioned {
position: relative;
top: 30px;
left: 30px;
}

这里我们给中间段落的 position 一个 relative 值——这属性本身不做任何事情,所以我们还添加了 topleft 属性。这样会让这个元素向下和向右各移动 30px 。看起来有点反直觉,不过想想就明白了:这里相当于是它左边和顶部的元素被“推开”一定距离,导致了它的向下向右移动。

添加此代码,然后你就能看到结果:

1
2
3
4
5
6
7
.positioned {
position: relative;
background: rgba(255,84,104,.3);
border: 2px solid rgb(255,84,104);
top: 30px;
left: 30px;
}

绝对定位

绝对定位用于将元素移出正常布局流(normal flow),以坐标的形式相对于它的容器定位到 web 页面的任何位置,以创建复杂的布局。它经常被用于与相对定位和浮动的协同工作。

回到我们最初的非定位示例,我们可以添加以下的CSS规则来实现绝对定位:

1
2
3
4
5
.positioned {
position: absolute;
top: 30px;
left: 30px;
}

这里我们给我们的中间段一个 position 的 absolute 值,并且和前面一样加上 topleft 属性,好像没啥区别,不过这里的 top 和 left 相对的地方不再是它的父元素,而是页面本身了。添加此代码将给出以下结果(手残选中了那个 absolutely ,不要管它):

1
2
3
4
5
6
7
.positioned {
position: absolute;
background: rgba(255,84,104,.3);
border: 2px solid rgb(255,84,104);
top: 30px;
left: 30px;
}

固定定位

这个和绝对定位有一点像,不同的一点是,它是相对浏览器窗口边框的位置,而不是相对页面容器的位置。

在这个例子里面,我们在HTML加了三段很长的文本来使得页面可滚动,又加了一个带有 position: fixed 的盒子。

1
2
3
4
5
<h1>Fixed positioning</h1>
<div class="positioned">Fixed</div>
<p>Paragraph 1.</p>
<!-- 同样为了保护森林省略了一大堆字 -->
<p>Paragraph 3.</p>
1
2
3
4
5
.positioned {
position: fixed;
top: 30px;
left: 30px;
}

(注意右边滚动条,滚了之后红色块是不动的)

粘性定位

粘性定位(sticky positioning)是最后一种我们能够使用的定位方式。它将默认的静态定位(static positioning)和固定定位(fixed positioning)相混合。当一个元素被指定了 position: sticky 时,它会在先正常布局流中滚动(就像静态的一样),直到它出现在了我们给它设定的相对于容器的位置,这时候它就会停止随滚动移动,就像它被应用了 position: fixed 一样。

简单地说就是你往下滚到看到那个元素,那个元素就“粘”在那里,再往下也不动了。这里不好截图,所以给了两张截图让大家意会一下。

1
2
3
4
5
.positioned {
position: sticky;
top: 30px;
left: 30px;
}

z-index

当我们替换掉默认的布局的时候,有些元素可能会叠在一起。这时候,如果要指定元素堆叠的顺序,就可能会需要用到 z-index 来指定它们的优先级。

“z-index”是对z轴的参考。你可以从源代码中的上一点回想一下,我们使用水平(x轴)和垂直(y轴)坐标来讨论网页,以确定像背景图像和阴影偏移之类的东西的位置。(0,0)位于页面(或元素)的左上角,x和y轴跨页面向右和向下。

那么同样的,z轴就是从垂直屏幕开始指向看网页的那个人。z-index 值影响定位元素位于该轴上的位置;正值将它们移动到堆栈上方,负值将它们向下移动到堆栈中。

默认情况下,定位的元素都具有 z-indexauto ,实际上为 0。如果一个元素是静态定位的(也是就是没有使用特殊定位),这个值不起作用。对于被定位的元素,后面定位的默认会排在上面。

比如我现在这个 HTML 变成这样:

1
2
3
4
<h1 >Positioning</h1>
<p class="omg">I am a OMG block.</p>
<p class="positioned">I am a positioned block.</p>
<p>I am a basic block level element.</p>
1
2
3
4
5
6
7
8
9
10
11
12
.omg{
position: absolute;
top: 50px;
left: 40px;
background: rgba(255,84,104,.3);
z-index: 1;
}
.positioned {
position: absolute;
top: 30px;
left: 30px;
}

这个 OMG 就叠在 positioned 上面了。如果去掉那个 z-index ,它本来应该被排在下面的。