浅谈JS中奇怪的字符串

当我再次温习 JS 中字符串的相关知识时,结合平常码代码的一些操作,头脑中又迸发了一些疑惑。

这一篇博客便统一记录一下,我对于 JS 中字符串的两个疑问、以及网上的解答,以便日后来进行查阅。

目录:

  1. 为何 JS 中字符串也会像对象那样,有方法可以调用?
  2. 字符串涉及 Unicode 字符时,为何长度的获取会如此混乱,如何统一解决?

字符串也是对象?

引子

在 JavaScript 里面,对象 (object) 是个频繁出现的东西。

在 ES6 的面向对象之前,我们提到的对象还不包含类 (class) 的概念,通常是自定义对象内置对象浏览器对象对象包含属性和方法,出于方便,我们可以自由地去设置并调用。

但是,当使用字符串时,我有一个疑惑:

1
2
var str = 'andy';
console.log(str.length); // 或者 console.log('andy'.length);

比如:在以上的代码中,为何 str 是一个字符串,却像对象那样有相应的方法可以调用呢?

答案是 JS 默认把我们的一些简单数据类型,包装成了复杂数据类型

“基本包装类型”是什么

数据类型

在 JavaScript 中,数据类型有基本类型和引用类型之分。

  • 基本类型:undefinednullstringnumberbooleansymbol (ES6 之后)

    特殊基本包装类型:StringNumberBoolean

  • 引用类型:ObjectArrayRegExpDateFunction

其中,“基本类型”和“引用类型”的区别是:引用类型值可添加属性和方法,而基本类型值则不可以。

我们可以注意到,为了方便我们操作基本数据类型,JS 提供了三个特殊的引用类型:StringNumberBoolean.

基本包装类型

基本包装类型的概念就是将简单数据类型包装成复杂数据类型,这样基本数据类型就有了相应的属性和方法。

也就是说,看似简单的一行 var str = 'andy',在 JS 内部解释器的执行过程其实是这样的:

1
2
3
4
5
6
// 1. 生成临时变量,把简单类型包装为复杂数据类型
var temp = String('andy'); // *
// 2. 赋值给我们声明的字符变量
str = temp;
// 3. 销毁临时变量
temp = null;

这样一来,变量 str 便有了 String特殊基本包装类型的属性和方法,也解释了我们心中的疑惑。

至于这里为何是 String,而不是 new String,我们稍后再说。

字符串“不可变”

由于 String 类型并非普通数据类型,这就涉及到了一个和对象相似的概念:字符串“不可变”。

字符串不可变,指的是里面的值是不变的。虽然看上去可以改变内容,但其实是地址变了,内存中开辟了一个新的内存单元。

1
2
3
4
var str = 'abc';
str = 'hello';
// 当重新给 str 赋值的时候,常量 'abc' 不会被修改,依然在内存中
// 重新给字符串赋值,会重新在内存中开辟空间,这个特点就是字符串的不可变

由于字符串的不可变性,我们在裁切字符串的时候也许不会分配内存空间 (参见 MDN),但在大量拼接字符串的时候很可能会有效率问题:

1
2
3
4
5
var str = '';
for (var i = 0; i < 10000000; i++) {
str = '1' + str;
}
console.log(str); // 这个结果需要花费大量的时间来显示,因为需要不断开辟新空间

为何不是 new String

通过上述的过程,我们知道在 JS 中:

每当读取一个基本类型值的时候,后台就会自动创建一个对应的基本包装类型的对象。而且,此对象只存在于一行代码的执行瞬间,然后立即被销毁。

至于为何不用 new,而是直接调用 String('andy') 或者 Number(1),其原因是:

把字符串,数字,布尔值传给构造函数的话,会得到对应的实例,如果对基本包装类型的实例调用 typeof 则会返回 object。所以,使用 new 操作符调用基本包装类型的构造函数,与直接调用同名的转型函数是不一样的。

我们可以用以下的代码来进行测试:

1
2
3
4
5
6
7
8
9
10
var value = '123456';
var num = Number(value);
console.log(typeof num); // number
var numObj = new Number(value);
console.log(typeof numObj); // object

// 把字符串,数字,布尔值传给构造函数就会创建相应的实例,
// 对基本包装类型的实例调用 typeof 会返回 object

console.log(num.toString()); // 测试一下方法

string 包含 Unicode 字符…

(一点点) Unicode 常识 (原文链接)

在深入研究 JavaScript 之前,先解释一下 Unicode 一些基础知识,这样在 Unicode 方面,我们至少都了解一些。

Unicode 是目前绝大多数程序使用的字符编码,定义也很简单,用一个码位 (code point) 映射一个字符。码位值的范围是从 U+0000U+10FFFF,可以表示超过 110 万个字符。下面是一些字符与它们的码位。

码位通常被格式化为十六进制数字,零填充至少四位数,格式为 U+前缀

  • Unicode 最前面的 65536 个字符位,称为基本多文种平面 (BMP - Basic Multilingual Plane, 又简称为 plane 0, 零号平面),它的码位范围是从 U+0000U+FFFF

    最常见的字符都放在这个平面上,这是 Unicode 最先定义和公布的一个平面。

  • 剩下的字符都放在 辅助平面 (Supplementary Plane) 或者**星形平面 (astral planes)**,码位范围从 U+010000 一直到 U+10FFFF,共 16 个辅助平面。

    辅助平面内的码位很容易识别:如果需要超过 4 个十六进制数字来表示码位,那么它就是一个辅助平面内的码。

比如说:

  • © 的码位是 U+00A9,在 U+0000U+FFFF 之间,它是在基本平面上的字符。
  • 😂 的码位是 U+1F602,在 U+010000 一直到 U+10FFFF 之间,它是在辅助平面上的字符。

现在对 Unicode 有了基本的了解,接下来看看它如何应用于 JavaScript 字符串。

转义字符

要在 JavaScript 的字符串中表示 Unicode 字符,我们通常使用转义符 \u

打开 Chrome 的调式工具,在 Console 中输入带有转义符的字符串,可以得到我们的转义结果:

1
2
3
4
5
>> '\u0041\u0042\u0043'
'ABC'

>> 'I \u2661 JavaScript!'
'I ♡ JavaScript!

这些被称为 Unicode 转义序列。它们由表示码位的 4 个十六进制数字组成。例如,\u2661 表示码位为 \U+2661 的字符——一个爱心。这种方法可以用于 U+0000U+FFFF 范围内的码位,即整个基本平面

但是其他的所有辅助平面呢?我们需要 4 个以上的十六进制数字来表示它们的码位,那么如何转义它们呢?

在 ECMAScript 6 中,这很简单,因为它引入了一种新的转义序列:Unicode 码位转义。例如:

1
2
3
4
5
>> '\u{41}\u{42}\u{43}'
'ABC'

>> '\u{1F602}'
'😂'

在大括号之间可以使用最多 6 个十六进制数字,这足以表示所有 Unicode 码位。

但是为了向后兼容 ECMAScript 5 和更旧的环境,不幸的解决方案是使用代理对:

1
2
'\ud83d\ude02'
'😂'

在这种情况下,每个转义项表示代理项,是最终”辅助平面“字符的一半的码位。这样的两个代理项便组成一个辅助码位。

注意

代理项对的码位与原始码位不相同。

有公式可以根据给定的辅助码位来计算代理项对码位,反之亦然——根据代理对计算原始辅助码位。

TLDR:如果有辅助码位 C,那么其两个代理对码位 HL 的计算方法为

1
2
H = Math.floor((C - 0x10000) / 0x400) + 0xD800
L = (C - 0x10000) % 0x400 + 0xDC00

反之,如果有两个代理项的码位 HL,则组成的辅助码位 C

1
C = (H - 0xD800) * 0x400 + L - 0xDC00 + 0x10000

那么问题就出来了…

假设你想要计算给定字符串中的字符个数。你会怎么做呢?

首先想到可能是使用 length 属性。

1
2
3
4
>> 'A'.length
1
>> '😂'.length
2

由于 JavaScript 内部 (ES5 之前) 将辅助平面内的字符表示为 2 个代理对,并将单独的代理对认作单独的 “字符”,所以如果仅使用 ECMAScript 5 兼容的转义序列来表示字符,将看到其 length 属性为 2。这是十分令人困惑的,因为人们通常只能看到一个 Unicode 字符或图形。

