diff --git a/baidusitemap.xml b/baidusitemap.xml index 8ac29b9a..dfea27c2 100644 --- a/baidusitemap.xml +++ b/baidusitemap.xml @@ -2,7 +2,7 @@ https://aadonkeyz.com/posts/ef1ff28e/ - 2023-11-11 + 2023-12-06 https://aadonkeyz.com/posts/1bc69df4/ @@ -229,7 +229,7 @@ 2021-01-23 - https://aadonkeyz.com/posts/9c2b83ad/ + https://aadonkeyz.com/posts/2317527/ 2021-01-23 @@ -237,7 +237,7 @@ 2021-01-23 - https://aadonkeyz.com/posts/2317527/ + https://aadonkeyz.com/posts/9c2b83ad/ 2021-01-23 @@ -269,11 +269,11 @@ 2020-12-31 - https://aadonkeyz.com/posts/9393c5c/ + https://aadonkeyz.com/posts/2798ec73/ 2020-12-12 - https://aadonkeyz.com/posts/2798ec73/ + https://aadonkeyz.com/posts/9393c5c/ 2020-12-12 @@ -317,19 +317,19 @@ 2020-12-12 - https://aadonkeyz.com/posts/22d36175/ + https://aadonkeyz.com/posts/45b9746d/ 2020-12-12 - https://aadonkeyz.com/posts/45b9746d/ + https://aadonkeyz.com/posts/22d36175/ 2020-12-12 - https://aadonkeyz.com/posts/1955d066/ + https://aadonkeyz.com/posts/3557a152/ 2020-12-12 - https://aadonkeyz.com/posts/3557a152/ + https://aadonkeyz.com/posts/1955d066/ 2020-12-12 diff --git a/css/main.css b/css/main.css index 88a353bf..5262e508 100644 --- a/css/main.css +++ b/css/main.css @@ -1998,7 +1998,7 @@ pre .javascript .function { width: 4px; height: 4px; border-radius: 50%; - background: #1cc1f6; + background: #0d75a7; } .links-of-blogroll { font-size: 13px; diff --git a/index.html b/index.html index e2565d6d..369a91e5 100644 --- a/index.html +++ b/index.html @@ -409,7 +409,7 @@

- + diff --git a/posts/500cdacf/index.html b/posts/500cdacf/index.html index 77b02e7a..4df6d46f 100644 --- a/posts/500cdacf/index.html +++ b/posts/500cdacf/index.html @@ -460,7 +460,7 @@

HTML标签

HTML标签大全

太多了,整理一会之后果断放弃,MDN上的介绍

-

HTML模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="简介" />
<meta name="keywords" content="关键字" />
<meta name="author" content="作者" />
<title>题目</title>
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="style.css" type="text/css" />
<style type="text/css">
/* CSS内部样式表 */
</style>
</head>
<body>
<!-- 这里放内容 -->
<script type="text/javascript" src="example.js"></script>
</body>
</html>
+

HTML模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="简介" />
<meta name="keywords" content="关键字" />
<meta name="author" content="作者" />
<title>题目</title>
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="style.css" type="text/css" />
<style type="text/css">
/* CSS内部样式表 */
</style>
</head>
<body>
<!-- 这里放内容 -->
<script type="text/javascript" src="example.js"></script>
</body>
</html>

a标签

<a>标签代表一个超链接,它有如下属性:

  • download:这个属性用于指示浏览器去下载href所指向的资源而不是进行页面跳转。如果给download属性设置一个值,这个值将是对应资源被保存成本地文件过程中的默认名称。
  • href:URL。
  • hreflang:用于指定链接资源的人类语言
  • ping:包含一个以空格分隔的 URL 列表,当跟随超链接时,将由浏览器发送带有正文的 PING 的 POST 请求,通常用于跟踪。
  • rel:该属性执行了目标对象到链接对象的关系。该值是空格分隔的列表类型值。
  • target:该属性指定在何处显示链接的资源。它的值包括:_self_blank_parent_top
  • type:该属性指定在一个 MIME type 链接目标的形式的媒体查询。其仅提供建议,并没有内置的功能。

<a>标签还有四个伪类:

  • link:具有href属性。
  • visited:被访问过。
  • hover:鼠标悬浮。
  • active:鼠标点击瞬间。
diff --git a/posts/9c2b83ad/index.html b/posts/9c2b83ad/index.html index 260938fe..0ed840c9 100644 --- a/posts/9c2b83ad/index.html +++ b/posts/9c2b83ad/index.html @@ -498,7 +498,7 @@

首先介绍 addEventListener() 方法,它的参数如下:

  • type:表示监听事件类型的字符串,需要注意的是没有 on 前缀
  • listener:作为事件处理程序的函数。
  • options(可选):一个对象。其属性如下:
    • capture:一个布尔值,默认为 false。当值为 true 时,listener 会在事件捕获阶段时被调用。
    • once:一个布尔值,默认为 false。当值为 true 时,listener 会在其被调用之后自动移除。
    • passive:一个布尔值,默认为 false。当值为 true 时,listener 内部不允许调用 event.preventDefault(),否则会抛出错误。
  • useCapture(可选):一个布尔值,默认为 false。当值为 true 时,listener 会在事件捕获阶段时被调用。

对于 optionsuseCapture 参数,它们都是该方法的第三个参数,options 是新标准,而 useCapture 是老标准。


接着介绍 removeEventListener() 方法,它的参数如下:

  • type:表示监听事件类型的字符串,需要注意的是没有 on 前缀
  • listener:作为事件处理程序的函数。
  • options(可选):一个对象。其属性如下:
    • capture:一个布尔值,默认为 false。当值为 true 时,表示要移除的 listener 是注册在事件捕获阶段的。
  • useCapture(可选):一个布尔值,默认为 false。当值为 true 时,表示要移除的 listener 是注册在事件捕获阶段的。

如果一个事件处理程序一共注册了两次,一次在事件捕获阶段,一次在事件冒泡阶段,那么这两次注册需要分别移除,两者不会互相干扰。

下面的例子用于观察 options.captureuseCapture 的效果。

-
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
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<style>
#outer, #inner {
display: block;
width: 500px;
height: 500px;
text-decoration: none;
}
#outer{
border: 1px solid red;
color: red;
}
#inner{
border: 1px solid green;
color: green;
width: 250px;
height: 250px;
margin: 125px auto;
}
</style>
</head>
<body>
<div id="outer">
outer, capture & none-capture
<div id="inner">
inner
</div>
</div>
<script type="text/javascript">
var outer = document.getElementById('outer')
var inner = document.getElementById('inner')

function captureListener1 () {
console.log('outer, capture1')
outer.removeEventListener('click', captureListener1, true)
}
function captureListener2 () {
console.log('outer, capture2')
}
function noneCaptureListener () {
console.log('outer, none-capture')
}
function innerListener () {
console.log('inner')
}

outer.addEventListener('click', captureListener1, { capture: true })
outer.addEventListener('click', captureListener2, true)
outer.addEventListener('click', noneCaptureListener)
inner.addEventListener('click', innerListener)
</script>
</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
44
45
46
47
48
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<style>
#outer, #inner {
display: block;
width: 500px;
height: 500px;
text-decoration: none;
}
#outer{
border: 1px solid red;
color: red;
}
#inner{
border: 1px solid green;
color: green;
width: 250px;
height: 250px;
margin: 125px auto;
}
</style>
</head>
<body>
<div id="outer">
outer, capture & none-capture
<div id="inner">
inner
</div>
</div>
<script type="text/javascript">
var outer = document.getElementById('outer')
var inner = document.getElementById('inner')

function captureListener1 () {
console.log('outer, capture1')
outer.removeEventListener('click', captureListener1, true)
}
function captureListener2 () {
console.log('outer, capture2')
}
function noneCaptureListener () {
console.log('outer, none-capture')
}
function innerListener () {
console.log('inner')
}

outer.addEventListener('click', captureListener1, { capture: true })
outer.addEventListener('click', captureListener2, true)
outer.addEventListener('click', noneCaptureListener)
inner.addEventListener('click', innerListener)
</script>
</body>
</html>

上例中 captureListener1captureListener2 都是注册在 outer 的捕获阶段,而 noneCaptureListenerinnerListener 分别注册在 outerinner 的冒泡阶段。并且 captureListener1 会在第一次调用后被移除。请多点击几次 inner 框,查看打印的结果。

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。这里只介绍 DOM 中的事件对象,忽略 IE 的。

无论注册事件处理程序时使用的是 DOM0 级还是 DOM2 级方法,兼容 DOM 的浏览器都会将一个 event 对象传入到事件处理程序中,这样就可以直接在函数内部访问到 event 对象了。

diff --git a/posts/ee7ae9c6/index.html b/posts/ee7ae9c6/index.html index 4c3dd494..27de86a9 100644 --- a/posts/ee7ae9c6/index.html +++ b/posts/ee7ae9c6/index.html @@ -465,11 +465,11 @@

由于 left 设置了margin-left: -100%,当 content 内容区的宽度小于 left 的宽度时,此时 left 左移的距离小于 left 自身的宽度,导致 left 并不会向上移动到与 middle 同层。简言之,布局乱了。

-
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
<div class="content">
<div class="middle"></div>
<div class="left"></div>
<div class="right"></div>
</div>

<style>
.content {
padding: 0 200px;
}
.middle {
width: 100%;
height: 80px;
float: left;
background: green;
}
.left {
width: 200px;
height: 80px;
float: left;
margin-left: -100%;
position: relative;
left: -200px;
background: red;
}
.right {
width: 200px;
height: 80px;
float: left;
margin-left: -200px;
position: relative;
right: -200px;
background: blue;
}
</style>
+
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
<div class="content">
<div class="middle"></div>
<div class="left"></div>
<div class="right"></div>
</div>

<style>
.content {
padding: 0 200px;
}
.middle {
width: 100%;
height: 80px;
float: left;
background: green;
}
.left {
width: 200px;
height: 80px;
float: left;
margin-left: -100%;
position: relative;
left: -200px;
background: red;
}
.right {
width: 200px;
height: 80px;
float: left;
margin-left: -200px;
position: relative;
right: -200px;
background: blue;
}
</style>

双飞翼布局

圣杯布局的升级版,修改 DOM 结构,然后使用margin代替padding

-
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
<div class="content">
<div class="middle">
<div class="inner-middle"></div>
</div>
<div class="left"></div>
<div class="right"></div>
</div>

<style>
.middle {
width: 100%;
height: 80px;
float: left;
}
.inner-middle {
margin: 0 200px;
height: 80px;
background: green;
}
.left {
width: 200px;
height: 80px;
float: left;
margin-left: -100%;
background: red;
}
.right {
width: 200px;
height: 80px;
float: left;
margin-left: -200px;
background: blue;
}
</style>
+
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
<div class="content">
<div class="middle">
<div class="inner-middle"></div>
</div>
<div class="left"></div>
<div class="right"></div>
</div>

<style>
.middle {
width: 100%;
height: 80px;
float: left;
}
.inner-middle {
margin: 0 200px;
height: 80px;
background: green;
}
.left {
width: 200px;
height: 80px;
float: left;
margin-left: -100%;
background: red;
}
.right {
width: 200px;
height: 80px;
float: left;
margin-left: -200px;
background: blue;
}
</style>
diff --git a/posts/ef1ff28e/index.html b/posts/ef1ff28e/index.html index dac93e13..abefd3f6 100644 --- a/posts/ef1ff28e/index.html +++ b/posts/ef1ff28e/index.html @@ -106,7 +106,7 @@ - + @@ -407,7 +407,7 @@

多行文本缩略的实现 - + @@ -464,7 +464,7 @@

多行文本缩略的实现
  1. 在非缩略状态下,通过读取每个文字的宽度,计算是否应该缩略
  2. 如果需要缩略,则使用一个 span 包裹前几行不需要缩略的文字,用另一个 span 包裹后几行需要缩略的文字
  3. 如果不需要缩略,使用 span 包裹每一个文本
  4. 如果文本更新了
    4.1 如果当前处于缩略状态,则先重置为非缩略状态,然后在跳转到步骤 1
    4.2 如果当前处于非缩略状态,则直接跳转到步骤 1

React 的示例代码如下所示:

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import React, { useState, useRef, useEffect } from 'react';

export interface AutoEllipsisProps {
text?: string;
maxLine?: number;
className?: string;
onEllipsis?: (isEllipsis: boolean) => void;
}

export default function AutoEllipsis(props: AutoEllipsisProps) {
const { text, maxLine = 1, className, onEllipsis } = props;

const [isEllipsis, setIsEllipsis] = useState(false);
const [ellipsisLineFirstTextIndex, setEllipsisLineFirstTextIndex] =
useState(0);
const [rerenderAfterReset, setRenderAfterReset] = useState(0);

const divRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (isEllipsis) {
setIsEllipsis(false);
setRenderAfterReset((pre) => pre + 1);
return;
}

if (divRef.current && text) {
const { offsetWidth } = divRef.current;

const headIndexList: number[] = [0];
let cumulativeWidth = 0;
const children = divRef.current.children as never as HTMLSpanElement[];

for (let i = 0; i < children.length; i++) {
if (cumulativeWidth + children[i].offsetWidth <= offsetWidth) {
cumulativeWidth += children[i].offsetWidth;
} else if (cumulativeWidth + children[i].offsetWidth > offsetWidth) {
cumulativeWidth = children[i].offsetWidth;
headIndexList.push(i);
}
}

if (headIndexList.length > maxLine) {
setIsEllipsis(true);
setEllipsisLineFirstTextIndex(headIndexList[maxLine - 1]);
}
}
}, [text, rerenderAfterReset]);

useEffect(() => {
onEllipsis?.(isEllipsis);
}, [isEllipsis]);

const renderText = () => {
if (!text) {
return null;
}

if (!isEllipsis) {
return text
?.split('')
.map((item, index) => <span key={index}>{item}</span>);
}

return (
<>
<span>{text.slice(0, ellipsisLineFirstTextIndex)}</span>
<span
style={{
display: 'inline-block',
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{text.slice(ellipsisLineFirstTextIndex)}
</span>
</>
);
};

return (
<div className={className} ref={divRef}>
{renderText()}
</div>
);
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import React, { useState, useRef, useEffect } from 'react';

export interface AutoEllipsisProps {
text?: string;
maxLine?: number;
className?: string;
onEllipsis?: (isEllipsis: boolean) => void;
}

export default function AutoEllipsis(props: AutoEllipsisProps) {
const { text, maxLine = 1, className, onEllipsis } = props;

const [isEllipsis, setIsEllipsis] = useState(false);
const [ellipsisLineFirstTextIndex, setEllipsisLineFirstTextIndex] =
useState(0);
const [rerenderAfterReset, setRenderAfterReset] = useState(0);

const divRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (isEllipsis) {
setIsEllipsis(false);
setRenderAfterReset((pre) => pre + 1);
return;
}

if (divRef.current && text) {
const containerWidth = divRef.current.getBoundingClientRect().width;

const headIndexList: number[] = [0];
let cumulativeWidth = 0;
const children = divRef.current.children as never as HTMLSpanElement[];

for (let i = 0; i < children.length; i++) {
const childWidth = children[i].getBoundingClientRect().width;
if (cumulativeWidth + childWidth <= containerWidth) {
cumulativeWidth += childWidth;
} else if (cumulativeWidth + childWidth > containerWidth) {
cumulativeWidth = childWidth;
headIndexList.push(i);
}
}

if (headIndexList.length > maxLine) {
setIsEllipsis(true);
setEllipsisLineFirstTextIndex(headIndexList[maxLine - 1]);
}
}
}, [text, rerenderAfterReset]);

useEffect(() => {
onEllipsis?.(isEllipsis);
}, [isEllipsis]);

const renderText = () => {
if (!text) {
return null;
}

if (!isEllipsis) {
return text
?.split('')
.map((item, index) => <span key={index}>{item}</span>);
}

return (
<>
<span>{text.slice(0, ellipsisLineFirstTextIndex)}</span>
<span
style={{
display: 'inline-block',
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{text.slice(ellipsisLineFirstTextIndex)}
</span>
</>
);
};

return (
<div className={className} ref={divRef}>
{renderText()}
</div>
);
}
diff --git a/search.xml b/search.xml index 3d7614be..8fea29f9 100644 --- a/search.xml +++ b/search.xml @@ -1,5 +1,34 @@ + + Unicode + /posts/1c0c8dfc/ + 字符和码位

+
  • 字符:又叫抽象字符,是用于组织、控制 或 表示 文本数据 的 信息单元。
  • 码位:是分配给单个字符的一个数字。U+<hex> 是码位的格式,其中 U+ 代表 Unicode 的前缀,而 <hex> 表示十六进制数字。

每一个字符都有对应的码位,如 A 对应 U+0041。但是并非所有码位都有关联字符。

+
+

Unicode 平面

+

平面是从 U+n0000U+nFFFF65536 个连续的码位,n 的取值范围从 016。平面将全部的 Unicode 码位分成了 17 个均等的组。

+
+

基本多文种平面为 U+0000U+FFFF,简称 BMP(Basic Multilingual Plane)。其余 16 个平面称为星光平面或辅助平面。

+

码元

+

码元是用于以给定编码格式,对每个字符编码的一个二进制位序列。在不同的编码格式中,一个码元有多少字节是不同的。
UTF-8 中一个码元可能有 1 - 4 个字节。UTF-16 中一个码元有 2 个字节。UTF-32 中一个码元有 4 个字节。

+
+

代理对

+

在 UTF-16 中代理对是对那些由 216 位长的码元所组成序列的单个抽象字符的表示方式。代理对中的首个值为高位代理码元,而第二个值为低位代理码元。高位代理码元从 0xD8000xDBFF 取值。低位代理码元从 0xDC000xDFFF 取值。

+
+

在 UTF-16 中,对于基本多文种平面,一个 16 位长的码元就可以解决了。但是对星光平面内的码元,如 U+1F600,需要两个 16 位长的码元才能将其表示,这就是代理对。U+1F600 对应的代理对是 0xD83D 0xDE00。转换方法如下所示。

+
function getSurrogatePair(astralCodePoint) {
let highSurrogate = Math.floor((astralCodePoint - 0x10000) / 0x400) + 0xd800;
let lowSurrogate = ((astralCodePoint - 0x10000) % 0x400) + 0xdc00;
return [highSurrogate, lowSurrogate];
}
getSurrogatePair(0x1f600); // => [0xD83D, 0xDE00]

function getAstralCodePoint(highSurrogate, lowSurrogate) {
return (highSurrogate - 0xd800) * 0x400 + lowSurrogate - 0xdc00 + 0x10000;
}
getAstralCodePoint(0xd83d, 0xde00); // => 0x1F600
+

组合符号

+

字位又称形素、字素、或符号,对一些书写系统来说是最小的构成单位。

+
+

在绝大多数情况下,单个 Unicode 字符表示单个的字位。但也存在单个字位包含一系列字符的情况。例如 å 在丹麦书写系统中是一个原子性的字位,展示它需要使用 U+0061U+030A 组合在一起。

+

组合字符会被用户认为是单个字符,但开发者必须使用 2 个码位来表示它。

+

参考文献

每个 JavaScript 开发者都应该了解的 Unicode
What every JavaScript developer should know about Unicode

+]]> + + CS + + Github+Hexo搭建个人博客 /posts/d4a0c543/ @@ -56,32 +85,19 @@ - Unicode - /posts/1c0c8dfc/ - 字符和码位
-
  • 字符:又叫抽象字符,是用于组织、控制 或 表示 文本数据 的 信息单元。
  • 码位:是分配给单个字符的一个数字。U+<hex> 是码位的格式,其中 U+ 代表 Unicode 的前缀,而 <hex> 表示十六进制数字。

每一个字符都有对应的码位,如 A 对应 U+0041。但是并非所有码位都有关联字符。

-
-

Unicode 平面

-

平面是从 U+n0000U+nFFFF65536 个连续的码位,n 的取值范围从 016。平面将全部的 Unicode 码位分成了 17 个均等的组。

-
-

基本多文种平面为 U+0000U+FFFF,简称 BMP(Basic Multilingual Plane)。其余 16 个平面称为星光平面或辅助平面。

-

码元

-

码元是用于以给定编码格式,对每个字符编码的一个二进制位序列。在不同的编码格式中,一个码元有多少字节是不同的。
UTF-8 中一个码元可能有 1 - 4 个字节。UTF-16 中一个码元有 2 个字节。UTF-32 中一个码元有 4 个字节。

-
-

代理对

-

在 UTF-16 中代理对是对那些由 216 位长的码元所组成序列的单个抽象字符的表示方式。代理对中的首个值为高位代理码元,而第二个值为低位代理码元。高位代理码元从 0xD8000xDBFF 取值。低位代理码元从 0xDC000xDFFF 取值。

+ 单例模式 + /posts/f1601c3e/ + 基础概念

单例模式结构

+
+
  1. 单例模式:保证一个类仅有一个实例,并提供一个访问该实例的全局访问点。
  2. 适用性:
    • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
    • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。
  3. 参与者: - Singleton - 定义了一个Instance操作,允许客户访问它的唯一实例。Instance是一个类操作(即它是一个静态成员方法)。 - 可能负责创建它自己的唯一实例。
-

在 UTF-16 中,对于基本多文种平面,一个 16 位长的码元就可以解决了。但是对星光平面内的码元,如 U+1F600,需要两个 16 位长的码元才能将其表示,这就是代理对。U+1F600 对应的代理对是 0xD83D 0xDE00。转换方法如下所示。

-
function getSurrogatePair(astralCodePoint) {
let highSurrogate = Math.floor((astralCodePoint - 0x10000) / 0x400) + 0xd800;
let lowSurrogate = ((astralCodePoint - 0x10000) % 0x400) + 0xdc00;
return [highSurrogate, lowSurrogate];
}
getSurrogatePair(0x1f600); // => [0xD83D, 0xDE00]

function getAstralCodePoint(highSurrogate, lowSurrogate) {
return (highSurrogate - 0xd800) * 0x400 + lowSurrogate - 0xdc00 + 0x10000;
}
getAstralCodePoint(0xd83d, 0xde00); // => 0x1F600
-

组合符号

-

字位又称形素、字素、或符号,对一些书写系统来说是最小的构成单位。

+

demo

+

基础概念中介绍的是单例模式的原理,而这个 demo 中展示的是使用代理来实现透明单例模式的例子,它们有些许不同。

-

在绝大多数情况下,单个 Unicode 字符表示单个的字位。但也存在单个字位包含一系列字符的情况。例如 å 在丹麦书写系统中是一个原子性的字位,展示它需要使用 U+0061U+030A 组合在一起。

-

组合字符会被用户认为是单个字符,但开发者必须使用 2 个码位来表示它。

-

参考文献

每个 JavaScript 开发者都应该了解的 Unicode
What every JavaScript developer should know about Unicode

+
class CreateSingleton {
constructor(name) {
this.name = name;
}
}

var Singleton = (function () {
var instance;
return function (name) {
if (!instance) {
instance = new CreateSingleton(name);
}
return instance;
};
})();

var a = new Singleton('a');
console.log(a.name); // a

var b = new Singleton('b');
console.log(b.name); // a

console.log(a === b); // true
]]> - CS + Design Pattern @@ -124,22 +140,6 @@ Blog - - 单例模式 - /posts/f1601c3e/ - 基础概念

单例模式结构

-
-
  1. 单例模式:保证一个类仅有一个实例,并提供一个访问该实例的全局访问点。
  2. 适用性:
    • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
    • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。
  3. 参与者: - Singleton - 定义了一个Instance操作,允许客户访问它的唯一实例。Instance是一个类操作(即它是一个静态成员方法)。 - 可能负责创建它自己的唯一实例。
-
-

demo

-

基础概念中介绍的是单例模式的原理,而这个 demo 中展示的是使用代理来实现透明单例模式的例子,它们有些许不同。

-
-
class CreateSingleton {
constructor(name) {
this.name = name;
}
}

var Singleton = (function () {
var instance;
return function (name) {
if (!instance) {
instance = new CreateSingleton(name);
}
return instance;
};
})();

var a = new Singleton('a');
console.log(a.name); // a

var b = new Singleton('b');
console.log(b.name); // a

console.log(a === b); // true
-]]>
- - Design Pattern - -
命令模式 /posts/78134e07/ @@ -184,22 +184,6 @@
  1. 主要有三种不同的情况
    • Creator类是一个抽象类并且不提供它所声明的工厂模式的实现。
    • Creator类是一个具体的类而且为工厂模式提供一个缺省的实现。
    • Creator类是一个抽象的类,但为工厂模式提供一个缺省的实现。
  2. 参数化工厂模式:一个工厂模式可以创建多中类型的对象。

demo

class Product1 {
constructor() {
this.type = '产品1';
}
}

class Product2 {
constructor() {
this.type = '产品2';
}
}

class Creator {
constructor(typeNum) {
if (typeNum === 1) {
return new Product1();
} else {
return new Product2();
}
}
}

let p1 = new Creator(1),
p2 = new Creator(2);

console.log(p1); // Product1 { type: '产品1' }
console.log(p2); // Product2 { type: '产品2' }
-]]>
- - Design Pattern - - - - 装饰者模式 - /posts/a708a60d/ - 基础概念

装饰者模式结构

-
-
  1. 装饰者模式:动态地给一个对象添加一些额外的职责。
  2. 适用性:
    • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
    • 处理那些可撤销的职责。
    • 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸式增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
  3. 参与者: - ComponentConcreteComponentDecorator的父类 - 定义一个对象接口,可以给这些对象动态地添加职责。 - ConcreteComponent - 定义一个对象,可以给这个对象添加一些职责。 - Decorator - 维持一个指向Component对象的指针。 - 定义一个与Component接口一致的接口。 - ConcreteDecorator - 实现添加装饰的接口。
-
-

注意事项

-
  1. 接口一致性:装饰对象的接口必须与它所装饰的对象的接口是一致的。
  2. 省略抽象的Decorator:当你仅需要添加一个装饰时,没有必要定义抽象Decorator类。
  3. 保持Component类的简单性:如果有Component类,保持这个类的简单性是很重要的,即它应集中于定义接口而不是存储数据。
  4. 改变对象外壳与改变对象内核:我们可以将Decorator看作一个对象的外壳,它可以改变这个对象的行为。另外一种方法是改变对象的内核(通过策略模式)。当需要改变的对象比较庞大时,推荐使用策略模式而不是装饰者模式。
-
-

demo

class Plane {
fire() {
console.log('发射普通子弹');
}
}

class EnforePlane {
constructor(plane) {
this.plane = plane;
}

fire() {
this.plane.fire();
console.log('发射超级导弹');
}
}

var plane = new Plane();
plane = new EnforePlane(plane);
plane.fire(); // 发射普通子弹
// 发射超级导弹
]]>
Design Pattern @@ -270,6 +254,22 @@ Data Structure and Algorithm
+ + 装饰者模式 + /posts/a708a60d/ + 基础概念

装饰者模式结构

+
+
  1. 装饰者模式:动态地给一个对象添加一些额外的职责。
  2. 适用性:
    • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
    • 处理那些可撤销的职责。
    • 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸式增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
  3. 参与者: - ComponentConcreteComponentDecorator的父类 - 定义一个对象接口,可以给这些对象动态地添加职责。 - ConcreteComponent - 定义一个对象,可以给这个对象添加一些职责。 - Decorator - 维持一个指向Component对象的指针。 - 定义一个与Component接口一致的接口。 - ConcreteDecorator - 实现添加装饰的接口。
+
+

注意事项

+
  1. 接口一致性:装饰对象的接口必须与它所装饰的对象的接口是一致的。
  2. 省略抽象的Decorator:当你仅需要添加一个装饰时,没有必要定义抽象Decorator类。
  3. 保持Component类的简单性:如果有Component类,保持这个类的简单性是很重要的,即它应集中于定义接口而不是存储数据。
  4. 改变对象外壳与改变对象内核:我们可以将Decorator看作一个对象的外壳,它可以改变这个对象的行为。另外一种方法是改变对象的内核(通过策略模式)。当需要改变的对象比较庞大时,推荐使用策略模式而不是装饰者模式。
+
+

demo

class Plane {
fire() {
console.log('发射普通子弹');
}
}

class EnforePlane {
constructor(plane) {
this.plane = plane;
}

fire() {
this.plane.fire();
console.log('发射超级导弹');
}
}

var plane = new Plane();
plane = new EnforePlane(plane);
plane.fire(); // 发射普通子弹
// 发射超级导弹
+]]>
+ + Design Pattern + +
图论算法 /posts/7de55cdc/ @@ -307,64 +307,6 @@

图、流图以及残余图

求解过程

-]]>
- - Data Structure and Algorithm - -
- - 排序算法总结 - /posts/cd65786b/ - 基本概念
-

排序算法可以分为以下两种类型:

  • 比较类非线性时间排序:通过比较来决定元素间的相对次序,其时间复杂度不能突破$O(nlogn)$
  • 非比较类线性时间排序:不通过比较来决定元素间的相对次序,其时间复杂度最低可以为$O(n)$

  • 稳定性:如果排序前后两个相等元素的相对次序不变,则算法稳定;反之算法不稳定
-
-

冒泡排序

-
  • 实现步骤(从小到大排序)
    1. 从最前面的两个元素开始,不断对相邻元素进行比较,根据情况决定是否交换相邻元素的位置,直到最后。这会将最大的元素“冒泡”到元素列的末尾位置。
    2. 步骤 1 需要被重复执行 n-1 次。但是每一趟排序时,不是全部元素都需要进行比较的。例如,对于第 m 趟排序,此时元素列最后面的 m 个元素是已经完成排序了的,所以不需要对它们进行重复的比较。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(n^2)$
  • 最好时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$
  • 稳定性:稳定
-
-
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function bubbleSort(array) {
console.log(array.join(' '));

let len = array.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (array[j] > array[j + 1]) {
[array[j], array[j + 1]] = [array[j + 1], array[j]];
}
}
console.log(array.join(' '));
}
}

bubbleSort(list);
-

冒泡排序

-

选择排序

-
  • 实现步骤(从小到大排序)
    1. 共需要进行 n-1 次循环。每次循环都会选定一个起点元素,例如,第一次循环是以第一个元素为起点。第 m 次循环是以第 m 个元素为起点。
    2. 在每次循环中,找出本次循环中从起点元素开始到最后一个元素为止最小的元素,如果这个元素正是起点元素则什么都不做,否则交换起点元素与该最小元素的位置。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(n^2)$
  • 最好时间复杂度:$O(n^2)$
  • 空间复杂度:$O(1)$
  • 稳定性:不稳定
-
-
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function selectionSort(array) {
console.log(array.join(' '));

let len = array.length;
let mixIndex;
for (let i = 0; i < len - 1; i++) {
mixIndex = i;
for (let j = i + 1; j < len; j++) {
if (array[j] < array[mixIndex]) {
mixIndex = j;
}
}
[array[mixIndex], array[i]] = [array[i], array[mixIndex]];
console.log(array.join(' '));
}
}

selectionSort(list);
-

选择排序

-

插入排序

-
  • 实现步骤(从小到大排序)
    1. 共需要进行 n-1 次循环。每次循环都会选定一个待插入元素,例如,第一次循环是以第二个元素为待插入元素。第 m 次循环是以第 m+1 个元素为待插入元素。
    2. 在每次循环中,不断将待插入元素与处于它前方的元素进行比较,找出适合它的位置并插入。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(n^2)$
  • 最好时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$
  • 稳定性:稳定
-
-
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function insertionSort(array) {
console.log(array.join(' '));

let len = array.length;
for (let i = 1; i < len; i++) {
let unsortValue = array[i];
let currentIndex = i - 1;
while (currentIndex >= 0 && array[currentIndex] > unsortValue) {
array[currentIndex + 1] = array[currentIndex];
currentIndex--;
}
array[currentIndex + 1] = unsortValue;
console.log(array.join(' '));
}
}

insertionSort(list);
-

插入排序

-

希尔排序

-
  • 实现步骤(从小到大排序)
    1. 按照步长n/2 => n/4 => ··· => 1进行循环。每次循环中根据步长对元素列进行分组,然后对这些分组执行插入排序。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(n^{1.3})$
  • 最好时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$
  • 稳定性:不稳定
-
-
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function shellSort(array) {
console.log(array.join(' '));

let len = array.length;
for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
// 内部为插入排序
for (let i = gap; i < len; i++) {
let unsortValue = array[i];
let currentIndex = i - gap;
while (currentIndex >= 0 && array[currentIndex] > unsortValue) {
array[currentIndex + gap] = array[currentIndex];
currentIndex = currentIndex - gap;
}
array[currentIndex + gap] = unsortValue;
}
console.log(array.join(' '));
}
}

shellSort(list);
-

希尔排序

-

归并排序

-
  • 实现步骤(从小到大排序)
    1. 不断地将元素列分成两个部分,然后对这两个部分递归执行归并排序。
    2. 当元素列只有 1 个元素时,停止递归。
    3. 对于被划分的两个部分,需要按照顺序将它们合并为一个整体。
  • 最坏时间复杂度:$O(nlogn)$
  • 平均时间复杂度:$O(nlogn)$
  • 最好时间复杂度:$O(nlogn)$
  • 空间复杂度:$O(n)$
  • 稳定性:稳定
-
-
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function mergeSort(array) {
let len = array.length;

if (len < 2) {
return array;
} else {
let middle = Math.floor(len / 2),
left = array.slice(0, middle),
right = array.slice(middle);

return merge(mergeSort(left), mergeSort(right));
}
}

function merge(left, right) {
let result = [];

while (left.length > 0 && right.length > 0) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}

while (left.length) {
result.push(left.shift());
}

while (right.length) {
result.push(right.shift());
}

return result;
}

mergeSort(list);
-

快速排序

-
  • 实现步骤(从小到大排序)
    1. 通过三数中值分割法,从元素列中选出最左侧,中间和最右侧的三个元素,并排序。
    2. 从步骤 1 中排序后的三个元素中选择中间元素作为枢纽元素,并交换枢纽元素和元素列倒数第二个元素的位置。
    3. 选择i = left + 1, j = right - 2
    4. 不断执行i++操作,直到i所处元素大于等于枢纽元素为止。
    5. 不断执行j--操作,直到j所处元素小于等于枢纽元素为止。
    6. 如果i < j,那么交换这两个位置上的元素,然后重复执行步骤 4、5。
    7. 如果i >= j,那么此刻位置i上的元素一定是大于等于枢纽元素的,此时交换位置i上的元素和枢纽元素的位置。交换之后,枢纽元素左侧的元素均小于等于它,而它右侧的元素均大于等于它。
    8. 不断对被枢纽元素分割开来的两个子元素列执行步骤 1 至步骤 7,直到排序完成。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(nlgn)$
  • 最好时间复杂度:$O(nlgn)$
  • 空间复杂度:$O(lgn)$ 至 $O(n)$ 之间
  • 稳定性:不稳定
-
-
-
  • 需要注意array[i]=array[j]=pivot的情况,不要陷入死循环。
  • 为什么当所遇元素与枢纽元素相等时,也要让ij停下来,并交换位置?
    这样可以确保被枢纽元素分割开来的两部分尽量均衡,减少插入排序算法总的时间复杂度。
-
-
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function quickSort(array, left, right) {
var left = typeof left !== 'number' ? 0 : left,
right = typeof right !== 'number' ? array.length - 1 : right;

if (left === right) {
return;
}

if (left + 1 === right) {
if (array[left] <= array[right]) {
return;
} else {
[array[left], array[right]] = [array[right], array[left]];
return;
}
}

let pivot = median3(array, left, right),
i = left + 1,
j = right - 2;

while (i < j) {
while (array[i] < pivot) {
i++;
}
while (pivot < array[j]) {
j--;
}
if (i < j) {
if (array[i] === array[j]) {
i++;
} else {
[array[i], array[j]] = [array[j], array[i]];
}
}
if (i >= j) {
break;
}
}

[array[i], array[right - 1]] = [array[right - 1], array[i]];

quickSort(array, left, i - 1);
quickSort(array, i + 1, right);
}

function median3(array, left, right) {
let center = Math.floor((left + right) / 2);

if (array[center] < array[left]) {
[array[left], array[center]] = [array[center], array[left]];
}
if (array[right] < array[left]) {
[array[left], array[right]] = [array[right], array[left]];
}
if (array[right] < array[center]) {
[array[center], array[right]] = [array[right], array[center]];
}

[array[center], array[right - 1]] = [array[right - 1], array[center]];

return array[right - 1];
}

quickSort(list);
-

堆排序

-
  • 实现步骤(从小到大排序)
    1. 首先将元素列转换为最大堆形式的数组。
    2. 数组中的最大元素总在 A[1] 中,把它与 A[heap.size] 进行互换,同时从堆中去掉最后一个结点(这一操作可以通过减少 heap.size 来实现)。
    3. 步骤 2 之后,最大堆的剩余结点中,原来根的孩子结点仍然是最大堆,而新的根节点可能会违背最大堆的性质。为了维护最大堆的性质,我们要做的就是通过下滤操作使其符合最大堆的性质。
    4. 重复步骤 2、3,直到 heap.size = 1。
  • 最坏时间复杂度:$O(nlgn)$
  • 平均时间复杂度:$O(nlgn)$
  • 最好时间复杂度:$O(nlgn)$
  • 空间复杂度:$O(1)$
  • 稳定性:不稳定
-
-
let list = [90, 84, 83, 88, 87, 61, 50, 70, 60, 80];

function heapSort(array) {
let heap = buildHeap(array);

for (let i = heap.size; i > 1; i--) {
[heap[i], heap[1]] = [heap[1], heap[i]];
heap.size--;
percolateDown(heap, 1);
}

return heap.slice(1);
}

function buildHeap(array) {
let heap = [];
heap.size = array.length;
for (let i = 0; i < array.length; i++) {
heap[i + 1] = array[i];
}

let currentIndex = Math.floor(heap.size / 2);
while (currentIndex > 0) {
percolateDown(heap, currentIndex);
currentIndex--;
}

return heap;
}

function percolateDown(heap, currentIndex) {
let leftSonIndex = 2 * currentIndex,
rightSonIndex = 2 * currentIndex + 1,
maxValueIndex = currentIndex;

if (leftSonIndex <= heap.size && heap[leftSonIndex] > heap[currentIndex]) {
maxValueIndex = leftSonIndex;
}

if (rightSonIndex <= heap.size && heap[rightSonIndex] > heap[maxValueIndex]) {
maxValueIndex = rightSonIndex;
}

if (currentIndex !== maxValueIndex) {
[heap[currentIndex], heap[maxValueIndex]] = [
heap[maxValueIndex],
heap[currentIndex],
];
percolateDown(heap, maxValueIndex);
}
}

console.log(heapSort(list)); // [ 50, 60, 61, 70, 80, 83, 84, 87, 88, 90 ]
-

计数排序

-
  • 实现步骤(从小到大排序)
    1. 假设元素列中的元素均处于 $[0, k]$ 区间内,$k$ 为某个整数。
    2. 准备一个长度为 $k+1$ 的数组 temp,索引为 $i$ 处记录着元素列中值为 $i$ 的元素的个数。
    3. 调整数组 temp,使得索引为 $i$ 处记录着元素列中值小于等于 $i$ 的元素的个数。
    4. 准备一个长度为 $n$ 的数组 res 用于存储排序后的元素列,然后从后至前的迭代待排序的元素列,根据 temp 确定每次迭代的元素应该处于哪个位置,同时对 temp 进行适当的修改。
  • 时间复杂度:$\Theta(n+k)$
  • 空间复杂度:$O(n+k)$
  • 稳定性:稳定
-
-
let list = [2, 5, 3, 0, 2, 3, 0, 3];

function countingSort(array) {
let temp = [];

array.forEach((item) => {
if (typeof temp[item] === 'number') {
temp[item] = temp[item] + 1;
} else {
temp[item] = 1;
}
});

for (let i = 0; i < temp.length; i++) {
let cur = typeof temp[i] === 'number' ? temp[i] : 0;
if (i === 0) {
temp[i] = cur;
} else {
temp[i] = cur + temp[i - 1];
}
}

let res = [];
for (let i = array.length - 1; i >= 0; i--) {
let value = array[i];
res[temp[value] - 1] = value;
temp[value] = temp[value] - 1;
}
return res;
}

console.log(countingSort(list)); // [ 0, 0, 2, 2, 3, 3, 3, 5 ]
-

桶排序

-
  • 实现步骤(从小到大排序)
    1. 假设元素列中的元素均处于 $[0, 1)$ 区间内。
    2. 准备一个长度为 10 的数组 temp。
    3. 假设元素的值为 $x$,则将它放入 temp 数组索引为 $\lfloor 10x \rfloor$ 的位置。
    4. 对数组 temp 下的每个“桶”进行排序,然后将排序后的“桶”拼接在一起。
  • 时间复杂度:需要考虑每个桶内排序所消耗的时间
  • 空间复杂度:$O(n+k)$
  • 稳定性:稳定
-
-
let list = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68];

function bucketSort(array) {
let temp = Array.from(Array(10), () => []);

array.forEach((item) => {
let index = Math.floor(item * 10);
temp[index].push(item);
});

temp = temp.map((item) => {
if (temp.length > 0) {
insertionSort(item);
}
return item;
});

return [].concat(...temp);
}

function insertionSort(array) {
let len = array.length;
for (let i = 1; i < len; i++) {
let unsortValue = array[i];
let currentIndex = i - 1;
while (currentIndex >= 0 && array[currentIndex] > unsortValue) {
array[currentIndex + 1] = array[currentIndex];
currentIndex--;
}
array[currentIndex + 1] = unsortValue;
}
}

console.log(bucketSort(list)); // [ 0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94 ]
-

基数排序

-
  • 实现步骤(从小到大排序)
    1. 从最低有效位开始,依次根据当前位的关键字对元素列使用稳定排序算法进行排序。
  • 时间复杂度
    1. 给定 $n$ 个 $d$ 位数,其中每一个数位有 $k$ 个可能的取值。如果使用的稳定排序方法耗时 $\Theta(n+k)$,那么它就可以在 $\Theta(d(n+k))$ 时间内将这些数排好序。
    2. 给定一个 $b$ 位数和任何正整数 $r \leq b$,如果使用稳定排序算法对数据取值区间为 $[0, k]$ 的输入进行排序耗时 $\Theta(n+k)$,那么它就可以在 $\Theta((b/r)(n+2^r))$ 时间内将这些数排好序。
  • 空间复杂度:由过程中使用的稳定排序算法决定
  • 稳定性:稳定
-
-
let list = [329, 457, 657, 839, 436, 720, 355, 11, 20, 4];

function radixSort(array, d) {
array = array.map((item) => {
let str = String(item),
len = str.length;

return '0'.repeat(d - len) + str;
});

for (let i = d - 1; i >= 0; i--) {
let temp = [];

array.forEach((item) => {
if (typeof temp[item[i]] === 'number') {
temp[item[i]] = temp[item[i]] + 1;
} else {
temp[item[i]] = 1;
}
});

for (let j = 0; j < temp.length; j++) {
let cur = typeof temp[j] === 'number' ? temp[j] : 0;
if (j === 0) {
temp[j] = cur;
} else {
temp[j] = cur + temp[j - 1];
}
}

let res = [];
for (let j = array.length - 1; j >= 0; j--) {
let value = array[j],
singleValue = array[j][i];
res[temp[singleValue] - 1] = value;
temp[singleValue] = temp[singleValue] - 1;
}
array = res;
}

return array.map((item) => parseInt(item));
}

console.log(radixSort(list, 3)); // [ 4, 11, 20, 329, 355, 436, 457, 657, 720, 839 ]
]]>
Data Structure and Algorithm @@ -420,37 +362,58 @@
- 算法的分析 - /posts/cb7b8910/ - 渐近记号
-
  • 如果存在正常数 $c$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq f(n) \leq cg(n)$, 则记为 $f(n) = O(g(n))$。
  • 如果存在正常数 $c$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq cg(n) \leq f(n)$, 则记为 $f(n) = \Omega(g(n))$。
  • 如果存在正常数 $c_1$、$c_2$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq c_1g(n) \leq f(n) \leq c_2g(n)$,则记为 $f(n) = \Theta(g(n))$。
  • $f(n) = O(g(n)) 且 f(n) = \Omega(g(n))$ $\iff$ $f(n) = \Theta(g(n))$。

  • 如果存在正常数 $c$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq f(n) < cg(n)$, 则记为 $f(n) = o(g(n))$。
  • 如果存在正常数 $c$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq cg(n) < f(n)$, 则记为 $f(n) = \omega(g(n))$。
-
-

渐近记号

-

常用函数

取整

-
  • 对任意实数 $x$,$x-1 < \lfloor x \rfloor \leq x \leq \lceil x \rceil < x+1$
  • 对任意整数 $n$,$\lceil n/2 \rceil + \lfloor n/2 \rfloor = n$
  • 对任意实数 $x \geq 0$ 和整数 $a,b > 0$有 - $\lceil \frac{\lceil x/a \rceil}{b} \rceil = \lceil \frac{x}{ab} \rceil$ - $\lfloor \frac{\lfloor x/a \rfloor}{b} \rfloor = \lfloor \frac{x}{ab} \rfloor$ - $\lceil \frac{a}{b} \rceil \leq \frac{a+(b-1)}{b}$ - $\lfloor \frac{a}{b} \rfloor \geq \frac{a-(b-1)}{b}$
+ 排序算法总结 + /posts/cd65786b/ + 基本概念
+

排序算法可以分为以下两种类型:

  • 比较类非线性时间排序:通过比较来决定元素间的相对次序,其时间复杂度不能突破$O(nlogn)$
  • 非比较类线性时间排序:不通过比较来决定元素间的相对次序,其时间复杂度最低可以为$O(n)$

  • 稳定性:如果排序前后两个相等元素的相对次序不变,则算法稳定;反之算法不稳定
-

模运算

-
  • $a \pmod n = a - n\lfloor a/n \rfloor$
  • $0 \leq a \pmod n < n$
+

冒泡排序

+
  • 实现步骤(从小到大排序)
    1. 从最前面的两个元素开始,不断对相邻元素进行比较,根据情况决定是否交换相邻元素的位置,直到最后。这会将最大的元素“冒泡”到元素列的末尾位置。
    2. 步骤 1 需要被重复执行 n-1 次。但是每一趟排序时,不是全部元素都需要进行比较的。例如,对于第 m 趟排序,此时元素列最后面的 m 个元素是已经完成排序了的,所以不需要对它们进行重复的比较。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(n^2)$
  • 最好时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$
  • 稳定性:稳定
-

对数

-

在计算机科学中,认为$lgn = logn = log_2n$

+
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function bubbleSort(array) {
console.log(array.join(' '));

let len = array.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (array[j] > array[j + 1]) {
[array[j], array[j + 1]] = [array[j + 1], array[j]];
}
}
console.log(array.join(' '));
}
}

bubbleSort(list);
+

冒泡排序

+

选择排序

+
  • 实现步骤(从小到大排序)
    1. 共需要进行 n-1 次循环。每次循环都会选定一个起点元素,例如,第一次循环是以第一个元素为起点。第 m 次循环是以第 m 个元素为起点。
    2. 在每次循环中,找出本次循环中从起点元素开始到最后一个元素为止最小的元素,如果这个元素正是起点元素则什么都不做,否则交换起点元素与该最小元素的位置。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(n^2)$
  • 最好时间复杂度:$O(n^2)$
  • 空间复杂度:$O(1)$
  • 稳定性:不稳定
-
-
  • $a = b^{log_ba}$
  • $log_c(ab) = log_ca + log_cb$
  • $log_ba^n = n log_ba$
  • $log_ba = \frac{log_ca}{log_cb}$
  • $log_b(1/a) = -log_ba$
  • $log_ba = \frac{1}{log_ab}$
  • $a^{log_bc} = c^{log_ba}$
+
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function selectionSort(array) {
console.log(array.join(' '));

let len = array.length;
let mixIndex;
for (let i = 0; i < len - 1; i++) {
mixIndex = i;
for (let j = i + 1; j < len; j++) {
if (array[j] < array[mixIndex]) {
mixIndex = j;
}
}
[array[mixIndex], array[i]] = [array[i], array[mixIndex]];
console.log(array.join(' '));
}
}

selectionSort(list);
+

选择排序

+

插入排序

+
  • 实现步骤(从小到大排序)
    1. 共需要进行 n-1 次循环。每次循环都会选定一个待插入元素,例如,第一次循环是以第二个元素为待插入元素。第 m 次循环是以第 m+1 个元素为待插入元素。
    2. 在每次循环中,不断将待插入元素与处于它前方的元素进行比较,找出适合它的位置并插入。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(n^2)$
  • 最好时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$
  • 稳定性:稳定
-

阶乘

-

详情查看斯特林近似公式

  • $n! = o(n^n)$
  • $n! = \omega(2^n)$
+
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function insertionSort(array) {
console.log(array.join(' '));

let len = array.length;
for (let i = 1; i < len; i++) {
let unsortValue = array[i];
let currentIndex = i - 1;
while (currentIndex >= 0 && array[currentIndex] > unsortValue) {
array[currentIndex + 1] = array[currentIndex];
currentIndex--;
}
array[currentIndex + 1] = unsortValue;
console.log(array.join(' '));
}
}

insertionSort(list);
+

插入排序

+

希尔排序

+
  • 实现步骤(从小到大排序)
    1. 按照步长n/2 => n/4 => ··· => 1进行循环。每次循环中根据步长对元素列进行分组,然后对这些分组执行插入排序。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(n^{1.3})$
  • 最好时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$
  • 稳定性:不稳定
-

主定理

-

主定理的证明请看《算法导论》第三版的 4.6 节。

+
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function shellSort(array) {
console.log(array.join(' '));

let len = array.length;
for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
// 内部为插入排序
for (let i = gap; i < len; i++) {
let unsortValue = array[i];
let currentIndex = i - gap;
while (currentIndex >= 0 && array[currentIndex] > unsortValue) {
array[currentIndex + gap] = array[currentIndex];
currentIndex = currentIndex - gap;
}
array[currentIndex + gap] = unsortValue;
}
console.log(array.join(' '));
}
}

shellSort(list);
+

希尔排序

+

归并排序

+
  • 实现步骤(从小到大排序)
    1. 不断地将元素列分成两个部分,然后对这两个部分递归执行归并排序。
    2. 当元素列只有 1 个元素时,停止递归。
    3. 对于被划分的两个部分,需要按照顺序将它们合并为一个整体。
  • 最坏时间复杂度:$O(nlogn)$
  • 平均时间复杂度:$O(nlogn)$
  • 最好时间复杂度:$O(nlogn)$
  • 空间复杂度:$O(n)$
  • 稳定性:稳定
-
-

令 $a \geq 1$ 和 $b > 1$是常数,$f(n)$ 是一个函数,$T(n)$ 是定义在非负整数上的递归式:

$T(n) = aT(n/b) + f(n)$

其中我们将 $n/b$ 解释为 $\lfloor n/b \rfloor$ 或 $\lceil n/b \rceil$。那么 $T(n)$ 有如下渐近界:

  1. 若对某个常数 $\epsilon > 0$ 有 $f(n) = O(n^{log_ba-\epsilon})$,则 $T(n)=\Theta(n^{log_ba})$
  2. 若 $f(n) = \Theta(n^{log_ba})$,则 $T(n) = \Theta(n^{log_ba}lgn)$
  3. 若对某个常数 $\epsilon > 0$ 有 $f(n) = \Omega(n^{log_ba+\epsilon})$,且对某个常数 $c < 1$ 和所有足够大的 $n$ 有 $af(n/b) \leq cf(n)$,则 $T(n) = \Theta(f(n))$
+
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function mergeSort(array) {
let len = array.length;

if (len < 2) {
return array;
} else {
let middle = Math.floor(len / 2),
left = array.slice(0, middle),
right = array.slice(middle);

return merge(mergeSort(left), mergeSort(right));
}
}

function merge(left, right) {
let result = [];

while (left.length > 0 && right.length > 0) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}

while (left.length) {
result.push(left.shift());
}

while (right.length) {
result.push(right.shift());
}

return result;
}

mergeSort(list);
+

快速排序

+
  • 实现步骤(从小到大排序)
    1. 通过三数中值分割法,从元素列中选出最左侧,中间和最右侧的三个元素,并排序。
    2. 从步骤 1 中排序后的三个元素中选择中间元素作为枢纽元素,并交换枢纽元素和元素列倒数第二个元素的位置。
    3. 选择i = left + 1, j = right - 2
    4. 不断执行i++操作,直到i所处元素大于等于枢纽元素为止。
    5. 不断执行j--操作,直到j所处元素小于等于枢纽元素为止。
    6. 如果i < j,那么交换这两个位置上的元素,然后重复执行步骤 4、5。
    7. 如果i >= j,那么此刻位置i上的元素一定是大于等于枢纽元素的,此时交换位置i上的元素和枢纽元素的位置。交换之后,枢纽元素左侧的元素均小于等于它,而它右侧的元素均大于等于它。
    8. 不断对被枢纽元素分割开来的两个子元素列执行步骤 1 至步骤 7,直到排序完成。
  • 最坏时间复杂度:$O(n^2)$
  • 平均时间复杂度:$O(nlgn)$
  • 最好时间复杂度:$O(nlgn)$
  • 空间复杂度:$O(lgn)$ 至 $O(n)$ 之间
  • 稳定性:不稳定
-

对于三种情况的每一种,我们将函数 $f(n)$ 与函数 $n^{log_ba}$ 进行比较。直觉上,两个函数较大者决定了递归式的解。

  • 若函数 $n^{log_ba}$ 更大,如情况 1,则解为 $T(n)=\Theta(n^{log_ba})$。
  • 若两个函数大小相当,如情况 2,则乘上一个对数因子,解为 $T(n) = \Theta(n^{log_ba}lgn) = \Theta(f(n)lgn) $
  • 若函数 $f(n)$ 更大,如情况 3,则解为 $T(n) = \Theta(f(n))$。

在此直觉之外,我们需要了解一些技术细节。在第一种情况中,不是 $f(n)$ 小于 $n^{log_ba}$ 就够了,而是要多项式意义上的小于。也就是说,$f(n)$ 必须渐近小于 $n^{log_ba}$,要相差一个因子 $n^{\epsilon}$。在第三种情况中,不是 $f(n)$ 大于 $n^{log_ba}$ 就够了,而是要多项式意义上的大于,而且还要满足条件 $af(n/b) \leq cf(n)$。

注意,这三种情况并未覆盖 $f(n)$ 的所有可能性。情况 1 和情况 2 之间有一定的间隙,情况 2 和情况 3 之间也有一定间隙。这样的情况下就不能使用主定理来求解递归式了。

+
  • 需要注意array[i]=array[j]=pivot的情况,不要陷入死循环。
  • 为什么当所遇元素与枢纽元素相等时,也要让ij停下来,并交换位置?
    这样可以确保被枢纽元素分割开来的两部分尽量均衡,减少插入排序算法总的时间复杂度。
-

主定理举例]

+
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80];

function quickSort(array, left, right) {
var left = typeof left !== 'number' ? 0 : left,
right = typeof right !== 'number' ? array.length - 1 : right;

if (left === right) {
return;
}

if (left + 1 === right) {
if (array[left] <= array[right]) {
return;
} else {
[array[left], array[right]] = [array[right], array[left]];
return;
}
}

let pivot = median3(array, left, right),
i = left + 1,
j = right - 2;

while (i < j) {
while (array[i] < pivot) {
i++;
}
while (pivot < array[j]) {
j--;
}
if (i < j) {
if (array[i] === array[j]) {
i++;
} else {
[array[i], array[j]] = [array[j], array[i]];
}
}
if (i >= j) {
break;
}
}

[array[i], array[right - 1]] = [array[right - 1], array[i]];

quickSort(array, left, i - 1);
quickSort(array, i + 1, right);
}

function median3(array, left, right) {
let center = Math.floor((left + right) / 2);

if (array[center] < array[left]) {
[array[left], array[center]] = [array[center], array[left]];
}
if (array[right] < array[left]) {
[array[left], array[right]] = [array[right], array[left]];
}
if (array[right] < array[center]) {
[array[center], array[right]] = [array[right], array[center]];
}

[array[center], array[right - 1]] = [array[right - 1], array[center]];

return array[right - 1];
}

quickSort(list);
+

堆排序

+
  • 实现步骤(从小到大排序)
    1. 首先将元素列转换为最大堆形式的数组。
    2. 数组中的最大元素总在 A[1] 中,把它与 A[heap.size] 进行互换,同时从堆中去掉最后一个结点(这一操作可以通过减少 heap.size 来实现)。
    3. 步骤 2 之后,最大堆的剩余结点中,原来根的孩子结点仍然是最大堆,而新的根节点可能会违背最大堆的性质。为了维护最大堆的性质,我们要做的就是通过下滤操作使其符合最大堆的性质。
    4. 重复步骤 2、3,直到 heap.size = 1。
  • 最坏时间复杂度:$O(nlgn)$
  • 平均时间复杂度:$O(nlgn)$
  • 最好时间复杂度:$O(nlgn)$
  • 空间复杂度:$O(1)$
  • 稳定性:不稳定
+
+
let list = [90, 84, 83, 88, 87, 61, 50, 70, 60, 80];

function heapSort(array) {
let heap = buildHeap(array);

for (let i = heap.size; i > 1; i--) {
[heap[i], heap[1]] = [heap[1], heap[i]];
heap.size--;
percolateDown(heap, 1);
}

return heap.slice(1);
}

function buildHeap(array) {
let heap = [];
heap.size = array.length;
for (let i = 0; i < array.length; i++) {
heap[i + 1] = array[i];
}

let currentIndex = Math.floor(heap.size / 2);
while (currentIndex > 0) {
percolateDown(heap, currentIndex);
currentIndex--;
}

return heap;
}

function percolateDown(heap, currentIndex) {
let leftSonIndex = 2 * currentIndex,
rightSonIndex = 2 * currentIndex + 1,
maxValueIndex = currentIndex;

if (leftSonIndex <= heap.size && heap[leftSonIndex] > heap[currentIndex]) {
maxValueIndex = leftSonIndex;
}

if (rightSonIndex <= heap.size && heap[rightSonIndex] > heap[maxValueIndex]) {
maxValueIndex = rightSonIndex;
}

if (currentIndex !== maxValueIndex) {
[heap[currentIndex], heap[maxValueIndex]] = [
heap[maxValueIndex],
heap[currentIndex],
];
percolateDown(heap, maxValueIndex);
}
}

console.log(heapSort(list)); // [ 50, 60, 61, 70, 80, 83, 84, 87, 88, 90 ]
+

计数排序

+
  • 实现步骤(从小到大排序)
    1. 假设元素列中的元素均处于 $[0, k]$ 区间内,$k$ 为某个整数。
    2. 准备一个长度为 $k+1$ 的数组 temp,索引为 $i$ 处记录着元素列中值为 $i$ 的元素的个数。
    3. 调整数组 temp,使得索引为 $i$ 处记录着元素列中值小于等于 $i$ 的元素的个数。
    4. 准备一个长度为 $n$ 的数组 res 用于存储排序后的元素列,然后从后至前的迭代待排序的元素列,根据 temp 确定每次迭代的元素应该处于哪个位置,同时对 temp 进行适当的修改。
  • 时间复杂度:$\Theta(n+k)$
  • 空间复杂度:$O(n+k)$
  • 稳定性:稳定
+
+
let list = [2, 5, 3, 0, 2, 3, 0, 3];

function countingSort(array) {
let temp = [];

array.forEach((item) => {
if (typeof temp[item] === 'number') {
temp[item] = temp[item] + 1;
} else {
temp[item] = 1;
}
});

for (let i = 0; i < temp.length; i++) {
let cur = typeof temp[i] === 'number' ? temp[i] : 0;
if (i === 0) {
temp[i] = cur;
} else {
temp[i] = cur + temp[i - 1];
}
}

let res = [];
for (let i = array.length - 1; i >= 0; i--) {
let value = array[i];
res[temp[value] - 1] = value;
temp[value] = temp[value] - 1;
}
return res;
}

console.log(countingSort(list)); // [ 0, 0, 2, 2, 3, 3, 3, 5 ]
+

桶排序

+
  • 实现步骤(从小到大排序)
    1. 假设元素列中的元素均处于 $[0, 1)$ 区间内。
    2. 准备一个长度为 10 的数组 temp。
    3. 假设元素的值为 $x$,则将它放入 temp 数组索引为 $\lfloor 10x \rfloor$ 的位置。
    4. 对数组 temp 下的每个“桶”进行排序,然后将排序后的“桶”拼接在一起。
  • 时间复杂度:需要考虑每个桶内排序所消耗的时间
  • 空间复杂度:$O(n+k)$
  • 稳定性:稳定
+
+
let list = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68];

function bucketSort(array) {
let temp = Array.from(Array(10), () => []);

array.forEach((item) => {
let index = Math.floor(item * 10);
temp[index].push(item);
});

temp = temp.map((item) => {
if (temp.length > 0) {
insertionSort(item);
}
return item;
});

return [].concat(...temp);
}

function insertionSort(array) {
let len = array.length;
for (let i = 1; i < len; i++) {
let unsortValue = array[i];
let currentIndex = i - 1;
while (currentIndex >= 0 && array[currentIndex] > unsortValue) {
array[currentIndex + 1] = array[currentIndex];
currentIndex--;
}
array[currentIndex + 1] = unsortValue;
}
}

console.log(bucketSort(list)); // [ 0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94 ]
+

基数排序

+
  • 实现步骤(从小到大排序)
    1. 从最低有效位开始,依次根据当前位的关键字对元素列使用稳定排序算法进行排序。
  • 时间复杂度
    1. 给定 $n$ 个 $d$ 位数,其中每一个数位有 $k$ 个可能的取值。如果使用的稳定排序方法耗时 $\Theta(n+k)$,那么它就可以在 $\Theta(d(n+k))$ 时间内将这些数排好序。
    2. 给定一个 $b$ 位数和任何正整数 $r \leq b$,如果使用稳定排序算法对数据取值区间为 $[0, k]$ 的输入进行排序耗时 $\Theta(n+k)$,那么它就可以在 $\Theta((b/r)(n+2^r))$ 时间内将这些数排好序。
  • 空间复杂度:由过程中使用的稳定排序算法决定
  • 稳定性:稳定
+
+
let list = [329, 457, 657, 839, 436, 720, 355, 11, 20, 4];

function radixSort(array, d) {
array = array.map((item) => {
let str = String(item),
len = str.length;

return '0'.repeat(d - len) + str;
});

for (let i = d - 1; i >= 0; i--) {
let temp = [];

array.forEach((item) => {
if (typeof temp[item[i]] === 'number') {
temp[item[i]] = temp[item[i]] + 1;
} else {
temp[item[i]] = 1;
}
});

for (let j = 0; j < temp.length; j++) {
let cur = typeof temp[j] === 'number' ? temp[j] : 0;
if (j === 0) {
temp[j] = cur;
} else {
temp[j] = cur + temp[j - 1];
}
}

let res = [];
for (let j = array.length - 1; j >= 0; j--) {
let value = array[j],
singleValue = array[j][i];
res[temp[singleValue] - 1] = value;
temp[singleValue] = temp[singleValue] - 1;
}
array = res;
}

return array.map((item) => parseInt(item));
}

console.log(radixSort(list, 3)); // [ 4, 11, 20, 329, 355, 436, 457, 657, 720, 839 ]
]]> Data Structure and Algorithm @@ -499,23 +462,6 @@ Data Structure and Algorithm - - 对docker数据持久化的理解 - /posts/9d0d99dd/ - 背景

项目需要读取本地的一个文件,采用的方式是挂载本地目录作为volume。但是在将这个项目部署到远端服务器时,项目image可以直接拉取,而项目所需文件的迁移就成了问题

-

解决方案

-
  1. 创建一个专门用于COPY数据的image,起名为data-image:1.0
  2. 接着就可以远程访问服务器,创建docker-compose.yml文件,通过挂载volume的方式实现数据共享
  3. 到此为止就可以拉取镜像、启动服务了
-
-
# Dockerfile
FROM alpine

COPY ./data /usr/src/data
-
# docker-compose.yml
version: '3'

services:
project:
image: project-image
volumes:
- project-data:/usr/src/data

data-container:
image: data-image:1.0
volumes:
- project-data:/usr/src/data

volumes:
project-data:
-

总结

-
  • 只有volume可以做到数据持久化,image内的数据是无法被更改的
  • 当项目启动后对依赖文件的改动会被记录在project-data中,而两个image/usr/src/data目录下的内容会一直保持它们最开始的状态
  • 启动服务后,project-data会被挂载到两个container上,实现数据的共享,这个时候两个container/usr/src/data目录下的内容与project-data同步
  • 如果删除了project-data,则对依赖文件的改动会丢失
-
-]]>
- - Docker - -
/posts/7b216a3b/ @@ -572,59 +518,57 @@ - volume的备份及还原 - /posts/e113cc4a/ - 现在Docker中有一个名为registry-data的volume,我想备份它。

-
-

思路如下:

  1. 创建一个文件夹,路径如下/d/Docker/backupbackup就是我保存备份的文件夹;
  2. 运行一个临时的容器,它需要做以下工作:
    • 将想要备份的registry-data挂载到容器上;
    • 将第一步创建的文件夹,挂载到容器上;
    • 通过命令压缩registry-data,而后保存到第一步创建的文件夹中。
-
-

运行如下命令,将会生成registry-data20190509.tar压缩文件,并将其保存在backup文件夹中。

-
docker run --rm -v registry-data:/source -v /d/Docker/backup:/backup alpine sh -c "cd /source && tar cvf /backup/registry-data20190509.tar ."
-

如果想将备份还原成volume,只需运行下面的命令即可:

-
docker run --rm -v another-registry-data:/source -v /d/Docker/backup:/backup alpine sh -c "cd /source && tar xvf /backup/registry-data20190509.tar ."
-

这样就会利用备份的压缩文件,为我们生成一个名为another-registry-data的volume。

-]]>
- - Docker - -
- - Broswer Event Loop - /posts/cf159629/ - -

不是我懒惰,是我怕翻译错
so

+ 算法的分析 + /posts/cb7b8910/ + 渐近记号
+
  • 如果存在正常数 $c$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq f(n) \leq cg(n)$, 则记为 $f(n) = O(g(n))$。
  • 如果存在正常数 $c$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq cg(n) \leq f(n)$, 则记为 $f(n) = \Omega(g(n))$。
  • 如果存在正常数 $c_1$、$c_2$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq c_1g(n) \leq f(n) \leq c_2g(n)$,则记为 $f(n) = \Theta(g(n))$。
  • $f(n) = O(g(n)) 且 f(n) = \Omega(g(n))$ $\iff$ $f(n) = \Theta(g(n))$。

  • 如果存在正常数 $c$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq f(n) < cg(n)$, 则记为 $f(n) = o(g(n))$。
  • 如果存在正常数 $c$ 和 $n_0$,使得当 $n \geq n_0$ 时,$0 \leq cg(n) < f(n)$, 则记为 $f(n) = \omega(g(n))$。
+
+

渐近记号

+

常用函数

取整

+
  • 对任意实数 $x$,$x-1 < \lfloor x \rfloor \leq x \leq \lceil x \rceil < x+1$
  • 对任意整数 $n$,$\lceil n/2 \rceil + \lfloor n/2 \rfloor = n$
  • 对任意实数 $x \geq 0$ 和整数 $a,b > 0$有 - $\lceil \frac{\lceil x/a \rceil}{b} \rceil = \lceil \frac{x}{ab} \rceil$ - $\lfloor \frac{\lfloor x/a \rfloor}{b} \rfloor = \lfloor \frac{x}{ab} \rfloor$ - $\lceil \frac{a}{b} \rceil \leq \frac{a+(b-1)}{b}$ - $\lfloor \frac{a}{b} \rfloor \geq \frac{a-(b-1)}{b}$
+
+

模运算

+
  • $a \pmod n = a - n\lfloor a/n \rfloor$
  • $0 \leq a \pmod n < n$
+
+

对数

+

在计算机科学中,认为$lgn = logn = log_2n$

+
+
+
  • $a = b^{log_ba}$
  • $log_c(ab) = log_ca + log_cb$
  • $log_ba^n = n log_ba$
  • $log_ba = \frac{log_ca}{log_cb}$
  • $log_b(1/a) = -log_ba$
  • $log_ba = \frac{1}{log_ab}$
  • $a^{log_bc} = c^{log_ba}$
+
+

阶乘

+

详情查看斯特林近似公式

  • $n! = o(n^n)$
  • $n! = \omega(2^n)$
+
+

主定理

+

主定理的证明请看《算法导论》第三版的 4.6 节。

+
+
+

令 $a \geq 1$ 和 $b > 1$是常数,$f(n)$ 是一个函数,$T(n)$ 是定义在非负整数上的递归式:

$T(n) = aT(n/b) + f(n)$

其中我们将 $n/b$ 解释为 $\lfloor n/b \rfloor$ 或 $\lceil n/b \rceil$。那么 $T(n)$ 有如下渐近界:

  1. 若对某个常数 $\epsilon > 0$ 有 $f(n) = O(n^{log_ba-\epsilon})$,则 $T(n)=\Theta(n^{log_ba})$
  2. 若 $f(n) = \Theta(n^{log_ba})$,则 $T(n) = \Theta(n^{log_ba}lgn)$
  3. 若对某个常数 $\epsilon > 0$ 有 $f(n) = \Omega(n^{log_ba+\epsilon})$,且对某个常数 $c < 1$ 和所有足够大的 $n$ 有 $af(n/b) \leq cf(n)$,则 $T(n) = \Theta(f(n))$
+
+
+

对于三种情况的每一种,我们将函数 $f(n)$ 与函数 $n^{log_ba}$ 进行比较。直觉上,两个函数较大者决定了递归式的解。

  • 若函数 $n^{log_ba}$ 更大,如情况 1,则解为 $T(n)=\Theta(n^{log_ba})$。
  • 若两个函数大小相当,如情况 2,则乘上一个对数因子,解为 $T(n) = \Theta(n^{log_ba}lgn) = \Theta(f(n)lgn) $
  • 若函数 $f(n)$ 更大,如情况 3,则解为 $T(n) = \Theta(f(n))$。

在此直觉之外,我们需要了解一些技术细节。在第一种情况中,不是 $f(n)$ 小于 $n^{log_ba}$ 就够了,而是要多项式意义上的小于。也就是说,$f(n)$ 必须渐近小于 $n^{log_ba}$,要相差一个因子 $n^{\epsilon}$。在第三种情况中,不是 $f(n)$ 大于 $n^{log_ba}$ 就够了,而是要多项式意义上的大于,而且还要满足条件 $af(n/b) \leq cf(n)$。

注意,这三种情况并未覆盖 $f(n)$ 的所有可能性。情况 1 和情况 2 之间有一定的间隙,情况 2 和情况 3 之间也有一定间隙。这样的情况下就不能使用主定理来求解递归式了。

+

主定理举例]

]]>
- FE + Data Structure and Algorithm
- CSS解析原理 - /posts/df6afb91/ - CSS选择器的解析顺序

在利用 DOM 和 CSSOM 合成 render tree 的时候,需要将样式表中的每一条 CSS 样式规则与对应的 DOM 元素关联起来。然而实际中,样式规则可能数量很大,但是绝大多数不会匹配到任何 DOM 元素上,所以有一个快速的方法来判断 CSS 选择器是否具有匹配的 DOM 元素是极其重要的。

-

以下面的例子讲解CSS 选择器的解析规则:

-
<div>
<div class="jartto">
<p><span> 111 </span></p>
<p><span> 222 </span></p>
<p><span> 333 </span></p>
<p><span class='yellow'> 444 </span></p>
</div>
</div>
<style>
div > div.jartto p span.yellow{
color: yellow;
}
</style>
-
-

如果采用从左至右的规则,过程如下所示:

  1. 首先找到所有 <div>
  2. 在每个 <div> 内寻找所有 class=jartto 的子 <div>
  3. 在步骤二中找到的每个子 <div> 内接着按 CSS 选择器进行寻找,直到最后。

这样的搜索过程对于一个只是匹配很少节点的选择器来说,效率是极低的,因为我们花费了大量的时间在回溯匹配不符合规则的节点。

-
-
-

如果采用从右至左的规则,过程如下所示:

  1. 首先找到所有 class=yellow<span>
  2. 然后判断这些 <span> 的父元素是否为 <p>
  3. 接着判断这些 <p> 的父元素是否为 class=jartto<div>
  4. 最后判断这些 <div> 的父元素是否也是 <div>

因为每一个元素都只拥有一个父元素,所以从右至左的解析 CSS 选择器可以有效减少无效匹配的次数,从而使匹配更快、性能更优。

-
-

computedStyle的计算

render tree 生成时,元素的 computedStyle 是经过层叠计算后得到的。在某些特定的情况下,浏览器会让不同元素之间共享它们的 computedStyle。也就是说,如果多个元素的 computedStyle 不通过计算就可以确认它们相等,那么这个 computedStyle 只会被计算一次,从而提高了性能。

-

只要元素之间满足以下条件,它们之间就可以共享 computedStyle。

+ volume的备份及还原 + /posts/e113cc4a/ + 现在Docker中有一个名为registry-data的volume,我想备份它。

-
  • 元素不能有 id 属性。
  • 元素的标签名必须相同,即必须是同类型的元素。
  • 元素的 class 属性必须相同。
  • 元素之间的 mappedAttribute(一些可以影响 CSS ComputedStyle 的 HTML 属性) 必须相等。
  • 元素不能有 style 属性,哪怕是这些元素的 style 属性值相同也不可以。
  • 不能使用 sibling selector。例如,first-child:last-selector+ selector
-
-

选择器书写建议

-
  • ID 选择器是非常高效的,且 ID 是唯一的,所以在使用的时候应该单独使用,不需要再指定标签名等。
  • 避免深层次的选择器。
  • 慎用子代选择器。
  • 属性选择的的解析速度非常慢,慎用。
+

思路如下:

  1. 创建一个文件夹,路径如下/d/Docker/backupbackup就是我保存备份的文件夹;
  2. 运行一个临时的容器,它需要做以下工作:
    • 将想要备份的registry-data挂载到容器上;
    • 将第一步创建的文件夹,挂载到容器上;
    • 通过命令压缩registry-data,而后保存到第一步创建的文件夹中。
-

参考文献

+

运行如下命令,将会生成registry-data20190509.tar压缩文件,并将其保存在backup文件夹中。

+
docker run --rm -v registry-data:/source -v /d/Docker/backup:/backup alpine sh -c "cd /source && tar cvf /backup/registry-data20190509.tar ."
+

如果想将备份还原成volume,只需运行下面的命令即可:

+
docker run --rm -v another-registry-data:/source -v /d/Docker/backup:/backup alpine sh -c "cd /source && tar xvf /backup/registry-data20190509.tar ."
+

这样就会利用备份的压缩文件,为我们生成一个名为another-registry-data的volume。

]]>
- FE + Docker
@@ -671,6 +615,34 @@ Docker + + 对docker数据持久化的理解 + /posts/9d0d99dd/ + 背景

项目需要读取本地的一个文件,采用的方式是挂载本地目录作为volume。但是在将这个项目部署到远端服务器时,项目image可以直接拉取,而项目所需文件的迁移就成了问题

+

解决方案

+
  1. 创建一个专门用于COPY数据的image,起名为data-image:1.0
  2. 接着就可以远程访问服务器,创建docker-compose.yml文件,通过挂载volume的方式实现数据共享
  3. 到此为止就可以拉取镜像、启动服务了
+
+
# Dockerfile
FROM alpine

COPY ./data /usr/src/data
+
# docker-compose.yml
version: '3'

services:
project:
image: project-image
volumes:
- project-data:/usr/src/data

data-container:
image: data-image:1.0
volumes:
- project-data:/usr/src/data

volumes:
project-data:
+

总结

+
  • 只有volume可以做到数据持久化,image内的数据是无法被更改的
  • 当项目启动后对依赖文件的改动会被记录在project-data中,而两个image/usr/src/data目录下的内容会一直保持它们最开始的状态
  • 启动服务后,project-data会被挂载到两个container上,实现数据的共享,这个时候两个container/usr/src/data目录下的内容与project-data同步
  • 如果删除了project-data,则对依赖文件的改动会丢失
+
+]]>
+ + Docker + +
+ + Broswer Event Loop + /posts/cf159629/ + +

不是我懒惰,是我怕翻译错
so

+
+]]> + + FE + + Ajax 与 Comet /posts/5ffed448/ @@ -785,24 +757,27 @@ - 前端路由的实现 - /posts/eb815672/ - 前端路由的实现有两种方式:hash 和 history。

-

hash

-
  1. 此时浏览器显示的 URL 为 https://aadonkeyz.com/posts/eb815672/
  2. 执行 location.hash = '#hash' 后,生成一条新的浏览记录,其 URL 为 https://aadonkeyz.com/posts/eb815672/#hash,并将这条新的记录加入到浏览器历史记录的栈顶。与此同时浏览器的浏览状态会跳转到这个最新的记录上。
  3. location.hash 改变时,触发了 window 对象上注册的 onhashchange 事件处理程序。
  4. 页面内容进行对应的更新。
  5. 当使用前进或者后退时,会再次触发 onhashchange 事件处理程序,页面内容会再次进行相应的更新。
  6. 前端路由实现。
+ CSS解析原理 + /posts/df6afb91/ + CSS选择器的解析顺序

在利用 DOM 和 CSSOM 合成 render tree 的时候,需要将样式表中的每一条 CSS 样式规则与对应的 DOM 元素关联起来。然而实际中,样式规则可能数量很大,但是绝大多数不会匹配到任何 DOM 元素上,所以有一个快速的方法来判断 CSS 选择器是否具有匹配的 DOM 元素是极其重要的。

+

以下面的例子讲解CSS 选择器的解析规则:

+
<div>
<div class="jartto">
<p><span> 111 </span></p>
<p><span> 222 </span></p>
<p><span> 333 </span></p>
<p><span class='yellow'> 444 </span></p>
</div>
</div>
<style>
div > div.jartto p span.yellow{
color: yellow;
}
</style>
+
+

如果采用从左至右的规则,过程如下所示:

  1. 首先找到所有 <div>
  2. 在每个 <div> 内寻找所有 class=jartto 的子 <div>
  3. 在步骤二中找到的每个子 <div> 内接着按 CSS 选择器进行寻找,直到最后。

这样的搜索过程对于一个只是匹配很少节点的选择器来说,效率是极低的,因为我们花费了大量的时间在回溯匹配不符合规则的节点。

-
-

在 URL 中,# 以及它之后的内容是用于在页面内定位的锚点,它们的改变不会引起浏览器发送网络请求

+
+

如果采用从右至左的规则,过程如下所示:

  1. 首先找到所有 class=yellow<span>
  2. 然后判断这些 <span> 的父元素是否为 <p>
  3. 接着判断这些 <p> 的父元素是否为 class=jartto<div>
  4. 最后判断这些 <div> 的父元素是否也是 <div>

因为每一个元素都只拥有一个父元素,所以从右至左的解析 CSS 选择器可以有效减少无效匹配的次数,从而使匹配更快、性能更优。

-

history

-
  1. 此时浏览器显示的 URL 为 https://aadonkeyz.com
  2. 更新页面内容,同时通过 history.pushState() 或者 history.replaceState() 新增/替换浏览器历史记录的栈顶记录,浏览器的浏览状态跳转到栈顶记录上,此时 URL 为 https://aadonkeyz.com/posts/eb815672通过这两个方法,不会触发任何事件,也不会引起浏览器的加载行为。
  3. 当浏览器前进或后退到某条由 history.pushState() 或者 history.replaceState() 生成的浏览记录时,会触发 window 对象上注册的 onpopstate 事件处理程序,并且在事件处理程序的内部,event.state 中保存着该浏览记录的状态对象的拷贝。因此可以根据这个 event.state 更新页面。
  4. 前端路由实现。
+

computedStyle的计算

render tree 生成时,元素的 computedStyle 是经过层叠计算后得到的。在某些特定的情况下,浏览器会让不同元素之间共享它们的 computedStyle。也就是说,如果多个元素的 computedStyle 不通过计算就可以确认它们相等,那么这个 computedStyle 只会被计算一次,从而提高了性能。

+

只要元素之间满足以下条件,它们之间就可以共享 computedStyle。

+
+
  • 元素不能有 id 属性。
  • 元素的标签名必须相同,即必须是同类型的元素。
  • 元素的 class 属性必须相同。
  • 元素之间的 mappedAttribute(一些可以影响 CSS ComputedStyle 的 HTML 属性) 必须相等。
  • 元素不能有 style 属性,哪怕是这些元素的 style 属性值相同也不可以。
  • 不能使用 sibling selector。例如,first-child:last-selector+ selector
-
-

采用这种方式时,为了防止浏览器真的去加载对应的 URL,从而返回 404 Not Found 的尴尬局面,需要服务器的支持,如果 URL 匹配不到任何静态资源,则应该返回根页面 HTML

+

选择器书写建议

+
  • ID 选择器是非常高效的,且 ID 是唯一的,所以在使用的时候应该单独使用,不需要再指定标签名等。
  • 避免深层次的选择器。
  • 慎用子代选择器。
  • 属性选择的的解析速度非常慢,慎用。
-

更多

「前端进阶」彻底弄懂前端路由 中对前端路由进行了简单的实现,感兴趣的可以阅读一下。如果想更细致的了解前端路由的实现方式,可以阅读 react-router 或者 vue-router 的源码。

参考文献

]]> @@ -949,79 +924,6 @@ FE - - 多行文本缩略的实现 - /posts/ef1ff28e/ - 目前大多数浏览器不支持多行文本缩略,因此需要做兼容处理。比较好的思路是在首次渲染后去读取每个文字的宽度,计算是否应该展示缩略效果。为了能够读取每个文字的宽度,需要使用 span 包裹每个文字。

-
-
  1. 在非缩略状态下,通过读取每个文字的宽度,计算是否应该缩略
  2. 如果需要缩略,则使用一个 span 包裹前几行不需要缩略的文字,用另一个 span 包裹后几行需要缩略的文字
  3. 如果不需要缩略,使用 span 包裹每一个文本
  4. 如果文本更新了
    4.1 如果当前处于缩略状态,则先重置为非缩略状态,然后在跳转到步骤 1
    4.2 如果当前处于非缩略状态,则直接跳转到步骤 1
-
-

React 的示例代码如下所示:

-
import React, { useState, useRef, useEffect } from 'react';

export interface AutoEllipsisProps {
text?: string;
maxLine?: number;
className?: string;
onEllipsis?: (isEllipsis: boolean) => void;
}

export default function AutoEllipsis(props: AutoEllipsisProps) {
const { text, maxLine = 1, className, onEllipsis } = props;

const [isEllipsis, setIsEllipsis] = useState(false);
const [ellipsisLineFirstTextIndex, setEllipsisLineFirstTextIndex] =
useState(0);
const [rerenderAfterReset, setRenderAfterReset] = useState(0);

const divRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (isEllipsis) {
setIsEllipsis(false);
setRenderAfterReset((pre) => pre + 1);
return;
}

if (divRef.current && text) {
const { offsetWidth } = divRef.current;

const headIndexList: number[] = [0];
let cumulativeWidth = 0;
const children = divRef.current.children as never as HTMLSpanElement[];

for (let i = 0; i < children.length; i++) {
if (cumulativeWidth + children[i].offsetWidth <= offsetWidth) {
cumulativeWidth += children[i].offsetWidth;
} else if (cumulativeWidth + children[i].offsetWidth > offsetWidth) {
cumulativeWidth = children[i].offsetWidth;
headIndexList.push(i);
}
}

if (headIndexList.length > maxLine) {
setIsEllipsis(true);
setEllipsisLineFirstTextIndex(headIndexList[maxLine - 1]);
}
}
}, [text, rerenderAfterReset]);

useEffect(() => {
onEllipsis?.(isEllipsis);
}, [isEllipsis]);

const renderText = () => {
if (!text) {
return null;
}

if (!isEllipsis) {
return text
?.split('')
.map((item, index) => <span key={index}>{item}</span>);
}

return (
<>
<span>{text.slice(0, ellipsisLineFirstTextIndex)}</span>
<span
style={{
display: 'inline-block',
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{text.slice(ellipsisLineFirstTextIndex)}
</span>
</>
);
};

return (
<div className={className} ref={divRef}>
{renderText()}
</div>
);
}
-]]>
- - FE - -
- - 事件 - /posts/9c2b83ad/ - JavaScript 与 HTML 之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间,比如:点击按钮、拖动鼠标等。

-

事件流

事件冒泡

事件冒泡是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的 HTML 页面为例:

-
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
-

如果你单击了页面中的 <div> 元素,那么这个 click 事件会按照如下顺序传播

-

事件冒泡

-

事件捕获

事件捕获的顺序与事件冒泡的顺序正好相反,以前面的HTML页面为例,它的顺序为:

-

事件捕获

-

事件流

DOM2 级事件规定的事件流包括三个阶段:事件捕获阶段处于目标阶段事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。还是以前面的HTML页面为例,它的顺序如下所示:

-

事件流

-

在 DOM 事件流中,实际的目标在捕获阶段不会接收到事件。这意味着在捕获阶段,事件从 document<html> 再到 <body> 后就停止了。下一个阶段是“处于目标”阶段,于是事件在 <div> 上发生,并在事件处理(后面将会讨论这个概念)中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回文档。

-

多数支持 DOM 事件流的浏览器都实现了一种特定的行为:即使DOM2 级事件规范明确要求捕获阶段不会涉及事件目标,但 IE9、Safari、Chrome、Firefox 和 Opera9.5 及更高版本都会在捕获阶段触发事件对象上的事件。结果就是有两个机会在目标对象上面操作事件。

-

事件处理程序

事件就是用户或浏览器自身执行的某种动作。诸如 click、load 和 mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字就是在事件名前面加上 "on",因此 click 事件的事件处理程序就是 onclick,load 事件的事件处理程序就是 onload。为事件注册处理程序的方式有好几种,下面一一进行介绍。

-

HTML 事件处理程序

某个元素支持的每种事件,都拥有一个与相应事件处理程序同名的 HTML 特性,可以通过这个特性来注册事件处理程序。下面以 onclick 事件为例:

-
// test.js
function showAnother (that) {
alert(that.value)
}
-
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<!-- 直接在HTML中指定具体动作 -->
<input type="button" value="1" onclick="alert(this.value)" />

<!-- 调用在<script>标签内定义的函数 -->
<input type="button" value="2" onclick="showMessage(this, event)" />

<!-- 调用外部js文件中定义的函数 -->
<input type="button" value="3" onclick="showAnother(this)" />

<script type="text/javascript" src="./test.js"></script>
<script type="text/javascript">
function showMessage (that, event) {
alert(that.value)
console.log(that)
console.log(event)
}
</script>
</body>
</html>
-

在 HTML 中注册事件处理程序,会创建一个封装着元素属性值的函数。这个函数中有一个局部变量 event,也就是事件对象(后面将会讨论这个概念),通过 event 变量可以直接访问事件对象。并且在这个函数内部,this 值等于事件的目标元素。所以你可以将 eventthis 当做参数,传递给要调用的函数。关于这一点,你可以查看上面例子中 showMessage() 函数打印的内容来验证。

-
-

HTML 事件处理程序的缺点:

  • 如果用户在页面解析 showMessage()showAnother() 之前就点击了对应的按钮,会抛出错误。可以使用 onclick="try {showMessage()} catch(ex) {}" 的形式来解决这个问题。
  • 事件处理程序的作用域链在不同的浏览器中会有不同的结果。
  • HTML 和 JS 代码紧密耦合。
-
-

DOM0 级事件处理程序

-
  • 使用 DOM0 级方法只能在元素上注册一个事件处理程序。
  • 对于相同的事件,DOM0 级事件处理程序与 HTML 事件处理程序事件处理程序无法共存。
  • 使用 DOM0 级方法注册的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行,所以此时函数中的 this 指向元素自身。
  • 在注册事件处理程序时,不推荐使用 箭头函数,因为它会造成 this 的丢失。
-
-

在添加的事件处理程序函数内部,可以直接通过 event 变量访问事件对象。也可以通过给程序处理函数定义参数或者使用 arguments 来访问事件对象,在下面有例子。

-

以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。你可以通过将事件处理程序属性的值设置为 null 来删除添加的事件处理程序。

-

来看一个例子:

-
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<input id="myButton" type="button" value="1" />
<script type="text/javascript">
var button = document.getElementById('myButton')
button.onclick = function (e) {
console.log(this)
console.log(event)

// true
console.log(event === e)

// true
console.log(event === arguments[0])

// 删除添加的事件处理程序
button.onclick = null
}
</script>
</body>
</html>
-

这个例子中的按钮,只有第一次点击时会打印内容,之后就没有任何反应,因为事件处理程序在第一次触发之后,就被删除了。

-

DOM2 级事件处理程序

DOM2 级事件定义了两个方法,addEventListener()removeEventListener(),分别用于注册和删除事件处理程序,所有 DOM 节点都包含这两个方法。

-
-
  • 使用 addEventListener() 可以在同一个元素上注册多个事件处理程序,触发的顺序为添加顺序。
  • 关于 thisevent 的使用规则,与 DOM0 级事件处理程序一致。
-
-
-

首先介绍 addEventListener() 方法,它的参数如下:

  • type:表示监听事件类型的字符串,需要注意的是没有 on 前缀
  • listener:作为事件处理程序的函数。
  • options(可选):一个对象。其属性如下:
    • capture:一个布尔值,默认为 false。当值为 true 时,listener 会在事件捕获阶段时被调用。
    • once:一个布尔值,默认为 false。当值为 true 时,listener 会在其被调用之后自动移除。
    • passive:一个布尔值,默认为 false。当值为 true 时,listener 内部不允许调用 event.preventDefault(),否则会抛出错误。
  • useCapture(可选):一个布尔值,默认为 false。当值为 true 时,listener 会在事件捕获阶段时被调用。

对于 optionsuseCapture 参数,它们都是该方法的第三个参数,options 是新标准,而 useCapture 是老标准。


接着介绍 removeEventListener() 方法,它的参数如下:

  • type:表示监听事件类型的字符串,需要注意的是没有 on 前缀
  • listener:作为事件处理程序的函数。
  • options(可选):一个对象。其属性如下:
    • capture:一个布尔值,默认为 false。当值为 true 时,表示要移除的 listener 是注册在事件捕获阶段的。
  • useCapture(可选):一个布尔值,默认为 false。当值为 true 时,表示要移除的 listener 是注册在事件捕获阶段的。

如果一个事件处理程序一共注册了两次,一次在事件捕获阶段,一次在事件冒泡阶段,那么这两次注册需要分别移除,两者不会互相干扰。

-
-

下面的例子用于观察 options.captureuseCapture 的效果。

-
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<style>
#outer, #inner {
display: block;
width: 500px;
height: 500px;
text-decoration: none;
}
#outer{
border: 1px solid red;
color: red;
}
#inner{
border: 1px solid green;
color: green;
width: 250px;
height: 250px;
margin: 125px auto;
}
</style>
</head>
<body>
<div id="outer">
outer, capture & none-capture
<div id="inner">
inner
</div>
</div>
<script type="text/javascript">
var outer = document.getElementById('outer')
var inner = document.getElementById('inner')

function captureListener1 () {
console.log('outer, capture1')
outer.removeEventListener('click', captureListener1, true)
}
function captureListener2 () {
console.log('outer, capture2')
}
function noneCaptureListener () {
console.log('outer, none-capture')
}
function innerListener () {
console.log('inner')
}

outer.addEventListener('click', captureListener1, { capture: true })
outer.addEventListener('click', captureListener2, true)
outer.addEventListener('click', noneCaptureListener)
inner.addEventListener('click', innerListener)
</script>
</body>
</html>
-

上例中 captureListener1captureListener2 都是注册在 outer 的捕获阶段,而 noneCaptureListenerinnerListener 分别注册在 outerinner 的冒泡阶段。并且 captureListener1 会在第一次调用后被移除。请多点击几次 inner 框,查看打印的结果。

-

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。这里只介绍 DOM 中的事件对象,忽略 IE 的。

-

无论注册事件处理程序时使用的是 DOM0 级还是 DOM2 级方法,兼容 DOM 的浏览器都会将一个 event 对象传入到事件处理程序中,这样就可以直接在函数内部访问到 event 对象了。

-

event 对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。下面简单的对事件对象的属性和方法进行了介绍,如果想查看详细信息,去移步 MDN

-
-

属性:

  • bubbles(只读):表明事件是否会冒泡。
  • cancelBubble:通过将该属性设置为 true 可阻止事件继续冒泡。
  • cancelable(只读):表明是否可以取消事件的默认行为。
  • currentTarget(只读):事件处理程序注册在哪个元素上,currentTarget 就指向哪个元素。
  • defaultPrevented(只读):表明是否已经调用了 preventDefault() 方法。
  • eventPhase(只读):表明处于事件流的哪个阶段。1 表示捕获阶段,2 表示处于目标,3 表示冒泡阶段。
  • target(只读):触发事件的那个元素,也就是事件流在“处于目标”阶段时的那个目标元素。
  • timeStamp(只读):表明事件对象的创建时间。
  • type(只读):表明事件对象的类型。
  • isTrusted(只读):当事件是由用户触发的时(比如点击鼠标),该属性值为 true。当事件是由脚本触发时,该属性值为 false

方法:

  • preventDefault():取消事件的默认行为。该方法只有在 cancelable 属性为 true 时才会起作用。
  • stopImmediatePropagation():取消事件的进一步捕获或冒泡,同时阻止其后的所有事件处理程序被调用。
  • stopPropagation():取消事件的进一步捕获或冒泡,但是不会阻止注册在当前 currentTarget 上的事件处理程序被调用。
-
-

事件类型

-

下面我只简单的介绍一下我认为比较常用的事件,如果你想比较全面的了解这里,点击下面的链接!

MDN,只有你想不到,没有找不到

-
-

内存和性能

在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

-

事件委托

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

-

移除事件处理程序

每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”,也是造成 Web 应用程序内存与性能问题的主要原因。

-

在两种情况下,可能会造成上述问题。第一种情况就是从文档中移除带有事件处理程序的元素时。这可能是通过纯粹的 DOM 操作,例如使用 removeChild() 方法,但更多地是发生在使用 innerHTML 替换页面中某一部分的时候。如果带有事件处理程序的元素被 innerHTML 删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。所以如果你知道某个元素即将被移除,那么最好在此之前手工移除事件处理程序。

-

另一种情况,就是卸载页面的时候。如果在页面被卸载之前没有清理干净事件处理程序,那它们就会滞留在内存中。每次加载完页面在卸载页面时(可能是在两个页面间来回切换,也可能是单击了“刷新”按钮),内存中滞留的对象数目就会增加,因为事件处理程序占用的内存并没有被释放。因此最好的做法就是在页面卸载之前,先通过 onunload 事件处理程序移除所有事件处理程序。

-]]>
- - FE - -
Web Worker /posts/da19b401/ @@ -1036,42 +938,24 @@ - 虚拟渲染列表简单介绍 - /posts/2b565654/ - 原理

虚拟渲染列表

-

容器的高度是给定的,且通过设置 overflow: auto 让它的子元素可以滚动。

-

列表只设置一个 height 属性即可,它的高度由列表项高度和列表项个数决定,它存在的意义是使得滚动条可以正确显示。

-

列表项分为真实渲染和虚拟渲染两种,真实渲染的列表项需要被包裹起来,然后对包裹层设置 transform: translateY(container's scrollTop)。如果不这么做,以第二个图为例,会导致第三个真实渲染列表项出现在第一个虚拟渲染列表项的位置。

-

进阶

-
  • buffer:不单单真实渲染出现在容器可视范围内的列表项,对超出的前后 n 个也进行真实渲染。
  • 动态高度:每一个列表项的高度可能不同。
  • 具有头部元素:比如 h5 场景下,虚拟列表上方还有一块信息展示区。为了避免处理两层滚动的麻烦和用户体验问题,可以将上方的信息展示区放入虚拟渲染列表的“列表”上方,这样全局就只有一个滚动条了。
-
-]]>
- - FE - -
- - 跨域问题常见解决方案 - /posts/713a5759/ - 我个人将跨域问题分类为两种:网络请求跨域非网络请求跨域

-

网络请求跨域

-

网络请求跨域的解决方案有:

  • 图像img
  • JSONP
  • CORS
  • Nginx反向代理、Nodejs中间件
  • SSE、Web Socket(它们是服务器推送技术,无跨域问题)

详细信息在这里

+ 前端路由的实现 + /posts/eb815672/ + 前端路由的实现有两种方式:hash 和 history。

+

hash

+
  1. 此时浏览器显示的 URL 为 https://aadonkeyz.com/posts/eb815672/
  2. 执行 location.hash = '#hash' 后,生成一条新的浏览记录,其 URL 为 https://aadonkeyz.com/posts/eb815672/#hash,并将这条新的记录加入到浏览器历史记录的栈顶。与此同时浏览器的浏览状态会跳转到这个最新的记录上。
  3. location.hash 改变时,触发了 window 对象上注册的 onhashchange 事件处理程序。
  4. 页面内容进行对应的更新。
  5. 当使用前进或者后退时,会再次触发 onhashchange 事件处理程序,页面内容会再次进行相应的更新。
  6. 前端路由实现。
-

非网络请求跨域

document.domain

-

如果要进行通信的窗口之间主域相同,子域不同,则可以直接将它们的 document.domain 修改为主域,这样就不存在跨域问题了。

+
+

在 URL 中,# 以及它之后的内容是用于在页面内定位的锚点,它们的改变不会引起浏览器发送网络请求

-
<iframe name="a" src="http://a.domain.com"></iframe>
<iframe name="b" src="http://b.domain.com"></iframe>
-
<!-- domain 初始值为 a.domain.com -->
<script>
document.domain = 'domain.com';
var user = 'a';
</script>
-
<!-- domain 初始值为 b.domain.com -->
<script>
document.domain = 'domain.com';
alert('get js data from a ---> ' + window.parent.frames.a.user);
</script>
-

location.hash

-

父窗口 A 修改子窗口 B 的 location.hash 值,会触发对应子窗口 B window 对象的 onhashchange 事件处理程序。这个过程是单向的,但是你可以通过在子窗口 B 中添加一个 iframe 标签,这个 iframesrc 域名与父窗口域名相同的,然后在这个 iframe 页面内调用父窗口 A 的函数,此时是不存在跨域的。

+

history

+
  1. 此时浏览器显示的 URL 为 https://aadonkeyz.com
  2. 更新页面内容,同时通过 history.pushState() 或者 history.replaceState() 新增/替换浏览器历史记录的栈顶记录,浏览器的浏览状态跳转到栈顶记录上,此时 URL 为 https://aadonkeyz.com/posts/eb815672通过这两个方法,不会触发任何事件,也不会引起浏览器的加载行为。
  3. 当浏览器前进或后退到某条由 history.pushState() 或者 history.replaceState() 生成的浏览记录时,会触发 window 对象上注册的 onpopstate 事件处理程序,并且在事件处理程序的内部,event.state 中保存着该浏览记录的状态对象的拷贝。因此可以根据这个 event.state 更新页面。
  4. 前端路由实现。
-

window.name

-

window 对象的 name 属性有一个独特之处,一个窗口只要一直存在并且没有被主动的更改 name 属性,那么它的 name 属性会一直保持不变。即窗口的 URL(或 iframesrc)随意变化,name 属性也不会变。name 属性内存最大可以支持 2MB,所以可以通过它来传递数据。

+
+

采用这种方式时,为了防止浏览器真的去加载对应的 URL,从而返回 404 Not Found 的尴尬局面,需要服务器的支持,如果 URL 匹配不到任何静态资源,则应该返回根页面 HTML

-

window.postMessage

通过 window.postMessage 方法和 message 事件完成数据的传递。

-

参考

    -
  • 前端常见跨域解决方案(全)
  • +

    更多

    「前端进阶」彻底弄懂前端路由 中对前端路由进行了简单的实现,感兴趣的可以阅读一下。如果想更细致的了解前端路由的实现方式,可以阅读 react-router 或者 vue-router 的源码。

    +

    参考文献

    ]]> @@ -1105,9 +989,23 @@ - 浏览器是如何渲染一个页面的? - /posts/b5fc6f17/ - 渲染流程

    浏览器渲染流程

    + 多行文本缩略的实现 + /posts/ef1ff28e/ + 目前大多数浏览器不支持多行文本缩略,因此需要做兼容处理。比较好的思路是在首次渲染后去读取每个文字的宽度,计算是否应该展示缩略效果。为了能够读取每个文字的宽度,需要使用 span 包裹每个文字。

    +
    +
    1. 在非缩略状态下,通过读取每个文字的宽度,计算是否应该缩略
    2. 如果需要缩略,则使用一个 span 包裹前几行不需要缩略的文字,用另一个 span 包裹后几行需要缩略的文字
    3. 如果不需要缩略,使用 span 包裹每一个文本
    4. 如果文本更新了
      4.1 如果当前处于缩略状态,则先重置为非缩略状态,然后在跳转到步骤 1
      4.2 如果当前处于非缩略状态,则直接跳转到步骤 1
    +
    +

    React 的示例代码如下所示:

    +
    import React, { useState, useRef, useEffect } from 'react';

    export interface AutoEllipsisProps {
    text?: string;
    maxLine?: number;
    className?: string;
    onEllipsis?: (isEllipsis: boolean) => void;
    }

    export default function AutoEllipsis(props: AutoEllipsisProps) {
    const { text, maxLine = 1, className, onEllipsis } = props;

    const [isEllipsis, setIsEllipsis] = useState(false);
    const [ellipsisLineFirstTextIndex, setEllipsisLineFirstTextIndex] =
    useState(0);
    const [rerenderAfterReset, setRenderAfterReset] = useState(0);

    const divRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
    if (isEllipsis) {
    setIsEllipsis(false);
    setRenderAfterReset((pre) => pre + 1);
    return;
    }

    if (divRef.current && text) {
    const containerWidth = divRef.current.getBoundingClientRect().width;

    const headIndexList: number[] = [0];
    let cumulativeWidth = 0;
    const children = divRef.current.children as never as HTMLSpanElement[];

    for (let i = 0; i < children.length; i++) {
    const childWidth = children[i].getBoundingClientRect().width;
    if (cumulativeWidth + childWidth <= containerWidth) {
    cumulativeWidth += childWidth;
    } else if (cumulativeWidth + childWidth > containerWidth) {
    cumulativeWidth = childWidth;
    headIndexList.push(i);
    }
    }

    if (headIndexList.length > maxLine) {
    setIsEllipsis(true);
    setEllipsisLineFirstTextIndex(headIndexList[maxLine - 1]);
    }
    }
    }, [text, rerenderAfterReset]);

    useEffect(() => {
    onEllipsis?.(isEllipsis);
    }, [isEllipsis]);

    const renderText = () => {
    if (!text) {
    return null;
    }

    if (!isEllipsis) {
    return text
    ?.split('')
    .map((item, index) => <span key={index}>{item}</span>);
    }

    return (
    <>
    <span>{text.slice(0, ellipsisLineFirstTextIndex)}</span>
    <span
    style={{
    display: 'inline-block',
    width: '100%',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
    }}
    >
    {text.slice(ellipsisLineFirstTextIndex)}
    </span>
    </>
    );
    };

    return (
    <div className={className} ref={divRef}>
    {renderText()}
    </div>
    );
    }
    +]]>
    + + FE + +
    + + 浏览器是如何渲染一个页面的? + /posts/b5fc6f17/ + 渲染流程

    浏览器渲染流程

    • JavaScript: 构建 DOM + CSSOM
    • Style: 构建 Render Tree
    @@ -1148,6 +1046,67 @@
  • Accelerated Rendering in Chrome
  • Chrome的First Paint
+]]> + + FE + + + + 虚拟渲染列表简单介绍 + /posts/2b565654/ + 原理

虚拟渲染列表

+

容器的高度是给定的,且通过设置 overflow: auto 让它的子元素可以滚动。

+

列表只设置一个 height 属性即可,它的高度由列表项高度和列表项个数决定,它存在的意义是使得滚动条可以正确显示。

+

列表项分为真实渲染和虚拟渲染两种,真实渲染的列表项需要被包裹起来,然后对包裹层设置 transform: translateY(container's scrollTop)。如果不这么做,以第二个图为例,会导致第三个真实渲染列表项出现在第一个虚拟渲染列表项的位置。

+

进阶

+
  • buffer:不单单真实渲染出现在容器可视范围内的列表项,对超出的前后 n 个也进行真实渲染。
  • 动态高度:每一个列表项的高度可能不同。
  • 具有头部元素:比如 h5 场景下,虚拟列表上方还有一块信息展示区。为了避免处理两层滚动的麻烦和用户体验问题,可以将上方的信息展示区放入虚拟渲染列表的“列表”上方,这样全局就只有一个滚动条了。
+
+]]>
+ + FE + +
+ + 跨域问题常见解决方案 + /posts/713a5759/ + 我个人将跨域问题分类为两种:网络请求跨域非网络请求跨域

+

网络请求跨域

+

网络请求跨域的解决方案有:

  • 图像img
  • JSONP
  • CORS
  • Nginx反向代理、Nodejs中间件
  • SSE、Web Socket(它们是服务器推送技术,无跨域问题)

详细信息在这里

+
+

非网络请求跨域

document.domain

+

如果要进行通信的窗口之间主域相同,子域不同,则可以直接将它们的 document.domain 修改为主域,这样就不存在跨域问题了。

+
+
<iframe name="a" src="http://a.domain.com"></iframe>
<iframe name="b" src="http://b.domain.com"></iframe>
+
<!-- domain 初始值为 a.domain.com -->
<script>
document.domain = 'domain.com';
var user = 'a';
</script>
+
<!-- domain 初始值为 b.domain.com -->
<script>
document.domain = 'domain.com';
alert('get js data from a ---> ' + window.parent.frames.a.user);
</script>
+

location.hash

+

父窗口 A 修改子窗口 B 的 location.hash 值,会触发对应子窗口 B window 对象的 onhashchange 事件处理程序。这个过程是单向的,但是你可以通过在子窗口 B 中添加一个 iframe 标签,这个 iframesrc 域名与父窗口域名相同的,然后在这个 iframe 页面内调用父窗口 A 的函数,此时是不存在跨域的。

+
+

window.name

+

window 对象的 name 属性有一个独特之处,一个窗口只要一直存在并且没有被主动的更改 name 属性,那么它的 name 属性会一直保持不变。即窗口的 URL(或 iframesrc)随意变化,name 属性也不会变。name 属性内存最大可以支持 2MB,所以可以通过它来传递数据。

+
+

window.postMessage

通过 window.postMessage 方法和 message 事件完成数据的传递。

+

参考

+]]>
+ + FE + +
+ + 防抖和节流 + /posts/45b9746d/ + 在某些场景下(scroll、resize 事件等),函数有可能被非常频繁地调用,这样会消耗不必要的性能。为了解决这个问题,引出了防抖(debounce)和节流(throttle)的概念。

+

debounce

+

在事件被触发时,设定 n 秒后执行对应的回调函数。如果在 n 秒之内事件被重复触发,则重新计时。如果连续触发事件,对应的回调函数可能永远不会被调用。

+
+
function debounce (fn, timeRange) {
var timer

return function (...args) {
clearTimeout(timer)

timer = setTimeout(() => {
fn.call(this, ...args)
}, timeRange)
}
}
+

throttle

+

一段时间周期内,最多只执行一次回调函数。

+
+
function throttle (fn, timeRange) {
var timer

return function (...args) {
if (timer) {
return
} else {
timer = setTimeout(() => {
clearTimeout(timer)

timer = null
fn.call(this, ...args)
}, timeRange)
}
}
}
+

lodash

在实际工作中给你,往往都是直接引用 lodash,而 lodash 的 debounce 和 throttle 的规则是可以在进行适当配置的。

]]>
FE @@ -1188,21 +1147,50 @@
- 防抖和节流 - /posts/45b9746d/ - 在某些场景下(scroll、resize 事件等),函数有可能被非常频繁地调用,这样会消耗不必要的性能。为了解决这个问题,引出了防抖(debounce)和节流(throttle)的概念。

-

debounce

-

在事件被触发时,设定 n 秒后执行对应的回调函数。如果在 n 秒之内事件被重复触发,则重新计时。如果连续触发事件,对应的回调函数可能永远不会被调用。

+ Git Branching + /posts/3557a152/ + Commands
# create a branch
# only create, don't switch
# if you don't type existbranch, use current branch
$ git branch <newbranch> [<existbranch>]

# switch your working directory
$ git checkout [<branchname> | <tagname> | <commitsha1>]

# rename a branch
# if the newbranch is already existed
# -m => rename fail
# -M => delete the existed one, and then rename
$ git branch (-m | -M) [<oldbranch>] <newbranch>

# create a branch and switch to it
# if you don't type existbranch, use current branch
$ git checkout -b <newbranch> [<existbranch>]

# move the branch pointer
$ git branch -f [<branchname> | <tagname> | <commitsha1>]

# merge the target branch into your current branch
$ git merge <targetbranch>

# merge the target branch into your current branch
# create a merge commit in all cases
# even when the merge could instead be resolved as a fast-forward
$ git merge <targetbranch> --no-ff

# no options => local branches list
# -r => remote branches list
# -a => both remote and local branches list
$ git branch [-r | -a]

# see the last commit on each branch
$ git branch -v

# filter the list to branches that you have or haven't merged into your current branch
$ git branch [--merged | --no-merged]

# fetch any data from the given remote that you don't yet have
# update your local database
# move your remote/branch pointer to its new, more up-to-data position.
# if you don't type remote, it will use origin as default
$ git fetch [<remotename>]

# push the local branch to the remote branch
$ git push <remotename> <localbranch:remotebranch>

# fetch the upstream branch and merge into the tracking branch
$ git pull

# set the current branch to track remotebranch
# you can use this command to change the relationship between a tracking branch and a upstream branch
$ git branch <-u | --set-upstream-to> <[[remotes/]origin/]remotebranch>

# if current branch is not a tracking branch, and you want to push it
# if remotebranch isn't exist, Git will automatically create it
$ git push --set-upstream <remotename> <remotebranch>

# see what tracking branches you have set up
$ git branch -vv

# delete a local branch
$ git branch -d <branchname>

# delete a remote branch
$ git push <remotename> --delete <branchname>

# take the patch(s) and replay it/them on top of somewhere else
# if there is no argument, it will rebase on the upstream of current local branch
$ git rebase [<branchname> | <tagname> | <commitsha1>]

# if there is any conflict while rebasing, use this command
$ git rebase <--continue | --abort | --skip>
+

Branches in a Nutshell

To really understand the way Git does branching, we need to take a back and examine how Git stores its data. Git doesn’t store data as a series of changesets or differents, but instead as a series of snapshots. When you make a commit, Git stores a commit object that contains a pointer to the snapshot of the content you staged. This object also contains the author’s name and email address, the message that you typed, and the pointer to the commit or commits that directly came before this commit (its parent or parents): zero parents for the inital commit, one parent for a normal commit, and multiple parents for a commit that results from a merge of two of more branches.

+
+

A branch in Git is simply a lightweight movable pointer to one of these commits.

-
function debounce (fn, timeRange) {
var timer

return function (...args) {
clearTimeout(timer)

timer = setTimeout(() => {
fn.call(this, ...args)
}, timeRange)
}
}
-

throttle

-

一段时间周期内,最多只执行一次回调函数。

+

When you create a new branch, Git creates a new pointer for you to move around. And How does Git know what branch you’re currently on? Git keeps a special pointer called HEAD, it is the symbolic name for the currently checkout out commit — it’s essentially what commit you’re working on top of. Normally HEAD attaches to a branch, when you commit, the status of that branch is altered. When HEAD attaches to a commit instead of a branch, this phenomenon is called detach.

+

Basic Branching and Merging

before-merge

+
$ git checkout -b bugFix
$ git commit
$ git checkout master
$ git commit
$ git merge bugFix
+

after-merge

+

Remote Branches

Look at an example, let’s say you have a Git server on your network at git.ourcompany.com. If you clone from this, Git’s clone command automatically names it origin for you, pulls down all its data, creates some pointers to correspond to remote branches, the name of remote branchs in local will has a prefix like remotes/ or remotes/origin/. Git also gives you your own local master branch starting at the same places as origin’s master branch, so you have something to work from.

+

Server and local repositories after cloning

+

If you do some work on your local master branch, and, in the meantime, someone else pushes to git.ourcompany.com and updates its master branch, then your histories move forward differently. Also, as long as you stay out of contact with your origin server, your origin/master pointer doesn’t move.

+

Local and remote work can diverge

+

To synchronize your work with a given remote, you run a git fetch <remote> command. This command fetches any data from the given remote that you don’t yet have, and updates your local database, moving your origin/master pointer to its new, more up-to-data position.

+
+

It’s important to note that when you do a fetch that brings down new remote-tracking branches, you don’t automatically have local, editable copies of them. In other words, in this case, you don’t have a new branch — you have only an origin/master pointer that you can’t modify.

-
function throttle (fn, timeRange) {
var timer

return function (...args) {
if (timer) {
return
} else {
timer = setTimeout(() => {
clearTimeout(timer)

timer = null
fn.call(this, ...args)
}, timeRange)
}
}
}
-

lodash

在实际工作中给你,往往都是直接引用 lodash,而 lodash 的 debounce 和 throttle 的规则是可以在进行适当配置的。

-]]> +

git fetch updates your remote-tracking branches

+

When you want to share a branch with the world, you need to push it up to a remote to which you have write access. Your local branches aren’t automatically synchronized to the remotes you write to — you have to explicitly push the branches you want to share. That way, you can use private branches for work you don’t want to share, and push up only the topic branches you want to collaborate on.

+

If you have a branch named serverfix that you want to work on with others, you can push it up the same way you pushed your first branch. Run git push <remote> <branch>:

+
$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
* [new branch] serverfix -> serverfix
+

This is a bit of a shortcut. Git automatically expands the serverfix branchname out to refs/heads/serverfix:refs/heads/serverfix, which means, “Take my serverfix local branch and push it to update the remote’s serverfix branch.” You can also do git push origin serverfix:serverfix, which does the same thing — it says, “Take my serverfix and make it the remote’s serverfix.” You can use this format to push a local branch into a remote branch that is named differently. If you didn’t want it to be called serverfix on the remote, you could instead run git push origin serverfix:awesomebranch to push your local serverfix branch to the awesomebranch branch on the remote project.

+

Checking out a local branch from a remote-tracking branch automatically creates what is called a “tracking branch”, and the branch it tracks is called an “upstream branch”. Tracking branches are local branches that have a direct relationship to a remote branch. If you’re on a tracking branch and type git pull, Git automatically know which server to fetch from and which branch to merge in. If you want to change the upstream branch you’re tracking, you can use the -u or --set-upstream-to option to git branch to explicitly set it at any time.

+

Rebasing

+

It turns out that in addition to the commit SHA-1 checksum, Git also calculates a checksum that is based just on the patch introduced with the commit, this is called a “patch-id”.

+
+

In addition to merge, if you want to integrate changes from experiment into master, there is another way.

+

Simple divergent history

+

You can take the patch of the change that was introduce in C4 and reapply it on top of C3. In Git, this is called rebasing. With the rebase command, you can take all the changes that were committed on one branch and replay them on a different branch.

+
$ git checkout experiment
$ git rebase master
+

Rebasing the change introduced in C4 onto C3

+

At this point, you can go back to the master branch and do a fast-forward merge.

+
$ git checkout master
$ git merge experiment
+

Fast-forwarding the master branch

+
+

Rebasing replays changes from one line of work onto another in the order they were introduced, whereas merging takes the endpoints and merges them together. There is no difference in the end product of the integration, but rebasing makes for a cleaner history. In addition, when you rebase stuff, you’re abanding existing commits and creating new ones that are similar but different.

+
+
+

If there is any conflict while you are rebasing, you should fix the conflict, and then use git add to stage the files, use git rebase --continue to finish the rebasing process. In this process, do not use git commit, it will create a commit and detach the HEAD!

+
]]> - FE + Git @@ -1247,66 +1235,46 @@ - Git Branching - /posts/3557a152/ - Commands
# create a branch
# only create, don't switch
# if you don't type existbranch, use current branch
$ git branch <newbranch> [<existbranch>]

# switch your working directory
$ git checkout [<branchname> | <tagname> | <commitsha1>]

# rename a branch
# if the newbranch is already existed
# -m => rename fail
# -M => delete the existed one, and then rename
$ git branch (-m | -M) [<oldbranch>] <newbranch>

# create a branch and switch to it
# if you don't type existbranch, use current branch
$ git checkout -b <newbranch> [<existbranch>]

# move the branch pointer
$ git branch -f [<branchname> | <tagname> | <commitsha1>]

# merge the target branch into your current branch
$ git merge <targetbranch>

# merge the target branch into your current branch
# create a merge commit in all cases
# even when the merge could instead be resolved as a fast-forward
$ git merge <targetbranch> --no-ff

# no options => local branches list
# -r => remote branches list
# -a => both remote and local branches list
$ git branch [-r | -a]

# see the last commit on each branch
$ git branch -v

# filter the list to branches that you have or haven't merged into your current branch
$ git branch [--merged | --no-merged]

# fetch any data from the given remote that you don't yet have
# update your local database
# move your remote/branch pointer to its new, more up-to-data position.
# if you don't type remote, it will use origin as default
$ git fetch [<remotename>]

# push the local branch to the remote branch
$ git push <remotename> <localbranch:remotebranch>

# fetch the upstream branch and merge into the tracking branch
$ git pull

# set the current branch to track remotebranch
# you can use this command to change the relationship between a tracking branch and a upstream branch
$ git branch <-u | --set-upstream-to> <[[remotes/]origin/]remotebranch>

# if current branch is not a tracking branch, and you want to push it
# if remotebranch isn't exist, Git will automatically create it
$ git push --set-upstream <remotename> <remotebranch>

# see what tracking branches you have set up
$ git branch -vv

# delete a local branch
$ git branch -d <branchname>

# delete a remote branch
$ git push <remotename> --delete <branchname>

# take the patch(s) and replay it/them on top of somewhere else
# if there is no argument, it will rebase on the upstream of current local branch
$ git rebase [<branchname> | <tagname> | <commitsha1>]

# if there is any conflict while rebasing, use this command
$ git rebase <--continue | --abort | --skip>
-

Branches in a Nutshell

To really understand the way Git does branching, we need to take a back and examine how Git stores its data. Git doesn’t store data as a series of changesets or differents, but instead as a series of snapshots. When you make a commit, Git stores a commit object that contains a pointer to the snapshot of the content you staged. This object also contains the author’s name and email address, the message that you typed, and the pointer to the commit or commits that directly came before this commit (its parent or parents): zero parents for the inital commit, one parent for a normal commit, and multiple parents for a commit that results from a merge of two of more branches.

-
-

A branch in Git is simply a lightweight movable pointer to one of these commits.

-
-

When you create a new branch, Git creates a new pointer for you to move around. And How does Git know what branch you’re currently on? Git keeps a special pointer called HEAD, it is the symbolic name for the currently checkout out commit — it’s essentially what commit you’re working on top of. Normally HEAD attaches to a branch, when you commit, the status of that branch is altered. When HEAD attaches to a commit instead of a branch, this phenomenon is called detach.

-

Basic Branching and Merging

before-merge

-
$ git checkout -b bugFix
$ git commit
$ git checkout master
$ git commit
$ git merge bugFix
-

after-merge

-

Remote Branches

Look at an example, let’s say you have a Git server on your network at git.ourcompany.com. If you clone from this, Git’s clone command automatically names it origin for you, pulls down all its data, creates some pointers to correspond to remote branches, the name of remote branchs in local will has a prefix like remotes/ or remotes/origin/. Git also gives you your own local master branch starting at the same places as origin’s master branch, so you have something to work from.

-

Server and local repositories after cloning

-

If you do some work on your local master branch, and, in the meantime, someone else pushes to git.ourcompany.com and updates its master branch, then your histories move forward differently. Also, as long as you stay out of contact with your origin server, your origin/master pointer doesn’t move.

-

Local and remote work can diverge

-

To synchronize your work with a given remote, you run a git fetch <remote> command. This command fetches any data from the given remote that you don’t yet have, and updates your local database, moving your origin/master pointer to its new, more up-to-data position.

-
-

It’s important to note that when you do a fetch that brings down new remote-tracking branches, you don’t automatically have local, editable copies of them. In other words, in this case, you don’t have a new branch — you have only an origin/master pointer that you can’t modify.

+ HTML标签 + /posts/500cdacf/ + HTML标签大全

太多了,整理一会之后果断放弃,MDN上的介绍

+

HTML模板

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="简介" />
<meta name="keywords" content="关键字" />
<meta name="author" content="作者" />
<title>题目</title>
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="style.css" type="text/css" />
<style type="text/css">
/* CSS内部样式表 */
</style>
</head>
<body>
<!-- 这里放内容 -->
<script type="text/javascript" src="example.js"></script>
</body>
</html>
+

a标签

+

<a>标签代表一个超链接,它有如下属性:

  • download:这个属性用于指示浏览器去下载href所指向的资源而不是进行页面跳转。如果给download属性设置一个值,这个值将是对应资源被保存成本地文件过程中的默认名称。
  • href:URL。
  • hreflang:用于指定链接资源的人类语言
  • ping:包含一个以空格分隔的 URL 列表,当跟随超链接时,将由浏览器发送带有正文的 PING 的 POST 请求,通常用于跟踪。
  • rel:该属性执行了目标对象到链接对象的关系。该值是空格分隔的列表类型值。
  • target:该属性指定在何处显示链接的资源。它的值包括:_self_blank_parent_top
  • type:该属性指定在一个 MIME type 链接目标的形式的媒体查询。其仅提供建议,并没有内置的功能。

<a>标签还有四个伪类:

  • link:具有href属性。
  • visited:被访问过。
  • hover:鼠标悬浮。
  • active:鼠标点击瞬间。
-

git fetch updates your remote-tracking branches

-

When you want to share a branch with the world, you need to push it up to a remote to which you have write access. Your local branches aren’t automatically synchronized to the remotes you write to — you have to explicitly push the branches you want to share. That way, you can use private branches for work you don’t want to share, and push up only the topic branches you want to collaborate on.

-

If you have a branch named serverfix that you want to work on with others, you can push it up the same way you pushed your first branch. Run git push <remote> <branch>:

-
$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
* [new branch] serverfix -> serverfix
-

This is a bit of a shortcut. Git automatically expands the serverfix branchname out to refs/heads/serverfix:refs/heads/serverfix, which means, “Take my serverfix local branch and push it to update the remote’s serverfix branch.” You can also do git push origin serverfix:serverfix, which does the same thing — it says, “Take my serverfix and make it the remote’s serverfix.” You can use this format to push a local branch into a remote branch that is named differently. If you didn’t want it to be called serverfix on the remote, you could instead run git push origin serverfix:awesomebranch to push your local serverfix branch to the awesomebranch branch on the remote project.

-

Checking out a local branch from a remote-tracking branch automatically creates what is called a “tracking branch”, and the branch it tracks is called an “upstream branch”. Tracking branches are local branches that have a direct relationship to a remote branch. If you’re on a tracking branch and type git pull, Git automatically know which server to fetch from and which branch to merge in. If you want to change the upstream branch you’re tracking, you can use the -u or --set-upstream-to option to git branch to explicitly set it at any time.

-

Rebasing

-

It turns out that in addition to the commit SHA-1 checksum, Git also calculates a checksum that is based just on the patch introduced with the commit, this is called a “patch-id”.

+

下面展示一个使用download属性的例子。

+
<a download="国旗" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALYAAAB5CAMAAACjkCtXAAAAe1BMVEX/AAD////XsbH0RETe2dnToqLi7e3wVVX80Rb80xb81Rb82Bf/MQX/JQT8xhX/FwL8yxX+VQn/Ogb83Rf8whT9nBD+TAj9jw/9lQ/9rhL9uBP8uxT+bgv9ohDGv7/SuLj9hA3+XQj9eAn+fw39qBL+Qwf+ZQv2OzvmbW2v1N+9AAACiklEQVR4nO3ZCW+bMByHYdjpg8PcEMhKUpL2+3/CORs9osUGofFzUP6vlKpqFfTINTZOPd//9WVTPf3wfd/z/e/epvpJbGDERkZsZMRGRmxkxEb2YOxkFc3slrLzVTSzW8QOgpIlQbCWaUaL2D0XnMsqXAs13bJJEkZSOZ3dy9ixGMRhJdGslrH73uuKlUSzmmbHN36Wji9nTbKT9pb7UxO/XqdJdp+drBeIq/9umtEku8mO1guEuYv1e4qtl7q95e1VUbCi6OETZYp9YDy3bCtpoTeeAX93TrGfBVel7QIqixzsllPsvWDCNrnPUZLbJ/8qGdjp+HePW82ux+/TG8N6TL3gbtipEoypdqgqxvTk7usmV4zJAe8zZBjtQ5RxLoSU7OKWUug7L2ucboxXGed2zwT7nIgcH2iuMt+S5S7jH+isdnkq+CfbSnKM3txiZ10E8VkXwHJ0i8HhQeZmdvY4vXlkvkDsZHmxsmu9kPDLgGfm2/H+HqUCxZlQjdBfTUNaNQ1vmuquHqWOeofkZz3mnCvDQ3dYSS4cHOEt7HgnRH7hdkzTTBdQKnIwSyzskxTt30Hu9GZjeH/Xlrvn9XimLOwiG97m7CnPzrff340vcGZ2HNUfd9qptZ1x8FlG+/pGc3JAN/Zgn297idNHq8XsYYMfXSaHkvXJ2d0D1jL2KWec8Xol04yWTpJGSpezZCE7zAflYHN8byG7q7xgv7l/gvzZikKHO9CjbTduIzYyYiMjNjJiIyM2MmIjIzYyYiMjNjJiIyM2MmIjIzYyYiMjNjJiIyM2MmIjIzYyYiMjNjJiIyM2MmIjIzYyYiMjNjJiIyM2MmIje2e/fN1SryP728Ya2Vtso+zffNMqoOoYARcAAAAASUVORK5CYII=">下载图片</a>
+

input标签

+

<input>标签用于接受来自用户的数据。它有如下属性:

  • autocomplete:一个字符串,描述输入应提供的任何类型的自动完成功能。自动完成的典型实现只是回忆在同一输入字段中输入的先前值,但可以存在更复杂的自动完成形式。例如,浏览器可以与设备的联系人列表集成,以在电子邮件输入字段中自动完成电子邮件地址。不过当type属性值为buttonfile等时,<input>不会返回文本或数值数据,此时autocomplete属性将被忽略。
  • autofocus
  • disabled
  • form:该属性值为一个<form>id,用于指示该<input>是属于它的。如果缺失,则该<input>属于最近的<form>或者不属于任何<form>
  • list:该属性值为一个<datalist>id<datalist>用于为该<input>提供建议列表。
  • name:该<input>的名称,与表单数据一起提交。
  • readonly
  • required
  • tabindex:该<input>在当前文档的 Tab 导航顺序中的位置。
  • type:该<input>的类型。包括:buttoncheckboxcolordatedatetime-localemailfilehiddenimagemonthnumberpasswordradiorangeresetsearchsubmitteltexttimeurlweek
  • value:该<input>的当前值。
-

In addition to merge, if you want to integrate changes from experiment into master, there is another way.

-

Simple divergent history

-

You can take the patch of the change that was introduce in C4 and reapply it on top of C3. In Git, this is called rebasing. With the rebase command, you can take all the changes that were committed on one branch and replay them on a different branch.

-
$ git checkout experiment
$ git rebase master
-

Rebasing the change introduced in C4 onto C3

-

At this point, you can go back to the master branch and do a fast-forward merge.

-
$ git checkout master
$ git merge experiment
-

Fast-forwarding the master branch

+

meta标签

<meta>标签中包含那些不能被<base><link><script><style><title>标签所表示的内容。并且<meta>标签一次只干一件事,如果想利用<meta>标签设置多个内容,那就多写几个<meta>标签。它有如下属性:

-

Rebasing replays changes from one line of work onto another in the order they were introduced, whereas merging takes the endpoints and merges them together. There is no difference in the end product of the integration, but rebasing makes for a cleaner history. In addition, when you rebase stuff, you’re abanding existing commits and creating new ones that are similar but different.

+
  • charset:该属性表明了页面的编码方式,通常为UTF-8
  • content:该属性值对应于http-equiv属性或name属性的值。
  • http-equiv:该属性定义 HTTP 的一些首部字段,对应的首部字段值在content属性中。
    • content-security-policy:定义页面的内容安全策略,内容策略主要指定允许的服务器源和脚本端点,这有助于防止 XSS 攻击。
    • refresh
      • 如果content属性值是一个正整数,则该正数代表页面重载的时间间隔(秒)。
      • 如果content属性值是一个正整数,并且后面跟着;url=和一个合法的 URL,则页面将在指定秒数后进行跳转。
  • name:该属性定义能定义如下内容,同样的,这些内容的值在content属性中。
    • author:定义作者。
    • description:定义页面的描述信息。在你收藏一个页面时,描述信息就是来自于这里的。
    • generator:生成页面的软件的标识符。
    • keywords:用,分隔的关键词。
    • referrer:控制所有从页面发出的 HTTP 请求的Referer首部字段。下面所述的页面的源包含协议、域名和端口
      • no-referrer:不发送Referer首部字段。
      • origin:发送页面的源。
      • no-referrer-when-downgrade
      • origin-when-cross-origin:对于与页面同源的请求发送页面完整的 URL,与页面不同源的请求只发送页面的源。
      • same-origin:对于与页面同源的请求发送页面的源,与页面不同源的请求不发送Referer首部字段。
      • strict-origin
      • strict-origin-when-cross-origin
      • unsafe-URL:发送页面完整的 URL。
    • theme-color:建议客户端应该用来使用的主题颜色。
    • color-scheme:指定一个或多个主题颜色。
      • normal
      • [light | dark]+
      • only light
    • creator:定义网页作者,即一个组织或者机构的名称。
    • googlebot
    • publisher:定义网页发布者。
    • robots
    • slurp
    • viewport:专门用于为移动端设备定义视口的大小。
      • width
      • height
      • initial-scale
      • maximum-scale
      • minimum-scale
      • user-scalable
-
-

If there is any conflict while you are rebasing, you should fix the conflict, and then use git add to stage the files, use git rebase --continue to finish the rebasing process. In this process, do not use git commit, it will create a commit and detach the HEAD!

-
]]> +]]> - Git + HTML&CSS - 圣杯布局和双飞翼布局的原理 - /posts/ee7ae9c6/ - 圣杯布局
-
  1. 三个部分必须都设置向左浮动。
  2. 先写 middle,让页面先渲染中间的重要区域。
  3. content 设置左右内边距是为了给 left 和 right 留出空间。
  4. middle 设置width: 100%,而 left 和 right 的width属性值分别为 content 的左右内边距。
  5. 为 left 和 right 分别设置margin-left,由于 middle 也是浮动元素,所以这样会使 left 和 right 向上层浮动。
  6. 通过position: relative,分别将 left 和 right 向左右两侧拉扯。
-
+ min-*、max-*、*的优先级 + /posts/df94ca98/ + 本文参考链接理解css中min-width和max-width,width与它们之间的区别联系

-

由于 left 设置了margin-left: -100%,当 content 内容区的宽度小于 left 的宽度时,此时 left 左移的距离小于 left 自身的宽度,导致 left 并不会向上移动到与 middle 同层。简言之,布局乱了。

+

widthheight的情况是一致的,下面以width为例进行说明。

-
<div class="content">
<div class="middle"></div>
<div class="left"></div>
<div class="right"></div>
</div>

<style>
.content {
padding: 0 200px;
}
.middle {
width: 100%;
height: 80px;
float: left;
background: green;
}
.left {
width: 200px;
height: 80px;
float: left;
margin-left: -100%;
position: relative;
left: -200px;
background: red;
}
.right {
width: 200px;
height: 80px;
float: left;
margin-left: -200px;
position: relative;
right: -200px;
background: blue;
}
</style>
-

双飞翼布局

-

圣杯布局的升级版,修改 DOM 结构,然后使用margin代替padding

+

min-width比width大时

+

min-width的优先级高于width,即使有!important也是如此。

+
+
<div></div>
<style>
div {
height: 200px;
min-width: 400px;
width: 200px !important;
background: black;
}
</style>
+

min-width比max-width大时

+

min-width的优先级高于max-width,即使有!important也是如此。

+
+
<div></div>
<style>
div {
height: 200px;
min-width: 400px;
max-width: 200px !important;
background: black;
}
</style>
+

max-width比width小时

+

max-width的优先级高于width,即使有!important也是如此。

-
<div class="content">
<div class="middle">
<div class="inner-middle"></div>
</div>
<div class="left"></div>
<div class="right"></div>
</div>

<style>
.middle {
width: 100%;
height: 80px;
float: left;
}
.inner-middle {
margin: 0 200px;
height: 80px;
background: green;
}
.left {
width: 200px;
height: 80px;
float: left;
margin-left: -100%;
background: red;
}
.right {
width: 200px;
height: 80px;
float: left;
margin-left: -200px;
background: blue;
}
</style>
+
<div></div>
<style>
div {
height: 200px;
max-width: 200px;
width: 400px !important;
background: black;
}
</style>
]]> HTML&CSS @@ -1351,70 +1319,102 @@ - 媒体查询 - /posts/d62e352c/ - 媒体类型
-
  • all:所有媒体
  • braile:盲文触觉设备
  • embossed:盲文打印机
  • handheld:手持设备
  • print:打印预览或打印机
  • projection:项目演示
  • screen:彩屏设备
  • speech:听觉类似的媒体
  • tty:不适用像素的设备,如电传打字机
  • tv:电视
+ 圣杯布局和双飞翼布局的原理 + /posts/ee7ae9c6/ + 圣杯布局
+
  1. 三个部分必须都设置向左浮动。
  2. 先写 middle,让页面先渲染中间的重要区域。
  3. content 设置左右内边距是为了给 left 和 right 留出空间。
  4. middle 设置width: 100%,而 left 和 right 的width属性值分别为 content 的左右内边距。
  5. 为 left 和 right 分别设置margin-left,由于 middle 也是浮动元素,所以这样会使 left 和 right 向上层浮动。
  6. 通过position: relative,分别将 left 和 right 向左右两侧拉扯。
-

媒体特性

-

以下特性,除scangrid外,都可以加上minmax前缀以指定范围

  • width:视口宽度
  • height:视口高度
  • device-width:渲染表面的宽度(可以认为是设备屏幕的宽度)
  • device-height:渲染表面的高度(可以认为是设备屏幕的高度)
  • orientation:设备方向是水平还是垂直
  • aspect-ratio:视口的宽高比。如16:9的宽屏显示器可以写成aspect-ratio: 16/9
  • color:颜色组分的位深。如min-color: 16表示设备至少支持16位深
  • color-index:设备颜色查找表中的条目数,值必须是数值,且不能为负
  • monochrome:单色帧缓冲中表示每个像素的位数,值必须是数值(整数),且不能为负
  • resolution:屏幕或打印分辨率。如min-resolution: 300dpi,也可以接受每厘米多少点,如min-resolution: 118dpcm
  • scan:针对电视的逐行扫描(progressive)和隔行扫描(interlace
  • grid:设备基于栅格还是位图
+
+

由于 left 设置了margin-left: -100%,当 content 内容区的宽度小于 left 的宽度时,此时 left 左移的距离小于 left 自身的宽度,导致 left 并不会向上移动到与 middle 同层。简言之,布局乱了。

-

媒体查询方式

link标签的media属性

<link>标签的media属性中指定媒体查询是CSS2的方式

-
<link rel="stylesheet" type="text/css" media="screen" href="screenstyles.css" />
-

@media

@media是在样式表中使用的媒体查询方式

-
@media screen { ... }
-

@import

@import可以根据媒体查询将其他样式表加载到当前样式表中

-
@import url("phone.css") screen
-

媒体查询语法

-
  • and:类似于逻辑与
  • ,:类似于逻辑或
  • not:用于对整个媒体查询取反
  • only:指定某种特定的媒体类型,用来对那些不支持媒体特性但支持媒体类型的设备隐藏样式表
+
<div class="content">
<div class="middle"></div>
<div class="left"></div>
<div class="right"></div>
</div>

<style>
.content {
padding: 0 200px;
}
.middle {
width: 100%;
height: 80px;
float: left;
background: green;
}
.left {
width: 200px;
height: 80px;
float: left;
margin-left: -100%;
position: relative;
left: -200px;
background: red;
}
.right {
width: 200px;
height: 80px;
float: left;
margin-left: -200px;
position: relative;
right: -200px;
background: blue;
}
</style>
+

双飞翼布局

+

圣杯布局的升级版,修改 DOM 结构,然后使用margin代替padding

-
/* 当设备是彩屏设备并且视口最小宽度大于等于400px时生效 */
@media screen and (min-width: 400px) { ... }


/* 当设备垂直或者视口最小宽度大于等于400px时生效 */
@media (orientation: portrait), (min-width: 400px) { ... }


/* 当视口最小宽度小于400px时生效 */
@media not all and (min-width: 400px) { ... }
/* 等价于 */
@media not (all and (min-width: 400px)) { ... }
/* 而不是 */
@media (not all) and (min-width: 400px) { ... }


@media only screen and (min-width: 400px) { ... }
+
<div class="content">
<div class="middle">
<div class="inner-middle"></div>
</div>
<div class="left"></div>
<div class="right"></div>
</div>

<style>
.middle {
width: 100%;
height: 80px;
float: left;
}
.inner-middle {
margin: 0 200px;
height: 80px;
background: green;
}
.left {
width: 200px;
height: 80px;
float: left;
margin-left: -100%;
background: red;
}
.right {
width: 200px;
height: 80px;
float: left;
margin-left: -200px;
background: blue;
}
</style>
]]> HTML&CSS - HTML标签 - /posts/500cdacf/ - HTML标签大全

太多了,整理一会之后果断放弃,MDN上的介绍

-

HTML模板

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="简介" />
<meta name="keywords" content="关键字" />
<meta name="author" content="作者" />
<title>题目</title>
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="style.css" type="text/css" />
<style type="text/css">
/* CSS内部样式表 */
</style>
</head>
<body>
<!-- 这里放内容 -->
<script type="text/javascript" src="example.js"></script>
</body>
</html>
-

a标签

-

<a>标签代表一个超链接,它有如下属性:

  • download:这个属性用于指示浏览器去下载href所指向的资源而不是进行页面跳转。如果给download属性设置一个值,这个值将是对应资源被保存成本地文件过程中的默认名称。
  • href:URL。
  • hreflang:用于指定链接资源的人类语言
  • ping:包含一个以空格分隔的 URL 列表,当跟随超链接时,将由浏览器发送带有正文的 PING 的 POST 请求,通常用于跟踪。
  • rel:该属性执行了目标对象到链接对象的关系。该值是空格分隔的列表类型值。
  • target:该属性指定在何处显示链接的资源。它的值包括:_self_blank_parent_top
  • type:该属性指定在一个 MIME type 链接目标的形式的媒体查询。其仅提供建议,并没有内置的功能。

<a>标签还有四个伪类:

  • link:具有href属性。
  • visited:被访问过。
  • hover:鼠标悬浮。
  • active:鼠标点击瞬间。
+ 事件 + /posts/9c2b83ad/ + JavaScript 与 HTML 之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间,比如:点击按钮、拖动鼠标等。

+

事件流

事件冒泡

事件冒泡是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的 HTML 页面为例:

+
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
+

如果你单击了页面中的 <div> 元素,那么这个 click 事件会按照如下顺序传播

+

事件冒泡

+

事件捕获

事件捕获的顺序与事件冒泡的顺序正好相反,以前面的HTML页面为例,它的顺序为:

+

事件捕获

+

事件流

DOM2 级事件规定的事件流包括三个阶段:事件捕获阶段处于目标阶段事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。还是以前面的HTML页面为例,它的顺序如下所示:

+

事件流

+

在 DOM 事件流中,实际的目标在捕获阶段不会接收到事件。这意味着在捕获阶段,事件从 document<html> 再到 <body> 后就停止了。下一个阶段是“处于目标”阶段,于是事件在 <div> 上发生,并在事件处理(后面将会讨论这个概念)中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回文档。

+

多数支持 DOM 事件流的浏览器都实现了一种特定的行为:即使DOM2 级事件规范明确要求捕获阶段不会涉及事件目标,但 IE9、Safari、Chrome、Firefox 和 Opera9.5 及更高版本都会在捕获阶段触发事件对象上的事件。结果就是有两个机会在目标对象上面操作事件。

+

事件处理程序

事件就是用户或浏览器自身执行的某种动作。诸如 click、load 和 mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字就是在事件名前面加上 "on",因此 click 事件的事件处理程序就是 onclick,load 事件的事件处理程序就是 onload。为事件注册处理程序的方式有好几种,下面一一进行介绍。

+

HTML 事件处理程序

某个元素支持的每种事件,都拥有一个与相应事件处理程序同名的 HTML 特性,可以通过这个特性来注册事件处理程序。下面以 onclick 事件为例:

+
// test.js
function showAnother (that) {
alert(that.value)
}
+
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<!-- 直接在HTML中指定具体动作 -->
<input type="button" value="1" onclick="alert(this.value)" />

<!-- 调用在<script>标签内定义的函数 -->
<input type="button" value="2" onclick="showMessage(this, event)" />

<!-- 调用外部js文件中定义的函数 -->
<input type="button" value="3" onclick="showAnother(this)" />

<script type="text/javascript" src="./test.js"></script>
<script type="text/javascript">
function showMessage (that, event) {
alert(that.value)
console.log(that)
console.log(event)
}
</script>
</body>
</html>
+

在 HTML 中注册事件处理程序,会创建一个封装着元素属性值的函数。这个函数中有一个局部变量 event,也就是事件对象(后面将会讨论这个概念),通过 event 变量可以直接访问事件对象。并且在这个函数内部,this 值等于事件的目标元素。所以你可以将 eventthis 当做参数,传递给要调用的函数。关于这一点,你可以查看上面例子中 showMessage() 函数打印的内容来验证。

+
+

HTML 事件处理程序的缺点:

  • 如果用户在页面解析 showMessage()showAnother() 之前就点击了对应的按钮,会抛出错误。可以使用 onclick="try {showMessage()} catch(ex) {}" 的形式来解决这个问题。
  • 事件处理程序的作用域链在不同的浏览器中会有不同的结果。
  • HTML 和 JS 代码紧密耦合。
-

下面展示一个使用download属性的例子。

-
<a download="国旗" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALYAAAB5CAMAAACjkCtXAAAAe1BMVEX/AAD////XsbH0RETe2dnToqLi7e3wVVX80Rb80xb81Rb82Bf/MQX/JQT8xhX/FwL8yxX+VQn/Ogb83Rf8whT9nBD+TAj9jw/9lQ/9rhL9uBP8uxT+bgv9ohDGv7/SuLj9hA3+XQj9eAn+fw39qBL+Qwf+ZQv2OzvmbW2v1N+9AAACiklEQVR4nO3ZCW+bMByHYdjpg8PcEMhKUpL2+3/CORs9osUGofFzUP6vlKpqFfTINTZOPd//9WVTPf3wfd/z/e/epvpJbGDERkZsZMRGRmxkxEb2YOxkFc3slrLzVTSzW8QOgpIlQbCWaUaL2D0XnMsqXAs13bJJEkZSOZ3dy9ixGMRhJdGslrH73uuKlUSzmmbHN36Wji9nTbKT9pb7UxO/XqdJdp+drBeIq/9umtEku8mO1guEuYv1e4qtl7q95e1VUbCi6OETZYp9YDy3bCtpoTeeAX93TrGfBVel7QIqixzsllPsvWDCNrnPUZLbJ/8qGdjp+HePW82ux+/TG8N6TL3gbtipEoypdqgqxvTk7usmV4zJAe8zZBjtQ5RxLoSU7OKWUug7L2ucboxXGed2zwT7nIgcH2iuMt+S5S7jH+isdnkq+CfbSnKM3txiZ10E8VkXwHJ0i8HhQeZmdvY4vXlkvkDsZHmxsmu9kPDLgGfm2/H+HqUCxZlQjdBfTUNaNQ1vmuquHqWOeofkZz3mnCvDQ3dYSS4cHOEt7HgnRH7hdkzTTBdQKnIwSyzskxTt30Hu9GZjeH/Xlrvn9XimLOwiG97m7CnPzrff340vcGZ2HNUfd9qptZ1x8FlG+/pGc3JAN/Zgn297idNHq8XsYYMfXSaHkvXJ2d0D1jL2KWec8Xol04yWTpJGSpezZCE7zAflYHN8byG7q7xgv7l/gvzZikKHO9CjbTduIzYyYiMjNjJiIyM2MmIjIzYyYiMjNjJiIyM2MmIjIzYyYiMjNjJiIyM2MmIjIzYyYiMjNjJiIyM2MmIjIzYyYiMjNjJiIyM2MmIje2e/fN1SryP728Ya2Vtso+zffNMqoOoYARcAAAAASUVORK5CYII=">下载图片</a>
-

input标签

-

<input>标签用于接受来自用户的数据。它有如下属性:

  • autocomplete:一个字符串,描述输入应提供的任何类型的自动完成功能。自动完成的典型实现只是回忆在同一输入字段中输入的先前值,但可以存在更复杂的自动完成形式。例如,浏览器可以与设备的联系人列表集成,以在电子邮件输入字段中自动完成电子邮件地址。不过当type属性值为buttonfile等时,<input>不会返回文本或数值数据,此时autocomplete属性将被忽略。
  • autofocus
  • disabled
  • form:该属性值为一个<form>id,用于指示该<input>是属于它的。如果缺失,则该<input>属于最近的<form>或者不属于任何<form>
  • list:该属性值为一个<datalist>id<datalist>用于为该<input>提供建议列表。
  • name:该<input>的名称,与表单数据一起提交。
  • readonly
  • required
  • tabindex:该<input>在当前文档的 Tab 导航顺序中的位置。
  • type:该<input>的类型。包括:buttoncheckboxcolordatedatetime-localemailfilehiddenimagemonthnumberpasswordradiorangeresetsearchsubmitteltexttimeurlweek
  • value:该<input>的当前值。
+

DOM0 级事件处理程序

+
  • 使用 DOM0 级方法只能在元素上注册一个事件处理程序。
  • 对于相同的事件,DOM0 级事件处理程序与 HTML 事件处理程序事件处理程序无法共存。
  • 使用 DOM0 级方法注册的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行,所以此时函数中的 this 指向元素自身。
  • 在注册事件处理程序时,不推荐使用 箭头函数,因为它会造成 this 的丢失。
+
+

在添加的事件处理程序函数内部,可以直接通过 event 变量访问事件对象。也可以通过给程序处理函数定义参数或者使用 arguments 来访问事件对象,在下面有例子。

+

以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。你可以通过将事件处理程序属性的值设置为 null 来删除添加的事件处理程序。

+

来看一个例子:

+
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<input id="myButton" type="button" value="1" />
<script type="text/javascript">
var button = document.getElementById('myButton')
button.onclick = function (e) {
console.log(this)
console.log(event)

// true
console.log(event === e)

// true
console.log(event === arguments[0])

// 删除添加的事件处理程序
button.onclick = null
}
</script>
</body>
</html>
+

这个例子中的按钮,只有第一次点击时会打印内容,之后就没有任何反应,因为事件处理程序在第一次触发之后,就被删除了。

+

DOM2 级事件处理程序

DOM2 级事件定义了两个方法,addEventListener()removeEventListener(),分别用于注册和删除事件处理程序,所有 DOM 节点都包含这两个方法。

+
+
  • 使用 addEventListener() 可以在同一个元素上注册多个事件处理程序,触发的顺序为添加顺序。
  • 关于 thisevent 的使用规则,与 DOM0 级事件处理程序一致。
-

meta标签

<meta>标签中包含那些不能被<base><link><script><style><title>标签所表示的内容。并且<meta>标签一次只干一件事,如果想利用<meta>标签设置多个内容,那就多写几个<meta>标签。它有如下属性:

-
  • charset:该属性表明了页面的编码方式,通常为UTF-8
  • content:该属性值对应于http-equiv属性或name属性的值。
  • http-equiv:该属性定义 HTTP 的一些首部字段,对应的首部字段值在content属性中。
    • content-security-policy:定义页面的内容安全策略,内容策略主要指定允许的服务器源和脚本端点,这有助于防止 XSS 攻击。
    • refresh
      • 如果content属性值是一个正整数,则该正数代表页面重载的时间间隔(秒)。
      • 如果content属性值是一个正整数,并且后面跟着;url=和一个合法的 URL,则页面将在指定秒数后进行跳转。
  • name:该属性定义能定义如下内容,同样的,这些内容的值在content属性中。
    • author:定义作者。
    • description:定义页面的描述信息。在你收藏一个页面时,描述信息就是来自于这里的。
    • generator:生成页面的软件的标识符。
    • keywords:用,分隔的关键词。
    • referrer:控制所有从页面发出的 HTTP 请求的Referer首部字段。下面所述的页面的源包含协议、域名和端口
      • no-referrer:不发送Referer首部字段。
      • origin:发送页面的源。
      • no-referrer-when-downgrade
      • origin-when-cross-origin:对于与页面同源的请求发送页面完整的 URL,与页面不同源的请求只发送页面的源。
      • same-origin:对于与页面同源的请求发送页面的源,与页面不同源的请求不发送Referer首部字段。
      • strict-origin
      • strict-origin-when-cross-origin
      • unsafe-URL:发送页面完整的 URL。
    • theme-color:建议客户端应该用来使用的主题颜色。
    • color-scheme:指定一个或多个主题颜色。
      • normal
      • [light | dark]+
      • only light
    • creator:定义网页作者,即一个组织或者机构的名称。
    • googlebot
    • publisher:定义网页发布者。
    • robots
    • slurp
    • viewport:专门用于为移动端设备定义视口的大小。
      • width
      • height
      • initial-scale
      • maximum-scale
      • minimum-scale
      • user-scalable
+

首先介绍 addEventListener() 方法,它的参数如下:

  • type:表示监听事件类型的字符串,需要注意的是没有 on 前缀
  • listener:作为事件处理程序的函数。
  • options(可选):一个对象。其属性如下:
    • capture:一个布尔值,默认为 false。当值为 true 时,listener 会在事件捕获阶段时被调用。
    • once:一个布尔值,默认为 false。当值为 true 时,listener 会在其被调用之后自动移除。
    • passive:一个布尔值,默认为 false。当值为 true 时,listener 内部不允许调用 event.preventDefault(),否则会抛出错误。
  • useCapture(可选):一个布尔值,默认为 false。当值为 true 时,listener 会在事件捕获阶段时被调用。

对于 optionsuseCapture 参数,它们都是该方法的第三个参数,options 是新标准,而 useCapture 是老标准。


接着介绍 removeEventListener() 方法,它的参数如下:

  • type:表示监听事件类型的字符串,需要注意的是没有 on 前缀
  • listener:作为事件处理程序的函数。
  • options(可选):一个对象。其属性如下:
    • capture:一个布尔值,默认为 false。当值为 true 时,表示要移除的 listener 是注册在事件捕获阶段的。
  • useCapture(可选):一个布尔值,默认为 false。当值为 true 时,表示要移除的 listener 是注册在事件捕获阶段的。

如果一个事件处理程序一共注册了两次,一次在事件捕获阶段,一次在事件冒泡阶段,那么这两次注册需要分别移除,两者不会互相干扰。

+
+

下面的例子用于观察 options.captureuseCapture 的效果。

+
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<style>
#outer, #inner {
display: block;
width: 500px;
height: 500px;
text-decoration: none;
}
#outer{
border: 1px solid red;
color: red;
}
#inner{
border: 1px solid green;
color: green;
width: 250px;
height: 250px;
margin: 125px auto;
}
</style>
</head>
<body>
<div id="outer">
outer, capture & none-capture
<div id="inner">
inner
</div>
</div>
<script type="text/javascript">
var outer = document.getElementById('outer')
var inner = document.getElementById('inner')

function captureListener1 () {
console.log('outer, capture1')
outer.removeEventListener('click', captureListener1, true)
}
function captureListener2 () {
console.log('outer, capture2')
}
function noneCaptureListener () {
console.log('outer, none-capture')
}
function innerListener () {
console.log('inner')
}

outer.addEventListener('click', captureListener1, { capture: true })
outer.addEventListener('click', captureListener2, true)
outer.addEventListener('click', noneCaptureListener)
inner.addEventListener('click', innerListener)
</script>
</body>
</html>
+

上例中 captureListener1captureListener2 都是注册在 outer 的捕获阶段,而 noneCaptureListenerinnerListener 分别注册在 outerinner 的冒泡阶段。并且 captureListener1 会在第一次调用后被移除。请多点击几次 inner 框,查看打印的结果。

+

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。这里只介绍 DOM 中的事件对象,忽略 IE 的。

+

无论注册事件处理程序时使用的是 DOM0 级还是 DOM2 级方法,兼容 DOM 的浏览器都会将一个 event 对象传入到事件处理程序中,这样就可以直接在函数内部访问到 event 对象了。

+

event 对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。下面简单的对事件对象的属性和方法进行了介绍,如果想查看详细信息,去移步 MDN

+
+

属性:

  • bubbles(只读):表明事件是否会冒泡。
  • cancelBubble:通过将该属性设置为 true 可阻止事件继续冒泡。
  • cancelable(只读):表明是否可以取消事件的默认行为。
  • currentTarget(只读):事件处理程序注册在哪个元素上,currentTarget 就指向哪个元素。
  • defaultPrevented(只读):表明是否已经调用了 preventDefault() 方法。
  • eventPhase(只读):表明处于事件流的哪个阶段。1 表示捕获阶段,2 表示处于目标,3 表示冒泡阶段。
  • target(只读):触发事件的那个元素,也就是事件流在“处于目标”阶段时的那个目标元素。
  • timeStamp(只读):表明事件对象的创建时间。
  • type(只读):表明事件对象的类型。
  • isTrusted(只读):当事件是由用户触发的时(比如点击鼠标),该属性值为 true。当事件是由脚本触发时,该属性值为 false

方法:

  • preventDefault():取消事件的默认行为。该方法只有在 cancelable 属性为 true 时才会起作用。
  • stopImmediatePropagation():取消事件的进一步捕获或冒泡,同时阻止其后的所有事件处理程序被调用。
  • stopPropagation():取消事件的进一步捕获或冒泡,但是不会阻止注册在当前 currentTarget 上的事件处理程序被调用。
+

事件类型

+

下面我只简单的介绍一下我认为比较常用的事件,如果你想比较全面的了解这里,点击下面的链接!

MDN,只有你想不到,没有找不到

+
+

内存和性能

在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

+

事件委托

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

+

移除事件处理程序

每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”,也是造成 Web 应用程序内存与性能问题的主要原因。

+

在两种情况下,可能会造成上述问题。第一种情况就是从文档中移除带有事件处理程序的元素时。这可能是通过纯粹的 DOM 操作,例如使用 removeChild() 方法,但更多地是发生在使用 innerHTML 替换页面中某一部分的时候。如果带有事件处理程序的元素被 innerHTML 删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。所以如果你知道某个元素即将被移除,那么最好在此之前手工移除事件处理程序。

+

另一种情况,就是卸载页面的时候。如果在页面被卸载之前没有清理干净事件处理程序,那它们就会滞留在内存中。每次加载完页面在卸载页面时(可能是在两个页面间来回切换,也可能是单击了“刷新”按钮),内存中滞留的对象数目就会增加,因为事件处理程序占用的内存并没有被释放。因此最好的做法就是在页面卸载之前,先通过 onunload 事件处理程序移除所有事件处理程序。

]]> - HTML&CSS + FE - min-*、max-*、*的优先级 - /posts/df94ca98/ - 本文参考链接理解css中min-width和max-width,width与它们之间的区别联系

-
-

widthheight的情况是一致的,下面以width为例进行说明。

-
-

min-width比width大时

-

min-width的优先级高于width,即使有!important也是如此。

+ 媒体查询 + /posts/d62e352c/ + 媒体类型
+
  • all:所有媒体
  • braile:盲文触觉设备
  • embossed:盲文打印机
  • handheld:手持设备
  • print:打印预览或打印机
  • projection:项目演示
  • screen:彩屏设备
  • speech:听觉类似的媒体
  • tty:不适用像素的设备,如电传打字机
  • tv:电视
-
<div></div>
<style>
div {
height: 200px;
min-width: 400px;
width: 200px !important;
background: black;
}
</style>
-

min-width比max-width大时

-

min-width的优先级高于max-width,即使有!important也是如此。

+

媒体特性

+

以下特性,除scangrid外,都可以加上minmax前缀以指定范围

  • width:视口宽度
  • height:视口高度
  • device-width:渲染表面的宽度(可以认为是设备屏幕的宽度)
  • device-height:渲染表面的高度(可以认为是设备屏幕的高度)
  • orientation:设备方向是水平还是垂直
  • aspect-ratio:视口的宽高比。如16:9的宽屏显示器可以写成aspect-ratio: 16/9
  • color:颜色组分的位深。如min-color: 16表示设备至少支持16位深
  • color-index:设备颜色查找表中的条目数,值必须是数值,且不能为负
  • monochrome:单色帧缓冲中表示每个像素的位数,值必须是数值(整数),且不能为负
  • resolution:屏幕或打印分辨率。如min-resolution: 300dpi,也可以接受每厘米多少点,如min-resolution: 118dpcm
  • scan:针对电视的逐行扫描(progressive)和隔行扫描(interlace
  • grid:设备基于栅格还是位图
-
<div></div>
<style>
div {
height: 200px;
min-width: 400px;
max-width: 200px !important;
background: black;
}
</style>
-

max-width比width小时

-

max-width的优先级高于width,即使有!important也是如此。

+

媒体查询方式

link标签的media属性

<link>标签的media属性中指定媒体查询是CSS2的方式

+
<link rel="stylesheet" type="text/css" media="screen" href="screenstyles.css" />
+

@media

@media是在样式表中使用的媒体查询方式

+
@media screen { ... }
+

@import

@import可以根据媒体查询将其他样式表加载到当前样式表中

+
@import url("phone.css") screen
+

媒体查询语法

+
  • and:类似于逻辑与
  • ,:类似于逻辑或
  • not:用于对整个媒体查询取反
  • only:指定某种特定的媒体类型,用来对那些不支持媒体特性但支持媒体类型的设备隐藏样式表
-
<div></div>
<style>
div {
height: 200px;
max-width: 200px;
width: 400px !important;
background: black;
}
</style>
+
/* 当设备是彩屏设备并且视口最小宽度大于等于400px时生效 */
@media screen and (min-width: 400px) { ... }


/* 当设备垂直或者视口最小宽度大于等于400px时生效 */
@media (orientation: portrait), (min-width: 400px) { ... }


/* 当视口最小宽度小于400px时生效 */
@media not all and (min-width: 400px) { ... }
/* 等价于 */
@media not (all and (min-width: 400px)) { ... }
/* 而不是 */
@media (not all) and (min-width: 400px) { ... }


@media only screen and (min-width: 400px) { ... }
]]> HTML&CSS @@ -1683,38 +1683,6 @@ HTML&CSS - - 选择器 - /posts/7c8269b4/ - 选择器种类

标签选择器

<div>
<span>选择器</span>
</div>

<style type="text/css">
span {
color: red;
}
</style>
-

类选择器

<div>
<span class="my-span">选择器</span>
</div>

<style type="text/css">
.my-span {
color: red;
}
</style>
-

id选择器

<div>
<span id="my-span">选择器</span>
</div>

<style type="text/css">
#my-span {
color: red;
}
</style>
-

通用选择器

<div>
<h1>h1</h1>
<p>p</p>
<span>span</span>
</div>

<style type="text/css">
* {
color: red;
}
</style>
-

属性选择器

-

假设元素内设置了data-attr="value1 value2 value3",下面介绍对它使用属性选择器的几种方式:

  • [data-attr]:根据属性名称;
  • [data-attr="value1 value2 value3"]:根据属性名和属性值;
  • [data-attr^="val"]:以……开头;
  • [data-attr*="value2"]:包含……;
  • [data-attr$="3"]:以……结尾。
-
-
<div>
<span data-attr="value1 value2 value3">选择器</span>
</div>

<style type="text/css">
[data-attr] {
color: red;
}

[data-attr="value1 value2 value3"] {
color: red;
}

[data-attr^="val"] {
color: red;
}

[data-attr*="value2"] {
color: red;
}

[data-attr$="3"] {
color: red;
}
</style>
-

选择器使用规则

-

选择器的组合使用:

  • div, span:同时选择<div><span>元素;
  • div.classValue:选择class属性值包含classValue<div>元素;
  • div#idValue:选择id属性值为idValue<div>元素;
  • div[data-attr]:选择具有data-attr属性的<div>元素;
  • div.class1.class2:选择class属性值同时包含class1class2<div>元素。

根据结构来使用选择器:

  • selector1 selector2:当selector2selector1的后代元素时,选择selector2
  • selector1 > selector2:当selector2selector1的子元素时,选择selector2
  • selector1 + selector2:当selector2selector1的同胞元素,且selector2紧跟着selector1时,选择selector2
  • selector1 ~ selector2:当selector2selector1的同胞元素,且selector2位于selector1后面时,选择selector2
  • selector1 * selector2:当selector2selector1的后代元素,且selector2不是selector1的子元素时,选择selector2
-
-

伪类和伪元素

MDN上伪类和伪元素的总结

-
-
  • 伪类用于给已经存在于文档树中的元素添加不存在的类,并为其添加样式;
  • 伪元素用于创建文档树中不存在的元素,并为其添加样式。
-
-

样式层叠

样式来源

-

以下就是浏览器层叠各个来源样式的顺序:

  • 浏览器默认样式表
  • 用户样式表
  • 作者链接样式表(按照它们链接到页面的先后顺序)
  • 作者嵌入样式
  • 作者行内样式
-
-

特指度

-
  1. 100:ID选择器。
  2. 10:类选择器、伪类选择器和属性选择器。
  3. 1:标签选择器和伪元素。
-
-

特指度

-

层叠规则

-
  1. 带有!import的优先级最高(开挂一样的存在)。
  2. 行内样式优先级仅次于!import
  3. 抛开!import和行内样式,其他情况下,特指度大的优先级高。
  4. 当特指度相同时,嵌入样式表 > 链接样式表 > 用户样式表 > 浏览器默认样式表。
  5. 当特指度相同时,则后出现的优先级高。
-
]]>
- - HTML&CSS - -
过渡、变形和动画 /posts/540576d8/ @@ -1752,6 +1720,38 @@ HTML&CSS + + 选择器 + /posts/7c8269b4/ + 选择器种类

标签选择器

<div>
<span>选择器</span>
</div>

<style type="text/css">
span {
color: red;
}
</style>
+

类选择器

<div>
<span class="my-span">选择器</span>
</div>

<style type="text/css">
.my-span {
color: red;
}
</style>
+

id选择器

<div>
<span id="my-span">选择器</span>
</div>

<style type="text/css">
#my-span {
color: red;
}
</style>
+

通用选择器

<div>
<h1>h1</h1>
<p>p</p>
<span>span</span>
</div>

<style type="text/css">
* {
color: red;
}
</style>
+

属性选择器

+

假设元素内设置了data-attr="value1 value2 value3",下面介绍对它使用属性选择器的几种方式:

  • [data-attr]:根据属性名称;
  • [data-attr="value1 value2 value3"]:根据属性名和属性值;
  • [data-attr^="val"]:以……开头;
  • [data-attr*="value2"]:包含……;
  • [data-attr$="3"]:以……结尾。
+
+
<div>
<span data-attr="value1 value2 value3">选择器</span>
</div>

<style type="text/css">
[data-attr] {
color: red;
}

[data-attr="value1 value2 value3"] {
color: red;
}

[data-attr^="val"] {
color: red;
}

[data-attr*="value2"] {
color: red;
}

[data-attr$="3"] {
color: red;
}
</style>
+

选择器使用规则

+

选择器的组合使用:

  • div, span:同时选择<div><span>元素;
  • div.classValue:选择class属性值包含classValue<div>元素;
  • div#idValue:选择id属性值为idValue<div>元素;
  • div[data-attr]:选择具有data-attr属性的<div>元素;
  • div.class1.class2:选择class属性值同时包含class1class2<div>元素。

根据结构来使用选择器:

  • selector1 selector2:当selector2selector1的后代元素时,选择selector2
  • selector1 > selector2:当selector2selector1的子元素时,选择selector2
  • selector1 + selector2:当selector2selector1的同胞元素,且selector2紧跟着selector1时,选择selector2
  • selector1 ~ selector2:当selector2selector1的同胞元素,且selector2位于selector1后面时,选择selector2
  • selector1 * selector2:当selector2selector1的后代元素,且selector2不是selector1的子元素时,选择selector2
+
+

伪类和伪元素

MDN上伪类和伪元素的总结

+
+
  • 伪类用于给已经存在于文档树中的元素添加不存在的类,并为其添加样式;
  • 伪元素用于创建文档树中不存在的元素,并为其添加样式。
+
+

样式层叠

样式来源

+

以下就是浏览器层叠各个来源样式的顺序:

  • 浏览器默认样式表
  • 用户样式表
  • 作者链接样式表(按照它们链接到页面的先后顺序)
  • 作者嵌入样式
  • 作者行内样式
+
+

特指度

+
  1. 100:ID选择器。
  2. 10:类选择器、伪类选择器和属性选择器。
  3. 1:标签选择器和伪元素。
+
+

特指度

+

层叠规则

+
  1. 带有!import的优先级最高(开挂一样的存在)。
  2. 行内样式优先级仅次于!import
  3. 抛开!import和行内样式,其他情况下,特指度大的优先级高。
  4. 当特指度相同时,嵌入样式表 > 链接样式表 > 用户样式表 > 浏览器默认样式表。
  5. 当特指度相同时,则后出现的优先级高。
+
]]>
+ + HTML&CSS + +
Array /posts/6e01c1fe/ @@ -2338,6 +2338,84 @@
let target = {}
let proxy = new Proxy(target, {
has (trapTarget, key) {
return Reflect.has(trapTarget, key)
}
})
let thing = Object.create(proxy)

// 触发了 has 陷阱函数
// false
console.log('name' in thing)

thing.name = 'thing'

// 没有触发 has 陷阱函数
// true
console.log('name' in thing)

将代理作为类的原型

类不能直接被修改为将代理用作自身的原型,因为它们的 prototype 属性是不可写入的。然而你可以使用一点变通手段,利用继承来创建一个把代理作为自身原型的类。

function Super () {}

let proxy = new Proxy({}, {})

Super.prototype = proxy

class Sub extends Super {}

let instance = new Sub()

let instanceProto = Object.getPrototypeOf(instance)
let secondLevelProto = Object.getPrototypeOf(instanceProto)

// true
console.log(instanceProto === Sub.prototype)

// true
console.log(secondLevelProto === proxy)
+]]> + + JavaScript + +
+ + Set 与 Map + /posts/d766545b/ + Set 是不包含重复值的列表。你一般不会像对待数组那样来访问 Set 中的某个项。相反更常见的是,只在 Set 中检查某个值是否存在。

+

Map 则是键与相对应的值的集合。因此,Map 中的每个项都存储了两块数据,通过指定所需读取的键即可检索对应的值。Map 常被用作缓存,存储数据以便此后快速检索。

+

ES5 中的 Set 与 Map

在 ES5 中,开发者使用对象属性来模拟 SetMap,就像这样:

+
let set = Object.create(null)

set.foo = true

// 检查属性的存在性
if (set.foo) {
// 一些操作
}
+

本例中的 set 变量是一个原型为 null 的对象,确保在此对象上没有继承属性。使用对象的属性作为需要检查的唯一值在 ES5 中是很常用的方法。当一个属性被添加到 set 对象时,它的值也被设为 true,因此条件判断语句就可以简单判断出该值是否存在。

+

使用对象模拟 Set 与模拟 Map 之间唯一真正的区别是所存储的值。例如:以下例子将对象作为 Map 使用:

+
let map = Object.create(null)

map.foo = 'bar'

// 提取一个值
let value = map.foo

// bar
console.log(value)
+

此代码将字符串值 "bar" 存储在 foo 键上。与 Set 不同,Map 多数被用来提取数据,而不是仅检查键的存在性。

+

变通方法的问题

尽管在简单情况下将对象作为 SetMap 来使用都是可行的,但一旦接触到对象属性的局限性,此方式就会遇到更多麻烦。研究如下代码:

+
let map = Object.create(null)
let key1 = 5
let key2 = '5'
let key3 = {}
let key4 = {}

map[key1] = 'number'

map[key3] = 'foo'

// number
console.log(map[key2])

// foo
console.log(map[key4])

// { '5': 'number', '[object Object]': 'foo' }
console.log(map)
+

造成上面例子中问题的原因是对象属性只可以是字符串或符号类型,所以当你为对象定义属性时,如果属性名不是字符串或符号类型,那么它会调用属性名的 toString() 方法将属性名转换为字符串,然后再进行后续操作。所以 map[key1] === map[key2]map[key3] === map[key4]

+

ES6 的 Set

ES6 新增了 Set 类型,这是一种无重复值的有序列表。Set 允许对它包含的数据进行快速访问,从而增加了一个追踪离散值的更有效方式。

+

创建 Set 并添加项目

Set 使用 new Set() 来创建,而调用 add() 方法就能向 Set 中添加项目,检查 size 属性还能查看其中包含有多少项。

+
let set = new Set()

set.add(5)
set.add('5')

// 2
console.log(set.size)
+

Set 不会使用强制类型转换来判断值是否重复。这意味着 Set 可以同时包含数值 5 与字符串 "5",将它们都作为相对独立的项(在 Set 内部的比较使用了 Object.is() 方法来判断两个值是否相等,唯一的例外是+0与-0被判断为是相等的)。你还可以向 Set 添加多个对象,它们不会被合并为同一项:

+
let set = new Set()
let key1 = {}
let key2 = {}

set.add(key1)
set.add(key2)

// 2
console.log(set.size)
+

由于 key1key2 并不会被转换为字符串,所以它们在这个 Set 内部被认为是两个不同的项(记住:如果它们被转换为字符串,那么都会等于 "[object Object]")。

+

如果 add() 方法用相同值进行了多次调用,那么在第一次之后的调用实际上会被忽略:

+
let set = new Set()

set.add(5)
set.add('5')

// 重复了,该调用被忽略
set.add(5)

// 2
console.log(set.size)
+

你可以使用数组来初始化一个 Set,并且 Set 构造器会确保不重复地使用这些值。

+
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5])

// 5
console.log(set.size)
+

Set 构造器实际上可以接收任意可迭代对象作为参数。能使用数组是因为它们默认就是可迭代的,Map 也是一样的。

+

你可以使用 has() 方法来检测某个值是否存在于 Set 中,就像这样:

+
let set = new Set()

set.add(5)
set.add('5')

// true
console.log(set.has(5))

// false
console.log(set.has(6))
+

移除值

使用 delete() 方法可以移除 Set 中的某个值,使用 clear() 方法可以移除 Set 中的所有值。

+
let set = new Set()
set.add(5)
set.add('5')

set.delete(5)

// false
console.log(set.has(5))

set.clear()

// 0
console.log(set.size)
+

Set 上的 forEach() 方法

Set上 的 forEach() 方法类似于数组中的 forEach() 方法,它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this 值。传入的函数会接收三个参数:Set 元素的值,Set 元素的值和目标 Set 自身。你没看错,第一个和第二个参数是完全相同的,这么设计的目的是为了保持所有对象上 forEach() 方法的统一。

+

将 Set 转换为数组

使用扩展运算符可以很轻松的将 Set 转换为数组,用一个数组去重的例子来展示它是如何转换的:

+
function eliminateDuplicates (array) {
return [...new Set(array)]
}

let numbers = [ 1, 2, 3, 3, 3, 4, 5 ]
let noDuplicates = eliminateDuplicates(numbers)

// [ 1, 2, 3, 4, 5 ]
console.log(noDuplicates)
+

Weak Set

由于 Set 类型存储对象引用的方式,它也可以被称为 Strong Set。对象存储在 Set 的一个实例中时,实际上相当于把对象存储在变量中。只要对 Set 实例的引用仍然存在,所存储的对象就无法被垃圾回收机制回收,从而无法释放内存。例如:

+
let set = new Set()
let key = {}

set.add(key)

// 1
console.log(set.size)

// 取消原始引用
key = null

// 1
console.log(set.size)

// 重新获得原始引用
key = [...set][0]
+

在本例中,将 key 设置为 null 清楚了对 key 对象的一个引用,但是另一个引用还存在于 set 内部。你仍然可以使用扩展运算符将 Set 转换为数组,然后访问数组的第一项,key 变量就取回了原先的对象。

+

为了缓解这个问题,ES6 也包含了 Weak Set,该类型只允许存储对象弱引用,而不能存储基本类型的值。对象的弱引用在它自己成为该对象的唯一引用时,不会阻止垃圾回收。

+

创建 Weak Set

Weak Set 使用 WeakSet 构造器来创建,并包含 add() 方法、has() 方法以及 delete() 方法。以下例子使用了这三个方法:

+
let set = new WeakSet()
let key = {}

set.add(key)

// true
console.log(set.has(key))

set.delete(key)

// false
console.log(set.has(key))
+

使用 Weak Set 很像在使用正规的 Set。你可以在 Weak Set 上添加、移除或检查引用,也可以给构造器传入一个可迭代对象来初始化 Weak Set的值:

+
let key1 = {}
let key2 = {}
let set = new WeakSet([key1, key2])

// true
console.log(set.has(key1))

// true
console.log(set.has(key2))
+

在本例中,一个数组被传给了 WeakSet 构造器。由于该数组包含了两个对象,这些对象就被添加到了 Weak Set 中。要记住若数组中包含了非对象的值,就会抛出错误,因为 WeakSet 构造器不接受基本类型的值。

+

Set 类型之间的关键差异

Weak Set 与正规 Set 之间最大的区别是对象的弱引用。此处有个例子说明了这种差异:

+
let set = new WeakSet()
let key = {}

set.add(key)

// true
console.log(set.has(key))

// 移除对于键的最后一个强引用,同时从Weak Set中移除
key = null
+

当代码被执行后,Weak Set 中的 key 引用就不能再访问了。核实这一点是不可能的,因为需要把对于该对象的一个引用传递给 has() 方法(而只要存在其他引用,Weak Set 内部的弱引用就不会消失)。这会使得难以对 Weak Set 的引用特征进行测试,但 JS 引擎已经正确地将引用移除了,这一点你可以信任。

+

这些例子演示了 Weak Set 与正规 Set 的一些共有特征,但是它们还有一些关键的差异,即:

+
+
  • 对于 WeakSet 的实例,若调用 add() 方法时传入非对象的参数,就会抛出错误(has()delete() 则会在传入了非对象的参数时返回 false)。
  • Weak Set 不可迭代,因此不能被用在 for-of 循环中。
  • Weak Set 无法暴露出任何迭代器,因此没有任何编程手段可用于判断 Weak Set 的内容。
  • Weak Set 没有 forEach() 方法。
  • Weak Set 没有 clear() 方法。
  • Weak Set 没有 size 属性。
+
+

ES6 的 Map

ES6 的 Map 类型是键值对的有序列表,而键和值都可以是任意类型。

+

创建 Map

Map 对键的处理方式与 Set 对值的处理一样,采用的是 Object.is() 方法,而不会进行强制类型转换,唯一的例外是 +0 与 -0 被判断为是相等的。

+

你可以调用 set() 方法并给它传递一个键与一个关联的值,来给 Map 添加项。此后使用键名来调用 get() 方法便能提取对应的值。如果任意一个键不存在于Map中,则 get() 方法就会返回特殊值 undefined。例如:

+
let map = new Map()
let key1 = {}
let key2 = {}

map.set(key1, 5)
map.set(key2, 42)

// 5
console.log(map.get(key1))

// 42
console.log(map.get(key2))

// undefined
console.log(map.get(111))
+

此代码使用了对象 key1key2 作为 Map 的键,并存储了两个不同的值。由于这些键不会被强制转换成其他形式,每个对象就都被认为是唯一的。这允许你给对象关联额外数据,而无须修改对象自身。

+

与 Set 类似,你能将数组传递给 Map 构造器,以便使用数据来初始化一个 Map。该数组中的每一项也必须是数组,内部数组的首个项会作为键,第二项则为对应值。因此整个 Map 就被这些双项数组所填充。例如:

+
let map = new Map([['name', 'Nicholas'], ['age', 25]])

// Nicholas
console.log(map.get('name'))

// 25
console.log(map.get('age'))
+

虽然有数组构成的数组看起来有点奇怪,但这对于准确表示键来说却是必要的。因为键允许是任意数据类型,将键存储在数组中,是确保它们在被添加到 Map 之前不会被强制转换为其他类型的唯一方法。

+

Map 的方法及属性

+
  • has(key):判断指定的键是否存在于 Map 中。
  • delete(key):移除 Map 中的键以及对应的值。
  • clear():移除 Map 中所有的键与值。
  • size:用于指示包含了多少个键值对。
+
+

Map 上的 forEach 方法

Map 的 forEach() 方法类似于 Set 与数组上的 forEach() 方法,不过它是按照键值对被添加到 Map 中的顺序来迭代的。它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this 值。传入的函数会接收三个参数:Map 项的值、该值所对应的的键和目标Map自身。

+

Weak Map

使用Weak Map

ES6 的 WeakMap 类型是键值对的无序列表,其中键必须是非空的对象,值则允许是任意类型。WeakMap 的接口与 Map 的非常相似,都使用 set()get() 方法来分别添加与提取数据。

+

类似于 Weak Set,它没有 size 属性,没有任何办法可以确定 Weak Map 是否为空。在其他引用被移除后,由于对键的引用不再有残留,也就无法调用 get() 方法来去取对应的值。Weak Map 已经切断了对于该值的访问,其所占的内存在垃圾回收器运行时便会被释放。

+

Weak Map 的初始化

为了初始化 Weak Map,需要把一个由数组构成的数组传递给 WeakMap 构造器。就像正规 Map 那样,每个内部数组都应当有两个项,第一项是作为键的对象,第二项则是对应的值(任意类型)。例如:

+
let key1 = {}
let key2 = {}
let map = new WeakMap([[key1, 'hello'], [key2, 42]])

// true
console.log(map.has(key1))

// hello
console.log(map.get(key1))

// true
console.log(map.has(key2))

// 42
console.log(map.get(key2))
+

对象 key1key2 被用作 Weak Map 的键,get()has() 方法则能访问他们。在传递给 WeakMap 构造器的参数中,若任意键值对使用了非对象的键,构造器就会抛出错误。

+

Weak Map 的方法

Weak Map 只有两个附加方法能用来与键值对交互。has() 方法用于判断指定的键是否存在于 Map 中,而 delete() 方法则用于移除一个特定的键值对。clear() 方法不存在,这是因为没有必要对键进行枚举,并且枚举 Weak Map 也是不可能的,这与 Weak Set 相同。

+

对象的私有数据

ES5 中的创建私有数据的方式:

+
var Person = (function () {
var privateData = {}
var privateId = 0

function Person (name) {
Object.defineProperty(this, '_id', {
value: privateId++
})

privateData[this._id] = {
name: name
}
}

Person.prototype.getName = function () {
return privateData[this._id].name
}

return Person
})()
+

此例用 IIFE 包裹了 Person 的定义,其中含有两个私有属性:privateDataprivateIdprivateData 对象存储了每个实例的私有信息,而 privateId 则被用于为每个实例产生一个唯一ID。当 Person 构造器被调用时,一个不可枚举、不可配置、不可写入的 _id 属性就被添加了。

+

接下来在 privateData 对象中建立了与实例ID对应的一个入口,其中存储着 name 的值。随后在 getName() 函数中,就能使用 this._id 作为 privateData 的键来提取该值。由于 privateData 无法从 IIFE 外部进行访问,实际的数据就是安全的,尽管 this._idprivateData 对象上依然是公开暴露的。

+

此方式的最大问题在于 privateData 中的数据永远不会消失,因为在对象实例被销毁时没有任何方法可以获知该数据,privateData 对象就将永远包含多余的数据。这个问题现在可以换用 Weak Map 来解决了,如下:

+
let Person = (function () {
let privateData = new WeakMap()

function Person (name) {
privateData.set(this, { name: name })
}

Person.prototype.getName = function () {
return privateData.get(this).name
}

return Person
})()
+

此版本的 Person 范例使用了 Weak Map 而不是对象来保存私有数据。由于 Person 对象的实例本身能被作为键来使用,于是也就无须再记录单独的 ID。当 Person 构造器被调用时,将 this 作为键在 Weak Map 上建立了一个入口,而包含私有信息的对象成为了对应的值,其中只存放了 name 属性。通过将 this 传递给 privateData.get() 方法,以获取值对象并访问其 name 属性,getName() 函数便能提取私有信息。这种技术让私有信息能够保持私有状态,并且当与之关联的对象实例被销毁时,私有信息也会被同时销毁。

]]>
JavaScript @@ -2568,88 +2646,10 @@
- Set 与 Map - /posts/d766545b/ - Set 是不包含重复值的列表。你一般不会像对待数组那样来访问 Set 中的某个项。相反更常见的是,只在 Set 中检查某个值是否存在。

-

Map 则是键与相对应的值的集合。因此,Map 中的每个项都存储了两块数据,通过指定所需读取的键即可检索对应的值。Map 常被用作缓存,存储数据以便此后快速检索。

-

ES5 中的 Set 与 Map

在 ES5 中,开发者使用对象属性来模拟 SetMap,就像这样:

-
let set = Object.create(null)

set.foo = true

// 检查属性的存在性
if (set.foo) {
// 一些操作
}
-

本例中的 set 变量是一个原型为 null 的对象,确保在此对象上没有继承属性。使用对象的属性作为需要检查的唯一值在 ES5 中是很常用的方法。当一个属性被添加到 set 对象时,它的值也被设为 true,因此条件判断语句就可以简单判断出该值是否存在。

-

使用对象模拟 Set 与模拟 Map 之间唯一真正的区别是所存储的值。例如:以下例子将对象作为 Map 使用:

-
let map = Object.create(null)

map.foo = 'bar'

// 提取一个值
let value = map.foo

// bar
console.log(value)
-

此代码将字符串值 "bar" 存储在 foo 键上。与 Set 不同,Map 多数被用来提取数据,而不是仅检查键的存在性。

-

变通方法的问题

尽管在简单情况下将对象作为 SetMap 来使用都是可行的,但一旦接触到对象属性的局限性,此方式就会遇到更多麻烦。研究如下代码:

-
let map = Object.create(null)
let key1 = 5
let key2 = '5'
let key3 = {}
let key4 = {}

map[key1] = 'number'

map[key3] = 'foo'

// number
console.log(map[key2])

// foo
console.log(map[key4])

// { '5': 'number', '[object Object]': 'foo' }
console.log(map)
-

造成上面例子中问题的原因是对象属性只可以是字符串或符号类型,所以当你为对象定义属性时,如果属性名不是字符串或符号类型,那么它会调用属性名的 toString() 方法将属性名转换为字符串,然后再进行后续操作。所以 map[key1] === map[key2]map[key3] === map[key4]

-

ES6 的 Set

ES6 新增了 Set 类型,这是一种无重复值的有序列表。Set 允许对它包含的数据进行快速访问,从而增加了一个追踪离散值的更有效方式。

-

创建 Set 并添加项目

Set 使用 new Set() 来创建,而调用 add() 方法就能向 Set 中添加项目,检查 size 属性还能查看其中包含有多少项。

-
let set = new Set()

set.add(5)
set.add('5')

// 2
console.log(set.size)
-

Set 不会使用强制类型转换来判断值是否重复。这意味着 Set 可以同时包含数值 5 与字符串 "5",将它们都作为相对独立的项(在 Set 内部的比较使用了 Object.is() 方法来判断两个值是否相等,唯一的例外是+0与-0被判断为是相等的)。你还可以向 Set 添加多个对象,它们不会被合并为同一项:

-
let set = new Set()
let key1 = {}
let key2 = {}

set.add(key1)
set.add(key2)

// 2
console.log(set.size)
-

由于 key1key2 并不会被转换为字符串,所以它们在这个 Set 内部被认为是两个不同的项(记住:如果它们被转换为字符串,那么都会等于 "[object Object]")。

-

如果 add() 方法用相同值进行了多次调用,那么在第一次之后的调用实际上会被忽略:

-
let set = new Set()

set.add(5)
set.add('5')

// 重复了,该调用被忽略
set.add(5)

// 2
console.log(set.size)
-

你可以使用数组来初始化一个 Set,并且 Set 构造器会确保不重复地使用这些值。

-
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5])

// 5
console.log(set.size)
-

Set 构造器实际上可以接收任意可迭代对象作为参数。能使用数组是因为它们默认就是可迭代的,Map 也是一样的。

-

你可以使用 has() 方法来检测某个值是否存在于 Set 中,就像这样:

-
let set = new Set()

set.add(5)
set.add('5')

// true
console.log(set.has(5))

// false
console.log(set.has(6))
-

移除值

使用 delete() 方法可以移除 Set 中的某个值,使用 clear() 方法可以移除 Set 中的所有值。

-
let set = new Set()
set.add(5)
set.add('5')

set.delete(5)

// false
console.log(set.has(5))

set.clear()

// 0
console.log(set.size)
-

Set 上的 forEach() 方法

Set上 的 forEach() 方法类似于数组中的 forEach() 方法,它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this 值。传入的函数会接收三个参数:Set 元素的值,Set 元素的值和目标 Set 自身。你没看错,第一个和第二个参数是完全相同的,这么设计的目的是为了保持所有对象上 forEach() 方法的统一。

-

将 Set 转换为数组

使用扩展运算符可以很轻松的将 Set 转换为数组,用一个数组去重的例子来展示它是如何转换的:

-
function eliminateDuplicates (array) {
return [...new Set(array)]
}

let numbers = [ 1, 2, 3, 3, 3, 4, 5 ]
let noDuplicates = eliminateDuplicates(numbers)

// [ 1, 2, 3, 4, 5 ]
console.log(noDuplicates)
-

Weak Set

由于 Set 类型存储对象引用的方式,它也可以被称为 Strong Set。对象存储在 Set 的一个实例中时,实际上相当于把对象存储在变量中。只要对 Set 实例的引用仍然存在,所存储的对象就无法被垃圾回收机制回收,从而无法释放内存。例如:

-
let set = new Set()
let key = {}

set.add(key)

// 1
console.log(set.size)

// 取消原始引用
key = null

// 1
console.log(set.size)

// 重新获得原始引用
key = [...set][0]
-

在本例中,将 key 设置为 null 清楚了对 key 对象的一个引用,但是另一个引用还存在于 set 内部。你仍然可以使用扩展运算符将 Set 转换为数组,然后访问数组的第一项,key 变量就取回了原先的对象。

-

为了缓解这个问题,ES6 也包含了 Weak Set,该类型只允许存储对象弱引用,而不能存储基本类型的值。对象的弱引用在它自己成为该对象的唯一引用时,不会阻止垃圾回收。

-

创建 Weak Set

Weak Set 使用 WeakSet 构造器来创建,并包含 add() 方法、has() 方法以及 delete() 方法。以下例子使用了这三个方法:

-
let set = new WeakSet()
let key = {}

set.add(key)

// true
console.log(set.has(key))

set.delete(key)

// false
console.log(set.has(key))
-

使用 Weak Set 很像在使用正规的 Set。你可以在 Weak Set 上添加、移除或检查引用,也可以给构造器传入一个可迭代对象来初始化 Weak Set的值:

-
let key1 = {}
let key2 = {}
let set = new WeakSet([key1, key2])

// true
console.log(set.has(key1))

// true
console.log(set.has(key2))
-

在本例中,一个数组被传给了 WeakSet 构造器。由于该数组包含了两个对象,这些对象就被添加到了 Weak Set 中。要记住若数组中包含了非对象的值,就会抛出错误,因为 WeakSet 构造器不接受基本类型的值。

-

Set 类型之间的关键差异

Weak Set 与正规 Set 之间最大的区别是对象的弱引用。此处有个例子说明了这种差异:

-
let set = new WeakSet()
let key = {}

set.add(key)

// true
console.log(set.has(key))

// 移除对于键的最后一个强引用,同时从Weak Set中移除
key = null
-

当代码被执行后,Weak Set 中的 key 引用就不能再访问了。核实这一点是不可能的,因为需要把对于该对象的一个引用传递给 has() 方法(而只要存在其他引用,Weak Set 内部的弱引用就不会消失)。这会使得难以对 Weak Set 的引用特征进行测试,但 JS 引擎已经正确地将引用移除了,这一点你可以信任。

-

这些例子演示了 Weak Set 与正规 Set 的一些共有特征,但是它们还有一些关键的差异,即:

-
-
  • 对于 WeakSet 的实例,若调用 add() 方法时传入非对象的参数,就会抛出错误(has()delete() 则会在传入了非对象的参数时返回 false)。
  • Weak Set 不可迭代,因此不能被用在 for-of 循环中。
  • Weak Set 无法暴露出任何迭代器,因此没有任何编程手段可用于判断 Weak Set 的内容。
  • Weak Set 没有 forEach() 方法。
  • Weak Set 没有 clear() 方法。
  • Weak Set 没有 size 属性。
-
-

ES6 的 Map

ES6 的 Map 类型是键值对的有序列表,而键和值都可以是任意类型。

-

创建 Map

Map 对键的处理方式与 Set 对值的处理一样,采用的是 Object.is() 方法,而不会进行强制类型转换,唯一的例外是 +0 与 -0 被判断为是相等的。

-

你可以调用 set() 方法并给它传递一个键与一个关联的值,来给 Map 添加项。此后使用键名来调用 get() 方法便能提取对应的值。如果任意一个键不存在于Map中,则 get() 方法就会返回特殊值 undefined。例如:

-
let map = new Map()
let key1 = {}
let key2 = {}

map.set(key1, 5)
map.set(key2, 42)

// 5
console.log(map.get(key1))

// 42
console.log(map.get(key2))

// undefined
console.log(map.get(111))
-

此代码使用了对象 key1key2 作为 Map 的键,并存储了两个不同的值。由于这些键不会被强制转换成其他形式,每个对象就都被认为是唯一的。这允许你给对象关联额外数据,而无须修改对象自身。

-

与 Set 类似,你能将数组传递给 Map 构造器,以便使用数据来初始化一个 Map。该数组中的每一项也必须是数组,内部数组的首个项会作为键,第二项则为对应值。因此整个 Map 就被这些双项数组所填充。例如:

-
let map = new Map([['name', 'Nicholas'], ['age', 25]])

// Nicholas
console.log(map.get('name'))

// 25
console.log(map.get('age'))
-

虽然有数组构成的数组看起来有点奇怪,但这对于准确表示键来说却是必要的。因为键允许是任意数据类型,将键存储在数组中,是确保它们在被添加到 Map 之前不会被强制转换为其他类型的唯一方法。

-

Map 的方法及属性

-
  • has(key):判断指定的键是否存在于 Map 中。
  • delete(key):移除 Map 中的键以及对应的值。
  • clear():移除 Map 中所有的键与值。
  • size:用于指示包含了多少个键值对。
-
-

Map 上的 forEach 方法

Map 的 forEach() 方法类似于 Set 与数组上的 forEach() 方法,不过它是按照键值对被添加到 Map 中的顺序来迭代的。它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this 值。传入的函数会接收三个参数:Map 项的值、该值所对应的的键和目标Map自身。

-

Weak Map

使用Weak Map

ES6 的 WeakMap 类型是键值对的无序列表,其中键必须是非空的对象,值则允许是任意类型。WeakMap 的接口与 Map 的非常相似,都使用 set()get() 方法来分别添加与提取数据。

-

类似于 Weak Set,它没有 size 属性,没有任何办法可以确定 Weak Map 是否为空。在其他引用被移除后,由于对键的引用不再有残留,也就无法调用 get() 方法来去取对应的值。Weak Map 已经切断了对于该值的访问,其所占的内存在垃圾回收器运行时便会被释放。

-

Weak Map 的初始化

为了初始化 Weak Map,需要把一个由数组构成的数组传递给 WeakMap 构造器。就像正规 Map 那样,每个内部数组都应当有两个项,第一项是作为键的对象,第二项则是对应的值(任意类型)。例如:

-
let key1 = {}
let key2 = {}
let map = new WeakMap([[key1, 'hello'], [key2, 42]])

// true
console.log(map.has(key1))

// hello
console.log(map.get(key1))

// true
console.log(map.has(key2))

// 42
console.log(map.get(key2))
-

对象 key1key2 被用作 Weak Map 的键,get()has() 方法则能访问他们。在传递给 WeakMap 构造器的参数中,若任意键值对使用了非对象的键,构造器就会抛出错误。

-

Weak Map 的方法

Weak Map 只有两个附加方法能用来与键值对交互。has() 方法用于判断指定的键是否存在于 Map 中,而 delete() 方法则用于移除一个特定的键值对。clear() 方法不存在,这是因为没有必要对键进行枚举,并且枚举 Weak Map 也是不可能的,这与 Weak Set 相同。

-

对象的私有数据

ES5 中的创建私有数据的方式:

-
var Person = (function () {
var privateData = {}
var privateId = 0

function Person (name) {
Object.defineProperty(this, '_id', {
value: privateId++
})

privateData[this._id] = {
name: name
}
}

Person.prototype.getName = function () {
return privateData[this._id].name
}

return Person
})()
-

此例用 IIFE 包裹了 Person 的定义,其中含有两个私有属性:privateDataprivateIdprivateData 对象存储了每个实例的私有信息,而 privateId 则被用于为每个实例产生一个唯一ID。当 Person 构造器被调用时,一个不可枚举、不可配置、不可写入的 _id 属性就被添加了。

-

接下来在 privateData 对象中建立了与实例ID对应的一个入口,其中存储着 name 的值。随后在 getName() 函数中,就能使用 this._id 作为 privateData 的键来提取该值。由于 privateData 无法从 IIFE 外部进行访问,实际的数据就是安全的,尽管 this._idprivateData 对象上依然是公开暴露的。

-

此方式的最大问题在于 privateData 中的数据永远不会消失,因为在对象实例被销毁时没有任何方法可以获知该数据,privateData 对象就将永远包含多余的数据。这个问题现在可以换用 Weak Map 来解决了,如下:

-
let Person = (function () {
let privateData = new WeakMap()

function Person (name) {
privateData.set(this, { name: name })
}

Person.prototype.getName = function () {
return privateData.get(this).name
}

return Person
})()
-

此版本的 Person 范例使用了 Weak Map 而不是对象来保存私有数据。由于 Person 对象的实例本身能被作为键来使用,于是也就无须再记录单独的 ID。当 Person 构造器被调用时,将 this 作为键在 Weak Map 上建立了一个入口,而包含私有信息的对象成为了对应的值,其中只存放了 name 属性。通过将 this 传递给 privateData.get() 方法,以获取值对象并访问其 name 属性,getName() 函数便能提取私有信息。这种技术让私有信息能够保持私有状态,并且当与之关联的对象实例被销毁时,私有信息也会被同时销毁。

-]]>
- - JavaScript - -
- - import 与 export - /posts/588c1481/ - 何为模块?
-

模块是使用不同方式加载的 JS 文件(与 JS 原先的脚本加载方式相对)。这种不同模式很有必要,因为它与脚本(script)有大大不同的语义:

  • 模块代码自动运行在严格模式下,并且没有任何办法跳出严格模式。
  • 在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域内部存在。
  • 模块顶级作用域的 this 值为 undefined
  • 模块不允许在代码中使用HTML风格的注释。
  • 对于需要让模块外部代码访问的内容,模块必须导出它们。
  • 允许模块从其他模块导入绑定。
+ import 与 export + /posts/588c1481/ + 何为模块?
+

模块是使用不同方式加载的 JS 文件(与 JS 原先的脚本加载方式相对)。这种不同模式很有必要,因为它与脚本(script)有大大不同的语义:

  • 模块代码自动运行在严格模式下,并且没有任何办法跳出严格模式。
  • 在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域内部存在。
  • 模块顶级作用域的 this 值为 undefined
  • 模块不允许在代码中使用HTML风格的注释。
  • 对于需要让模块外部代码访问的内容,模块必须导出它们。
  • 允许模块从其他模块导入绑定。

CommonJS 是在 ES6 之前的模块规范,至今仍然被广泛使用。点击这里了解 CommonJS

导出

ES6 的导出有两种形式:命名导出和默认导出。

@@ -2759,79 +2759,6 @@

foo() 内部创建的箭头函数会捕获调用时 foo()this。由于 foo()this 绑定到 obj1bar(引用箭头函数)的 this 也会绑定到 obj1箭头函数的绑定无法被修改

箭头函数可以像 bind() 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制

有关箭头函数的详细介绍点这里

-]]>
- - JavaScript - - - - 函数 - /posts/9595c646/ - 带参数默认值的函数

ES6 中的参数默认值

ES6 能更容易地为参数提供默认值,它使用了初始化形式,以便在参数未被正式传递进来时使用。在函数声明中能指定任意一个参数的默认值,即使该参数排在未指定默认值的参数之前也是可以的。

-

只有在某个参数未传递,或明确传递 undefined 时,才会应用参数的默认值。null 值被认为是有效的。

-
function makeRequest(url, timeout = 2000, callback) {
// 函数的剩余部分
}

// 使用默认的timeout
makeRequest('/foo', undefined, function (body) {
doSomething(body)
})

// 使用默认的timeout
makeRequest('/foo')

// 不使用默认值
makeRequest('/foo', null, function (body) {
doSomething(body)
})
-

默认值表达式和暂时性死区

参数默认值最有意思的特性或许就是默认值并不要求一定是基本类型的值。例如,你可以执行一个函数来产生参数的默认值:

-
function getValue () {
return 5
}

function add (first, second = getValue()) {
return first + second
}

// 2
console.log(add(1, 1))

// 6
console.log(add(1))
-

需要注意的是,仅在调用 add() 函数而未提供第二个参数时,getValue() 函数才会被调用,而在 getValue() 函数声明初次被解析时并不会进行调用。另外在书写代码时要小心,将函数调用作为参数的默认值时一定不要遗漏了括号,否则含义就变了。

-

参数默认值与 letconst 声明类似,都存在着暂时性死区。函数每个参数都会创建一个新的标识符绑定,它在初始化之前不允许被访问,否则会抛出错误。参数初始化会在函数被调用时进行,无论是给参数传递了一个值、还是使用了参数的默认值。

-

所以在为函数参数指定默认值时,后面的参数可以使用前面的参数,反过来则会抛出错误。

-
// 后面参数使用前面参数
function getValue (value) {
return value + 5
}
function add1 (first, second = getValue(first)) {
return first + second
}

// 2
console.log(add1(1, 1))

// 7
console.log(add1(1))

// 前面参数使用后面参数
function add2 (first = second, second) {
return first + second
}

// 2
console.log(add2(1, 1))

// ReferenceError: second is not defined
console.log(add2(undefined, 1))
-

剩余参数

剩余参数由三个点(…)与一个紧跟着的具名参数指定,它会是包含传递给函数的其余参数的一个数组,名称中的“剩余”也由此而来。需要注意的是,函数的 length 属性用于指示具名参数的数量,而剩余参数对其毫无影响。

-
function pick (string, ...keys) {
console.log(pick.length)
console.log(string)
console.log(keys)
}

// 1
// undefined
// []
pick()

// 1
// 1
// [2]
pick(1, 2)
-

剩余参数的两个限制条件:

-
-
  • 函数只能有一个剩余参数,并且它必须被放在最后。
  • 在对象访问器属性的 set 函数中,不能使用剩余参数。原因是对象的 set 被限定只能使用单个参数,而剩余参数按照定义是不限制参数数量的。
-
-

扩展运算符

与剩余参数关联最密切的就是扩展运算符。剩余参数允许你把多个独立的参数合并到一个数组中。而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。

-

用扩展运算符传递参数,使得更容易将数组作为函数参数来使用,你会发现在大部分场景中扩展运算符都是 apply() 方法的合适替代品。并且扩展运算符可以与其他参数混用。

-
-

扩展运算符的使用没有位置和数量限制。

-
-
let values = [25, 50, 75, 100]
let others = [789, 234]

// 100
console.log(Math.max.apply(Math, values))

// 789
console.log(Math.max(...values, ...others))

// 110
console.log(Math.max(...values, 110))
-

ES6 的名称属性

定义函数有各种各样的方式,在 JavaScript 中识别函数就变得很有挑战性。此外,匿名函数表达的流行使得调试有点困难,经常导致堆栈跟踪难以被阅读与解释。正因为此,ES6 给所有函数添加了 name 属性。

-

需要注意的是,函数的 name 属性值未必会关联到同名变量。name 属性是为了在调试时获得有用的相关信息,所以不能用 name 属性值去获取对函数的引用。

-

选择合适的名称

ES6 中所有函数都有适当的 name 属性值。为了理解其实际运作,请看下例————它展示了一个函数与一个函数表达式,并将二者的 name 属性都打印出来:

-
function doSomething () {
// ...
}
var doAnotherThing = function () {
// ...
}

// doSomething
console.log(doSomething.name)

// doAnotherThing
console.log(doAnotherThing.name)
-

在此代码中,由于是一个函数声明,doSomething() 就拥有一个值为 "doSomething"name 属性。而匿名函数表达式 doAnotherThing()name 属性值是 "doAnotherThing",因为这是该函数所赋值的变量的名称。

-

名称属性的特殊情况

虽然函数声明与函数表达式的名称易于查找,但 ES6 更进一步确保了所有函数都拥有合适的名称。为了表明这点,请参考如下程序:

-
var doSomething = function doSomethingElse () {
// ...
}

var person = {
get firstName () {
return 'Nicholas'
},
sayName: function () {
console.log(this.name)
}
}

// doSomethingElse
console.log(doSomething.name)

// sayName
console.log(person.sayName.name)

var descriptor = Object.getOwnPropertyDescriptor(person, 'firstName')

// get firstName
console.log(descriptor.get.name)
-

本例中的 doSomething.name 的值是 "doSomethingElse",因为该函数表达式自己拥有一个名称,并且此名称的优先级要高于赋值目标的变量名。person.sayName()name 属性值是 "sayName",正如对象字面量指定的那样。类似的, person.firstName 实际上是个访问器属性的 get 函数,因此它的名称是 "get firstName",以标明它的特征。同样,访问器属性的 set 函数也会带有 set 前缀( getset 函数都必须用 Object.getOwnPropertyDescriptor() 来检索)。

-

函数名称还有另外两个特殊情况。使用 bind() 创建的函数会在名称属性值之前带有 "bound" 前缀。而使用 Function 构造器创建的函数,其名称属性则会有 "anonymous" 前缀,正如此例:

-
var doSomething = function () {
// ...
}

// bound doSomething
console.log(doSomething.bind().name)

// anonymous
console.log((new Function()).name)
-

明确函数的双重用途

JavaScript 为函数提供了两个不同的内部方法:[[Call]][[Construct]]。当函数未使用 new 进行调用时,[[Call]] 方法会被执行,运行的是代码中显示的函数体。而当函数使用 new 进行调用时,[[Construct]] 方法则会被执行,负责创建一个新的对象,并且使用该对象作为 this 去执行函数体。

-

记住并不是所有函数都拥有 [[Construct]] 方法(比如箭头函数),因此不是所有函数都可以用 new 来调用。

-

在 ES5 中判断函数如何被调用

在 ES5 中判断函数是不是使用了 new 来调用,最流行的方式是使用 instanceof,但是这个方式存在漏洞。

-
function Person (name) {
if (this instanceof Person) {
this.name = name
} else {
throw new Error('you must use new with Person.')
}
}

// 成功了
var person = new Person('Nicholas')

// 抛出错误
var person = Person('Nicholas')

// 本应抛出错误,但是用 call 方法更改 this 值,产生了欺骗
// 成功了
var notPerson = Person.call(person, 'Michael')
-

new.target元属性

为了解决上述问题,ES6 引入了 new.target 元属性。当函数的 [[Construct]] 方法被调用时,new.target 的值为一个指向该构造函数的引用。而当 [[Call]] 方法被调用时,new.target 的值为 undefined

-
function Person (name) {
if (new.target === Person) {
this.name = name
} else {
throw new Error('you must use new with Person.')
}
}

function AnotherPerson (name) {
Person.call(this, name)
}

// 成功了
var person = new Person('Nicholas')

// 抛出错误
var person = new AnotherPerson('Nicholas')
-

箭头函数

ES6 最有意思的一个新部分就是箭头函数。箭头函数正如名称所示那样使用一个“箭头”(=>)来定义,但它的行为在很多重要方面与传统的 JavaScript 函数不同:

-
-
  • 没有 thissuperargumentsnew.target:箭头函数本身没有 thissuperargumentsnew.target,如果在箭头函数中引用了这些变量,那这些变量也是外层作用域的,跟它没关系。
  • 不能被使用 new 调用:箭头函数没有 [[Construct]] 方法,因此不能被用为构造函数,使用 new 调用箭头函数会抛出错误。
  • 没有原型:既然不能对箭头函数使用 new,那么它也不需要原型,也就是没有 prototype 属性。
  • 不允许重复的具名参数:箭头函数不允许拥有重复的具名参数,无论是否在严格模式下。而相对来说,传统函数只有在非严格模式下才禁止这种重复。
  • 不能用于创建生成器函数
-
-

箭头函数语法

-

没有花括号,就不允许有 return,否则会抛出错误

-
-
var reflect = value => value
// 等价于
var reflect = function (value) {
return value
}

// ========================================
var sum = (num1, num2) => num1 + num2
// 等价于
var sum = function (num1, num2) {
return num1 + num2
}

// ========================================
var getTempItem = id => ({ id: id, name: 'Temp'})
// 等价于
var getTempItem = function (id) {
return {
id: id,
name: 'Temp'
}
}

// ========================================
var getName = () => 'Nicholas'
// 等价于
var getName = function () {
return 'Nicholas'
}

// ========================================
var add = (num1, num2) => {
return num1 + num2
}
// 等价于
var add = function (num1, num2) {
return num1 + num2
}

// ========================================
var doNothing = () => {}
// 等价于
var doNothing = function () {}
-

创建立即调用函数表达式

使用传统函数创建立即调用函数表达式时,(function(){/*函数体*/})()(function(){/*函数体*/}()) 两种方式都是可行的。

-

但若使用箭头函数,只有 (()=>{/*函数体*/})() 这一种方式可行,也就是说括号必须仅包裹箭头函数的定义。

-

没有 this 绑定

JavaScript 最常见的错误领域之一就是在函数内的 this 绑定。请看下面的例子:

-
var PageHandler = {
id: 123456,

init: function () {
document.addEventListener('click', function (event) {
// 运行init时会抛出错误
this.doSomething(event.type)
}, false)
},

doSomething: function (type) {
console.log('Handling' + type + 'for' + this.id)
}
}
-

调用 this.doSomething() 会抛出错误的原因是 this 是对事件目标对象(在此案例中就是 document)的一个引用,而不是被绑定到 PageHandler 上。下面的代码将使用 bind() 方法修复这个问题。

-
var PageHandler = {
id: 123456,

init: function () {
document.addEventListener('click', (function (event) {
// 没有错误
this.doSomething(event.type)
}).bind(this), false)
},

doSomething: function (type) {
console.log('Handling' + type + 'for' + this.id)
}
}
-

现在此代码能像预期那样运行,但看起来有点奇怪。接着让我们看看使用箭头函数如何解决这个问题的。

-
var PageHandler = {
id: 123456,

init: function () {
document.addEventListener('click', (event) => {
this.doSomething(event.type)
}, false)
},

doSomething: function (type) {
console.log('Handling' + type + 'for' + this.id)
}
}
-
-

因为箭头函数没有 this 绑定,意味着在箭头函数内部使用 this 值时,引擎是通过作用域链来确定的,而 JavaScript 中的作用域机制是词法作用域,所以这个箭头函数内部使用的 thisinit() 方法的 this

因为箭头函数没有 this 绑定,所以对箭头函数使用 call()apply()bind() 方法时,函数内的 this 并不会受影响。

永远要记住,this 是在函数调用时进行绑定的!

-
-
let obj1 = {
a: 1,
func: function showThisA () {
(() => {
console.log(this.a)
})()
}
}

let obj2 = {
a: 2,
func: obj1.func
}

// 1
obj1.func()

// 2
obj2.func()
-

上面例子中箭头函数内使用的 this 是函数 showThisAthis,但是函数 showThisAthis 具体绑定到哪里是由它的调用方式决定的。

-

尾调用优化

在 ES6 中对函数最有趣的改动或许就是一项引擎优化,它改变了尾部调用的系统。尾调用指的是调用函数的语句是另一个函数的最后语句,就像这样:

-
function doSomething () {
// 尾调用
return doSomethingElse()
}
-

在 ES5 引擎中实现的尾调用,其处理就像其他函数调用一样:一个新的栈帧被创建并推到调用栈之上,用于表示该次函数调用。这意味着之前每个栈帧都被保留在内存中,当调用栈太大时会出问题。

-

ES6 在严格模式下力图为特定尾调用减少调用栈的大小(非严格模式的尾调用则保持不变)。当满足以下条件时,尾调用优化会清除当前栈帧并在此利用它,而不是为尾调用创建新的栈帧:

-
-
  • 尾调用的函数内部不能引用当前栈帧中的变量(意味着该函数不能是闭包)。
  • 进行尾调用的函数在尾调用返回结果后不能做额外操作。
  • 尾调用的结果作为当前函数的返回值。
-
]]>
JavaScript @@ -2948,6 +2875,79 @@ JavaScript
+ + 函数 + /posts/9595c646/ + 带参数默认值的函数

ES6 中的参数默认值

ES6 能更容易地为参数提供默认值,它使用了初始化形式,以便在参数未被正式传递进来时使用。在函数声明中能指定任意一个参数的默认值,即使该参数排在未指定默认值的参数之前也是可以的。

+

只有在某个参数未传递,或明确传递 undefined 时,才会应用参数的默认值。null 值被认为是有效的。

+
function makeRequest(url, timeout = 2000, callback) {
// 函数的剩余部分
}

// 使用默认的timeout
makeRequest('/foo', undefined, function (body) {
doSomething(body)
})

// 使用默认的timeout
makeRequest('/foo')

// 不使用默认值
makeRequest('/foo', null, function (body) {
doSomething(body)
})
+

默认值表达式和暂时性死区

参数默认值最有意思的特性或许就是默认值并不要求一定是基本类型的值。例如,你可以执行一个函数来产生参数的默认值:

+
function getValue () {
return 5
}

function add (first, second = getValue()) {
return first + second
}

// 2
console.log(add(1, 1))

// 6
console.log(add(1))
+

需要注意的是,仅在调用 add() 函数而未提供第二个参数时,getValue() 函数才会被调用,而在 getValue() 函数声明初次被解析时并不会进行调用。另外在书写代码时要小心,将函数调用作为参数的默认值时一定不要遗漏了括号,否则含义就变了。

+

参数默认值与 letconst 声明类似,都存在着暂时性死区。函数每个参数都会创建一个新的标识符绑定,它在初始化之前不允许被访问,否则会抛出错误。参数初始化会在函数被调用时进行,无论是给参数传递了一个值、还是使用了参数的默认值。

+

所以在为函数参数指定默认值时,后面的参数可以使用前面的参数,反过来则会抛出错误。

+
// 后面参数使用前面参数
function getValue (value) {
return value + 5
}
function add1 (first, second = getValue(first)) {
return first + second
}

// 2
console.log(add1(1, 1))

// 7
console.log(add1(1))

// 前面参数使用后面参数
function add2 (first = second, second) {
return first + second
}

// 2
console.log(add2(1, 1))

// ReferenceError: second is not defined
console.log(add2(undefined, 1))
+

剩余参数

剩余参数由三个点(…)与一个紧跟着的具名参数指定,它会是包含传递给函数的其余参数的一个数组,名称中的“剩余”也由此而来。需要注意的是,函数的 length 属性用于指示具名参数的数量,而剩余参数对其毫无影响。

+
function pick (string, ...keys) {
console.log(pick.length)
console.log(string)
console.log(keys)
}

// 1
// undefined
// []
pick()

// 1
// 1
// [2]
pick(1, 2)
+

剩余参数的两个限制条件:

+
+
  • 函数只能有一个剩余参数,并且它必须被放在最后。
  • 在对象访问器属性的 set 函数中,不能使用剩余参数。原因是对象的 set 被限定只能使用单个参数,而剩余参数按照定义是不限制参数数量的。
+
+

扩展运算符

与剩余参数关联最密切的就是扩展运算符。剩余参数允许你把多个独立的参数合并到一个数组中。而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。

+

用扩展运算符传递参数,使得更容易将数组作为函数参数来使用,你会发现在大部分场景中扩展运算符都是 apply() 方法的合适替代品。并且扩展运算符可以与其他参数混用。

+
+

扩展运算符的使用没有位置和数量限制。

+
+
let values = [25, 50, 75, 100]
let others = [789, 234]

// 100
console.log(Math.max.apply(Math, values))

// 789
console.log(Math.max(...values, ...others))

// 110
console.log(Math.max(...values, 110))
+

ES6 的名称属性

定义函数有各种各样的方式,在 JavaScript 中识别函数就变得很有挑战性。此外,匿名函数表达的流行使得调试有点困难,经常导致堆栈跟踪难以被阅读与解释。正因为此,ES6 给所有函数添加了 name 属性。

+

需要注意的是,函数的 name 属性值未必会关联到同名变量。name 属性是为了在调试时获得有用的相关信息,所以不能用 name 属性值去获取对函数的引用。

+

选择合适的名称

ES6 中所有函数都有适当的 name 属性值。为了理解其实际运作,请看下例————它展示了一个函数与一个函数表达式,并将二者的 name 属性都打印出来:

+
function doSomething () {
// ...
}
var doAnotherThing = function () {
// ...
}

// doSomething
console.log(doSomething.name)

// doAnotherThing
console.log(doAnotherThing.name)
+

在此代码中,由于是一个函数声明,doSomething() 就拥有一个值为 "doSomething"name 属性。而匿名函数表达式 doAnotherThing()name 属性值是 "doAnotherThing",因为这是该函数所赋值的变量的名称。

+

名称属性的特殊情况

虽然函数声明与函数表达式的名称易于查找,但 ES6 更进一步确保了所有函数都拥有合适的名称。为了表明这点,请参考如下程序:

+
var doSomething = function doSomethingElse () {
// ...
}

var person = {
get firstName () {
return 'Nicholas'
},
sayName: function () {
console.log(this.name)
}
}

// doSomethingElse
console.log(doSomething.name)

// sayName
console.log(person.sayName.name)

var descriptor = Object.getOwnPropertyDescriptor(person, 'firstName')

// get firstName
console.log(descriptor.get.name)
+

本例中的 doSomething.name 的值是 "doSomethingElse",因为该函数表达式自己拥有一个名称,并且此名称的优先级要高于赋值目标的变量名。person.sayName()name 属性值是 "sayName",正如对象字面量指定的那样。类似的, person.firstName 实际上是个访问器属性的 get 函数,因此它的名称是 "get firstName",以标明它的特征。同样,访问器属性的 set 函数也会带有 set 前缀( getset 函数都必须用 Object.getOwnPropertyDescriptor() 来检索)。

+

函数名称还有另外两个特殊情况。使用 bind() 创建的函数会在名称属性值之前带有 "bound" 前缀。而使用 Function 构造器创建的函数,其名称属性则会有 "anonymous" 前缀,正如此例:

+
var doSomething = function () {
// ...
}

// bound doSomething
console.log(doSomething.bind().name)

// anonymous
console.log((new Function()).name)
+

明确函数的双重用途

JavaScript 为函数提供了两个不同的内部方法:[[Call]][[Construct]]。当函数未使用 new 进行调用时,[[Call]] 方法会被执行,运行的是代码中显示的函数体。而当函数使用 new 进行调用时,[[Construct]] 方法则会被执行,负责创建一个新的对象,并且使用该对象作为 this 去执行函数体。

+

记住并不是所有函数都拥有 [[Construct]] 方法(比如箭头函数),因此不是所有函数都可以用 new 来调用。

+

在 ES5 中判断函数如何被调用

在 ES5 中判断函数是不是使用了 new 来调用,最流行的方式是使用 instanceof,但是这个方式存在漏洞。

+
function Person (name) {
if (this instanceof Person) {
this.name = name
} else {
throw new Error('you must use new with Person.')
}
}

// 成功了
var person = new Person('Nicholas')

// 抛出错误
var person = Person('Nicholas')

// 本应抛出错误,但是用 call 方法更改 this 值,产生了欺骗
// 成功了
var notPerson = Person.call(person, 'Michael')
+

new.target元属性

为了解决上述问题,ES6 引入了 new.target 元属性。当函数的 [[Construct]] 方法被调用时,new.target 的值为一个指向该构造函数的引用。而当 [[Call]] 方法被调用时,new.target 的值为 undefined

+
function Person (name) {
if (new.target === Person) {
this.name = name
} else {
throw new Error('you must use new with Person.')
}
}

function AnotherPerson (name) {
Person.call(this, name)
}

// 成功了
var person = new Person('Nicholas')

// 抛出错误
var person = new AnotherPerson('Nicholas')
+

箭头函数

ES6 最有意思的一个新部分就是箭头函数。箭头函数正如名称所示那样使用一个“箭头”(=>)来定义,但它的行为在很多重要方面与传统的 JavaScript 函数不同:

+
+
  • 没有 thissuperargumentsnew.target:箭头函数本身没有 thissuperargumentsnew.target,如果在箭头函数中引用了这些变量,那这些变量也是外层作用域的,跟它没关系。
  • 不能被使用 new 调用:箭头函数没有 [[Construct]] 方法,因此不能被用为构造函数,使用 new 调用箭头函数会抛出错误。
  • 没有原型:既然不能对箭头函数使用 new,那么它也不需要原型,也就是没有 prototype 属性。
  • 不允许重复的具名参数:箭头函数不允许拥有重复的具名参数,无论是否在严格模式下。而相对来说,传统函数只有在非严格模式下才禁止这种重复。
  • 不能用于创建生成器函数
+
+

箭头函数语法

+

没有花括号,就不允许有 return,否则会抛出错误

+
+
var reflect = value => value
// 等价于
var reflect = function (value) {
return value
}

// ========================================
var sum = (num1, num2) => num1 + num2
// 等价于
var sum = function (num1, num2) {
return num1 + num2
}

// ========================================
var getTempItem = id => ({ id: id, name: 'Temp'})
// 等价于
var getTempItem = function (id) {
return {
id: id,
name: 'Temp'
}
}

// ========================================
var getName = () => 'Nicholas'
// 等价于
var getName = function () {
return 'Nicholas'
}

// ========================================
var add = (num1, num2) => {
return num1 + num2
}
// 等价于
var add = function (num1, num2) {
return num1 + num2
}

// ========================================
var doNothing = () => {}
// 等价于
var doNothing = function () {}
+

创建立即调用函数表达式

使用传统函数创建立即调用函数表达式时,(function(){/*函数体*/})()(function(){/*函数体*/}()) 两种方式都是可行的。

+

但若使用箭头函数,只有 (()=>{/*函数体*/})() 这一种方式可行,也就是说括号必须仅包裹箭头函数的定义。

+

没有 this 绑定

JavaScript 最常见的错误领域之一就是在函数内的 this 绑定。请看下面的例子:

+
var PageHandler = {
id: 123456,

init: function () {
document.addEventListener('click', function (event) {
// 运行init时会抛出错误
this.doSomething(event.type)
}, false)
},

doSomething: function (type) {
console.log('Handling' + type + 'for' + this.id)
}
}
+

调用 this.doSomething() 会抛出错误的原因是 this 是对事件目标对象(在此案例中就是 document)的一个引用,而不是被绑定到 PageHandler 上。下面的代码将使用 bind() 方法修复这个问题。

+
var PageHandler = {
id: 123456,

init: function () {
document.addEventListener('click', (function (event) {
// 没有错误
this.doSomething(event.type)
}).bind(this), false)
},

doSomething: function (type) {
console.log('Handling' + type + 'for' + this.id)
}
}
+

现在此代码能像预期那样运行,但看起来有点奇怪。接着让我们看看使用箭头函数如何解决这个问题的。

+
var PageHandler = {
id: 123456,

init: function () {
document.addEventListener('click', (event) => {
this.doSomething(event.type)
}, false)
},

doSomething: function (type) {
console.log('Handling' + type + 'for' + this.id)
}
}
+
+

因为箭头函数没有 this 绑定,意味着在箭头函数内部使用 this 值时,引擎是通过作用域链来确定的,而 JavaScript 中的作用域机制是词法作用域,所以这个箭头函数内部使用的 thisinit() 方法的 this

因为箭头函数没有 this 绑定,所以对箭头函数使用 call()apply()bind() 方法时,函数内的 this 并不会受影响。

永远要记住,this 是在函数调用时进行绑定的!

+
+
let obj1 = {
a: 1,
func: function showThisA () {
(() => {
console.log(this.a)
})()
}
}

let obj2 = {
a: 2,
func: obj1.func
}

// 1
obj1.func()

// 2
obj2.func()
+

上面例子中箭头函数内使用的 this 是函数 showThisAthis,但是函数 showThisAthis 具体绑定到哪里是由它的调用方式决定的。

+

尾调用优化

在 ES6 中对函数最有趣的改动或许就是一项引擎优化,它改变了尾部调用的系统。尾调用指的是调用函数的语句是另一个函数的最后语句,就像这样:

+
function doSomething () {
// 尾调用
return doSomethingElse()
}
+

在 ES5 引擎中实现的尾调用,其处理就像其他函数调用一样:一个新的栈帧被创建并推到调用栈之上,用于表示该次函数调用。这意味着之前每个栈帧都被保留在内存中,当调用栈太大时会出问题。

+

ES6 在严格模式下力图为特定尾调用减少调用栈的大小(非严格模式的尾调用则保持不变)。当满足以下条件时,尾调用优化会清除当前栈帧并在此利用它,而不是为尾调用创建新的栈帧:

+
+
  • 尾调用的函数内部不能引用当前栈帧中的变量(意味着该函数不能是闭包)。
  • 进行尾调用的函数在尾调用返回结果后不能做额外操作。
  • 尾调用的结果作为当前函数的返回值。
+
+]]>
+ + JavaScript + +
块级绑定 /posts/3432e35f/ @@ -2991,198 +2991,178 @@ - 基本概念 - /posts/2c31c1a2/ - 变量

ECMAScript 的变量是松散类型的,可以用来保存任何类型的数据,换句话说,变量只是一个用于保存值得占位符而已,变量没有类型,数据才有类型!

+ 对象&原型&继承 + /posts/bb5d40f2/ + 理解对象

属性类型

数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。

-
  • 使用 var 声明变量,变量为声明该变量的作用域中的局部变量。即在全局作用域中声明的变量为全局变量,在局部作用域中声明的变量为局部变量。
  • 使用 var 声明的变量如果未初始化,则变量保存的值默认为 undefined
  • 对没有声明的变量直接进行赋值操作(或者说是 LHS 操作),在非严格模式下,会创建一个全局变量。但是在严格模式下,会抛出错误。
+
  • configurable:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,这个特性默认值为 true
  • enumerable:表示能否通过 for-in 循环返回属性。直接在对象上定义的属性,这个特性默认值为 true
  • writable:表示能否修改属性的值。直接在对象上定义的属性,这个特性默认值为 true
  • value:包含这个属性的数据值。读取属性值得时候,从这个位置读。写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined

可以使用 Object.defineProperty(obj, prop, descriptor) 修改数据属性的特性:

  • obj:数据属性所在的对象。
  • prop:数据属性的名称,可以是字符串或符号。
  • descriptor:描述符对象,可包含的属性有 configurableenumerablewritablevalue

如果通过 Object.defineProperty(obj, prop, descriptor) 定义一个新的数据属性,descriptor 中缺失的特性会被赋予 falseundefined

-

数据类型

JavaScript 的数据类型分为“基本数据类型”和“引用数据类型”两种。

+
var obj = { test: 1 }
var desc = Object.getOwnPropertyDescriptor(obj, 'test')

// { value: 1, writable: true, enumerable: true, configurable: true }
console.log(desc)

Object.defineProperty(obj, 'demo', {
configurable: false
})
desc = Object.getOwnPropertyDescriptor(obj, 'demo')

// { value: undefined, writable: false, enumerable: false, configurable: false }
console.log(desc)
+

关于writable:当 writablefalse 时,在非严格模式下通过赋值语句修改属性值,赋值操作将被忽略。在严格模式下则会抛出错误。但是如果通过 Object.defineProperty() 方法修改 value 特性则不会有任何问题

+
var obj = { test: 1}
Object.defineProperty(obj, 'test', {
writable: false
})
obj.test = 2

// { test: 1 }
console.log(obj)

Object.defineProperty(obj, 'test', {
value: 3
})

// { test: 3 }
console.log(obj)
+

关于configurable:当 configurablefalse 时,不允许删除属性,不允许修改属性的 enumerableconfigurable,不可以将 writablefalse 修改为 true,但是可以将 writabletrue 修改为 false,也可以修改属性的 value 特性。

+

当 writable 和 configurable 均为 false 时,不允许通过任何方式修改属性值,直接赋值或者通过 Object.defineProperty() 都不可以!

+
/**
* 下面的实验运行于非严格模式下
*/
var obj = { test: 1 }

// 在 configurable 为 false 时尝试删除属性
Object.defineProperty(obj, 'test', {
configurable: false
})
delete obj.test

// { test: 1 }
console.log(obj)

var desc = Object.getOwnPropertyDescriptor(obj, 'test')

// { value: 1, writable: true, enumerable: true, configurable: false }
console.log(desc)

// 在 configurable 为 false 时尝试修改 enumerable
// Uncaught TypeError: Cannot redefine property: test
Object.defineProperty(obj, 'test', {
enumerable: false
})

// 在 configurable 为 false 时尝试修改 configurable
// Uncaught TypeError: Cannot redefine property: test
Object.defineProperty(obj, 'test', {
configurable: true
})

// 在 configurable 为 false 时尝试修改 value
Object.defineProperty(obj, 'test', {
value: '此时 configurable 为 false'
})

// { test: "此时 configurable 为 false" }
console.log(obj)

// 在 configurable 为 false 时尝试将 writable 由 true 修改为 false
Object.defineProperty(obj, 'test', {
writable: false
})
var desc = Object.getOwnPropertyDescriptor(obj, 'test')

// { value: "此时 configurable 为 false", writable: false, enumerable: true, configurable: false }
console.log(desc)

// 在 configurable 为 false 时尝试将 writable 由 false 修改为 true
// Uncaught TypeError: Cannot redefine property: test
Object.defineProperty(obj, 'test', {
writable: true
})

// 在 configurable 和 writable 均为 false时,尝试修改属性值
obj.test = '直接赋值可以吗'
// {test: "此时configurable为false"}
console.log(obj)
// Uncaught TypeError: Cannot redefine property: test
Object.defineProperty(obj, 'test', {
value: '通过 Object.defineProperty 可以吗'
})
+

访问器属性

访问器属性不包含数据值。它们包含一对 getset 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 get 函数,这个函数负责返回有效的值。在写入访问器属性时,会调用 set 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性。

-
  • 基本数据类型UndefinedNullBooleanNumberStringSymbol(ES6 提出)。
  • 引用数据类型Object
+
  • configurable:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。直接在对象上定义的属性,它们的这个特性默认值为 true
  • enumerable:表示能否通过 for-in 循环返回属性。直接在对象上定义的属性,它们的这个特性默认值为 true
  • get:在读取属性时调用的函数。默认值为 undefined
  • set:在写入属性时调用的函数。默认值为 undefined
-

检测类型

typeof 操作符

对一个值使用 typeof 操作符,可能返回下列某个字符串(均为英文单词小写形式):

+

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。

+
var book = {
_year: 2004,
edition: 1
}
Object.defineProperty(book, 'year', {
get: function () {
return _this.year
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue
this.edition += newValue - 2004
}
}
})
book.year = 2005

// { _year: 2005, edition: 2 }
console.log(book)
+

不一定非要同时指定 getset。只指定 get 意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了 get 函数的属性会抛出错误。而读取只指定 set 的属性会返回 undefined

+

可以通过 Object.defineProperty() 实现数据属性与访问器属性的转换,但是切记不能同时指定数据属性和访问器属性,这样会抛出错误!

+

定义多个属性

ES5 定义了一个 Object.defineProperties() 方法用来为对象定义多个属性。

+
var book = {}
Object.defineProperties(book, {
_year: {
value
},
edition: {
value: 1
},
year: {
get: function () {
return _this.year
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue
this.edition += newValue - 2004
}
}
}
})
+

读取属性的特性

使用 ES5 的 Object.getOwnPropertyDescriptor() 方法可以取得给定属性的描述符。该方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象。这个方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用。

+

禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions()。如果想检测一个对象是否可以添加新属性,可以使用 Object.isExtensible()

+
+

不可以添加新属性,但是删除旧属性还是可以的。

+
+
var myObject = { a: 2 }
Object.preventExtensions(myObject)

// false
console.log(Object.isExtensible(myObject))

myObject.b = 3

// undefined
console.log(myObject.b)

delete myObject.a

// undefined
console.log(myObject.a)
+

在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。

+

密封

Object.seal() 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions() 并把所有现有属性标记为 configurable: false

+

冻结

Object.freeze() 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal() 并把所有“数据访问”属性标记为 writable: false

+

创建对象

工厂模式

工厂模式就是调用函数返回一个包含特定属性和方法的对象,工厂模式的问题在于它没有解决对象识别的问题(即怎样知道一个对象的类型)

+
function createPerson (name, age) {
var o = {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
return o
}
var person1 = createPerson('Nicholas', 29)
var person2 = createPerson('Greg', 27)
+

构造函数模式

ES 中可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法,下面使用构造函数模式重写工厂模式中的例子。

+
function Person (name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
var person1 = new Person('Nicholas', 29)
var person2 = new Person('Greg', 27)

// true
console.log(person1 instanceof Person)
// true
console.log(person2 instanceof Person)
+

构造函数模式与工厂模式的区别

-
  • "undefined": 如果这个值未定义。
  • "boolean": 如果这个值是布尔值。
  • "string": 如果这个值是字符串。
  • "number": 如果这个值是数值。
  • "symbol": 如果这个值是符号。
  • "function": 如果这个值是函数(其实函数属于对象的一种)。
  • "object": 如果这个值是对象或 null
+
  • 没有显示地创建对象.
  • 直接将属性和方法赋给了 this 对象.
  • 没有 return 语句。
-

instanceof 操作符

用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

-

object instanceof constructor => true | false

-

《JavaScript 高级程序设计》中说:“根据规定,所有引用数据类型的值都是Object的实例。因此,在检测一个引用数据类型值和Object构造函数时,instanceof操作符始终会返回true。
随着版本的迭代,这个说法变得不准确了,有例外了!!!

-
var a = Object.create(null)
console.log(a instanceof Object) // false
-

Object.prototype.toString.call()

返回一个表示类型的字符串,详情请看 MDN官网

-

基本数据类型

Undefined

Undefined 类型只有一个值,即特殊的 undefined。在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined。对未初始化的变量执行 typeof 操作会返回 undefined ,而对未声明的变量执行 typeof 操作同样会返回 undefined

-

除了可以对未声明的变量执行 typeof 操作之外,在非严格模式下还可以对其进行 delete 操作。除了这两种情况之外,对未声明的变量进行任何其他操作,都会抛出错误。

-

Null

Null 类型也只有一个值 - null

-
typeof null === 'object'    // true
null instanceof Object // false
null == undefined // true
Boolean(null) === false // true
-

Boolean

Boolean 类型的字面值 truefalse 是区分大小写的。也就是说,TrueFalse 都不是 Boolean 值,只是标识符。

-

Number

-
  • 八进制字面量在严格模式下是无效的,会导致支持的JavaScript引擎抛出错误。
  • 浮点数值的最高精度是17位小数,但在进行算术计算时其精确度远远不如整数。
  • 绝对值超过 Number.MAX_VALUE 的值会被自动转换为 infinity-infinity
  • 可以使用 isFinite() 函数来确定数值是否为无穷。如果不是无穷,则返回 true
  • NaN 与自身都不相等,可以使用 isNaN()Number.isNaN() 确定数值是否为 NaN。在传入数据非数值时,isNaN() 会首先调用 Number() 将其转换为数值类型再进行判断,而 Number.isNaN() 则会直接返回 false
  • Number() 用于把任何类型数据转换为数值。
  • parseInt()parseFloat() 用于把字符串转换为数值。
  • parseInt() 在转换字符串时比 Number() 好用,但是最好为 parseInt() 提供第二个参数(转换的基数2、8、10、16)。
+

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历一下 4 个步骤:

+
+
  1. 创建一个新对象(因为用了 new)。
  2. 为新对象连接原型。
  3. 将构造函数的作用域内的 this 绑定到这个新对象。
  4. 执行构造函数的代码。
  5. 返回新对象。
-

String

-
  • 任何字符串的长度都可以通过访问其 length 属性取得,包含双子节字符的可能不准确。
  • 字符串是不可变的,一旦创建就不能改变,要改变某个变量保存的字符串,首先要销毁原来的字符串然后再用新的替代。
  • 数值、布尔值、对象和字符串值都有 toString() 方法,并且数值的 toString() 方法可以接收一个参数(数值的基数),但是 nullundefined 没有。
  • 在不知道要转换值的类型时,可以使用 String() 方法。
+
+

使用 new 创建新对象的时候,如果存在类的继承,那么在 ES5 和 ES6 中这个过程是有差别的。查看详情

-

引用数据类型

Object

ECMAScript 中的对象其实就是一组数据和功能的集合,它的属性或方法是没有顺序之分的!!!

-

Object 类型是所有它的实例的基础,它所具有的任何属性和方法也同样存在于更具体的对象中。Object 的每个实例都具有下列属性和方法:

+

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那么它就可以作为构造函数。

+

构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍,但是有的方法是所有实例都应该共享的,没有创建多次的必要。

+

原型模式

+

我们创建的每一个函数都有一个 prototype(原型)属性,这个属性值是一个对象的引用,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

+
+

理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的引用。

+
function Person () {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log(this.name)
}

var person1 = new Person()

// Nicholas
person1.sayName()

var person2 = new Person()

// Nicholas
person2.sayName()

// true
console.log(person1.sayName === person2.sayName)
+

理解原型对象

+

上图展示了 Person 构造函数、Person 的原型属性以及 Person 现有的两个实例之间的关系。注意 Person 的每个实例,person1person2 都包含一个内部属性 [[Prototype]],该属性仅仅指向了 Person.prototype。换句话说,对象实例与构造函数没有直接的关系

-
  • constructor
  • hasOwnProperty(propertyName)
  • isPrototypeOf(object)
  • propertyIsEnumerable(propertyName)
  • toLocaleString()
  • toString()
  • valueOf()
+
  • isPrototypeOf():用于测试一个对象是否存在于另一个对象的原型链上。
  • hasOwnProperty():用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在,该方法是从 Object 继承而来的。
  • Object.getPrototypeOf():返回指定对象的原型(对象内部 [[Prototype]] 属性的值)。
-

复制变量值

在将一个值赋给变量时,如果这个值是基本数据类型,则变量中保存的是这个值本身。如果这个值是引用数据类型,则变量中保存的只是该值的一个引用(通过引用,可以在内存中找到这个引用数据类型的值)!

-
// ===================================================
// 在从一个变量向另一个变量复制基本数据类型值和引用数据类型值的区别
// ===================================================

//复制基本数据类型值
var a = 1
var b = a
b = 2
console.log(a, b) // 1, 2

// 复制引用数据类型值
// c 和 d 中保存的都是指向内存中同一对象的引用
var c = { num: 1 }
var d = c
d.num = 2
console.log(c.num, d.num) //2, 2
-

传递参数

ECMAScript 中所有函数的参数都是按值传递的。简单来说就是数据进行复制,然后传递给给函数作为参数。

-

语句

for-in语句

ECMAScript 对象的属性没有顺序。因此,通过 for-in 循环输出的属性名的顺序是不可预测的。

-

for-in 不能对 nullundefined 进行迭代。

-

switch语句

可以在 switch 语句中使用任何数据类型(在很多其他语言中只能使用数值),并且每个 case 的值不一定是常量,可以是变量,甚至是表达式。

-

switch对 case 进行匹配时,遵循的是 === 严格相等。

-

函数

理解参数

首先说明我的观点,不推荐使用 arguments !!!

-

ECMAScript 中的函数参数是用一个数组来表示的,可以通过 arguments 对象来访问这个数组,arguments 是一个类数组对象。

-

关于 arguments命名参数 之间是如何相互影响的,《JavaScript高级程序设计》与《深入理解ES6》的说法有些矛盾,我在谷歌浏览器中简单的试了一下,他们之间的同步关系好像是双向的。

-

没有重载

ECMAScript 中的函数没有重载,如果声明了多个同名函数,后面的会覆盖前面的。

-

垃圾收集

JavaScript 中的变量分为全局变量和局部变量,其中全局变量一直存在,不会被清除。

-

而局部变量只在函数执行的过程中存在,当函数执行结束后,局部变量会被自动清除。

-

当然如果存在闭包的话,局部变量被清除的时机需要取决于闭包。

-]]> - - JavaScript - - - - 强制类型转换 - /posts/1750eb3e/ - 强制类型转换规则

引用类型转换为基本类型

-
  1. 调用自身的 valueOf() 方法,如果返回基本类型的值,则转换成功。
  2. 如果 valueOf() 方法返回的还是引用类型值,则改为调用自身的 toString() 方法。如果 toString() 方法返回基本类型的值,则转换成功。
  3. 如果 toString() 方法返回的是引用类型值,抛出错误。

需要注意的是,数组的默认 toString() 方法经过了重新定义,会将所有单元字符串化以后再用 "," 连接起来。

-
-

转换为字符串

-
  • null"null"
  • undefined"undefined"
  • 布尔值:"true""false"
  • 数值:通常直接转换为字符串即可,但是对于极小或极大的数值,会使用指数形式。
  • 引用类型值:返回自身的 toString() 方法的返回值。
-
-
// "null"
console.log(String(null))

// "undefined"
console.log(String(undefined))

// "false"
console.log(String(false))

// "123"
console.log(String(123))

// "1.23e+21"
console.log(String(123 * 10000000000000000000))

// "[object Object]"
console.log(String({a: 1}))

// "1,2,3"
console.log(String([1, 2, 3]))
-

转换为数值

-
  • null0
  • undefinedNaN
  • 布尔值:01
  • 字符串:十进制数值或 NaN
  • 引用类型值:先将引用类型值转换为基本类型值,然后再将该基本类型值转换为数值。
-
-
// 0
console.log(Number(null))

// NaN
console.log(Number(undefined))

// 0
console.log(Number(false))

// 123
console.log(Number('123'))

// NaN
console.log(Number('123abc'))

// NaN
console.log(Number({a: 1}))

// NaN
console.log(Number([1, 2, 3]))

var obj = {
valueOf () {
return 42
}
}

var array = [1, 2, 3]
array.valueOf = function () {
return 123
}

// 42
console.log(Number(obj))

// 123
console.log(Number(array))
-

转换为布尔值

-

在将值转换为布尔值时,除了下述 5 种情况,其他所有情况都会转换为 true

  • null
  • undefined
  • false
  • +0-0NaN
  • ""
-
-

显示强制类型转换

-
  • 一元运算符 +-++-- 都会调用 Number() 将其他类型转换为数值,或将日期对象转换为对应的毫秒数。注意不要混淆 ++++ +---- -
  • 一元运算符 ~ 会先将值强制类型转换为 32 位数值,然后执行按位非操作,可以将 ~x 等价于 -(x+1)。但是 ~-1 的结果是 0 而不是 -0,因为 ~ 是字位操作而非数学运算。
  • 将值转换为布尔值时,可以使用 ! 运算符。
-
-
var a = '1'
var b = +a
var c = -a
var d = - -a
var e = --a

// 1
console.log(b)

// -1
console.log(c)

// 1
console.log(d)

// 0
console.log(e)

var f = new Date()

// 1557055687256
console.log(+f)

// ==============================

var x = -42

// 41
console.log(~x)

// ==============================

var a = '0'
var b = []
var c = {}
var d = ''
var e = 0
var f = null
var g

// true
console.log(!!a)

// true
console.log(!!b)

// true
console.log(!!c)

// false
console.log(!!d)

// false
console.log(!!e)

// false
console.log(!!f)

// false
console.log(!!g)
-

隐式强制类型转换

-

+- 作为一元运算符和作为多元运算符时具有不同的含义,别混淆了!

-
-

+ 运算符

+ 运算符既能用于数值加法,也能用于字符串拼接。JavaScript 怎样来判断我们要执行的是哪个操作?

+

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例开始,如果在实例中找到了具有给定名字的属性,则返回该属性。如果没有找到,则沿着对象的原型链向上逐层查找具有给定名字的属性,如果找到了则返回这个属性的值。

+

属性设置和屏蔽

用赋值语句给实例对象设置已经在原型链上层存在的同名属性时,会有以下三种情况:

-
  1. 如果操作数中有字符串,进行字符串拼接操作。
  2. 如果操作数中有引用类型值,首先将引用类型值转换为基本类型值,然后进行后续操作。
  3. 其他情况全都进行数值加法。
-
-

- 运算符

+ 运算符不同,- 运算符会只会执行减法运算。所以它会先将非数值类型的数据转换为数值,然后进行减法运算。

-
var a = '12'
var b = 1

// 11
console.log(a-b)
-

宽松相等和严格相等

-
  • == 允许在相等比较中进行强制类型转换,而 === 不允许。
  • NaN 不等于 NaN
  • +0 严格等于 -0
  • !=!== 分别类似于 =====
-
-

下面主要介绍 == 是如何进行强制类型转换的。

-

字符串和数值

-

首先将字符串转换为数值类型,然后进行比较。

-
-
var a = 42
var b = '42'

// true
console.log(a == b)
-

其他类型和布尔类型

-

首先将布尔类型的值转换为数值类型,然后进行比较。

-
-
var a = '42'
var b = true

// false
console.log(a == b)

// true
console.log('1' == true)
-

null 和 undefined

-

==nullundefined 相等(它们也与其自身相等),除此之外其他情况都不相等。

+
  • 如果在原型链上层存在的同名属性没有被标记为只读,即 writable: true,那么会直接在实例中添加一个同名的新属性,它是屏蔽属性。
  • 如果在原型链上层存在的同名属性被标记为只读,即 writable: false,那么无法修改已有属性,也无法在实例对象上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。如果在非严格模式下,赋值语句会被忽略。
  • 如果在原型链上层存在的同名属性具有 set 描述符,那么一定会调用这个 set。实例对象上并不会添加新的属性,也不会重新定义这个 set
-
var a = null
var b

// true
console.log(a == b)

// true
console.log(a == null)

// true
console.log(b == null)

// false
console.log(a == false)

// false
console.log(b == false)

// false
console.log(a == '')

// false
console.log(b == '')

// false
console.log(a == 0)

// false
console.log(a == 0)
-

引用类型和基本类型

-

首先将引用类型转换为基本类型,然后进行比较。

+

但是 JS 这门语言很灵活,如果上述所说的存在于原型链上层的同名属性中保存的是某一个引用类型值的引用,那么你还是可以修改这个引用类型的值的(并没有违反规则,因为保存的引用并没有改变)!比如,这个属性保存的是某一个数组的引用,那么我就可以通过 push 方法去改变这个数组。

+

如果你无论如何也想要屏蔽原型链上层的属性,那么你可以使用 Object.defineProperty() 方法!

+

有些情况下会隐式产生屏蔽,一定要当心。思考下面的代码:

+
var anotherObject = { a: 2 }
var myObject = Object.create(anotherObject)

// 2
anotherObject.a

// 2
myObject.a

// true
anotherObject.hasOwnProperty('a')

// false
myObject.hasOwnProperty('a')

// 隐式屏蔽!
myObject.a++

// 2
anotherObject.a

// 3
myObject.a

// true
myObject.hasOwnProperty('a')
+

属性的获取

有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。

+

在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。

+

在使用 for-in 循环时返回的是所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包含存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在 for-in 循环中返回。

+

要取得对象上所有可枚举的实例属性,可以使用 ES5 的 Object.keys() 方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

+

如果你想要得到所有实例属性,无论它是否可枚举,可以使用 Object.getOwnPropertyNames() 方法。

+

更简单的原型语法

简单来说就是用对象字面量形式来重写 Person.prototype,但是这样会导致新原型对象的 constructor 属性指向 Object 而不是 Person,尽管此时 instanceof 操作符还能返回正确的结果,但是通过 constructor 已经无法确定对象的类型了,所以如果 constructor 属性比较重要的话,还需要用 Object.defineProperty() 方法定义 constructor 的数据属性。

+
function Person () {}

Person.prototype = {
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name)
}
}

var friend = new Person()

// true
console.log(friend instanceof Object)

// true
console.log(friend instanceof Person)

// true
console.log(friend.constructor === Object)

// false
console.log(friend.constructor === Person)

Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})

// true
console.log(friend.constructor === Person)
+

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。即使是先创建了实例后修改原型也是如此。

+
function Person () {}
var friend = new Person ()
Person.prototype.sayHi = function () {
console.log('hi')
}

// hi
friend.sayHi()
+

但是如果先创建了实例然后重写整个原型对象,那么情况就不一样了。具体的变化看图吧!

+

重写原型对象

+

此时 instanceof 操作符已经不好使了!
构造函数找不到最初的原型对象了!
现有实例也找不到新的原型对象了!

+
function Person () {}

var friend = new Person()

Person.prototype = {
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})

// true
console.log(friend instanceof Object)

// false
console.log(friend instanceof Person)

// Uncaught TypeError: friend.sayName is not a function
friend.sayName()
+

原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型模式的最大问题。原型模式的最大问题是由其共享的本性所导致的(主要针对引用类型值的属性来说)。

+

组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存,另外这种混成模式还支持向构造函数传递参数。

+
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Shelby', 'Court']
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person('Nicholas', 29, 'Software Engineer')
var person2 = new Person('Greg', 27, 'Doctor')
person1.friends.push('Van')

// ["Shelby", "Court", "Van"]
console.log(person1.friends)

// ["Shelby", "Court"]
console.log(person2.friends)

// false
console.log(person1.friends === person2.friends)

// true
console.log(person1.sayName === person2.sayName)
+

动态原型模式

动态原型模式把所有信息封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效来决定是否需要初始化原型。

+
function Person (name, age, job) {
// 属性
this.name = name
this.age = age
this.job = job

// 方法
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function () {
console.log(this.name)
}
}
}

var friend = new Person('Nicholas', 29, 'Software Engineer')

// Nicholas
friend.sayName()
+

在使用动态原型模式时,禁止使用对象字面量重写原型!

+

寄生构造函数模式

在前面几种模式都不适用的情况下,可以使用寄生构造函数模式,这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

+
function Person (name, age, job) {
var o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}

var friend = new Person('Nicholas', 29, 'Software Engineer')

// Nicholas
friend.sayName()
+

在这个例子中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

+

关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

+

稳妥构造函数模式

道格拉斯·克罗克福德发明了 JavaScript 中的稳妥对象这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 thisnew),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与计生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this。二是不使用 new 操作符调用构造函数。

+
function Person (name, age, job) {
// 创建要返回的对象
var o = new Object()

// 可以在这里定义私有变量和函数

// 添加方法
o.sayName = function () {
console.log(name)
}
return o
}

var friend = Person('Nicholas', 29, 'Software Engineer')

// Nicholas
friend.sayName()
+

小结

创建对象模式总结

+

继承

原型链

function SuperType () {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}

function SubType () {
this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}

var instance = new SubType()

// true
console.log(instance.getSuperValue())
+

我觉得用文字解释这个原型链有点绕嘴,没有上图方便,就直接看下面的图片吧!

+

原型链

+
+
  • instanceof 操作符用于测试构造函数的 prototype 属性是否出现在对象的原型链中。
  • isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
  • 子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
  • 在通过原型链实现继承时,不能使用对象字面量语法重写原型。
-
var a = 42
var b = [42]

// true
console.log(a == b)
-

引用类型和引用类型

这种情况就是判断两个变量的引用是否相同

+

原型链的第一个问题类似于上面介绍的原型模式的问题,这里就不详细介绍了。
它的第二个问题是在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

+

借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数(通过 call()apply() 方法)。

+
function SuperType (name) {
this.name = name
}

function SubType () {
// 继承了SuperType,同时还传递了参数
SuperType.call(this, 'Nicholas')

// 实例属性
this.age = 29
}

var instance = new SubType()

// Nicholas
console.log(instance.name)

// 29
console.log(instance.age)
+

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题-方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

+

组合继承

组合继承有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

+
function SuperType (name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}

function SubType (name, age) {
// 继承属性
SuperType.call(this, name)

this.age = age
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function () {
console.log(this.age)
}

var instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')

// ["red", "blue", "green", "black"]
console.log(instance1.colors)

// Nicholas
instance1.sayName()

// 29
instance1.sayAge()

var instance2 = new SubType('Greg', 27)

// ["red", "blue", "green"]
console.log(instance2.colors)

// Greg
instance2.sayName()

// 27
instance2.sayAge()
+

两个实例上的 colors 属性屏蔽了原型链上的同名属性。

+

原型式继承

function object (o) {
function F () {}
F.prototype = o
return new F()
}
+

object() 函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质讲,object() 对传入其中的对象执行了一次浅复制。ES5 新增的 Object.create() 方法规范化了原型式继承。

+

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

+
function object (o) {
function F () {}
F.prototype = o
return new F()
}

function createAnother (original) {
// 通过调用函数创建一个新对象
var clone = object(original)

// 以某种方式来增强这个对象
clone.sayHi = function () {
console.log('hi')
}

// 返回这个对象
return clone
}

var person = {
name: 'Nicholas',
friengd: ['Shelby', 'Court', 'Van']
}
var anotherPerson = createAnother(person)

// hi
anotherPerson.sayHi()
+

寄生组合式继承

前面说过组合继承是 JavaScript 最常用的继承函数,不过它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

+
function SuperType (name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}

function SubType (name, age) {
// 第二次调用SuperType()
SuperType.call(this, name)

this.age = age
}

// 第一次调用SuperType()
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function () {
console.log(this.age)
}
+

为了解决上述问题,我们使用寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

+
function object (o) {
function F () {}
F.prototype = o
return new F()
}

function inheritPrototype (subType, superType) {
// 创建对象
var prototype = object(superType.prototype)

// 增强对象
prototype.constructor = subType

// 指定原型
subType.prototype = prototype
}

function SuperType (name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}

function SubType (name, age) {
SuperType.call(this, name)

this.age = age
}

inheritPrototype(SubType, SuperType)

SubType.prototype.sayAge = function () {
console.log(this.age)
}
+

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变。因此,还能够正常使用 instanceofisPrototypeOf()

+

小结

继承总结

]]> JavaScript - 符号与符号属性 - /posts/67215a69/ - 在 JS 已有的基本类型之外,ES6 引入了一种新的基本类型:符号(Symbol)。符号起初被设计用于创建对象私有成员,而这也是JS开发者期待已久的特性。

-

创建符号值

符号没有字面量形式,这在JS的基本类型中是独一无二的。你可以使用全局 Symbol 函数来创建一个符号值,正如下面这个例子:

-
let firstName = Symbol()
let person = {}

person[firstName] = 'Nicholas'

// Nicholas
console.log(person[firstName])
-

此代码创建了一个符号类型的 firstName 变量,并将它作为 person 对象的一个属性,而每次访问该属性都要使用这个符号值。

-

由于符号值是基本类型的值,因此调用 new Symbol() 将会抛出错误。你可以通过 new Object(yourSymbol) 来创建一个符号实例,但尚不清楚这能有什么作用。

-

Symbol 函数还可以接受一个额外的参数用于描述符号值,该描述并不能用来访问对应属性,但它能用于调试,例如:

-
let firstName = Symbol('first name')
let person = {}

person[firstName] = 'Nicholas'

// false
console.log('first name' in person)

// Nicholas
console.log(person[firstName])

// Symbol(first name)
console.log(firstName)
-

符号的描述信息被存储在内部属性 [[Description]] 中,当符号的 toString() 方法被显式或隐式调用时,该属性都会被读取。

-

由于符号是基本类型的值,你可以使用 typeof 运算符来判断一个变量是否为符号。ES6 扩充了 typeof 的功能以便让它在作用于符号值的时候能够返回 symbol

-

使用符号值

你可以在任意能使用“需计算属性名”的场合使用符号。此外还可以在 Object.defineProperty()Object.defineProperties() 调用中使用它。

-

由于符号不存在字面量形式,所以如果以符号作为对象的属性名,就算该属性的 enumerable 被设置为 true,该属性也无法用 for-in 循环,并且不会显示在 Object.keys() 的结果中。但是你可以使用 in 操作符来判断该属性是否存在!

-
let firstName = Symbol('first name')
let person = {
[firstName]: 'Nicholas',
normalAttr: 1
}

let desc = Object.getOwnPropertyDescriptor(person, firstName)

// Nicholas
console.log(desc.value)

// true
console.log(desc.writable)

// true
console.log(desc.enumerable)

// true
console.log(desc.configurable)

// true
console.log(firstName in person)

// [ 'normalAttr' ]
console.log(Object.keys(person))

// normalAttr
for (let key in person) {
console.log(key)
}
-

共享符号值

你或许想在不同的代码段中使用相同的符号值,例如:假设在应用中需要在两个不同的对象类型中使用同一个符号属性,用来表示一个唯一标识符。跨越文件或代码来追踪符号值是很困难并且易错的,为此,ES6 提供了“全局符号注册表”供你在任意时间点进行访问。

-

若你想创建共享符号值,应使用 Symbol.for() 方法而不是 Symbol() 方法。Symbol.for() 方法仅接受单个字符串类型的参数,作为目标符号值的标识符,同时此参数也会成为该符号的描述信息。例如:

-
let uid = Symbol.for('uid')
let object = {}

object[uid] = 123456

// 123456
console.log(object[uid])

// Symbol(uid)
console.log(uid)
-

Symbol.for() 方法首先会搜索全局符号注册表,看是否存在一个键值为 "uid" 的符号值。若是,该方法会返回这个已存在的符号值。否则,会创建一个新的符号值,并使用该键值将其记录到全局符号注册表中,然后返回这个新的符号值。这就意味着此后使用同一个键值去调用 Symbol.for() 方法都将返回同一个符号值,就像下面这个例子:

-
let uid = Symbol.for('uid')
let object = {
[uid]: 123456
}

// 123456
console.log(object[uid])

// Symbol(uid)
console.log(uid)

let uid2 = Symbol.for('uid')

// true
console.log(uid === uid2)

// 123456
console.log(object[uid2])

// Symbol(uid)
console.log(uid2)
-

共享符号值还有另一个独特用法,你可以使用 Symbol.keyFor() 方法在全局符号注册表中根据符号值检索出对应的键值,例如:

-
let uid = Symbol.for('uid')

// uid
console.log(Symbol.keyFor(uid))

let uid2 = Symbol.for('uid')

// uid
console.log(Symbol.keyFor(uid2))

let uid3 = Symbol('uid')

// undefined
console.log(Symbol.keyFor(uid3))
-

注意:使用符号值 uiduid2 都返回了键值 "uid",而符号值 uid3 在全局符号注册表中并不存在,因此没有关联的键值,Symbol.keyFot() 只会返回 undefined

-

符号值的转换

类型转换是 JS 语言重要的一部分,能够非常灵活地将一种数据类型转换为另一种。然而符号类型在进行转换时非常不灵活,因为其他类型缺乏与符号值的合理等价,尤其是符号值无法被转换为字符串值或数值。因此将符号作为属性所达成的效果,是其他类型所无法替代的。

-

在之前的例子中使用了 console.log() 来展示符号值的输出,能这么做是由于自动调用了符号值的 String() 方法来产生输出。你也可以直接调用 String() 方法来获取相同的结果,例如:

-
let uid = Symbol.for('uid')
let desc = String(uid)

// Symbol(uid)
console.log(desc)
-

String() 方法调用了 uid.toString() 来获取符号的字符串描述信息。但若你想直接将符号转换为字符串,则会引发错误:

-
let uid = Symbol.for('uid')

// 引发错误
let desc = uid + ''
-

uid与空字符串相连接,会首先要求把uid转换为一个字符串,而这会引发错误,从而阻止了转换行为。

-

相似地,你也不能将符号转换为数值,对符号使用所有数学运算符都会引发错误,例如:

-
let uid = Symbol.for('uid')

// 引发错误
let desc = uid / 1
-

此例试图把符号值除以 1,同样引发了错误。无论对符号使用哪种数学运算符都会导致错误,但使用逻辑运算符则不会,因为符号值在运算符中会被认为等价于 true

-

检索符号属性

只能使用 ES6 新增的 Object.getOwnPropertySymbols() 方法用来检索对象的符号属性。Object.keys()Object.getOwnPropertyNames() 方法都不行。

-

使用知名符号暴露内部方法

ES6 定义了“知名符号”来代表JS中一些公共行为,而这些行为此前被认为只能是内部操作。每一个知名符号都对应全局 Symbol 对象的一个属性,这些知名符号是:

+ 基本概念 + /posts/2c31c1a2/ + 变量

ECMAScript 的变量是松散类型的,可以用来保存任何类型的数据,换句话说,变量只是一个用于保存值得占位符而已,变量没有类型,数据才有类型!

-
  • Symbol.hasInstance:供 instanceof 运算符使用的一个方法,用于判断对象继承关系。
  • Symbol.isConcatSpreadable:一个布尔类型值,在集合对象作为参数传递给 Array.prototype.concat() 方法时,指示是否要将该集合的元素扁平化。
  • Symbol.iterator:返回迭代器的一个方法。
  • Symbol.match:供 String.prototype.match() 函数使用的一个方法,用于比较字符串。
  • Symbol.replace:供 String.prototype.replace() 函数使用的一个方法,用于替换子字符串。
  • Symbol.search:供 String.prototype.search() 函数使用的一个方法,用于定位子字符串。
  • Symbol.species:用于产生派生对象的构造器。
  • Symbol.split:供 String.prototype.split() 函数使用的一个方法,用于分割字符串。
  • Symbol.toPrimitive:返回对象所对应的基本类型值的一个方法。
  • Symbol.toStringTag:供 String.prototype.toString() 函数使用的一个方法,用于创建对象的描述信息。
  • Symbol.unscopables:一个对象,该对象的属性指示了那些属性名不允许被包含在 with 语句中。
+
  • 使用 var 声明变量,变量为声明该变量的作用域中的局部变量。即在全局作用域中声明的变量为全局变量,在局部作用域中声明的变量为局部变量。
  • 使用 var 声明的变量如果未初始化,则变量保存的值默认为 undefined
  • 对没有声明的变量直接进行赋值操作(或者说是 LHS 操作),在非严格模式下,会创建一个全局变量。但是在严格模式下,会抛出错误。
-

下面将介绍其中的一些知名符号。

-

Symbol.hasInstance

每个函数都具有一个 Symbol.hasInstance 方法,用于判断指定对象是否为本函数的一个实例。这个方法定义在 Function.prototype 上,因此所有函数都继承了面对 instanceof 运算符时的默认行为。Symbol.hasInstance 属性自身是不可写入、不可配置、不可枚举的,从而保证它不会被错误地重写。

-

Symbol.hasInstance 方法只接受单个参数,即需要检测的值。如果该值是本函数的一个实例,则方法会返回 true。为了理解该方法是如何工作的,可研究下述代码:

-
obj instanceof Array
// 等价于
Array[Symbol.hasInstance](obj)
-

ES6 从本质上将 instanceof 运算符重定义为上述方法调用的简写语法,这样使用 instanceof 便会出发一次方法调用,实际上允许你改变该运算符的工作。

-

假设你想定义一个函数,使得任意对象都不会被判断为该函数的一个实例,你可以采用硬编码的方式来让该函数的 Symbol.hasInstance 方法始终返回 false,就像这样:

-
function MyObject () {
// ...
}

Object.defineProperty(MyObject, Symbol.hasInstance, {
value (v) {
return false
}
})

let obj = new MyObject()

// false
console.log(obj instanceof MyObject)
-

上例中通过 Object.defineProperty() 方法在 MyObject 对象上设置了 Symbol.hasInstance 属性,从而屏蔽了原型上不可写入的 Symbol.hasInstance 属性。

-

Symbol.isConcatSpreadable

首先请看下面数组 concat() 方法的例子:

-
let colors1 = [ 'red', 'green' ]
let colors2 = colors1.concat([ 'blue', 'black' ])
let colors3 = colors1.concat([ 'blue', 'black' ], 'brown')

// [ 'red', 'green', 'blue', 'black' ]
console.log(colors2)

// [ 'red', 'green', 'blue', 'black', 'brown' ]
console.log(colors3)
-

concat() 方法会区别对待自己接收到的参数,如果参数为数组类型,那么它会自动的将数组扁平化(即分离数组中的元素)。而其他非数组类型的参数无需如此处理。在 ES6 之前,没有任何手段可以改变这种行为。

-

Symbol.isConcatSpreadable 属性是一个布尔类型的属性,它默认情况下并不会作为任意常规对象的属性。它只出现在特定类型的对象上,用来标示该对象作为 concat() 参数时应如何工作。

-

成功使用这个属性的前提条件是拥有该属性的对象,要在两个方面与数组类似:拥有数值类型的键拥有 length 属性

-

当该属性为 true 时,将该属性所属对象传递给 concat() 方法时,将所属对象扁平化。当该属性为 false 时,所属对象不会被扁平化。请看下面的例子:

-
let obj1 = {
0: 'hello',
1: 'world',
length: 2,
[Symbol.isConcatSpreadable]: true
}

let messages1 = [ 'hi' ].concat(obj1)

// [ 'hi', 'hello', 'world' ]
console.log(messages1)

let obj2 = {
0: 'hello',
length: 2,
[Symbol.isConcatSpreadable]: false
}

let messages2 = [ 'hi' ].concat(obj2)

// [ 'hi', { '0': 'hello', length: 2, [Symbol.isConcatSpreadable]: false } ]
console.log(messages2)
-

Symbol.match、Symbol.replace、Symbol.search 与 Symbol.split

在 JS 中,字符串与正则表达式有着密切的联系,尤其是字符串具有几个可以接受正则表达式作为参数的方法:match、replace、search和split方法

-

在 ES6 之前这些方法的实现细节对开发者是隐藏的,使得开发者无法将自定义对象模拟成正则表达式(并将它们传递给字符串的这些方法)。而 ES6 定义了 4 个符号以及对应的方法,将原生行为外包到内置的 RegExp 对象上。

+

数据类型

JavaScript 的数据类型分为“基本数据类型”和“引用数据类型”两种。

-
  • Symbol.match:此函数接受一个字符串参数,并返回一个包含匹配结果的数组。若匹配失败,则返回 null
  • Symbol.replace:此函数接受一个字符串参数与一个替换用的字符串,并返回替换后的结果字符串。
  • Symbol.search:此函数接受一个字符串参数,并返回匹配结果的数值索引。若匹配失败,则返回 -1。
  • Symbol.split:此函数接受一个字符串参数,并返回一个用匹配值分割而成的字符串数组。
+
  • 基本数据类型UndefinedNullBooleanNumberStringSymbol(ES6 提出)。
  • 引用数据类型Object
-

在对象上定义这些属性,允许你创建能过进行模式匹配的对象,而无需使用这则表达式,并且允许在任何需要正则表达式的方法中使用该对象。这里有一个例子,展示了这些符号的使用:

-
// 有效等价于/^.{10}$/
let hasLengthOf10 = {
[Symbol.match] (value) {
return value.length === 10 ? [value.substring(0, 10)] : null
},
[Symbol.replace] (value, replacement) {
return value.length === 10 ?
replacement + value.substring(10) : value
},
[Symbol.search] (value) {
return value.length === 10 ? 0 : -1
},
[Symbol.split] (value) {
return value.length === 10 ? [ '', '' ] : [value]
}
}

// 11 characters
let message1 = 'Hello world'

// 10 characters
let message2 = 'Hello John'

let match1 = message1.match(hasLengthOf10)
let match2 = message2.match(hasLengthOf10)

// null
console.log(match1)

// [ 'Hello John' ]
console.log(match2)

let replace1 = message1.replace(hasLengthOf10, 'Howdy!')
let replace2 = message2.replace(hasLengthOf10, 'Howdy!')

// Hello world
console.log(replace1)

// Howdy!
console.log(replace2)

let search1 = message1.search(hasLengthOf10)
let search2 = message2.search(hasLengthOf10)

// -1
console.log(search1)

// 0
console.log(search2)

let split1 = message1.split(hasLengthOf10)
let split2 = message2.split(hasLengthOf10)

// [ 'Hello world' ]
console.log(split1)

// [ '', '' ]
console.log(split2)
-

Symbol.toPrimitive

Symbol.toPrimitive 方法被定义在所有常规类型的原型上,规定了在对象被转换为基本类型值的时候会发生什么。当需要转换时,Symbol.toPrimitive 会被调用,并按照规范传入一个提示性的字符串参数。该参数有 3 种可能:当参数值为 number 的时候,应当返回一个数值。当参数值为 string 的时候,应当返回一个字符串。而当参数为 default 的时候,对返回值类型没有特别要求。

-

对于大部分常规对象,“数值模式”依次会有下述行为:

+

检测类型

typeof 操作符

对一个值使用 typeof 操作符,可能返回下列某个字符串(均为英文单词小写形式):

-
  1. 调用 valueOf() 方法,如果方法返回值是一个基本类型值,那么返回它。
  2. 否则,调用 toString() 方法,如果方法返回值是一个基本类型值,那么返回它。
  3. 否则,抛出一个错误。
+
  • "undefined": 如果这个值未定义。
  • "boolean": 如果这个值是布尔值。
  • "string": 如果这个值是字符串。
  • "number": 如果这个值是数值。
  • "symbol": 如果这个值是符号。
  • "function": 如果这个值是函数(其实函数属于对象的一种)。
  • "object": 如果这个值是对象或 null
-

类似的,对于大部分常规对象,“字符串模式”依次会有下述行为:

+

instanceof 操作符

用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

+

object instanceof constructor => true | false

+

《JavaScript 高级程序设计》中说:“根据规定,所有引用数据类型的值都是Object的实例。因此,在检测一个引用数据类型值和Object构造函数时,instanceof操作符始终会返回true。
随着版本的迭代,这个说法变得不准确了,有例外了!!!

+
var a = Object.create(null)
console.log(a instanceof Object) // false
+

Object.prototype.toString.call()

返回一个表示类型的字符串,详情请看 MDN官网

+

基本数据类型

Undefined

Undefined 类型只有一个值,即特殊的 undefined。在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined。对未初始化的变量执行 typeof 操作会返回 undefined ,而对未声明的变量执行 typeof 操作同样会返回 undefined

+

除了可以对未声明的变量执行 typeof 操作之外,在非严格模式下还可以对其进行 delete 操作。除了这两种情况之外,对未声明的变量进行任何其他操作,都会抛出错误。

+

Null

Null 类型也只有一个值 - null

+
typeof null === 'object'    // true
null instanceof Object // false
null == undefined // true
Boolean(null) === false // true
+

Boolean

Boolean 类型的字面值 truefalse 是区分大小写的。也就是说,TrueFalse 都不是 Boolean 值,只是标识符。

+

Number

+
  • 八进制字面量在严格模式下是无效的,会导致支持的JavaScript引擎抛出错误。
  • 浮点数值的最高精度是17位小数,但在进行算术计算时其精确度远远不如整数。
  • 绝对值超过 Number.MAX_VALUE 的值会被自动转换为 infinity-infinity
  • 可以使用 isFinite() 函数来确定数值是否为无穷。如果不是无穷,则返回 true
  • NaN 与自身都不相等,可以使用 isNaN()Number.isNaN() 确定数值是否为 NaN。在传入数据非数值时,isNaN() 会首先调用 Number() 将其转换为数值类型再进行判断,而 Number.isNaN() 则会直接返回 false
  • Number() 用于把任何类型数据转换为数值。
  • parseInt()parseFloat() 用于把字符串转换为数值。
  • parseInt() 在转换字符串时比 Number() 好用,但是最好为 parseInt() 提供第二个参数(转换的基数2、8、10、16)。
+
+

String

+
  • 任何字符串的长度都可以通过访问其 length 属性取得,包含双子节字符的可能不准确。
  • 字符串是不可变的,一旦创建就不能改变,要改变某个变量保存的字符串,首先要销毁原来的字符串然后再用新的替代。
  • 数值、布尔值、对象和字符串值都有 toString() 方法,并且数值的 toString() 方法可以接收一个参数(数值的基数),但是 nullundefined 没有。
  • 在不知道要转换值的类型时,可以使用 String() 方法。
+
+

引用数据类型

Object

ECMAScript 中的对象其实就是一组数据和功能的集合,它的属性或方法是没有顺序之分的!!!

+

Object 类型是所有它的实例的基础,它所具有的任何属性和方法也同样存在于更具体的对象中。Object 的每个实例都具有下列属性和方法:

-
  1. 调用 toString() 方法,如果方法返回值是一个基本类型值,那么返回它。
  2. 否则,调用 valueOf() 方法,如果方法返回值是一个基本类型值,那么返回它。
  3. 否则,抛出一个错误。
+
  • constructor
  • hasOwnProperty(propertyName)
  • isPrototypeOf(object)
  • propertyIsEnumerable(propertyName)
  • toLocaleString()
  • toString()
  • valueOf()
-

在多数情况下,常规对象的默认模式都等价于数值模式(只有 Date 类型例外,它默认使用字符串模式)。通过定义 Symbol.toPrimitive 方法,你可以重写这些默认的转换行为。

-

使用 Symbol.toPrimitive 属性并将一个函数赋值给它,便可以重写默认的转换行为,例如:

-
function Temperature (degrees) {
this.degrees = degrees
}

Temperature.prototype[Symbol.toPrimitive] = function (hint) {
switch (hint) {
case 'string':
return this.degrees + '\u00b0'
case 'number':
return this.degrees
case 'default':
return this.degrees + ' degrees'
}
}

let freezing = new Temperature(32)

// 32 degrees!
console.log(freezing + '!')

// 16
console.log(freezing / 2)

// 32°
console.log(String(freezing))
-

Symbol.toStringTag

JS 最有趣的课题之一是在多个不同的全局执行环境中使用,这种情况会在浏览器页面包含内联帧(iframe)的时候出现,此时页面与内联帧均拥有各自的全局执行环境。大多数情况下这并不是一个问题,使用一些轻量级的转换操作就能够在不同的运行环境之间传递数据。问题出现在想要识别目标对象到底是什么类型的时候,而此时该对象已经在环境之间经历了传递。

-

该问题的典型例子就是从内联帧向容器页面传递数组,或者反过来。在 ES6 术语中,内联帧与包含它的容器页面分别拥有一个不同的“域”,以作为 JS 的运行环境,每个“域”都拥有各自的全局作用域以及各自的全局对象拷贝。无论哪个“域”创建的数组都是正规的数组,但当它跨域进行传递时,使用 instanceof Array 进行检测却会得到 false 的结果,因为该数组是由另外一个“域”的数组构造器创建的,有别于当前“域”的数组构造器。

-

识别问题的变通解决方案

变通的解决方案为 Object.prototype.toString.call()

-

ES6 给出的答案

ES6 通过 Symbol.toStringTag 重定义了相关行为,该符号是对象的一个属性,定义了 Object.prototype.toString.call() 被调用时应当返回什么值。

-
function Person (name) {
this.name = name
}

Person.prototype[Symbol.toStringTag] = 'Person'

let me = new Person('Nicholas')

// [object Person]
console.log(me.toString())

// [object Person]
console.log(Object.prototype.toString.call(me))

Person.prototype.toString = function () {
return this.name
}

// Nicholas
console.log(me.toString())

// [object Person]
console.log(Object.prototype.toString.call(me))
-

Symbol.unscopables

尽管将来的代码无疑会停用 with 语句,但 ES6 仍然在非严格模式中提供了对于 with 语句的支持,以便向下兼容。为此需要寻找方法让使用 with 语句的代码能够适当地继续工作。为了理解这个任务的复杂性,可研究如下代码:

-
let values = [1, 2, 3]
let colors = ['red', 'green', 'blue']
let color = 'black'

with (colors) {
push(color)
push(...values)
}

// [ 'red', 'green', 'blue', 'black', 1, 2, 3 ]
console.log(colors)
-

在此例中,...values 引用了 with 语句之外的变量 values

-

但ES6为数组添加了一个 values 方法(迭代器与生成器的知识),这意味着在 ES6 的环境中,with 语句内部的 values 并不会指向 with 语句之外的变量 values,而是会指向数组的 values 方法,从而会破坏代码的意图。这也是 Symbol.unscopables 符号出现的理由。

-

Symbol.unscopables 符号在 Array.prototype 上使用,以指定哪些属性不允许在 with 语句内被绑定。Symbol.unscopables 属性是一个对象,当提供该属性时,它的键就是用于忽略 with 语句绑定的标识符,键值为 true 代表屏蔽绑定。以下是数组的 Symbol.unscopables 属性的默认值:

-
// 默认内置在 ES6 中
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
keys: true,
values: true
})
]]>
+

复制变量值

在将一个值赋给变量时,如果这个值是基本数据类型,则变量中保存的是这个值本身。如果这个值是引用数据类型,则变量中保存的只是该值的一个引用(通过引用,可以在内存中找到这个引用数据类型的值)!

+
// ===================================================
// 在从一个变量向另一个变量复制基本数据类型值和引用数据类型值的区别
// ===================================================

//复制基本数据类型值
var a = 1
var b = a
b = 2
console.log(a, b) // 1, 2

// 复制引用数据类型值
// c 和 d 中保存的都是指向内存中同一对象的引用
var c = { num: 1 }
var d = c
d.num = 2
console.log(c.num, d.num) //2, 2
+

传递参数

ECMAScript 中所有函数的参数都是按值传递的。简单来说就是数据进行复制,然后传递给给函数作为参数。

+

语句

for-in语句

ECMAScript 对象的属性没有顺序。因此,通过 for-in 循环输出的属性名的顺序是不可预测的。

+

for-in 不能对 nullundefined 进行迭代。

+

switch语句

可以在 switch 语句中使用任何数据类型(在很多其他语言中只能使用数值),并且每个 case 的值不一定是常量,可以是变量,甚至是表达式。

+

switch对 case 进行匹配时,遵循的是 === 严格相等。

+

函数

理解参数

首先说明我的观点,不推荐使用 arguments !!!

+

ECMAScript 中的函数参数是用一个数组来表示的,可以通过 arguments 对象来访问这个数组,arguments 是一个类数组对象。

+

关于 arguments命名参数 之间是如何相互影响的,《JavaScript高级程序设计》与《深入理解ES6》的说法有些矛盾,我在谷歌浏览器中简单的试了一下,他们之间的同步关系好像是双向的。

+

没有重载

ECMAScript 中的函数没有重载,如果声明了多个同名函数,后面的会覆盖前面的。

+

垃圾收集

JavaScript 中的变量分为全局变量和局部变量,其中全局变量一直存在,不会被清除。

+

而局部变量只在函数执行的过程中存在,当函数执行结束后,局部变量会被自动清除。

+

当然如果存在闭包的话,局部变量被清除的时机需要取决于闭包。

+]]>
JavaScript @@ -3290,132 +3270,143 @@
- 对象&原型&继承 - /posts/bb5d40f2/ - 理解对象

属性类型

数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。

-
-
  • configurable:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,这个特性默认值为 true
  • enumerable:表示能否通过 for-in 循环返回属性。直接在对象上定义的属性,这个特性默认值为 true
  • writable:表示能否修改属性的值。直接在对象上定义的属性,这个特性默认值为 true
  • value:包含这个属性的数据值。读取属性值得时候,从这个位置读。写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined

可以使用 Object.defineProperty(obj, prop, descriptor) 修改数据属性的特性:

  • obj:数据属性所在的对象。
  • prop:数据属性的名称,可以是字符串或符号。
  • descriptor:描述符对象,可包含的属性有 configurableenumerablewritablevalue

如果通过 Object.defineProperty(obj, prop, descriptor) 定义一个新的数据属性,descriptor 中缺失的特性会被赋予 falseundefined

+ 强制类型转换 + /posts/1750eb3e/ + 强制类型转换规则

引用类型转换为基本类型

+
  1. 调用自身的 valueOf() 方法,如果返回基本类型的值,则转换成功。
  2. 如果 valueOf() 方法返回的还是引用类型值,则改为调用自身的 toString() 方法。如果 toString() 方法返回基本类型的值,则转换成功。
  3. 如果 toString() 方法返回的是引用类型值,抛出错误。

需要注意的是,数组的默认 toString() 方法经过了重新定义,会将所有单元字符串化以后再用 "," 连接起来。

-
var obj = { test: 1 }
var desc = Object.getOwnPropertyDescriptor(obj, 'test')

// { value: 1, writable: true, enumerable: true, configurable: true }
console.log(desc)

Object.defineProperty(obj, 'demo', {
configurable: false
})
desc = Object.getOwnPropertyDescriptor(obj, 'demo')

// { value: undefined, writable: false, enumerable: false, configurable: false }
console.log(desc)
-

关于writable:当 writablefalse 时,在非严格模式下通过赋值语句修改属性值,赋值操作将被忽略。在严格模式下则会抛出错误。但是如果通过 Object.defineProperty() 方法修改 value 特性则不会有任何问题

-
var obj = { test: 1}
Object.defineProperty(obj, 'test', {
writable: false
})
obj.test = 2

// { test: 1 }
console.log(obj)

Object.defineProperty(obj, 'test', {
value: 3
})

// { test: 3 }
console.log(obj)
-

关于configurable:当 configurablefalse 时,不允许删除属性,不允许修改属性的 enumerableconfigurable,不可以将 writablefalse 修改为 true,但是可以将 writabletrue 修改为 false,也可以修改属性的 value 特性。

-

当 writable 和 configurable 均为 false 时,不允许通过任何方式修改属性值,直接赋值或者通过 Object.defineProperty() 都不可以!

-
/**
* 下面的实验运行于非严格模式下
*/
var obj = { test: 1 }

// 在 configurable 为 false 时尝试删除属性
Object.defineProperty(obj, 'test', {
configurable: false
})
delete obj.test

// { test: 1 }
console.log(obj)

var desc = Object.getOwnPropertyDescriptor(obj, 'test')

// { value: 1, writable: true, enumerable: true, configurable: false }
console.log(desc)

// 在 configurable 为 false 时尝试修改 enumerable
// Uncaught TypeError: Cannot redefine property: test
Object.defineProperty(obj, 'test', {
enumerable: false
})

// 在 configurable 为 false 时尝试修改 configurable
// Uncaught TypeError: Cannot redefine property: test
Object.defineProperty(obj, 'test', {
configurable: true
})

// 在 configurable 为 false 时尝试修改 value
Object.defineProperty(obj, 'test', {
value: '此时 configurable 为 false'
})

// { test: "此时 configurable 为 false" }
console.log(obj)

// 在 configurable 为 false 时尝试将 writable 由 true 修改为 false
Object.defineProperty(obj, 'test', {
writable: false
})
var desc = Object.getOwnPropertyDescriptor(obj, 'test')

// { value: "此时 configurable 为 false", writable: false, enumerable: true, configurable: false }
console.log(desc)

// 在 configurable 为 false 时尝试将 writable 由 false 修改为 true
// Uncaught TypeError: Cannot redefine property: test
Object.defineProperty(obj, 'test', {
writable: true
})

// 在 configurable 和 writable 均为 false时,尝试修改属性值
obj.test = '直接赋值可以吗'
// {test: "此时configurable为false"}
console.log(obj)
// Uncaught TypeError: Cannot redefine property: test
Object.defineProperty(obj, 'test', {
value: '通过 Object.defineProperty 可以吗'
})
-

访问器属性

访问器属性不包含数据值。它们包含一对 getset 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 get 函数,这个函数负责返回有效的值。在写入访问器属性时,会调用 set 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性。

-
-
  • configurable:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。直接在对象上定义的属性,它们的这个特性默认值为 true
  • enumerable:表示能否通过 for-in 循环返回属性。直接在对象上定义的属性,它们的这个特性默认值为 true
  • get:在读取属性时调用的函数。默认值为 undefined
  • set:在写入属性时调用的函数。默认值为 undefined
+

转换为字符串

+
  • null"null"
  • undefined"undefined"
  • 布尔值:"true""false"
  • 数值:通常直接转换为字符串即可,但是对于极小或极大的数值,会使用指数形式。
  • 引用类型值:返回自身的 toString() 方法的返回值。
-

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。

-
var book = {
_year: 2004,
edition: 1
}
Object.defineProperty(book, 'year', {
get: function () {
return _this.year
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue
this.edition += newValue - 2004
}
}
})
book.year = 2005

// { _year: 2005, edition: 2 }
console.log(book)
-

不一定非要同时指定 getset。只指定 get 意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了 get 函数的属性会抛出错误。而读取只指定 set 的属性会返回 undefined

-

可以通过 Object.defineProperty() 实现数据属性与访问器属性的转换,但是切记不能同时指定数据属性和访问器属性,这样会抛出错误!

-

定义多个属性

ES5 定义了一个 Object.defineProperties() 方法用来为对象定义多个属性。

-
var book = {}
Object.defineProperties(book, {
_year: {
value
},
edition: {
value: 1
},
year: {
get: function () {
return _this.year
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue
this.edition += newValue - 2004
}
}
}
})
-

读取属性的特性

使用 ES5 的 Object.getOwnPropertyDescriptor() 方法可以取得给定属性的描述符。该方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象。这个方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用。

-

禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions()。如果想检测一个对象是否可以添加新属性,可以使用 Object.isExtensible()

-
-

不可以添加新属性,但是删除旧属性还是可以的。

+
// "null"
console.log(String(null))

// "undefined"
console.log(String(undefined))

// "false"
console.log(String(false))

// "123"
console.log(String(123))

// "1.23e+21"
console.log(String(123 * 10000000000000000000))

// "[object Object]"
console.log(String({a: 1}))

// "1,2,3"
console.log(String([1, 2, 3]))
+

转换为数值

+
  • null0
  • undefinedNaN
  • 布尔值:01
  • 字符串:十进制数值或 NaN
  • 引用类型值:先将引用类型值转换为基本类型值,然后再将该基本类型值转换为数值。
-
var myObject = { a: 2 }
Object.preventExtensions(myObject)

// false
console.log(Object.isExtensible(myObject))

myObject.b = 3

// undefined
console.log(myObject.b)

delete myObject.a

// undefined
console.log(myObject.a)
-

在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。

-

密封

Object.seal() 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions() 并把所有现有属性标记为 configurable: false

-

冻结

Object.freeze() 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal() 并把所有“数据访问”属性标记为 writable: false

-

创建对象

工厂模式

工厂模式就是调用函数返回一个包含特定属性和方法的对象,工厂模式的问题在于它没有解决对象识别的问题(即怎样知道一个对象的类型)

-
function createPerson (name, age) {
var o = {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
return o
}
var person1 = createPerson('Nicholas', 29)
var person2 = createPerson('Greg', 27)
-

构造函数模式

ES 中可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法,下面使用构造函数模式重写工厂模式中的例子。

-
function Person (name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
var person1 = new Person('Nicholas', 29)
var person2 = new Person('Greg', 27)

// true
console.log(person1 instanceof Person)
// true
console.log(person2 instanceof Person)
-

构造函数模式与工厂模式的区别

-
-
  • 没有显示地创建对象.
  • 直接将属性和方法赋给了 this 对象.
  • 没有 return 语句。
+
// 0
console.log(Number(null))

// NaN
console.log(Number(undefined))

// 0
console.log(Number(false))

// 123
console.log(Number('123'))

// NaN
console.log(Number('123abc'))

// NaN
console.log(Number({a: 1}))

// NaN
console.log(Number([1, 2, 3]))

var obj = {
valueOf () {
return 42
}
}

var array = [1, 2, 3]
array.valueOf = function () {
return 123
}

// 42
console.log(Number(obj))

// 123
console.log(Number(array))
+

转换为布尔值

+

在将值转换为布尔值时,除了下述 5 种情况,其他所有情况都会转换为 true

  • null
  • undefined
  • false
  • +0-0NaN
  • ""
-

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历一下 4 个步骤:

+

显示强制类型转换

+
  • 一元运算符 +-++-- 都会调用 Number() 将其他类型转换为数值,或将日期对象转换为对应的毫秒数。注意不要混淆 ++++ +---- -
  • 一元运算符 ~ 会先将值强制类型转换为 32 位数值,然后执行按位非操作,可以将 ~x 等价于 -(x+1)。但是 ~-1 的结果是 0 而不是 -0,因为 ~ 是字位操作而非数学运算。
  • 将值转换为布尔值时,可以使用 ! 运算符。
+
+
var a = '1'
var b = +a
var c = -a
var d = - -a
var e = --a

// 1
console.log(b)

// -1
console.log(c)

// 1
console.log(d)

// 0
console.log(e)

var f = new Date()

// 1557055687256
console.log(+f)

// ==============================

var x = -42

// 41
console.log(~x)

// ==============================

var a = '0'
var b = []
var c = {}
var d = ''
var e = 0
var f = null
var g

// true
console.log(!!a)

// true
console.log(!!b)

// true
console.log(!!c)

// false
console.log(!!d)

// false
console.log(!!e)

// false
console.log(!!f)

// false
console.log(!!g)
+

隐式强制类型转换

+

+- 作为一元运算符和作为多元运算符时具有不同的含义,别混淆了!

+
+

+ 运算符

+ 运算符既能用于数值加法,也能用于字符串拼接。JavaScript 怎样来判断我们要执行的是哪个操作?

-
  1. 创建一个新对象(因为用了 new)。
  2. 为新对象连接原型。
  3. 将构造函数的作用域内的 this 绑定到这个新对象。
  4. 执行构造函数的代码。
  5. 返回新对象。
+
  1. 如果操作数中有字符串,进行字符串拼接操作。
  2. 如果操作数中有引用类型值,首先将引用类型值转换为基本类型值,然后进行后续操作。
  3. 其他情况全都进行数值加法。
-
-

使用 new 创建新对象的时候,如果存在类的继承,那么在 ES5 和 ES6 中这个过程是有差别的。查看详情

+

- 运算符

+ 运算符不同,- 运算符会只会执行减法运算。所以它会先将非数值类型的数据转换为数值,然后进行减法运算。

+
var a = '12'
var b = 1

// 11
console.log(a-b)
+

宽松相等和严格相等

+
  • == 允许在相等比较中进行强制类型转换,而 === 不允许。
  • NaN 不等于 NaN
  • +0 严格等于 -0
  • !=!== 分别类似于 =====
-

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那么它就可以作为构造函数。

-

构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍,但是有的方法是所有实例都应该共享的,没有创建多次的必要。

-

原型模式

-

我们创建的每一个函数都有一个 prototype(原型)属性,这个属性值是一个对象的引用,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

+

下面主要介绍 == 是如何进行强制类型转换的。

+

字符串和数值

+

首先将字符串转换为数值类型,然后进行比较。

-

理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的引用。

-
function Person () {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log(this.name)
}

var person1 = new Person()

// Nicholas
person1.sayName()

var person2 = new Person()

// Nicholas
person2.sayName()

// true
console.log(person1.sayName === person2.sayName)
-

理解原型对象

-

上图展示了 Person 构造函数、Person 的原型属性以及 Person 现有的两个实例之间的关系。注意 Person 的每个实例,person1person2 都包含一个内部属性 [[Prototype]],该属性仅仅指向了 Person.prototype。换句话说,对象实例与构造函数没有直接的关系

-
-
  • isPrototypeOf():用于测试一个对象是否存在于另一个对象的原型链上。
  • hasOwnProperty():用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在,该方法是从 Object 继承而来的。
  • Object.getPrototypeOf():返回指定对象的原型(对象内部 [[Prototype]] 属性的值)。
+
var a = 42
var b = '42'

// true
console.log(a == b)
+

其他类型和布尔类型

+

首先将布尔类型的值转换为数值类型,然后进行比较。

-

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例开始,如果在实例中找到了具有给定名字的属性,则返回该属性。如果没有找到,则沿着对象的原型链向上逐层查找具有给定名字的属性,如果找到了则返回这个属性的值。

-

属性设置和屏蔽

用赋值语句给实例对象设置已经在原型链上层存在的同名属性时,会有以下三种情况:

+
var a = '42'
var b = true

// false
console.log(a == b)

// true
console.log('1' == true)
+

null 和 undefined

+

==nullundefined 相等(它们也与其自身相等),除此之外其他情况都不相等。

+
+
var a = null
var b

// true
console.log(a == b)

// true
console.log(a == null)

// true
console.log(b == null)

// false
console.log(a == false)

// false
console.log(b == false)

// false
console.log(a == '')

// false
console.log(b == '')

// false
console.log(a == 0)

// false
console.log(a == 0)
+

引用类型和基本类型

+

首先将引用类型转换为基本类型,然后进行比较。

+
+
var a = 42
var b = [42]

// true
console.log(a == b)
+

引用类型和引用类型

这种情况就是判断两个变量的引用是否相同

+]]> + + JavaScript + + + + 符号与符号属性 + /posts/67215a69/ + 在 JS 已有的基本类型之外,ES6 引入了一种新的基本类型:符号(Symbol)。符号起初被设计用于创建对象私有成员,而这也是JS开发者期待已久的特性。

+

创建符号值

符号没有字面量形式,这在JS的基本类型中是独一无二的。你可以使用全局 Symbol 函数来创建一个符号值,正如下面这个例子:

+
let firstName = Symbol()
let person = {}

person[firstName] = 'Nicholas'

// Nicholas
console.log(person[firstName])
+

此代码创建了一个符号类型的 firstName 变量,并将它作为 person 对象的一个属性,而每次访问该属性都要使用这个符号值。

+

由于符号值是基本类型的值,因此调用 new Symbol() 将会抛出错误。你可以通过 new Object(yourSymbol) 来创建一个符号实例,但尚不清楚这能有什么作用。

+

Symbol 函数还可以接受一个额外的参数用于描述符号值,该描述并不能用来访问对应属性,但它能用于调试,例如:

+
let firstName = Symbol('first name')
let person = {}

person[firstName] = 'Nicholas'

// false
console.log('first name' in person)

// Nicholas
console.log(person[firstName])

// Symbol(first name)
console.log(firstName)
+

符号的描述信息被存储在内部属性 [[Description]] 中,当符号的 toString() 方法被显式或隐式调用时,该属性都会被读取。

+

由于符号是基本类型的值,你可以使用 typeof 运算符来判断一个变量是否为符号。ES6 扩充了 typeof 的功能以便让它在作用于符号值的时候能够返回 symbol

+

使用符号值

你可以在任意能使用“需计算属性名”的场合使用符号。此外还可以在 Object.defineProperty()Object.defineProperties() 调用中使用它。

+

由于符号不存在字面量形式,所以如果以符号作为对象的属性名,就算该属性的 enumerable 被设置为 true,该属性也无法用 for-in 循环,并且不会显示在 Object.keys() 的结果中。但是你可以使用 in 操作符来判断该属性是否存在!

+
let firstName = Symbol('first name')
let person = {
[firstName]: 'Nicholas',
normalAttr: 1
}

let desc = Object.getOwnPropertyDescriptor(person, firstName)

// Nicholas
console.log(desc.value)

// true
console.log(desc.writable)

// true
console.log(desc.enumerable)

// true
console.log(desc.configurable)

// true
console.log(firstName in person)

// [ 'normalAttr' ]
console.log(Object.keys(person))

// normalAttr
for (let key in person) {
console.log(key)
}
+

共享符号值

你或许想在不同的代码段中使用相同的符号值,例如:假设在应用中需要在两个不同的对象类型中使用同一个符号属性,用来表示一个唯一标识符。跨越文件或代码来追踪符号值是很困难并且易错的,为此,ES6 提供了“全局符号注册表”供你在任意时间点进行访问。

+

若你想创建共享符号值,应使用 Symbol.for() 方法而不是 Symbol() 方法。Symbol.for() 方法仅接受单个字符串类型的参数,作为目标符号值的标识符,同时此参数也会成为该符号的描述信息。例如:

+
let uid = Symbol.for('uid')
let object = {}

object[uid] = 123456

// 123456
console.log(object[uid])

// Symbol(uid)
console.log(uid)
+

Symbol.for() 方法首先会搜索全局符号注册表,看是否存在一个键值为 "uid" 的符号值。若是,该方法会返回这个已存在的符号值。否则,会创建一个新的符号值,并使用该键值将其记录到全局符号注册表中,然后返回这个新的符号值。这就意味着此后使用同一个键值去调用 Symbol.for() 方法都将返回同一个符号值,就像下面这个例子:

+
let uid = Symbol.for('uid')
let object = {
[uid]: 123456
}

// 123456
console.log(object[uid])

// Symbol(uid)
console.log(uid)

let uid2 = Symbol.for('uid')

// true
console.log(uid === uid2)

// 123456
console.log(object[uid2])

// Symbol(uid)
console.log(uid2)
+

共享符号值还有另一个独特用法,你可以使用 Symbol.keyFor() 方法在全局符号注册表中根据符号值检索出对应的键值,例如:

+
let uid = Symbol.for('uid')

// uid
console.log(Symbol.keyFor(uid))

let uid2 = Symbol.for('uid')

// uid
console.log(Symbol.keyFor(uid2))

let uid3 = Symbol('uid')

// undefined
console.log(Symbol.keyFor(uid3))
+

注意:使用符号值 uiduid2 都返回了键值 "uid",而符号值 uid3 在全局符号注册表中并不存在,因此没有关联的键值,Symbol.keyFot() 只会返回 undefined

+

符号值的转换

类型转换是 JS 语言重要的一部分,能够非常灵活地将一种数据类型转换为另一种。然而符号类型在进行转换时非常不灵活,因为其他类型缺乏与符号值的合理等价,尤其是符号值无法被转换为字符串值或数值。因此将符号作为属性所达成的效果,是其他类型所无法替代的。

+

在之前的例子中使用了 console.log() 来展示符号值的输出,能这么做是由于自动调用了符号值的 String() 方法来产生输出。你也可以直接调用 String() 方法来获取相同的结果,例如:

+
let uid = Symbol.for('uid')
let desc = String(uid)

// Symbol(uid)
console.log(desc)
+

String() 方法调用了 uid.toString() 来获取符号的字符串描述信息。但若你想直接将符号转换为字符串,则会引发错误:

+
let uid = Symbol.for('uid')

// 引发错误
let desc = uid + ''
+

uid与空字符串相连接,会首先要求把uid转换为一个字符串,而这会引发错误,从而阻止了转换行为。

+

相似地,你也不能将符号转换为数值,对符号使用所有数学运算符都会引发错误,例如:

+
let uid = Symbol.for('uid')

// 引发错误
let desc = uid / 1
+

此例试图把符号值除以 1,同样引发了错误。无论对符号使用哪种数学运算符都会导致错误,但使用逻辑运算符则不会,因为符号值在运算符中会被认为等价于 true

+

检索符号属性

只能使用 ES6 新增的 Object.getOwnPropertySymbols() 方法用来检索对象的符号属性。Object.keys()Object.getOwnPropertyNames() 方法都不行。

+

使用知名符号暴露内部方法

ES6 定义了“知名符号”来代表JS中一些公共行为,而这些行为此前被认为只能是内部操作。每一个知名符号都对应全局 Symbol 对象的一个属性,这些知名符号是:

-
  • 如果在原型链上层存在的同名属性没有被标记为只读,即 writable: true,那么会直接在实例中添加一个同名的新属性,它是屏蔽属性。
  • 如果在原型链上层存在的同名属性被标记为只读,即 writable: false,那么无法修改已有属性,也无法在实例对象上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。如果在非严格模式下,赋值语句会被忽略。
  • 如果在原型链上层存在的同名属性具有 set 描述符,那么一定会调用这个 set。实例对象上并不会添加新的属性,也不会重新定义这个 set
+
  • Symbol.hasInstance:供 instanceof 运算符使用的一个方法,用于判断对象继承关系。
  • Symbol.isConcatSpreadable:一个布尔类型值,在集合对象作为参数传递给 Array.prototype.concat() 方法时,指示是否要将该集合的元素扁平化。
  • Symbol.iterator:返回迭代器的一个方法。
  • Symbol.match:供 String.prototype.match() 函数使用的一个方法,用于比较字符串。
  • Symbol.replace:供 String.prototype.replace() 函数使用的一个方法,用于替换子字符串。
  • Symbol.search:供 String.prototype.search() 函数使用的一个方法,用于定位子字符串。
  • Symbol.species:用于产生派生对象的构造器。
  • Symbol.split:供 String.prototype.split() 函数使用的一个方法,用于分割字符串。
  • Symbol.toPrimitive:返回对象所对应的基本类型值的一个方法。
  • Symbol.toStringTag:供 String.prototype.toString() 函数使用的一个方法,用于创建对象的描述信息。
  • Symbol.unscopables:一个对象,该对象的属性指示了那些属性名不允许被包含在 with 语句中。
-

但是 JS 这门语言很灵活,如果上述所说的存在于原型链上层的同名属性中保存的是某一个引用类型值的引用,那么你还是可以修改这个引用类型的值的(并没有违反规则,因为保存的引用并没有改变)!比如,这个属性保存的是某一个数组的引用,那么我就可以通过 push 方法去改变这个数组。

-

如果你无论如何也想要屏蔽原型链上层的属性,那么你可以使用 Object.defineProperty() 方法!

-

有些情况下会隐式产生屏蔽,一定要当心。思考下面的代码:

-
var anotherObject = { a: 2 }
var myObject = Object.create(anotherObject)

// 2
anotherObject.a

// 2
myObject.a

// true
anotherObject.hasOwnProperty('a')

// false
myObject.hasOwnProperty('a')

// 隐式屏蔽!
myObject.a++

// 2
anotherObject.a

// 3
myObject.a

// true
myObject.hasOwnProperty('a')
-

属性的获取

有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。

-

在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。

-

在使用 for-in 循环时返回的是所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包含存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在 for-in 循环中返回。

-

要取得对象上所有可枚举的实例属性,可以使用 ES5 的 Object.keys() 方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

-

如果你想要得到所有实例属性,无论它是否可枚举,可以使用 Object.getOwnPropertyNames() 方法。

-

更简单的原型语法

简单来说就是用对象字面量形式来重写 Person.prototype,但是这样会导致新原型对象的 constructor 属性指向 Object 而不是 Person,尽管此时 instanceof 操作符还能返回正确的结果,但是通过 constructor 已经无法确定对象的类型了,所以如果 constructor 属性比较重要的话,还需要用 Object.defineProperty() 方法定义 constructor 的数据属性。

-
function Person () {}

Person.prototype = {
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name)
}
}

var friend = new Person()

// true
console.log(friend instanceof Object)

// true
console.log(friend instanceof Person)

// true
console.log(friend.constructor === Object)

// false
console.log(friend.constructor === Person)

Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})

// true
console.log(friend.constructor === Person)
-

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。即使是先创建了实例后修改原型也是如此。

-
function Person () {}
var friend = new Person ()
Person.prototype.sayHi = function () {
console.log('hi')
}

// hi
friend.sayHi()
-

但是如果先创建了实例然后重写整个原型对象,那么情况就不一样了。具体的变化看图吧!

-

重写原型对象

-

此时 instanceof 操作符已经不好使了!
构造函数找不到最初的原型对象了!
现有实例也找不到新的原型对象了!

-
function Person () {}

var friend = new Person()

Person.prototype = {
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})

// true
console.log(friend instanceof Object)

// false
console.log(friend instanceof Person)

// Uncaught TypeError: friend.sayName is not a function
friend.sayName()
-

原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型模式的最大问题。原型模式的最大问题是由其共享的本性所导致的(主要针对引用类型值的属性来说)。

-

组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存,另外这种混成模式还支持向构造函数传递参数。

-
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Shelby', 'Court']
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person('Nicholas', 29, 'Software Engineer')
var person2 = new Person('Greg', 27, 'Doctor')
person1.friends.push('Van')

// ["Shelby", "Court", "Van"]
console.log(person1.friends)

// ["Shelby", "Court"]
console.log(person2.friends)

// false
console.log(person1.friends === person2.friends)

// true
console.log(person1.sayName === person2.sayName)
-

动态原型模式

动态原型模式把所有信息封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效来决定是否需要初始化原型。

-
function Person (name, age, job) {
// 属性
this.name = name
this.age = age
this.job = job

// 方法
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function () {
console.log(this.name)
}
}
}

var friend = new Person('Nicholas', 29, 'Software Engineer')

// Nicholas
friend.sayName()
-

在使用动态原型模式时,禁止使用对象字面量重写原型!

-

寄生构造函数模式

在前面几种模式都不适用的情况下,可以使用寄生构造函数模式,这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

-
function Person (name, age, job) {
var o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}

var friend = new Person('Nicholas', 29, 'Software Engineer')

// Nicholas
friend.sayName()
-

在这个例子中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

-

关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

-

稳妥构造函数模式

道格拉斯·克罗克福德发明了 JavaScript 中的稳妥对象这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 thisnew),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与计生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this。二是不使用 new 操作符调用构造函数。

-
function Person (name, age, job) {
// 创建要返回的对象
var o = new Object()

// 可以在这里定义私有变量和函数

// 添加方法
o.sayName = function () {
console.log(name)
}
return o
}

var friend = Person('Nicholas', 29, 'Software Engineer')

// Nicholas
friend.sayName()
-

小结

创建对象模式总结

-

继承

原型链

function SuperType () {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}

function SubType () {
this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}

var instance = new SubType()

// true
console.log(instance.getSuperValue())
-

我觉得用文字解释这个原型链有点绕嘴,没有上图方便,就直接看下面的图片吧!

-

原型链

+

下面将介绍其中的一些知名符号。

+

Symbol.hasInstance

每个函数都具有一个 Symbol.hasInstance 方法,用于判断指定对象是否为本函数的一个实例。这个方法定义在 Function.prototype 上,因此所有函数都继承了面对 instanceof 运算符时的默认行为。Symbol.hasInstance 属性自身是不可写入、不可配置、不可枚举的,从而保证它不会被错误地重写。

+

Symbol.hasInstance 方法只接受单个参数,即需要检测的值。如果该值是本函数的一个实例,则方法会返回 true。为了理解该方法是如何工作的,可研究下述代码:

+
obj instanceof Array
// 等价于
Array[Symbol.hasInstance](obj)
+

ES6 从本质上将 instanceof 运算符重定义为上述方法调用的简写语法,这样使用 instanceof 便会出发一次方法调用,实际上允许你改变该运算符的工作。

+

假设你想定义一个函数,使得任意对象都不会被判断为该函数的一个实例,你可以采用硬编码的方式来让该函数的 Symbol.hasInstance 方法始终返回 false,就像这样:

+
function MyObject () {
// ...
}

Object.defineProperty(MyObject, Symbol.hasInstance, {
value (v) {
return false
}
})

let obj = new MyObject()

// false
console.log(obj instanceof MyObject)
+

上例中通过 Object.defineProperty() 方法在 MyObject 对象上设置了 Symbol.hasInstance 属性,从而屏蔽了原型上不可写入的 Symbol.hasInstance 属性。

+

Symbol.isConcatSpreadable

首先请看下面数组 concat() 方法的例子:

+
let colors1 = [ 'red', 'green' ]
let colors2 = colors1.concat([ 'blue', 'black' ])
let colors3 = colors1.concat([ 'blue', 'black' ], 'brown')

// [ 'red', 'green', 'blue', 'black' ]
console.log(colors2)

// [ 'red', 'green', 'blue', 'black', 'brown' ]
console.log(colors3)
+

concat() 方法会区别对待自己接收到的参数,如果参数为数组类型,那么它会自动的将数组扁平化(即分离数组中的元素)。而其他非数组类型的参数无需如此处理。在 ES6 之前,没有任何手段可以改变这种行为。

+

Symbol.isConcatSpreadable 属性是一个布尔类型的属性,它默认情况下并不会作为任意常规对象的属性。它只出现在特定类型的对象上,用来标示该对象作为 concat() 参数时应如何工作。

+

成功使用这个属性的前提条件是拥有该属性的对象,要在两个方面与数组类似:拥有数值类型的键拥有 length 属性

+

当该属性为 true 时,将该属性所属对象传递给 concat() 方法时,将所属对象扁平化。当该属性为 false 时,所属对象不会被扁平化。请看下面的例子:

+
let obj1 = {
0: 'hello',
1: 'world',
length: 2,
[Symbol.isConcatSpreadable]: true
}

let messages1 = [ 'hi' ].concat(obj1)

// [ 'hi', 'hello', 'world' ]
console.log(messages1)

let obj2 = {
0: 'hello',
length: 2,
[Symbol.isConcatSpreadable]: false
}

let messages2 = [ 'hi' ].concat(obj2)

// [ 'hi', { '0': 'hello', length: 2, [Symbol.isConcatSpreadable]: false } ]
console.log(messages2)
+

Symbol.match、Symbol.replace、Symbol.search 与 Symbol.split

在 JS 中,字符串与正则表达式有着密切的联系,尤其是字符串具有几个可以接受正则表达式作为参数的方法:match、replace、search和split方法

+

在 ES6 之前这些方法的实现细节对开发者是隐藏的,使得开发者无法将自定义对象模拟成正则表达式(并将它们传递给字符串的这些方法)。而 ES6 定义了 4 个符号以及对应的方法,将原生行为外包到内置的 RegExp 对象上。

-
  • instanceof 操作符用于测试构造函数的 prototype 属性是否出现在对象的原型链中。
  • isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
  • 子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
  • 在通过原型链实现继承时,不能使用对象字面量语法重写原型。
+
  • Symbol.match:此函数接受一个字符串参数,并返回一个包含匹配结果的数组。若匹配失败,则返回 null
  • Symbol.replace:此函数接受一个字符串参数与一个替换用的字符串,并返回替换后的结果字符串。
  • Symbol.search:此函数接受一个字符串参数,并返回匹配结果的数值索引。若匹配失败,则返回 -1。
  • Symbol.split:此函数接受一个字符串参数,并返回一个用匹配值分割而成的字符串数组。
-

原型链的第一个问题类似于上面介绍的原型模式的问题,这里就不详细介绍了。
它的第二个问题是在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

-

借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数(通过 call()apply() 方法)。

-
function SuperType (name) {
this.name = name
}

function SubType () {
// 继承了SuperType,同时还传递了参数
SuperType.call(this, 'Nicholas')

// 实例属性
this.age = 29
}

var instance = new SubType()

// Nicholas
console.log(instance.name)

// 29
console.log(instance.age)
-

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题-方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

-

组合继承

组合继承有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

-
function SuperType (name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}

function SubType (name, age) {
// 继承属性
SuperType.call(this, name)

this.age = age
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function () {
console.log(this.age)
}

var instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')

// ["red", "blue", "green", "black"]
console.log(instance1.colors)

// Nicholas
instance1.sayName()

// 29
instance1.sayAge()

var instance2 = new SubType('Greg', 27)

// ["red", "blue", "green"]
console.log(instance2.colors)

// Greg
instance2.sayName()

// 27
instance2.sayAge()
-

两个实例上的 colors 属性屏蔽了原型链上的同名属性。

-

原型式继承

function object (o) {
function F () {}
F.prototype = o
return new F()
}
-

object() 函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质讲,object() 对传入其中的对象执行了一次浅复制。ES5 新增的 Object.create() 方法规范化了原型式继承。

-

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

-
function object (o) {
function F () {}
F.prototype = o
return new F()
}

function createAnother (original) {
// 通过调用函数创建一个新对象
var clone = object(original)

// 以某种方式来增强这个对象
clone.sayHi = function () {
console.log('hi')
}

// 返回这个对象
return clone
}

var person = {
name: 'Nicholas',
friengd: ['Shelby', 'Court', 'Van']
}
var anotherPerson = createAnother(person)

// hi
anotherPerson.sayHi()
-

寄生组合式继承

前面说过组合继承是 JavaScript 最常用的继承函数,不过它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

-
function SuperType (name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}

function SubType (name, age) {
// 第二次调用SuperType()
SuperType.call(this, name)

this.age = age
}

// 第一次调用SuperType()
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function () {
console.log(this.age)
}
-

为了解决上述问题,我们使用寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

-
function object (o) {
function F () {}
F.prototype = o
return new F()
}

function inheritPrototype (subType, superType) {
// 创建对象
var prototype = object(superType.prototype)

// 增强对象
prototype.constructor = subType

// 指定原型
subType.prototype = prototype
}

function SuperType (name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}

function SubType (name, age) {
SuperType.call(this, name)

this.age = age
}

inheritPrototype(SubType, SuperType)

SubType.prototype.sayAge = function () {
console.log(this.age)
}
-

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变。因此,还能够正常使用 instanceofisPrototypeOf()

-

小结

继承总结

-]]>
- - JavaScript - -
- - 运算符优先级 - /posts/9393c5c/ - 请移步 MDN

-]]>
+

在对象上定义这些属性,允许你创建能过进行模式匹配的对象,而无需使用这则表达式,并且允许在任何需要正则表达式的方法中使用该对象。这里有一个例子,展示了这些符号的使用:

+
// 有效等价于/^.{10}$/
let hasLengthOf10 = {
[Symbol.match] (value) {
return value.length === 10 ? [value.substring(0, 10)] : null
},
[Symbol.replace] (value, replacement) {
return value.length === 10 ?
replacement + value.substring(10) : value
},
[Symbol.search] (value) {
return value.length === 10 ? 0 : -1
},
[Symbol.split] (value) {
return value.length === 10 ? [ '', '' ] : [value]
}
}

// 11 characters
let message1 = 'Hello world'

// 10 characters
let message2 = 'Hello John'

let match1 = message1.match(hasLengthOf10)
let match2 = message2.match(hasLengthOf10)

// null
console.log(match1)

// [ 'Hello John' ]
console.log(match2)

let replace1 = message1.replace(hasLengthOf10, 'Howdy!')
let replace2 = message2.replace(hasLengthOf10, 'Howdy!')

// Hello world
console.log(replace1)

// Howdy!
console.log(replace2)

let search1 = message1.search(hasLengthOf10)
let search2 = message2.search(hasLengthOf10)

// -1
console.log(search1)

// 0
console.log(search2)

let split1 = message1.split(hasLengthOf10)
let split2 = message2.split(hasLengthOf10)

// [ 'Hello world' ]
console.log(split1)

// [ '', '' ]
console.log(split2)
+

Symbol.toPrimitive

Symbol.toPrimitive 方法被定义在所有常规类型的原型上,规定了在对象被转换为基本类型值的时候会发生什么。当需要转换时,Symbol.toPrimitive 会被调用,并按照规范传入一个提示性的字符串参数。该参数有 3 种可能:当参数值为 number 的时候,应当返回一个数值。当参数值为 string 的时候,应当返回一个字符串。而当参数为 default 的时候,对返回值类型没有特别要求。

+

对于大部分常规对象,“数值模式”依次会有下述行为:

+
+
  1. 调用 valueOf() 方法,如果方法返回值是一个基本类型值,那么返回它。
  2. 否则,调用 toString() 方法,如果方法返回值是一个基本类型值,那么返回它。
  3. 否则,抛出一个错误。
+
+

类似的,对于大部分常规对象,“字符串模式”依次会有下述行为:

+
+
  1. 调用 toString() 方法,如果方法返回值是一个基本类型值,那么返回它。
  2. 否则,调用 valueOf() 方法,如果方法返回值是一个基本类型值,那么返回它。
  3. 否则,抛出一个错误。
+
+

在多数情况下,常规对象的默认模式都等价于数值模式(只有 Date 类型例外,它默认使用字符串模式)。通过定义 Symbol.toPrimitive 方法,你可以重写这些默认的转换行为。

+

使用 Symbol.toPrimitive 属性并将一个函数赋值给它,便可以重写默认的转换行为,例如:

+
function Temperature (degrees) {
this.degrees = degrees
}

Temperature.prototype[Symbol.toPrimitive] = function (hint) {
switch (hint) {
case 'string':
return this.degrees + '\u00b0'
case 'number':
return this.degrees
case 'default':
return this.degrees + ' degrees'
}
}

let freezing = new Temperature(32)

// 32 degrees!
console.log(freezing + '!')

// 16
console.log(freezing / 2)

// 32°
console.log(String(freezing))
+

Symbol.toStringTag

JS 最有趣的课题之一是在多个不同的全局执行环境中使用,这种情况会在浏览器页面包含内联帧(iframe)的时候出现,此时页面与内联帧均拥有各自的全局执行环境。大多数情况下这并不是一个问题,使用一些轻量级的转换操作就能够在不同的运行环境之间传递数据。问题出现在想要识别目标对象到底是什么类型的时候,而此时该对象已经在环境之间经历了传递。

+

该问题的典型例子就是从内联帧向容器页面传递数组,或者反过来。在 ES6 术语中,内联帧与包含它的容器页面分别拥有一个不同的“域”,以作为 JS 的运行环境,每个“域”都拥有各自的全局作用域以及各自的全局对象拷贝。无论哪个“域”创建的数组都是正规的数组,但当它跨域进行传递时,使用 instanceof Array 进行检测却会得到 false 的结果,因为该数组是由另外一个“域”的数组构造器创建的,有别于当前“域”的数组构造器。

+

识别问题的变通解决方案

变通的解决方案为 Object.prototype.toString.call()

+

ES6 给出的答案

ES6 通过 Symbol.toStringTag 重定义了相关行为,该符号是对象的一个属性,定义了 Object.prototype.toString.call() 被调用时应当返回什么值。

+
function Person (name) {
this.name = name
}

Person.prototype[Symbol.toStringTag] = 'Person'

let me = new Person('Nicholas')

// [object Person]
console.log(me.toString())

// [object Person]
console.log(Object.prototype.toString.call(me))

Person.prototype.toString = function () {
return this.name
}

// Nicholas
console.log(me.toString())

// [object Person]
console.log(Object.prototype.toString.call(me))
+

Symbol.unscopables

尽管将来的代码无疑会停用 with 语句,但 ES6 仍然在非严格模式中提供了对于 with 语句的支持,以便向下兼容。为此需要寻找方法让使用 with 语句的代码能够适当地继续工作。为了理解这个任务的复杂性,可研究如下代码:

+
let values = [1, 2, 3]
let colors = ['red', 'green', 'blue']
let color = 'black'

with (colors) {
push(color)
push(...values)
}

// [ 'red', 'green', 'blue', 'black', 1, 2, 3 ]
console.log(colors)
+

在此例中,...values 引用了 with 语句之外的变量 values

+

但ES6为数组添加了一个 values 方法(迭代器与生成器的知识),这意味着在 ES6 的环境中,with 语句内部的 values 并不会指向 with 语句之外的变量 values,而是会指向数组的 values 方法,从而会破坏代码的意图。这也是 Symbol.unscopables 符号出现的理由。

+

Symbol.unscopables 符号在 Array.prototype 上使用,以指定哪些属性不允许在 with 语句内被绑定。Symbol.unscopables 属性是一个对象,当提供该属性时,它的键就是用于忽略 with 语句绑定的标识符,键值为 true 代表屏蔽绑定。以下是数组的 Symbol.unscopables 属性的默认值:

+
// 默认内置在 ES6 中
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
keys: true,
values: true
})
]]> JavaScript @@ -3493,70 +3484,12 @@
- Cookie - /posts/59cc01ec/ - 什么是 Cookie

对于一个网站来说,如果想要做到仅允许特定用户进行访问,或者为不同用户提供不同的内容,前提条件都是它能够识别当前用户。但由于 HTTP 是无状态协议,所以需要额外使用 Cookie 来辅助完成用户的识别。

-
-

Cookie 技术有四个组成部分:

  • HTTP 响应头中的一行 cookie 信息
  • HTTP 请求头中的一行 cookie 信息
  • 保存在用户终端的 cookie 文件
  • 服务端用于存储 cookie 数据库

Cookie 工作流程:

  1. 客户端发送 HTTP 请求到服务器
  2. 当服务器收到请求后,在响应头中添加一个 Set-Cookie 字段
  3. 浏览器收到响应后将 Cookie 保存下来
  4. 后续对该服务器的每一次请求,浏览器都会自动为其添加对应的 Cookie 请求头(安全政策允许的话)
-
-

Cookie 的属性

Name

cookie 的 Name 由 US-ASCII 字符组成,不允许包含:控制字符(CTLs)、空格、制表符(Tab)、()@,;:\"/[]?={}

-

虽然 RFC 并没有规定必须使用 URL encoding 对 Name 进行编码,但是编码可以保证 Name 中不会出现不符合规定的字符。

-

如果 Name 是以 __Secure- 为前缀,那么必须同时设置 Secure 属性。

-

如果 Name 是以 __Host- 为前缀,那么必须同时设置 Secure 属性、禁止设置 Domain 属性、Path 属性的值必须为 /

-

Value

cookie 的 Value 由 US-ASCII 字符组成,不允许包含:控制字符(CTLs)、空格、双引号、逗号、分号、反斜线。

-

虽然 RFC 并没有规定必须使用 URL encoding 对 Value 进行编码,但是编码可以保证 Value 中不会出现不符合规定的字符。

-

Domain

Domain 属性指定了那些域名可以接受这条 cookie。

-

如果不指定,那么默认值为当前文档访问地址中的域名,不包含子域名

-

如果指定了 Domain,则子域名也会包含在内。

-

因此,指定 Domain 比省略它的限制要少。通常当子域名需要共享有关用户信息时,会指定 Domain 属性。

-
-

stackoverflow: Share cookie between subdomain and domain
In RFC 2109, a domain without a leading dot meant that it could not be used on subdomains, and only a leading dot (.mydomain.com) would allow it to be used across multiple subdomains.

However, all modern browsers respect the newer specification RFC 6265, and will ignore any leading dot, meaning you can use the cookie on subdomains as well as the top-level domain.

-
-
-

stackoverflow: Domain set cookie for subdomain
The user agent will accept a cookie with a Domain attribute of example.com or of foo.example.com from foo.example.com, but the user agent will not accept a cookie with a Domain attribute of bar.example.com or of baz.foo.example.com from foo.example.com.

-
-

Path

Path 属性制定了一个 URL 路径片段,该路径片段必须出现在要请求的资源路径中才可以携带这条 cookie。假设 Path=/docs,那么 /docs/Web 会携带 cookie,/test 则不会携带 cookie。

-

Expires

cookie 的最长有效时间,形式为符合 HTTP-date 规范的时间戳。

-

如果没有设置这个属性,那么表示这是一个 会话期 cookie,当客户端关闭后,会话结束,会话期 cookie 会被浏览器移除。

-
-

很多浏览器支持会话恢复功能,这个功能可以使浏览器保留所有的tab标签,然后在重新打开浏览器的时候将其还原。与此同时,会话期 cookie 也会恢复,就跟从来没有关闭浏览器一样。

-
-

Max-Age

在 cookie 失效前需要经过的秒数。秒数为 0 或负数会使 cookie 直接过期。

-

如果同时设置了 ExpiresMax-AgeMax-Age 的优先级更高。

-

Secure

设置该属性后,除 localhost 外,仅使用 HTTPS 的请求才能使用 cookie。

-

HTTP 站点无法为 cookie 设置 Secure 属性(在 Chrome 52+ 和 Firefox 52+ 中新引入的限制)。

-

HttpOnly

设置该属性后,无法通过 document.cookie 访问到这条 cookie。通过该属性可以防止 XSS 攻击获取 cookie 信息。

-

SameSite

-

该属性用于控制在跨站请求时,是否允许携带这条 cookie。

  • Lax(默认值):跨站请求时不能携带这条 cookie,但如果是由外部站点导航的请求(如一个链接),可以携带这条 cookie。
  • Strict:跨站请求不能携带这条 cookie。
  • None:只有同时设置了 Secure 时,SameSite = None 才会生效,此时允许跨站请求携带这条 cookie。
-
-

如何设置 cookie

Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Path=<path-value>; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly; SameSite=Strict
-

通过 document.cookie 来访问/修改 cookie,该方法只能访问到没有设置 HttpOnly 的 cookie。

-

Cookie 的不足

    -
  • cookie 只能以纯文本的形式保存,任何人都有可能对其进行修改
  • -
  • 不同浏览器对 cookie 的限制不同,通常 cookie 文件总大小被限制在 4kb 以内,单个域名下的 cookie 个数被限制在 20 以内
  • -
  • 在跨站请求时,cookie 有可能被禁止使用,即不会在请求时在头部附加对应 cookie 信息
  • -
-

参考文献

-]]>
- - Network - -
- - HTTP Caching - /posts/8837602f/ - 我是 MDN 的搬运工。

+ 运算符优先级 + /posts/9393c5c/ + 请移步 MDN

]]>
- Network + JavaScript
@@ -3665,6 +3598,73 @@ JavaScript + + Cookie + /posts/59cc01ec/ + 什么是 Cookie

对于一个网站来说,如果想要做到仅允许特定用户进行访问,或者为不同用户提供不同的内容,前提条件都是它能够识别当前用户。但由于 HTTP 是无状态协议,所以需要额外使用 Cookie 来辅助完成用户的识别。

+
+

Cookie 技术有四个组成部分:

  • HTTP 响应头中的一行 cookie 信息
  • HTTP 请求头中的一行 cookie 信息
  • 保存在用户终端的 cookie 文件
  • 服务端用于存储 cookie 数据库

Cookie 工作流程:

  1. 客户端发送 HTTP 请求到服务器
  2. 当服务器收到请求后,在响应头中添加一个 Set-Cookie 字段
  3. 浏览器收到响应后将 Cookie 保存下来
  4. 后续对该服务器的每一次请求,浏览器都会自动为其添加对应的 Cookie 请求头(安全政策允许的话)
+
+

Cookie 的属性

Name

cookie 的 Name 由 US-ASCII 字符组成,不允许包含:控制字符(CTLs)、空格、制表符(Tab)、()@,;:\"/[]?={}

+

虽然 RFC 并没有规定必须使用 URL encoding 对 Name 进行编码,但是编码可以保证 Name 中不会出现不符合规定的字符。

+

如果 Name 是以 __Secure- 为前缀,那么必须同时设置 Secure 属性。

+

如果 Name 是以 __Host- 为前缀,那么必须同时设置 Secure 属性、禁止设置 Domain 属性、Path 属性的值必须为 /

+

Value

cookie 的 Value 由 US-ASCII 字符组成,不允许包含:控制字符(CTLs)、空格、双引号、逗号、分号、反斜线。

+

虽然 RFC 并没有规定必须使用 URL encoding 对 Value 进行编码,但是编码可以保证 Value 中不会出现不符合规定的字符。

+

Domain

Domain 属性指定了那些域名可以接受这条 cookie。

+

如果不指定,那么默认值为当前文档访问地址中的域名,不包含子域名

+

如果指定了 Domain,则子域名也会包含在内。

+

因此,指定 Domain 比省略它的限制要少。通常当子域名需要共享有关用户信息时,会指定 Domain 属性。

+
+

stackoverflow: Share cookie between subdomain and domain
In RFC 2109, a domain without a leading dot meant that it could not be used on subdomains, and only a leading dot (.mydomain.com) would allow it to be used across multiple subdomains.

However, all modern browsers respect the newer specification RFC 6265, and will ignore any leading dot, meaning you can use the cookie on subdomains as well as the top-level domain.

+
+
+

stackoverflow: Domain set cookie for subdomain
The user agent will accept a cookie with a Domain attribute of example.com or of foo.example.com from foo.example.com, but the user agent will not accept a cookie with a Domain attribute of bar.example.com or of baz.foo.example.com from foo.example.com.

+
+

Path

Path 属性制定了一个 URL 路径片段,该路径片段必须出现在要请求的资源路径中才可以携带这条 cookie。假设 Path=/docs,那么 /docs/Web 会携带 cookie,/test 则不会携带 cookie。

+

Expires

cookie 的最长有效时间,形式为符合 HTTP-date 规范的时间戳。

+

如果没有设置这个属性,那么表示这是一个 会话期 cookie,当客户端关闭后,会话结束,会话期 cookie 会被浏览器移除。

+
+

很多浏览器支持会话恢复功能,这个功能可以使浏览器保留所有的tab标签,然后在重新打开浏览器的时候将其还原。与此同时,会话期 cookie 也会恢复,就跟从来没有关闭浏览器一样。

+
+

Max-Age

在 cookie 失效前需要经过的秒数。秒数为 0 或负数会使 cookie 直接过期。

+

如果同时设置了 ExpiresMax-AgeMax-Age 的优先级更高。

+

Secure

设置该属性后,除 localhost 外,仅使用 HTTPS 的请求才能使用 cookie。

+

HTTP 站点无法为 cookie 设置 Secure 属性(在 Chrome 52+ 和 Firefox 52+ 中新引入的限制)。

+

HttpOnly

设置该属性后,无法通过 document.cookie 访问到这条 cookie。通过该属性可以防止 XSS 攻击获取 cookie 信息。

+

SameSite

+

该属性用于控制在跨站请求时,是否允许携带这条 cookie。

  • Lax(默认值):跨站请求时不能携带这条 cookie,但如果是由外部站点导航的请求(如一个链接),可以携带这条 cookie。
  • Strict:跨站请求不能携带这条 cookie。
  • None:只有同时设置了 Secure 时,SameSite = None 才会生效,此时允许跨站请求携带这条 cookie。
+
+

如何设置 cookie

Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Path=<path-value>; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly; SameSite=Strict
+

通过 document.cookie 来访问/修改 cookie,该方法只能访问到没有设置 HttpOnly 的 cookie。

+

Cookie 的不足

    +
  • cookie 只能以纯文本的形式保存,任何人都有可能对其进行修改
  • +
  • 不同浏览器对 cookie 的限制不同,通常 cookie 文件总大小被限制在 4kb 以内,单个域名下的 cookie 个数被限制在 20 以内
  • +
  • 在跨站请求时,cookie 有可能被禁止使用,即不会在请求时在头部附加对应 cookie 信息
  • +
+

参考文献

+]]>
+ + Network + +
+ + HTTP Caching + /posts/8837602f/ + 我是 MDN 的搬运工。

+]]>
+ + Network + +
HTTPS /posts/c9a2a716/ @@ -3804,18 +3804,6 @@ Network - - 如何读取命令行指令 - /posts/1fe87ebc/ - 读取命令行指令

两个链接,读完就懂了:How to parse command line argumentsyargs-parser

-

参考文献

-]]>
- - Node - -
Node Event Loop /posts/1bc69df4/ @@ -3834,6 +3822,18 @@
  • New Changes to the Timers and Microtasks in Node v11.0.0 (and above)
  • Event Loop Best Practices — NodeJS Event Loop Part 5
  • +]]> + + Node + +
    + + 如何读取命令行指令 + /posts/1fe87ebc/ + 读取命令行指令

    两个链接,读完就懂了:How to parse command line argumentsyargs-parser

    +

    参考文献

    ]]>
    Node @@ -3847,6 +3847,25 @@
    command = {
    _: [],
    c: true,
    config: true,
    };

    生成 options

    const { options } = await getConfigs(command);

    /**
    * 分界线
    **/

    async function getConfigs(
    command: any
    ): Promise<{ options: MergedRollupOptions[]; warnings: BatchWarnings }> {
    if (command.config) {
    // getConfigPath 在项目根目录下寻找 rollup.config.(mjs | cjs | ts | js),然后返回对应文件的绝对路径
    const configFile = await getConfigPath(command.config);
    const { options, warnings } = await loadAndParseConfigFile(
    configFile,
    command
    );
    return { options, warnings };
    }
    return await loadConfigFromCommand(command);
    }

    async function loadAndParseConfigFile(
    fileName: string,
    commandOptions: any = {}
    ): Promise<{ options: MergedRollupOptions[]; warnings: BatchWarnings }> {
    const configs = await loadConfigFile(fileName, commandOptions);
    const warnings = batchWarnings();
    try {
    const normalizedConfigs: MergedRollupOptions[] = [];
    for (const config of configs) {
    const options = mergeOptions(config, commandOptions, warnings.add);
    await addCommandPluginsToInputOptions(options, commandOptions);
    normalizedConfigs.push(options);
    }
    return { options: normalizedConfigs, warnings };
    } catch (err) {
    warnings.flush();
    throw err;
    }
    }

    根据 options 进行 build

    const { options, warnings } = await getConfigs(command);
    for (const inputOptions of options) {
    await build(inputOptions, warnings, command.silent);
    }

    /**
    * 分界线
    **/

    async function build(
    inputOptions: MergedRollupOptions,
    warnings: BatchWarnings,
    silent = false
    ): Promise<unknown> {
    const outputOptions = inputOptions.output;
    const useStdout = !outputOptions[0].file && !outputOptions[0].dir;

    const bundle = await rollup(inputOptions as any);

    if (useStdout) {
    await bundle.generate(output);

    return;
    }

    await Promise.all(outputOptions.map(bundle.write));
    await bundle.close();
    }
    +]]> + + FE tools + rollup 2.69.2 + +
    + + 如何在 rollup 源码中 debug + /posts/d9c5729d/ + 使项目可以本地运行

    因为 rollup 是使用 ts 编写的,所以需要下载 ts-node 来支持本地运行。过程中主要遇到两类问题:

    +
      +
    • SyntaxError: Cannot use import statement outside a module
    • +
    • ts 报错
    • +
    +

    第一个问题通过修改 tsconfig.json 来避免,第二个问题通过 @ts-ignore 来忽略。

    +

    修改 cli 文件

    修改 rollup cli 文件,然后本地运行 cli 文件的,是改动最小的方式。这里的主要改动是让 rollup 去读我们指定的 rollup.config.ts 文件,而不是去项目根目录下寻找。

    +

    打包配置

    在项目根目录下创建 learn 文件夹,里面保存着我们的 rollup.config.ts 和 build.ts。然后在 package.json 中添加对应的命令,就可以在源码中进行 debug 了!

    +

    debug

    具体修改内容,可参照 commit

    +

    如果觉得修改麻烦,可以直接在 learn 分支上进行 debug。

    ]]>
    FE tools @@ -3871,25 +3890,6 @@

    Graph.generateModuleGraph 方法会从入口文件开始,通过对源代码进行分析,从而梳理文件之间的依赖关系,逐一的为每个文件生成一个 Module 实例,然后登记在 graph.moduleLoader.modulesById 上。

    在对源代码进行分析时,就是通过 graph.acornParse 生成 ast 语法树,然后再基于 rollup 自己定义在 src/nodes/shared 下的节点生成对应的实例,保存在 module.ast 上。在这一系列动作之后,已经知道了当前源文件依赖哪些其他文件,然后就可以顺着依赖关系再对其他文件进行同样的解析。

    当然,在这一过程中,会在关键的节点调用 graph.pluginDriver 上的合适方法来抛出对应的钩子,触发用户声明要在编译过程中执行的 plugins。

    -]]> - - FE tools - rollup 2.69.2 - -
    - - 如何在 rollup 源码中 debug - /posts/d9c5729d/ - 使项目可以本地运行

    因为 rollup 是使用 ts 编写的,所以需要下载 ts-node 来支持本地运行。过程中主要遇到两类问题:

    -
      -
    • SyntaxError: Cannot use import statement outside a module
    • -
    • ts 报错
    • -
    -

    第一个问题通过修改 tsconfig.json 来避免,第二个问题通过 @ts-ignore 来忽略。

    -

    修改 cli 文件

    修改 rollup cli 文件,然后本地运行 cli 文件的,是改动最小的方式。这里的主要改动是让 rollup 去读我们指定的 rollup.config.ts 文件,而不是去项目根目录下寻找。

    -

    打包配置

    在项目根目录下创建 learn 文件夹,里面保存着我们的 rollup.config.ts 和 build.ts。然后在 package.json 中添加对应的命令,就可以在源码中进行 debug 了!

    -

    debug

    具体修改内容,可参照 commit

    -

    如果觉得修改麻烦,可以直接在 learn 分支上进行 debug。

    ]]>
    FE tools diff --git a/sitemap.xml b/sitemap.xml index e3af1306..2abc2ba4 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -4,7 +4,7 @@ https://aadonkeyz.com/posts/ef1ff28e/ - 2023-11-11T13:59:36.455Z + 2023-12-06T09:43:02.136Z @@ -401,7 +401,7 @@ - https://aadonkeyz.com/posts/9c2b83ad/ + https://aadonkeyz.com/posts/2317527/ 2021-01-23T15:29:57.113Z @@ -415,7 +415,7 @@ - https://aadonkeyz.com/posts/2317527/ + https://aadonkeyz.com/posts/9c2b83ad/ 2021-01-23T15:29:57.113Z @@ -478,14 +478,14 @@ - https://aadonkeyz.com/posts/9393c5c/ + https://aadonkeyz.com/posts/2798ec73/ 2020-12-12T23:39:43.989Z - https://aadonkeyz.com/posts/2798ec73/ + https://aadonkeyz.com/posts/9393c5c/ 2020-12-12T23:39:43.989Z @@ -562,28 +562,28 @@ - https://aadonkeyz.com/posts/22d36175/ + https://aadonkeyz.com/posts/45b9746d/ 2020-12-12T23:39:43.980Z - https://aadonkeyz.com/posts/45b9746d/ + https://aadonkeyz.com/posts/22d36175/ 2020-12-12T23:39:43.980Z - https://aadonkeyz.com/posts/1955d066/ + https://aadonkeyz.com/posts/3557a152/ 2020-12-12T23:39:43.980Z - https://aadonkeyz.com/posts/3557a152/ + https://aadonkeyz.com/posts/1955d066/ 2020-12-12T23:39:43.980Z