那么我们该如何区分出来辅助平面的字符呢?以下有几个典型的 case.

字符串长度

我们可以用正则匹配来找出这些代理对,把两个代理对替换成一个字符,然后再用 length 来得出字符串长度。这种方法是兼容 ES5 的:

1
2
3
4
5
6
7
8
9
var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;

function countSymbols(string) {
return string
// Replace every surrogate pair with a BMP symbol.
.replace(regexAstralSymbols, '_')
// …and *then* get the length.
.length;
}

ES6 中有 Array.from() 方法,它能从一个类似数组或可迭代对象,创建一个新的、浅拷贝的数组实例。这个方法自身能识别出字符串中的代理对:

1
2
3
function countSymbols(string) {
return Array.from(string).length;
}
取字符串

同样地,使用 Array.from(),我们能很方便地从带有 Unicode 字符的 String 中,取出我们想要的字符。此时,ES6 以下的兼容方法是使用相应的 polyfill.

1
2
3
function slice(string, start, end) {
return Array.from(string).slice(start, end).join('');
}
字符串翻转

字符串翻转的实现也同理:

1
2
3
function reverse(string) {
return Array.from(string).reverse().join('');
}

还有一个问题…

上述方法虽好,但是却不能解决组合标记的问题:

1
2
3
4
5
6
7
8
9
10
function countSymbols(string) {
return Array.from(string).length;
}
// ----------------------------------
var str1 = '😹se\xF1orita';
var str2 = '😹sen\u0303orita';
console.log(str1); // 😹señorita
console.log(countSymbols(str1)); // 9
console.log(str2); // 😹señorita
console.log(countSymbols(str2)); // 10

比如在上述代码中,str1str2 看上去都是 señorita。但是在获取长度的时候,一个字符串长为 9,另一个则是 10.

这个问题看上去并不是很大,但是如果我们将上述字符串翻转的话:

1
2
3
4
5
6
7
8
9
10
function reverse(string) {
return Array.from(string).reverse().join('');
}
// ----------------------------------
var str1 = '😹se\xF1orita';
var str2 = '😹sen\u0303orita';
console.log(str1); // 😹señorita
console.log(reverse(str1)); // atiroñes😹
console.log(str2); // 😹señorita
console.log(reverse(str2)); // atirõnes😹

ñ 是单个字符的情况下,字符串翻转一切正常。
但在 ñ 为两个字符的情况下,字符串在翻转之后,n 上面的波浪号竟然跑到了 o 的上面!这可咋办?

答案则是**规范化 (normalize)**。

我们可以利用 String.prototype.normalize,使 str1str2 这两个格式相互转化。如果在需要翻转的时候,我们通常把字符串统一为单字符 (即 str1) 的格式。

浅谈尾递归和尾调用

引言

之前在学习 JS 中 Promise 的时候,随便找了一个某厂相关的面试题来练手。

找到的题目很简单,要求是用 Promise 来实现红绿灯的功能,无限循环。但当看到那个样例代码的时候,我陷入了深深的沉思:

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
function light() {
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('红');
resolve();
}, 1000);
})
.then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('绿');
resolve();
}, 1000);
});
})
.then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('黄');
resolve();
}, 1000);
});
})
.then(() => {
return light(); // ???????????????????
});
}

light();

这个过程其实是简单而且优雅的,但是最后那一句 return light() 是什么鬼!

无限地调用自己,短期内看上去相当可行。但运行时间长的话,确定不会栈溢出或者起码浪费资源吗?

我内心里一直有上述这样的疑问,直到我了解到了“尾调用” (Tail Calls) 这一机制。

尾调用 & 尾递归

为什么需要尾调用优化

为了解决递归时调用栈溢出的问题,除了把递归函数改为迭代之外,改为尾递归的形式也可以解决。

注意⚠️

在 Node 6、Node 7 中可以使用命令行参数 --harmony-tailcalls 开启尾递归优化功能。但是这个特性已经被 V8 移除了,对应的 Node 8 之后的版本都已经没有了尾递归优化功能。在 Chrome 之前的某些版本确实可以通过 chrome://flags/#enable-javascript-harmony 开启,但是也已经被移除了。因为尾递归优化会破坏函数的调用栈信息,TC39 也正在寻找新的 JS 语法来显式指明需要使用尾递归优化。

事实上,在 JavaScript 世界中,尾调用只短暂地被 Node.js 支持过,但后来都由于技术原因而被废弃。到目前为止,只有 Safari 这一浏览器支持“尾调用优化” (Tail Call Elimination),而无论是 Chrome 中的 V8、还是 Node.js 都还不支持。2016 年的 TC39 提案中,有关于这一点的公开讨论,一些开发人员仍强烈希望在 JavaScript 中支持 PCE。

尾调用优化目前还是 Stage 0 Draft 阶段,但是我们有理由相信在不久的将来这个一定会进入 ES 标准。即使目前大部分浏览器没有对尾递归 (尾调用) 做优化、依然会导致栈溢出,了解尾递归的优化方式还是很有价值的。

回归正文

当函数 func1 的最后一个动作是调用函数 func2 时,那么对函数 func2 的调用形式就是尾调用。例如:

1
2
3
4
5
6
7
8
9
10
11
const func2 = (a) => {
let b = a + 1;
return b;
}

const func1 = (x) => {
let y = x + 1;
return func2(y);
}

const result = func1(1);

尾调用优化的模式下,执行该段代码的步骤就是:

  1. 执行函数 func1(1),将其压入调用栈 (call stack) 中。

  2. 先运行完 func1 的主体部分,在执行到 return func2(y) 时,

    func1 的调用即可以出栈,并把对调用 func1 的信息传给 func2

  3. 然后把 func2(y) 以及来自 func1 的信息,压入调用栈,

    最后在 func2 执行完成之后,把 func2 的结果返回到当初执行 func1 的那一步。

按照以上步骤,在执行 fun1func2 时,始终只占用一个调用栈。

如果是在递归的环境下也只需一个调用栈,这样便大大减小了调用栈的大小。

什么是尾调用 (尾递归)

尾调用就是函数的最后一步是执行一个函数的过程,而尾递归就是通过尾调用来完成的递归过程。

譬如,形如以下代码的都是尾调用:

1
2
3
const a = () => {
return b();
}

而下面代码,并不是尾调用

1
2
3
const a = () => {
b();
}

原因是 b() 之后还隐式地执行了一段 return undefined,所以其最后一步并不是调用一个函数。

同理,如果我们要写一个计算阶乘的程序,按照以下方式进行编写

1
2
3
4
5
6
function fac(n) {
if (n === 1) return 1;
return n * fac(n - 1);
}

fac(5);

这样并不是尾调用,

因为函数 fac(n) 中,程序在执行完 fac(n-1) 的函数调用之后,还需取回值来进行乘法运算。所以程序需要保留 fac(n) 的调用栈,以便在获得值之后相乘。

我们可以通过一下代码,将其转为尾递归的写法:

1
2
3
4
5
6
function fac(n, total) {
if (n === 1) return total;
return fac(n - 1, n * total);
}

fac(5, 1);

然而

其实,尾调用就是给数据处理提供一个新的方式。

而递归通常能被写成迭代的方式,因为在目前的 V8、Node.js 等环境都还不支持尾调用 (可以通过这个网站查看”尾调用优化“的支持情况),我们可以通过将尾递归改写成循环,来实现节省空间的目的。

Trampoline 是对尾递归函数进行处理的一种技巧。对于一段累加的代码:

1
2
3
4
const sum = (n, prevSum = 0) => {
if (n <= 1) return n + prevSum;
return sum(n-1, n + prevSum)
}

我们可以先把上面的 sum 函数改造一下,再由 trampoline 函数处理即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const sum0 = (n, prevSum = 0) => {
if (n <= 1) return n + prevSum;
return () => sum0(n-1, n + prevSum)
}
const trampoline = f => (...args) => {
let result = f(...args);
while (typeof result === 'function') {
result = result();
}
return result;
}

const sum = trampoline(sum0);
console.log(sum(1000000)); // 不会栈溢出

可以看到,这里实际上就是把原本的递归改成了迭代,这样就不会有栈溢出的问题啦。

虽然递归理论上都能改写为迭代,但有些场景下使用递归可能会更加直观。如果一个递归能被转为”尾递归“,你就可以间接地用 trampoline 函数进行处理,或者把它改写成迭代的方法。

完全搞懂promise

上一次梳理了原型链和继承的知识,这两者在 JS 的面向对象编程中可以说是非常常见的,尤其是开发 React 中的类式组件次次都要接触到 (虽我目前还是主要使用 Vue)。

然而其实在 JS 中,promise 出现的次数也相当频繁。只要涉及到异步事件的处理,几乎随处可见的就是 promise 的身影。

在这个笔记中,我决定记录下我在学习 promise 的课程中,涉及的的相关知识。

引子

在了解 Promise 之前,我们也要弄清楚一些概念。

函数对象?实例对象?

了解了 JS 的原型和继承之后,我们知道:在 Javascript 里面,严格来说其实并没有“类”的概念。类的实现基本是靠构造函数原型对象来进行模拟得到的。

一个大神的笔记中梳理了以下关系图,通过这张图,我们能够更好地理解,JS 中的(构造)函数对象和实例对象的关系。

构造函数和实例对象的关系

在这里,也不再过多赘述函数对象、实例对象以及继承链的更多细节了 (上一个笔记已写)。简单来说如下:

  • 函数对象:将函数作为对象使用时,即为函数对象。构造函数也是一个函数,只不过它于普通函数有点不同。
  • 实例对象:通过 new 一个构造函数产生的对象称为实例对象,简称对象

两种回调函数

同步回调函数

同步回调函数是立即执行的,完全执行完了才结束,它不会被放入回调队列中。常见的同步回调有:

  • 数组遍历相关的回调函数
  • Promise 中的 executor 函数

举一个例子,比如有以下的代码:

1
2
3
4
5
const arr = [1,3,5]
arr.forEach(item => { //遍历回调
console.log(item)
})
console.log('forEach() 之后')

问上述两个 console.log 谁先执行?
答案是:先 console.log 数组元素,再输出 forEach() 之后

异步回调函数

异步回调对比同步回调不同的是,他是放入回调队列中将来执行的,等到同步代码执行完之后才进行执行。

还是一个例子:

1
2
3
4
setTimeout(() => { // 通过 setTimeout 可以创建一个异步回调
console.log('timeout callback()')
}, 0);
console.log('setTimeout() 之后');

在这里先输出 setTimeout() 之后,然后才输出 timeout callback()

备注

在异步回调中,还有微任务 (microtask)宏任务 (task) 之分。这里微任务、宏任务等概念涉及 JS 的事件循环 (event loop) 机制,暂不赘述。

简单来说就是,因为 JS 的本质是单线程的,所以 JS 有个单线程的执行机制。其顺序为:

执行一个宏任务 (先执行同步代码) => 执行所有微任务 => UI Render => 执行下一个宏任务 => 执行所有微任务 => UI Render => …

其中,setTimeout 就是宏任务,promise.thenmutation 等是微任务。

JS 中的 error 处理

JS 错误的类型

  • Error:所有错误的父类型

  • ReferenceError:引用的变量不存在

    1
    2
    // a 未定义
    console.log(a) // 报错
  • TypeError:数据类型不正确的错误

    1
    2
    3
    4
    5
    6
    let b;
    console.log(b.xxx) // 报错
    // b 不是 Object, 无 xxx 属性
    b = {};
    b.xxx() // 报错
    // b 有 xxx 属性,但不是一个函数
  • RangeError:数据值不在其所允许的范围内

    1
    2
    3
    4
    function fn() {
    fn();
    }
    fn(); // 报错,因为栈空间溢出
  • SyntaxError:语法错误

    1
    let a = """";  // 报错

错误处理

  • 捕获错误:try ... catch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try {
    let d;
    console.log(d.xxx);
    } catch (error) {
    console.log(error);
    }

    // error 是一个对象 (Object)

    // 此时 error 是 TypeError 的实例
    // 这里的 error.__proto__ = Error
  • 抛出错误:throw error

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 一个栗子
    function something() {
    if (Date.now() % 2 === 1) {
    console.log('当前时间为奇数,可以执行任务');
    } else {
    throw new Error('当前时间为偶数,无法执行任务')
    }
    }

    // 在外面捕获异常
    try {
    something();
    } catch (error) {
    alert(error.message);
    }

错误对象

错误对象包含两个主要属性:

  • message 属性:错误相关信息
  • stack 属性:函数调用栈记录信息

promise 的理解和使用

什么是 promise?

  1. 抽象表达

    Promise 是 JS 中进行异步编程的新的解决方案。旧的是纯回调形式

  2. 具体表达

    • 从语法上来说:Promise 是一个构造函数
    • 从功能上来说:promise 对象用来封装一个异步操作并可以获取其结果

promise 的状态

状态改变

  1. pending 变为 resolved
  2. pending 变为 rejected

基本流程

MDN 文档的 Promise 执行流程图

基本使用

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
// 1. 创建一个新的 promise 对象
// 为了方便后期取到 value/reason,将 promise 对象赋给 p

const p = new Promise((resolve, reject) => {
// 执行器 executor 函数,同步回调

// 2. 执行异步操作任务
setTimeout(() => {
// 随便写一个异步任务
const time = Date.now();
if (time % 2 == 0) {
// 3a. 如果成功了,调用 resolve(value)
resolve("成功" + time);
} else {
// 3b. 如果失败了,调用 reject(reason)
reject("失败" + time);
}
}, 1000);

});

p.then(
// 异步回调
value => { // 接收得到成功的 value 数据 onResolved
console.log('成功的回调' + value);
},
reason => { // 接收得到失败的 reason 数据 onRejected
console.log('失败的回调' + reason)
},
);

为什么“promise”?

  1. 指定回调函数的方式更加灵活。

    • 旧方法:必须在启动异步任务前指定回调函数;

    • 使用 promise:启动异步任务 => 返回 promise 对象 => 给 promise 对象绑定回调函数,甚至可以在异步任务完成之后绑定

  2. 支持链式调用,可以解决回调地狱的问题。回调地狱:回调函数嵌套调用,不便于阅读,不便于异常处理;

    • 解决方案:promise 链式调用;
    • 终极解决方案:async / await
回调地狱 demo
1
2
3
4
5
6
7
8
// 2a. 回调地狱的一个 demo
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback)
}, failureCallback)
}, failureCallback)

肉眼可见,在有非常多的回调函数的时候,这种逻辑和不断缩进的代码会弄的人非常头大。

1
2
3
4
5
6
7
8
9
10
11
12
// 2b. 使用 promise 的链式调用,可以解决回调地狱
doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback)

在使用 promise 的链式调用之后,整个代码的逻辑变得清晰很多。

1
2
3
4
5
6
7
8
9
10
11
// 2c. async/await:回调地狱的终极解决方案
async function request() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log('Got the final result: ' + finalResult);
} catch (error) {
failureCallback(error);
}
}

将异步操作用 async/await 进行书写,就和写同步操作一样简单。

如何使用 promise

构造函数

Promise 的构造函数:Promise(executor) {}

  • executor 函数:同步执行 (resolve, reject) => {}
  • resolve 函数:内部定义成功时我们调用的函数 value => {}
  • reject 函数:内部定义失败时我们调用的函数 reason => {}

说明executor 会在 Promise 内部立即同步回调,异步操作是在执行器中执行的。

then 方法

Promise.prototype.then 方法:(onResolved, onRejected) => {}

  • onResolved 函数:成功的回调函数 (value) => {}
  • onRejected 函数:失败的回调函数 (reason) => {}

说明:设置用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调,返回一个新的 promise 对象。

catch 方法

Promise.prototype.catch 方法:(onRejected) => {}

  • onRejected 函数:失败的回调函数 (reason) => {}

说明then() 的语法糖,相当于 then(undefined, onRejected)

resolve 方法

Promise.resolve(value) 方法:(value) => {}

  • value:成功的数据或 promise 对象
1
2
3
4
5
// 产生一个成功值为 1 的 promise 对象
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = Promise.resolve(2);

说明:返回一个成功/失败的 promise 对象。

reject 方法

Promise.reject 方法:(reason) => {}

  • reason:失败的原因

说明:返回一个失败的 promise 对象。

all 方法

Promise.all 方法:(promises) => {}

  • promises:包含 n 个 promise 的数组
1
const pAll = Promise.all([p1, p2, p3]);

说明:返回一个新的 promise,只有所有的 promise 都成功才算成功,只要有一个失败了就直接失败。

race 方法

Promise.race 方法:(promises) => {}

  • promises:包含 n 个 promise 的数组

说明:返回一个新的 promise,第一个完成的 promise 的结果状态就是最终的结果状态。

注意点

如何改变 promise 的状态?

  1. resolve(value):如果当前状态是 pending,就会变为 resolved

  2. reject(reason):如果当前状态是 pending,就会变为 rejected

  3. 抛出异常:如果当前状态是 pending,就会变为 rejected

    注意:这里可以 throw 任何东西,并不一定要是一个 error。比如说除了 throw new Error('出错了') 以外,也可以 throw 3

一个 promise 指定多个成功/失败回调函数,都会调用吗?

Yes。当 promise 改变为对应状态时都会调用。

改变 promise 状态和指定回调函数,谁先谁后?

  • 都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调;

  • 如何先改状态再指定回调?

    • 在执行器中直接调用 resolve() / reject()
    • 延迟更长时间才调用 then()
  • 什么时候才能得到数据?

    • 如果先指定回调函数,那就当状态发生改变时,回调函数就会调用,得到数据;

      1
      2
      3
      4
      5
      6
      7
      8
      new Promise((resolve, reject) => {
      setTimeout(() => {
      resolve(1); // 后改变的状态 (同时指定数据)
      }, 1000);
      }).then( // 先指定回调函数,保存当前指定的回调
      value => {},
      reason => {console.log('reason', reason)}
      )
    • 如果先改变状态,那当指定回调函数时,回调函数就会调用,得到数据。

      1
      2
      3
      4
      5
      6
      new Promise((resolve, reject) => {
      resolve(1); // 先改变的状态 (同时指定数据)
      }).then( // 后指定回调函数,异步执行回调函数
      value => {},
      reason => {console.log('reason', reason)}
      )

promise.then() 返回新的结果状态

例如有以下的代码,试问其执行结果是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise((resolve, reject) => {
resolve(1);
}).then(
value => {
console.log('onResolved1()', value);
}
reason => {
console.log('onRejected1()', reason);
}
).then(
value => {
console.log('onResolved2()', value);
}
reason => {
console.log('onRejected2()', reason);
}
)

输出为

1
2
onResolved1() 1
onResolved2() undefined

那么,promise.then() 返回的新 promise 的结果状态,由什么决定?

  • 简单表达:由 then() 指定的回调函数执行的结果决定;
  • 详细表达:
    • 如果抛出异常,新 promise 变为 rejectedreason 为抛出的异常
    • 如果返回的是非 promise 的任意值,新 promise 变为 resolvedvalue 为返回的值
    • 如果返回的是另一个新 promise,此 promise 的结果就会成为新 promise 的结果

promise 如何串联多个操作任务?

  • promisethen() 返回一个新的 promise,可以看成 then() 的链式调用;

    注意:同步操作可以 return 值,但异步操作必须包裹一个新的 promise 对象

  • 通过 then 的链式调用串连多个同步/异步任务。

promise 异常穿透?

  • 当使用 promisethen 链式调用时,可以在最后指定失败的回调;
  • 前面任何操作发生异常,都会传到最后失败的回调中处理。

如何中断 promise 链?

  • 当使用 promisethen 链式调用时,在中间中断,不再调用后面的回调函数;
  • 方法:在回调函数中返回一个 pending 状态的 promise 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise((resolve, reject) => {
resolve(1);
}).then(
value => {
// ***
return new Promise(() => {});
// 返回一个 pending 的 promise
// ***
}
).then(
value => {
console.log('onResolved2()', value);
}
reason => {
console.log('onRejected2()', reason);
}
)

简单实现Vue中的MVVM

Talk is cheap, show me the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Compiler {
constructor(el, vm) {
// 判断 el 属性 是不是一个元素 如果不是元素 那就获取他
this.el = this.isElementNode(el) ? el : document.querySeletor(el);

this.vm = vm;

let fragment = this.node2fragment(this.el);

// 把节点中的内容进行替换

// 把内容再次塞到页面中
this.el.appendChild(fragment);
}

// 是不是元素节点
isElementNode(node) {
return node.nodeType === 1;
}

// 编译文本的方法
compileText () {
let attributes = node.attributes;
[...attributes].forEach(attr => {

});
}

// 编译元素的方法
compileElement() {

}
// 编译内存中的 DOM 节点
compile(node) {
let childNodes = node.childNode;
[...childNodes].forEach(child => {
// 如果子节点是元素节点
if (this.isElementNode(child)) {
this.compileElement(child);
} else {
this.compileText(child);
}
});
}

// 把节点移动到内存中
node2fragment(node) {
// 创建一个文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = node.firstChild) {
// appendChild 具有移动性
fragment.appendChild(firstChild);
}
}
}

class Vue {
constructor(options) {
// this.$el $data $options
this.$el = options.el;
this.$data = options.data;
// 这个根元素 存在编译模板
if (this.$el) {
new Compiler(this.$el, this);
}
}
}

JS中的继承和原型链

作为 Js 新学习者,最近好好整理了下 Js 中的原型链和继承机制。结合文档和网课,我把这个话题相关的文字给码了下。

对象 Object

在了解继承和原型链之前,首先我们需要掌握一个对象 Object 是如何被创建的。

总的来说,在 JavaScript 中我们有以下的几种方式来创建一个新的 Object

  • 原生的 Object 方法
  • 对象字面量
  • 工厂模式
  • 构造函数
  • Prototype 原型

原生 Object

1
2
3
4
5
6
7
8
9
10
11
// 使用 Object 方式创建用户对象
var user = new Object();
// 定义对象
user.name = "张三";
user.pwd = "123456";
// 定义对象的方法
user.showInfo = function () {
document.write(this.name + "-" + this.pwd + "<br />")
}
// 调用对象
user.showInfo();

对象字面量

  • 对象定义的一种简写形式
  • 简化创建包含大量属性的对象的过程
  • 在为函数传递大量可选参数时,可考虑使用对象字面量
1
2
3
4
5
6
7
8
9
10
// 使用对象字面量方式,创建用户对象
// 事实上 var user = {};
// 即为 var user = new Object();
var user = {
name: "张三",
pwd: "123456",
showInfo: function() {
document.write(this.name + "-" + this.pwd + "<br />");
}
}

除此之外,

var arr = [] 其实是 var arr = new Array() 的语法糖,

function Foo() {...} 其实是 var Foo = new Function {...} 的语法糖

工厂模式

  • 软件工程领域的一种设计模式
  • 抽象了创建对象的过程
  • 通过函数封装创建对象的细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用工厂模式创建用户对象
function createUser(name, pwd) {
var user = new Object();
// 定义对象的属性
user.name = name;
user.pwd = pwd;
// 定义对象的方法
user.showInfo = function() {
document.write(this.name + "-" + this.pwd + "<br />");
}
return user;
}

// 创建用户对象
// 弊端1: 看不出来数据类型
var user1 = createUser("张三", "123456");
var user2 = createUser("李四", "654321");

构造函数 (!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用构造函数方式创建用户对象
function User(name, pwd) {
// 定义对象的属性
this.name = name;
this.pwd = pwd;
// 定义对象的方法
this.showInfo = function() {
document.write(this.name + "-" + this.pwd + "<br />");
}
// 构造函数这里默认有一个 return this; 只不过我们不写了
}

// 创建用户对象
// 弊端1: 看不出数据类型
var user1 = new User("张三", "123456");
var user2 = new User("李四", "654321");

构造函数也就是一个函数,只不过它于普通的函数又有点不同:

  • 没有显示的创建对象;
  • 直接将属性和方法赋给this
  • 没有return语句;

原型 prototype

  • 每个函数都有一个 prototype (原型) 属性
  • 其是一个指针,指向一个对象 (Object)
  • 这个对象的用途是包含可以由特定类型的所有实例的属性和方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用构造函数方式创建用户对象
function User(name, pwd) {
// 定义对象的属性
this.name = name;
this.pwd = pwd;
}

// 原型对象
User.prototype.showInfo = function() {
document.write(this.name + "-" + this.pwd + "<br />");
}

// 创建用户对象(同上)
// 弊端1: 看不出数据类型
var user1 = new User("张三", "123456");
var user2 = new User("李四", "654321");

上述用构造函数+prototype这种方法构造对象,也叫做用混合方式构造对象,即

  • 构造函数:来写函数的属性
  • 原型 prototype:来写函数的方法

继承

关于 prototype 和继承

上一节里我们用到了 prototype,那么 prototype 到底是什么呢?

  • prototype 属性包含一个对象 (prototype 对象)
  • 所有实例对象需要共享的属性和方法,都放在这个对象里
  • 实例对象一旦创建,将自动引用 (共享) prototype 对象的属性和方法
  • prototype 对象好像是实例对象的原型,而实例对象则好像“继承”了 prototype 对象一样

几种继承的方式

更改 prototype 指向

常见的方法

  1. Student.prototype = new Person()
  2. Studentprototype 指向一个 Person 的实例,所有“学生”的实例,就能继承 Person
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person() {
this.foot = 2;
this.head = 1;
}

function Student(name, no) {
this.name = name;
this.no = no;
}

// 通过 prototype 属性实现继承
Student.prototype = new Person();
// Student 的 prototype 属性指向一个 Person 实例的写法,
// 相当于完全删除了 prototype 原先的值,赋了一个新值

Student.prototype.constructor = Student;
// 每个实例的 prototype 对象都有一个 constructor 属性
// 直接通过原型链继承后,会发生“继承链紊乱”,需要手动理顺

注意

如果替换了 prototype 对象,即 o.prototype = {};

要为新的 prototype 对象加上 constuctor 属性,并将这个属性指回原来的构造函数,即 o.prototype.constructor = o;

直接继承 prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {}
Person.prototype.foot = 2;
Person.prototype.head = 1;

function Student(name, no) {
this.name = name;
this.no = no;
}

// 通过直接继承 prototype 属性来继承
Student.prototype = Person.prototype
// 手动理顺一下继承链
Student.prototype.constructor = Student;

这样继承的优点在于效率比较高 (不用执行和建立 Person 的实例了),

缺点Student.prototypePerson.prototype 现在指向了同一个对象,任何对 Student.prototype 的修改,都会反映到 Person.prototype

利用空对象作为中介

为了解决上述更改 Student.prototype 都会反映到 Person.prototype 的问题,我们可以利用空对象作为中介来进行继承。其优点在于:

  • 空对象,几乎不占用内存
  • 修改 Studentprototype 对象,不会影响到 Personprototype 对象
  • 可以封装成函数
1
2
3
4
5
6
7
// F 为空对象,几乎不占用内存
var F = function () {};
F.prototype = Person.prototype;
Student.prototype = new F();
// 由于 Student 的 constructor 是 Person,则还需要手动理顺继承链
var stu1 = new Student("张三", "s001");
Student.prototype.constructor = Student;

将上述代码封装成一个函数,即

1
2
3
4
5
6
function extend(Child, Parent) {
var F = function() {}
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}

(有 ES6 的 extends 那味儿了)

等到需要用到继承的时候,我们便可以直接通过 extend(Student, Person) 来实现了,省去了不少工夫。

构造函数绑定来进行继承

为什么要绑定?

假设有以下代码,提问下面的 alert 会弹出什么结果?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() {
this.foot = 2;
this.head = 1;
this.favorColor = ["red", "yellow"]; // **
}
function Student(name, no) {
this.name = name;
this.no = no;
}

Student.prototype = new Person();
var stu1 = new Student("张三", "s001");
stu1.favorColor.push("blue");

// 以下代码弹出什么结果?
var stu2 = new Student("李四", "s002");
alert(stu2.favorColor);

答案为 red, yellow, blue,而不是 red, yellow,为什么会这样呢?

原因是在这里的所有实例即 stu1stu2,共享了父函数的非基本类型属性,如数组对象,一个实例对属性的修改会影响所有实例。

构造函数绑定
  • 在子类型构造函数的内部调用父类型构造函数
  • 通过 call()apply() 方法

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person() {
this.foot = 2;
this.head = 1;
this.favorColor = ["red", "yellow"];
}
function Student(name, no) {
// 将父对象的构造函数,绑定在子对象上
// 借调父类的构造函数
Person.call(this);
this.name = name;
this.no = no;
}

var stu1 = new Student("张三", "s001");
stu1.favorColor.push("blue");
alert(stu1.foot);
alert(stu1.head);
alert(stu1.favorColor); // red, yellow, blue

var stu2 = new Student("李四", "s002");
alert(stu2.favorColor); // red, yellow

组合继承

上述代码只能实现属性的继承,那么如何对方法也进行继承呢?我们可以用组合继承的方法。

组合继承也叫伪经典继承:

  • 其将原型链继承和构造函数继承组合在一块
  • 原型链实现对原型属性和方法的继承
  • 借用构造函数实现对实例属性的继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person() {
this.foot = 2;
this.head = 1;
this.favorColor = ["red", "yellow"];
}
Person.prototype.sayColor = function() {
alert("Hello, 我最爱的颜色是: " + this.favorColor);
}

function Student(name, no) {
// 使用构造函数绑定,实现了对父类属性的继承
Person.call(this);
this.name = name;
this.no = no;
}

//原型链继承: 继承对象的属性和方法
Student.prototype = new Person();
Student.prototype.constructor = Student;

拷贝继承

通过对父类原型的属性遍历拷贝,也能把父类的属性放到子类上。简单且暴力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Student(name, no) {
this.name = name;
this.no = no;
}

// 拷贝继承
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
}

extend2(Student, Person);
var stu1 = new Student("张三", "s001");

原型链

原型规则

了解以上继承知识之后,我们便能将原型链的概念好好梳理下了。原型有以下几点规则:

  1. 所有的引用类型,即数组对象函数,都具有对象特性,即可自由扩展属性 (除了 null)。
  2. 所有的引用类型,都有一个 __proto__ 属性,属性值是一个普通的对象。
  3. 所有的函数,都有一个 prototype 属性,属性值也是一个普通的对象。
  4. 所有的引用类型__proto__ 属性值都指向它的构造函数prototype 属性值。
  5. 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的 __proto__ (即它的构造函数的 prototype) 中寻找。
  6. 这样为了得到某个属性,沿着其 __proto__ 属性进行寻找的链条,我们叫做原型链

instanceof

f instanceof Foo 可用来判断元素 f 是否为 Foo 的原型,

其判断逻辑就是:对 f__proto__ 一层一层往上,看是否能对应到 Foo.prototype,即

f.__proto__ = Foo.prototypef.__proto__.__proto__... = Foo.prototype

移动端Web开发笔记

几个概念

视口

视口 viewport 就是浏览器显示页面内容的屏幕区域。视口可以分为布局视口视觉视口理想视口

布局视口 layout viewport

  • 一般移动设备的浏览器都默认配置了一个布局视口,用于解决早期的 PC 端页面在手机上显示的问题;
  • iOS、Android 基本都将这个视口分辨率设置为 980px,所以 PC 上的网页大多都能在手机上呈现,只不过元素看上去很小,一般默认可以通过手动缩放网页。

layout viewport

视觉视口 visual viewport

  • 字面意思,它是用户正在看到的网站的区域。注意:是网站的区域。
  • 我们可以通过缩放去操作视觉视口,但不会影响布局视口,布局视口仍保持原来的宽度。

visual viewport

理想视口 ideal viewport

  • 为了使网站在移动端有最理想的浏览和阅读宽度而设定;
  • 理想视口,对设备来讲,是最理想的视口尺寸;
  • 需要手动填写meta视口标签,通知浏览器操作;
  • meta视口标签的主要目的:布局视口的宽度应该与理想视口的宽度一致,简单理解就是设备有多宽,我们的布局视口就有多宽。

meta 视口标签

1
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
属性 解释说明
width 宽度设置的是 viewport 宽度,可以设置 device-width 特殊值
initial-scale 初始缩放比,大于0的数字
maximum-scale 最大缩放比,大于0的数字
minimum-scale 最小缩放比,大于0的数字
user-scalable 用户是否可以缩放,yesno(1或0)

标准的视口设置

  • 视口宽度和设备保持一致
  • 视口的默认缩放比例为 1.0
  • 不允许用户自行缩放
  • 最大允许的缩放比例 1.0
  • 最小允许的缩放比例 1.0

二倍图

物理像素 & 物理像素比

  • 真实开发的时候,1px 不是一定等于1个物理像素的;
  • PC 端页面,1px 基本等于1个物理像素,但是移动端就不尽相同;
  • 一个px能显示的物理像素点的个数,成为物理像素比或屏幕像素比。
设备 尺寸(英寸) 开发尺寸(px) 物理像素比
iPhone 4/4S 3.5 320*480 2.0
iPhone 5/5s/5c 3.5 320*568 2.0
HTC One M8 4.5 360*640 3.0
iPhone 6/7/8 4.7 375*667 2.0
iPhone 6 Plus 5.5 414*736 3.0
Nexus 5x 5.2 411*731 2.6
iPad Mini 7.9 768*1024 1.0

多倍图

  • 对于一张 50px * 50px 的图片,在手机 Retina 屏中打开。按照刚才的物理像素比会放大倍数,这样会造成图片模糊。
  • 在标准的 viewport 设置中,使用倍图来提高图片质量,解决在高清设备中的模糊问题。
  • 通常使用二倍图,因为 iPhone 6/7/8 的影响。但是现在还存在3倍图、4倍图的情况,这个看实际的开发需求。
  • 背景图片也要注意缩放的问题。
1
2
3
4
5
6
7
8
9
10
/* 在 iPhone 8 下面 */
img {
/* 原始图片 100px*100px */
width: 50px;
height: 50px;
}
.box {
/* 原始图片 100px*100px */
background-size: 50px 50px;
}

移动端开发技术

解决方案

移动端浏览器

以 webkit 为主

CSS 初始化 normalize.css

移动端 CSS 初始化可以使用 necolas 的 normalize.css (github 链接). 原因是:

  • 它保护了有价值的默认值
  • 它修复了浏览器的bug
  • 它是模块化的
  • 它拥有详细的文档

CSS3 盒子模型 box-sizing

  • 传统模式宽度计算:盒子的宽度 = CSS中设置的width + border + padding
  • CSS3盒子模型: 盒子的宽度 = CSS中设置的宽度width里面包含了border和padding
    也就是说,我们CSS3中的盒子模型,padding 和 border 不会撑大盒子了

特殊样式设置

1
2
3
4
5
6
7
8
9
/* CSS3盒子模型 */
box-sizing: border-box;
-webkit-box-sizing: border-box;
/* 点击高亮我们需要清除,设置为 transparent 完全透明 */
-webkit-tap-highlight-color: transparent;
/* 清除移动端浏览器默认的外观,在 iOS 里加上这个属性才能给按钮和输入框自定义样式 */
-webkit-appearance: none;
/* 禁用长按页面时的弹出菜单 */
img, a { -webkit-touch-callout: none; }

常见布局

移动端技术选型

移动端布局和PC端有所区别

  1. 单独制作移动端页面 (主流)

    • 流式布局 (百分比布局)
    • flex 弹性布局 (推荐)
    • less + rem + 媒体查询布局
    • 混合布局
  2. 响应式页面兼容移动端 (其次)

    • 媒体查询
    • bootstrap
    • ……

流式布局 (百分比布局)

  • 流式布局,就是百分比布局,也称非固定像素布局;
  • 通过将盒子的宽度设置成百分比,来根据屏幕的宽度进行伸缩,不受固定像素的限制,内容向两侧填充;
  • 流式布局方式是移动web开发中比较常见的布局方式。
  • max-width 最大宽度 (max-height 最大高度)
  • min-width 最小宽度 (min-height 最小高度)

flex 布局

布局原理

就是通过给父盒子添加 flex 属性,来控制子盒子的位置和排列方式。

flex-direction 设置主轴的方向

flex-direction 属性决定主轴的方向 (即项目的排列方向)
注意:主轴和侧轴是会变化的,就看 flex-direction 设置谁为主轴,剩下的就是侧轴。主元素是跟着主轴来进行排列的。

属性值 说明
row 默认值从左到右
row-reverse 从右到左
column 从上到下
column-reverse 从下到上
justify-content 设置主轴上子元素的排列方式

justify-content 属性定义了项目在主轴上的对齐方式。

属性值 说明
flex-start 默认值,从头部开始,如果主轴是x轴,则从左到右
flex-end 从尾部开始排列
center 在主轴居中对齐
space-around 平分剩余空间
space-between 先两边贴边,再平分剩余空间
flex-wrap 子元素是否换行

默认情况下,项目都排在一条线 (又称“轴线”) 上。flex-wrap 属性定义flex布局默认是不换行的。

属性值 说明
nowrap 默认值,不换行
wrap 换行
align-items 设置侧轴上的子元素排列方式 (单行)

该属性是控制子项在侧轴 (默认是y轴) 上的排列方式,在子项为单行的时候使用。

属性值 说明
flex-start 从上到下
flex-end 从下到上
center 挤在一起居中 (垂直居中)
stretch 拉伸 (默认值)
align-content 设置侧轴上的子元素排列方式 (多行)

设置子项在侧轴上的排列方式,并且只能用于子项出现换行的情况 (多行),在单行下是没有效果的。

属性值 说明
flex-start 默认值在侧轴的头部开始排列
flex-end 在侧轴的尾部开始排列
center 在侧轴中间显示
space-around 子项在侧轴平分剩余空间
space-between 子项在侧轴先分布在两头,再平分剩余空间
stretch 设置子项元素高度平分父元素高度
align-itemsalign-content 的区别
  • align-items 适用于单行情况下,只有上对齐、下对齐、居中和拉伸;
  • align-content 适用于多行的情况下 (单行情况下无效),可以设置上对齐、下对齐、居中、拉伸以及平均分配剩余空间等属性值。
  • 总结:单行找align-items,多行找align-content

rem 适配布局

rem 基础
  • em 单位,在当前元素设置 font-size 时,以当前元素为参照;在当前元素未设置 font-size 时,以父元素为参照。
  • rem (root em) 是一个相对单位,类似于 em,em 是父元素字体大小,不同的是 rem 的基准是相对于 HTML 元素的字体大小。
    比如,根元素 (HTML) 设置 font-size: 12px,非根元素设置 width: 2rem,则换成 px 表示就是 24px。
  • em 是相对于父元素的字体大小来说的,rem是相对于 HTML 元素的字体大小来说的。
媒体查询
什么是媒体查询?

媒体查询 (Media Query) 是 CSS3 的新语法。

  • 使用 @media 查询,可以针对不同的媒体类型定义不同的样式;
  • @media 可以针对不同的屏幕尺寸设置不同的样式
  • 当你重置浏览器大小的过程中,页面也会根据浏览器的宽度和高度重新渲染页面;
  • 目前针对很多苹果手机、安卓手机、平板等设备,都用得到媒体查询。
语法规范
1
2
3
@media mediatype and|not|only (media feature) {
CSS-Code;
}
  • @media 开头,注意@符号
  • mediatype 为媒体类型
  • 关键词 andnotonly
  • media feature 媒体特性,必须有小括号包含

mediatype 媒体类型
将不同的终端设备划分为不同的类型,称为媒体类型

解释说明
all 用于所有设备
print 用于打印机和打印预览
screen 用于电脑屏幕、平板电脑、智能手机等

关键字:and/not/only
关键字将媒体类型或多个媒体特性连接到一起作为媒体查询的条件。

  • and:可以将多个媒体特性连接到一起,相当于“且”的意思。
  • not:排除某个媒体类型,相当于“非”的意思,可以省略。
  • only:指定某个特定的媒体类型,可以省略。

media feature 媒体特性
每种媒体类型都具有各自不同的特性,根据不同媒体类型的媒体特性设置不同的展示风格。我们暂且了解三个。

解释说明
width 定义输出设备中页面可见区域的宽度
min-width 定义输出设备中页面最小可见区域宽度
max-width 定义输出设备中页面最大可见区域宽度
1
2
3
4
5
6
7
8
9
10
11
@media screen and (max-width: 800px) {
body {
background-color: pink;
}
}

@media screen and (max-width: 500px) {
body {
background-color: purple;
}
}
引入资源

当样式比较繁多的时候,我们可以针对不同的媒体使用不同的样式表。例如对于不同的屏幕尺寸,调用不同的 CSS 文件。
语法规范:

1
<link rel="stylesheet" media="mediatype and|not|only (media feature)" href="mystylesheet.css">
rem 适配方案
原理
  1. 让一些不能等比自适应的元素,达到当设备尺寸发生改变的时候,等比例适配当前设备;
  2. 使用媒体查询根据不同设备按比例设置 html 的字体大小,然后页面元素使用 rem 做尺寸单位。当 html 字体大小变化,元素尺寸也会发生变化,从而达到等比缩放的适配。
技术方案
  1. less + 媒体查询 + rem
  2. flexible.js (阿里团队开源的一个库) + rem

nuxt.js个人使用笔记

Nuxt.js 简介

  • 随着工程量的不断增加,业务处理复杂度提高,传统架构的项目整体加载速度可能会越来越慢;与此同时,SPA 在内的组件化开发对 SEO 也非常不友好。
  • 现在很多应用就开始使用 SSR 来解决这两个问题,但采用 SSR 在另一方面也给服务器增加了压力。
  • Nuxt.js 便是一个支持 SSR 的前端框架,基于 Vue.js,通过对客户端 / 服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI 渲染。

官网:https://www.nuxtjs.cn/guide

创建 Nuxt 项目

安装

Nuxt.js 团队创建了脚手架工具 create-nuxt-app,这里直接使用 npx 安装。
期间会提示项目名称、UI 框架、Web 框架、SSR 模式还是单页面模式等的选择,根据自己的需要进行选择。

1
npx create-nuxt-app 项目名

加载

等项目 build 完成,进入项目根目录下启动项目。

1
npm run dev

查看

启动默认端口为3000,在启动完成会给出主页的url,在浏览器输入:

1
http://localhost:3000

目录结构

1
2
3
4
5
6
7
8
9
10
.nuxt       打包的项目
assets 存放资源文件 (图片, 默认 CSS 代码等)
components 组件文件
layouts 布局目录
middleware 中间件
pages 默认路由对应的页面
plugins 插件配置文件
static 静态文件 (也可以存放图片等资源)
store Vuex 文件
nuxt.config.js 配置文件 (vue.config.js 相似)

Nuxt.js 使用

CSS 相关

全局引入

  1. 在 assets 目录下新建 css/global.css 文件,写入如下内容:
    1
    2
    3
    body {
    background-color: red;
    }
  2. 然后在 nuxt.config.js 的 css 下修改为下面配置:
    1
    2
    3
    css: [
    '@/assets/css/global.css'
    ],

注意:Nuxt 团队给出了 nuxt.config.js 中 css 项的加载顺序,是按照 css 的扩展文件名来排序的,而不是单纯的只看前后顺序。
3. 运行项目,可以看到所有项目背景都为红色了。

路由过渡动画

Nuxt.js 使用 Vue.js 的组件来实现路由切换时的过渡动效。
修改上面建的 assets/css/global.css 文件,添加下面样式:

1
2
3
4
5
6
.page-enter-active, .page-leave-active {
transition: opacity .5s;
}
.page-enter, .page-leave-active {
opacity: 0;
}

再次点击跳转,便有了动画效果。

路由

Nuxt 默认会监听 pages 目录下的文件变化,新建 .vue 文件时会自动添加到路由中,路由模式便是文件的位置信息,可在 .nuxt/routes.json 中查看。

基础路由

pages的目录结构:

1
2
3
4
5
pages/
--| user/
-----| index.vue
--| index.vue
--| test.vue

routes.json自动生成的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'test',
path: '/test',
component: 'pages/test.vue'
},
{
name: 'user',
path: '/user',
component: 'pages/user/index.vue'
}
]

路由跳转

1
2
3
<template>
<nuxt-link to="/">首页</nuxt-link>
</template>

动态路由

需要创建对应的以下划线作为前缀的 Vue 文件或目录:

1
2
3
4
pages/
--| users/
-----| _id.vue
--| index.vue

routes.json 自动生成的配置:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'users-id',
path: '/users/:id?',
component: 'pages/users/_id.vue'
}
]

users-id 的路由路径带有 :id? 参数,表示该路由是可选的,可在跳转时传递参数。

1
2
3
<template>
<nuxt-link to="/users/123">首页</nuxt-link>
</template>

users/_id.vue 页面中获取参数:

1
let param = this.$route.params.id;

嵌套路由

嵌套路由,可在页面中嵌套子页面,可用于 table 切换之类的场景。

1
2
3
4
5
pages/
--| users/
-----| _id.vue
-----| index.vue
--| users.vue

routes.json 自动生成的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
path: '/users',
component: 'pages/users.vue',
children: [
{
path: '',
component: 'pages/users/index.vue',
name: 'users'
},
{
path: ':id',
component: 'pages/users/_id.vue',
name: 'users-id'
}
]
}
]

嵌套路由需在父容器中使用 nuxt-child 显示子页面,默认显示 users/index.vue,切换子页面同样使用:nuxt-link 。

1
<nuxt-child keep-alive :foobar="123"></nuxt-child>

中间件

中间件允许您定义一个自定义函数运行在一个页面或一组页面渲染之前,每一个中间件应放置在 middleware 目录下。

比如我可以利用中间件,来进行路由跳转时的验证:

  1. 新建middleware/auth.js文件,写入如下内容:
1
2
3
4
export default function (context) {
console.log("start");
context.userAgent = process.server ? context.req.headers['user-agent'] : navigator.userAgent;
}
  1. nuxt.config.js文件中添加:
1
2
3
router: {
middleware: 'auth'
}
  1. 再次运行,便可看到每次打开页面都会打印日志。

布局

默认布局

可通过添加 layouts/default.vue 文件来扩展应用的默认布局。
但一定要添加显示页面的主体内容。

1
2
3
4
5
6
<template>
<div>
<div>标题</div>
<nuxt />
</div>
</template>

错误页面

新建layouts/error.vue文件,添加如下内容,样式可自己修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="container">
<h1>404</h1>
<h1 v-if="error.statusCode === 404">页面不存在</h1>
<h1 v-else>应用发生错误异常</h1>
<nuxt-link to="/">首 页</nuxt-link>
</div>
</template>

<script>
export default {
props: ['error'],
layout: 'blog' // 你可以为错误页面指定自定义的布局
}
</script>
<style>
.container {
width: 20%;
margin: 50px auto;
}
</style>

异步数据

asyncData

asyncData 方法会在组件 (限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用。

在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你可以利用 asyncData 方法来获取数据,Nuxt.js 会将 asyncData 返回的数据融合组件 data 方法返回的数据一并返回给当前组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
import axiox from 'axios';
export default {
async asyncData({params}) {
return axiox.get("http://localhost/GetData/" + params.id)
.then(res => {
console.log(res.data.title)
return {title: res.data.title}
});
},
data() {
return {
title: 'NG'
}
}
}
</script>

async / await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
import axiox from 'axios';
export default {
async asyncData({params}) {
const { data } = await axiox.get("http://localhost/GetData/" + params.id);
console.log(data);
return {title: data.title}
},
data() {
return {
title: 'NG'
}
}
}
</script>

回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
import axiox from 'axios';
export default {
asyncData({params},callback) {
axiox.get("http://localhost/GetData/" + params.id)
.then(res => {
console.log(res.data.title)
callback(null, { title: res.data.title })
});
},
data() {
return {
title: 'NG'
}
}
}
</script>

vuex学习笔记

概述

组件之间共享数据的方式

父向子传值:v-bind属性绑定

子向父传值:v-on事件绑定

兄弟组件之间共享数据:EventBus

  • $on接收数据的那个组件

  • $emit发送数据的那个组件

但上述方式只适合小范围的共享,因此就引入了 vuex

什么是 vuex?

vuex 是实现组件全局状态(数据)管理的一种机制

Redux

虽然上面是 redux 的图,但换成 vuex 其实几乎一样

没有 vuex:要子向父、父再向子传递数据,才能进行共享

使用 vuex:共享数据统一放入store

使用 vuex 统一管理状态的好处

  1. 能够在 vuex 中集中管理共享的数据,易于开发和后期维护;
  2. 能够高效地实现组件之间的数据共享,提高开发效率;
  3. 存储在 vuex 中的数据都是响应式的,能够实时保持数据与页面的同步。

什么数据适合存储到 vuex 中

一般情况下,只有组件之间共享的数据,才有必要存储到 vuex 中;对于组件中的私有数据,依旧存储在组件自身的data中即可。

vuex 的基本使用

安装 vuex 依赖包

1
npm install vuex --save

导入 vuex 包

1
2
import Vuex from 'vuex'
Vue.use(Vuex)

创建 store 对象

1
2
3
4
const store = new Vuex.Store({
// state 中存放的就是全局共享的数据
state: { count: 0 }
})

将 store 对象挂载到 vue 实例中

1
2
3
4
5
6
7
8
new Vue({
el: '#app',
render: h => h(app),
router,
// 将创建的共享数据对象,挂载到 Vue 实例中
// 所有的组件,就可以直接从 store 中获取全局的数据了
store
})

vuex 的核心概念

核心概念概述

Vuex 中的主要核心概念如下:

  • State
  • Mutation
  • Action
  • Getter

State

State 提供唯一的公共数据源,所有共享的数据都要统一放到 Store 的 State 中进行存储。

1
2
3
4
// 创建 store 数据源,提供唯一的公共数据
const store = new Vuex.Store({
state: { count: 0}
})
  1. 组件访问 State 中数据的第一种方式
1
this.$route.state.全局数据名称
  1. 组件访问 State 中数据的第二种方式
1
2
// ① 从 vuex 中按需导入 mapState 函数
import { mapState } from 'vuex'

通过刚才导入的mapState函数,将当前组件需要的全局数据,映射为当前组件的computed计算属性:

1
2
3
4
// ② 将全局数据,映射为当前组件的计算属性
computed: {
...mapState(['count'])
}

Mutation

vue 非常不推荐直接修改 store 里面的数据,而是推荐使用 mutation 修改全局数据。

mutation用于变更 store 中的数据。

  1. 只能通过 mutation 变更 store 数据,不可以直接操作 store 中的数据

  2. 通过这种方式虽然操作起来繁琐一些,但是可以集中监控所有数据的变化。

触发 mutations 的第一种方式
1
2
3
4
5
6
7
8
9
10
11
12
// 定义 Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
add(state) {
// 变更状态的操作
state.count++
}
}
})
1
2
3
4
5
6
7
// 触发 Mutation
methods: {
handle1() {
// 触发 mutations 的第一种方式
this.$store.commit('add');
}
}

mutations 中的函数中的第一个形参,永远是 state。mutations 中的函数也可以传参

1
2
3
4
5
6
7
8
9
10
11
12
// 定义 Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
addN(state, step) {
// 变更状态的操作
state.count += step
}
}
})
1
2
3
4
5
6
7
// 触发 Mutation
methods: {
handle2() {
// 在调用 commit 函数,触发 mutations 时携带参数
this.$store.commit('addN', 3);
}
}
触发 mutations 函数的第二种方式

this.$store.commit()是触发 mutations 的第一种方式,触发 mutations 的第二种方式

1
2
// ① 从 vuex 中按需导入 mapMutations 函数
import { mapMutations } from 'vuex'

通过刚才导入的mapMutations函数,将需要的 mutations 函数,映射为当前组件的methods方法:

1
2
3
4
// ② 将指定的 mutations 函数,映射为当前组件的 methods 函数
methods: {
...mapMutations(['add', 'addN'])
}

注意:mutations中不要执行异步操作。

action

Action 用于处理异步任务。

如果通过异步操作变更数据,必须通过 Action,而不能使用 Mutation,但是在 Action 中还是要通过触发 Mutation 的方式间接变更数据。

触发 actions 的第一种方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义 Action
const store = new Vuex.Store({
// ...省略其他代码
mutations: {
add(state) {
state.count++
}
}
actions: {
addAsync(context) {
setTimeout(() => {
context.commit('add')
}, 1000)
}
}
})
1
2
3
4
5
6
7
// 触发 Action
methods: {
handle() {
// 触发 actions 的第一种方式
this.$store.dispatch('addAsync')
}
}

Action 中函数的第一个形参必然是 context,context 为一个 vue 实例。

注意规范写法:改变数据使用 commit触发 mutation,异步操作使用 dispatch触发 action。

触发 action 异步任务,也可以携带参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义 Action
const store = new Vuex.Store({
// ...省略其他代码
mutations: {
addN(state, step) {
state.count += step
}
}
actions: {
addNAsync(context, step) {
setTimeout(() => {
context.commit('addN', step)
}, 1000)
}
}
})
1
2
3
4
5
6
7
// 触发 Action
methods: {
handle() {
// 在调用 dispatch 函数,触发 actions 时携带参数
this.$store.dispatch('addNAsync', 5)
}
}
触发 action 的第二种方式

this.$store.dispatch()是触发 actions 的第一种方式,触发 actions 的第二种方式:

1
2
// ① 从 vuex 中按需导入 mapActions 函数
import { mapActions } from 'vuex'

通过刚才导入的mapActions函数,将需要的 actions 函数,映射为当前组件的 methods 方法:

1
2
3
4
// ② 将指定的 actions 函数,映射为当前组件的 methods 函数
methods: {
...mapActions(['addASync', 'addNASync'])
}

Getter

Getter 用于对 Store 中的数据进行加工处理形成新的数据。

  1. Getter 可以对 Store 中已有的数据加工处理之后形成新的数据,类似 Vue 的 computed计算属性;
  2. Store 中的数据发生变化,Getter 的数据也会跟着变化。
1
2
3
4
5
6
7
8
9
10
11
// 定义 Getter
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
showNum: state => {
return '当前最新的数量是【' + state.count + '】'
}
}
})

使用 getters 的第一种方式

1
this.$store.getters.名称

使用 getters 的第二种方式

1
2
3
4
5
import { mapGetters } from 'vuex'

computed: {
...mapGetters(['showNum'])
}

实现table在页面中居中显示

方法1

将table的margin设置为auto,在该方法下必须指定table的宽度

CSS样式中

1
2
3
4
5
6
7
8
9
* {
margin: 0;
padding: 0;
}
table {
margin: auto; /* !!! */
width: 100%;
border: 1px solid black;
}

方法2 (不推荐)

使用align="center"来进行div内容的居中,

此使用方法已在 XHTML 1.0 Strict DTD 中放弃支持。

1
2
3
4
5
6
<table style="width: 50%; " align="center">   
<tr>
<td>&nbsp;样例1</td>
<td>&nbsp;样例2</td>
</tr>
</table>

切换不同版本的node

查看目前已有的node

如果在安装nvm之前,系统已经安装了Node.js,那么这个版本的Node.js将会成为system版本。

此时,在Terminal里输入:nvm ls,查看都安装了哪些node版本,会出现如下返回结果:

1
2
3
4
->       system
iojs -> N/A (default)
node -> stable (-> N/A) (default)
unstable -> N/A (default)

上面返回的内容,代表,此时只有系统原来带的node版本,没有通过nvm安装任何的node版本。

安装其它版本的node

安装最新版本的node:

1
nvm install node

安装特定版本的node:

1
nvm install 10.15.3

其中,10.15.3为想要安装的node的版本号。

可以查看目前有哪些node可以安装:

1
nvm ls-remote

安装一个node的新版本以后,通过指令 nvm ls 查看会返回如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
->     v10.15.3
system
default -> 10.15.3 (-> v10.15.3)
node -> stable (-> v10.15.3) (default)
stable -> 10.15 (-> v10.15.3) (default)
iojs -> N/A (default)
unstable -> N/A (default)
lts/* -> lts/dubnium (-> v10.15.3)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.16.0 (-> N/A)
lts/dubnium -> v10.15.3

可以发现,除了system以外,还出现了一个v10.15.3,而且自动应用了这个v10.15.3版本。

node版本切换

使用如下指令切换node版本:

1
nvm use system

返回如下内容,代表已经切换到system版本:

1
Now using system version of node: v8.11.3 (npm v6.9.0)

输入如下指令,切换到刚刚安装的版本:

1
nvm use 10.15.3

返回如下内容,代表已经切换到10.15.3版本:

1
Now using node v10.15.3 (npm v6.4.1)