diff --git a/baidusitemap.xml b/baidusitemap.xml
index 8ac29b9a..dfea27c2 100644
--- a/baidusitemap.xml
+++ b/baidusitemap.xml
@@ -2,7 +2,7 @@
太多了,整理一会之后果断放弃,MDN上的介绍
-
+
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标签大全
HTML模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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
<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
:鼠标点击瞬间。首先介绍
addEventListener()
方法,它的参数如下:
type
:表示监听事件类型的字符串,需要注意的是没有 on
前缀。listener
:作为事件处理程序的函数。options(可选)
:一个对象。其属性如下:capture
:一个布尔值,默认为 false
。当值为 true
时,listener
会在事件捕获阶段时被调用。once
:一个布尔值,默认为 false
。当值为 true
时,listener
会在其被调用之后自动移除。passive
:一个布尔值,默认为 false
。当值为 true
时,listener
内部不允许调用 event.preventDefault()
,否则会抛出错误。useCapture(可选)
:一个布尔值,默认为 false
。当值为 true
时,listener
会在事件捕获阶段时被调用。对于 options
和 useCapture
参数,它们都是该方法的第三个参数,options
是新标准,而 useCapture
是老标准。
接着介绍 removeEventListener()
方法,它的参数如下:
type
:表示监听事件类型的字符串,需要注意的是没有 on
前缀。listener
:作为事件处理程序的函数。options(可选)
:一个对象。其属性如下:capture
:一个布尔值,默认为 false
。当值为 true
时,表示要移除的 listener
是注册在事件捕获阶段的。useCapture(可选)
:一个布尔值,默认为 false
。当值为 true
时,表示要移除的 listener
是注册在事件捕获阶段的。如果一个事件处理程序一共注册了两次,一次在事件捕获阶段,一次在事件冒泡阶段,那么这两次注册需要分别移除,两者不会互相干扰。
下面的例子用于观察 options.capture
和 useCapture
的效果。
1 |
|
1 |
|
上例中 captureListener1
和 captureListener2
都是注册在 outer
的捕获阶段,而 noneCaptureListener
和 innerListener
分别注册在 outer
和 inner
的冒泡阶段。并且 captureListener1
会在第一次调用后被移除。请多点击几次 inner 框,查看打印的结果。
在触发 DOM 上的某个事件时,会产生一个事件对象 event
,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。这里只介绍 DOM 中的事件对象,忽略 IE 的。
无论注册事件处理程序时使用的是 DOM0 级还是 DOM2 级方法,兼容 DOM 的浏览器都会将一个 event
对象传入到事件处理程序中,这样就可以直接在函数内部访问到 event
对象了。
由于 left 设置了margin-left: -100%
,当 content 内容区的宽度小于 left 的宽度时,此时 left 左移的距离小于 left 自身的宽度,导致 left 并不会向上移动到与 middle 同层。简言之,布局乱了。
1 | <div class="content"> |
1 | <div class="content"> |
圣杯布局的升级版,修改 DOM 结构,然后使用margin
代替padding
。
1 | <div class="content"> |
1 | <div class="content"> |
React 的示例代码如下所示:
-1 | import React, { useState, useRef, useEffect } from 'react'; |
1 | import React, { useState, useRef, useEffect } from 'react'; |
U+<hex>
是码位的格式,其中 U+
代表 Unicode 的前缀,而 <hex>
表示十六进制数字。每一个字符都有对应的码位,如 A
对应 U+0041
。但是并非所有码位都有关联字符。
平面是从 U+n0000
到 U+nFFFF
的 65536
个连续的码位,n
的取值范围从 0
到 16
。平面将全部的 Unicode 码位分成了 17
个均等的组。
基本多文种平面为 U+0000
到 U+FFFF
,简称 BMP(Basic Multilingual Plane)。其余 16
个平面称为星光平面或辅助平面。
码元是用于以给定编码格式,对每个字符编码的一个二进制位序列。在不同的编码格式中,一个码元有多少字节是不同的。
UTF-8 中一个码元可能有 1 - 4
个字节。UTF-16 中一个码元有 2
个字节。UTF-32 中一个码元有 4
个字节。
在 UTF-16 中代理对是对那些由 2
个 16
位长的码元所组成序列的单个抽象字符的表示方式。代理对中的首个值为高位代理码元,而第二个值为低位代理码元。高位代理码元从 0xD800
到 0xDBFF
取值。低位代理码元从 0xDC00
到 0xDFFF
取值。
在 UTF-16 中,对于基本多文种平面,一个 16
位长的码元就可以解决了。但是对星光平面内的码元,如 U+1F600
,需要两个 16
位长的码元才能将其表示,这就是代理对。U+1F600
对应的代理对是 0xD83D 0xDE00
。转换方法如下所示。
function getSurrogatePair(astralCodePoint) { |
字位又称形素、字素、或符号,对一些书写系统来说是最小的构成单位。
+在绝大多数情况下,单个 Unicode 字符表示单个的字位。但也存在单个字位包含一系列字符的情况。例如 å 在丹麦书写系统中是一个原子性的字位,展示它需要使用 U+0061
和 U+030A
组合在一起。
组合字符会被用户认为是单个字符,但开发者必须使用 2
个码位来表示它。
每个 JavaScript 开发者都应该了解的 Unicode
What every JavaScript developer should know about Unicode
U+<hex>
是码位的格式,其中 U+
代表 Unicode 的前缀,而 <hex>
表示十六进制数字。每一个字符都有对应的码位,如 A
对应 U+0041
。但是并非所有码位都有关联字符。
平面是从 U+n0000
到 U+nFFFF
的 65536
个连续的码位,n
的取值范围从 0
到 16
。平面将全部的 Unicode 码位分成了 17
个均等的组。
基本多文种平面为 U+0000
到 U+FFFF
,简称 BMP(Basic Multilingual Plane)。其余 16
个平面称为星光平面或辅助平面。
码元是用于以给定编码格式,对每个字符编码的一个二进制位序列。在不同的编码格式中,一个码元有多少字节是不同的。
UTF-8 中一个码元可能有 1 - 4
个字节。UTF-16 中一个码元有 2
个字节。UTF-32 中一个码元有 4
个字节。
在 UTF-16 中代理对是对那些由 2
个 16
位长的码元所组成序列的单个抽象字符的表示方式。代理对中的首个值为高位代理码元,而第二个值为低位代理码元。高位代理码元从 0xD800
到 0xDBFF
取值。低位代理码元从 0xDC00
到 0xDFFF
取值。
Singleton
- 定义了一个Instance
操作,允许客户访问它的唯一实例。Instance
是一个类操作(即它是一个静态成员方法)。 - 可能负责创建它自己的唯一实例。在 UTF-16 中,对于基本多文种平面,一个 16
位长的码元就可以解决了。但是对星光平面内的码元,如 U+1F600
,需要两个 16
位长的码元才能将其表示,这就是代理对。U+1F600
对应的代理对是 0xD83D 0xDE00
。转换方法如下所示。
function getSurrogatePair(astralCodePoint) { |
字位又称形素、字素、或符号,对一些书写系统来说是最小的构成单位。
+基础概念中介绍的是单例模式的原理,而这个 demo 中展示的是使用代理来实现透明单例模式的例子,它们有些许不同。
在绝大多数情况下,单个 Unicode 字符表示单个的字位。但也存在单个字位包含一系列字符的情况。例如 å 在丹麦书写系统中是一个原子性的字位,展示它需要使用 U+0061
和 U+030A
组合在一起。
组合字符会被用户认为是单个字符,但开发者必须使用 2
个码位来表示它。
每个 JavaScript 开发者都应该了解的 Unicode
What every JavaScript developer should know about Unicode
class CreateSingleton { |
Singleton
- 定义了一个Instance
操作,允许客户访问它的唯一实例。Instance
是一个类操作(即它是一个静态成员方法)。 - 可能负责创建它自己的唯一实例。基础概念中介绍的是单例模式的原理,而这个 demo 中展示的是使用代理来实现透明单例模式的例子,它们有些许不同。
-class CreateSingleton { |
Creator
类是一个抽象类并且不提供它所声明的工厂模式的实现。Creator
类是一个具体的类而且为工厂模式提供一个缺省的实现。Creator
类是一个抽象的类,但为工厂模式提供一个缺省的实现。class Product1 { |
Component
:ConcreteComponent
和Decorator
的父类 - 定义一个对象接口,可以给这些对象动态地添加职责。 - ConcreteComponent
- 定义一个对象,可以给这个对象添加一些职责。 - Decorator
- 维持一个指向Component
对象的指针。 - 定义一个与Component
接口一致的接口。 - ConcreteDecorator
- 实现添加装饰的接口。Decorator
类:当你仅需要添加一个装饰时,没有必要定义抽象Decorator
类。Component
类的简单性:如果有Component
类,保持这个类的简单性是很重要的,即它应集中于定义接口而不是存储数据。Decorator
看作一个对象的外壳,它可以改变这个对象的行为。另外一种方法是改变对象的内核(通过策略模式)。当需要改变的对象比较庞大时,推荐使用策略模式而不是装饰者模式。class Plane { |
Component
:ConcreteComponent
和Decorator
的父类 - 定义一个对象接口,可以给这些对象动态地添加职责。 - ConcreteComponent
- 定义一个对象,可以给这个对象添加一些职责。 - Decorator
- 维持一个指向Component
对象的指针。 - 定义一个与Component
接口一致的接口。 - ConcreteDecorator
- 实现添加装饰的接口。Decorator
类:当你仅需要添加一个装饰时,没有必要定义抽象Decorator
类。Component
类的简单性:如果有Component
类,保持这个类的简单性是很重要的,即它应集中于定义接口而不是存储数据。Decorator
看作一个对象的外壳,它可以改变这个对象的行为。另外一种方法是改变对象的内核(通过策略模式)。当需要改变的对象比较庞大时,推荐使用策略模式而不是装饰者模式。class Plane { |
排序算法可以分为以下两种类型:
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
n/2 => n/4 => ··· => 1
进行循环。每次循环中根据步长对元素列进行分组,然后对这些分组执行插入排序。let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
i = left + 1, j = right - 2
。i++
操作,直到i
所处元素大于等于枢纽元素为止。j--
操作,直到j
所处元素小于等于枢纽元素为止。i < j
,那么交换这两个位置上的元素,然后重复执行步骤 4、5。i >= j
,那么此刻位置i
上的元素一定是大于等于枢纽元素的,此时交换位置i
上的元素和枢纽元素的位置。交换之后,枢纽元素左侧的元素均小于等于它,而它右侧的元素均大于等于它。array[i]=array[j]=pivot
的情况,不要陷入死循环。i
和j
停下来,并交换位置?let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
let list = [90, 84, 83, 88, 87, 61, 50, 70, 60, 80]; |
let list = [2, 5, 3, 0, 2, 3, 0, 3]; |
let list = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]; |
let list = [329, 457, 657, 839, 436, 720, 355, 11, 20, 4]; |
排序算法可以分为以下两种类型:
在计算机科学中,认为$lgn = logn = log_2n$
+let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
详情查看斯特林近似公式
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
n/2 => n/4 => ··· => 1
进行循环。每次循环中根据步长对元素列进行分组,然后对这些分组执行插入排序。主定理的证明请看《算法导论》第三版的 4.6 节。
+let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
令 $a \geq 1$ 和 $b > 1$是常数,$f(n)$ 是一个函数,$T(n)$ 是定义在非负整数上的递归式:
let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
i = left + 1, j = right - 2
。i++
操作,直到i
所处元素大于等于枢纽元素为止。j--
操作,直到j
所处元素小于等于枢纽元素为止。i < j
,那么交换这两个位置上的元素,然后重复执行步骤 4、5。i >= j
,那么此刻位置i
上的元素一定是大于等于枢纽元素的,此时交换位置i
上的元素和枢纽元素的位置。交换之后,枢纽元素左侧的元素均小于等于它,而它右侧的元素均大于等于它。对于三种情况的每一种,我们将函数 $f(n)$ 与函数 $n^{log_ba}$ 进行比较。直觉上,两个函数较大者决定了递归式的解。
在此直觉之外,我们需要了解一些技术细节。在第一种情况中,不是 $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
的情况,不要陷入死循环。i
和j
停下来,并交换位置?let list = [84, 83, 88, 87, 61, 50, 70, 60, 80]; |
let list = [90, 84, 83, 88, 87, 61, 50, 70, 60, 80]; |
let list = [2, 5, 3, 0, 2, 3, 0, 3]; |
let list = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]; |
let list = [329, 457, 657, 839, 436, 720, 355, 11, 20, 4]; |
项目需要读取本地的一个文件,采用的方式是挂载本地目录作为volume
。但是在将这个项目部署到远端服务器时,项目image
可以直接拉取,而项目所需文件的迁移就成了问题
COPY
数据的image
,起名为data-image:1.0
docker-compose.yml
文件,通过挂载volume
的方式实现数据共享# Dockerfile |
# docker-compose.yml |
volume
可以做到数据持久化,image
内的数据是无法被更改的project-data
中,而两个image
中/usr/src/data
目录下的内容会一直保持它们最开始的状态project-data
会被挂载到两个container
上,实现数据的共享,这个时候两个container
中/usr/src/data
目录下的内容与project-data
同步project-data
,则对依赖文件的改动会丢失registry-data
的volume,我想备份它。
-思路如下:
/d/Docker/backup
,backup
就是我保存备份的文件夹;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。
不是我懒惰,是我怕翻译错
so
在计算机科学中,认为$lgn = logn = log_2n$
+详情查看斯特林近似公式
主定理的证明请看《算法导论》第三版的 4.6 节。
+令 $a \geq 1$ 和 $b > 1$是常数,$f(n)$ 是一个函数,$T(n)$ 是定义在非负整数上的递归式:
对于三种情况的每一种,我们将函数 $f(n)$ 与函数 $n^{log_ba}$ 进行比较。直觉上,两个函数较大者决定了递归式的解。
在此直觉之外,我们需要了解一些技术细节。在第一种情况中,不是 $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 之间也有一定间隙。这样的情况下就不能使用主定理来求解递归式了。
在利用 DOM 和 CSSOM 合成 render tree 的时候,需要将样式表中的每一条 CSS 样式规则与对应的 DOM 元素关联起来。然而实际中,样式规则可能数量很大,但是绝大多数不会匹配到任何 DOM 元素上,所以有一个快速的方法来判断 CSS 选择器是否具有匹配的 DOM 元素是极其重要的。
-以下面的例子讲解CSS 选择器的解析规则:
-<div> |
如果采用从左至右的规则,过程如下所示:
<div>
。<div>
内寻找所有 class=jartto
的子 <div>
。<div>
内接着按 CSS 选择器进行寻找,直到最后。这样的搜索过程对于一个只是匹配很少节点的选择器来说,效率是极低的,因为我们花费了大量的时间在回溯匹配不符合规则的节点。
-如果采用从右至左的规则,过程如下所示:
class=yellow
的 <span>
。<span>
的父元素是否为 <p>
<p>
的父元素是否为 class=jartto
的 <div>
。<div>
的父元素是否也是 <div>
。因为每一个元素都只拥有一个父元素,所以从右至左的解析 CSS 选择器可以有效减少无效匹配的次数,从而使匹配更快、性能更优。
-render tree 生成时,元素的 computedStyle 是经过层叠计算后得到的。在某些特定的情况下,浏览器会让不同元素之间共享它们的 computedStyle。也就是说,如果多个元素的 computedStyle 不通过计算就可以确认它们相等,那么这个 computedStyle 只会被计算一次,从而提高了性能。
-只要元素之间满足以下条件,它们之间就可以共享 computedStyle。
+registry-data
的volume,我想备份它。
id
属性。class
属性必须相同。style
属性,哪怕是这些元素的 style
属性值相同也不可以。first-child
、:last-selector
、+ selector
。思路如下:
/d/Docker/backup
,backup
就是我保存备份的文件夹;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。
项目需要读取本地的一个文件,采用的方式是挂载本地目录作为volume
。但是在将这个项目部署到远端服务器时,项目image
可以直接拉取,而项目所需文件的迁移就成了问题
COPY
数据的image
,起名为data-image:1.0
docker-compose.yml
文件,通过挂载volume
的方式实现数据共享# Dockerfile |
# docker-compose.yml |
volume
可以做到数据持久化,image
内的数据是无法被更改的project-data
中,而两个image
中/usr/src/data
目录下的内容会一直保持它们最开始的状态project-data
会被挂载到两个container
上,实现数据的共享,这个时候两个container
中/usr/src/data
目录下的内容与project-data
同步project-data
,则对依赖文件的改动会丢失不是我懒惰,是我怕翻译错
so
https://aadonkeyz.com/posts/eb815672/
。location.hash = '#hash'
后,生成一条新的浏览记录,其 URL 为 https://aadonkeyz.com/posts/eb815672/#hash
,并将这条新的记录加入到浏览器历史记录的栈顶。与此同时浏览器的浏览状态会跳转到这个最新的记录上。location.hash
改变时,触发了 window
对象上注册的 onhashchange
事件处理程序。onhashchange
事件处理程序,页面内容会再次进行相应的更新。在利用 DOM 和 CSSOM 合成 render tree 的时候,需要将样式表中的每一条 CSS 样式规则与对应的 DOM 元素关联起来。然而实际中,样式规则可能数量很大,但是绝大多数不会匹配到任何 DOM 元素上,所以有一个快速的方法来判断 CSS 选择器是否具有匹配的 DOM 元素是极其重要的。
+以下面的例子讲解CSS 选择器的解析规则:
+<div> |
如果采用从左至右的规则,过程如下所示:
<div>
。<div>
内寻找所有 class=jartto
的子 <div>
。<div>
内接着按 CSS 选择器进行寻找,直到最后。这样的搜索过程对于一个只是匹配很少节点的选择器来说,效率是极低的,因为我们花费了大量的时间在回溯匹配不符合规则的节点。
在 URL 中,#
以及它之后的内容是用于在页面内定位的锚点,它们的改变不会引起浏览器发送网络请求。
如果采用从右至左的规则,过程如下所示:
class=yellow
的 <span>
。<span>
的父元素是否为 <p>
<p>
的父元素是否为 class=jartto
的 <div>
。<div>
的父元素是否也是 <div>
。因为每一个元素都只拥有一个父元素,所以从右至左的解析 CSS 选择器可以有效减少无效匹配的次数,从而使匹配更快、性能更优。
https://aadonkeyz.com
。history.pushState()
或者 history.replaceState()
新增/替换浏览器历史记录的栈顶记录,浏览器的浏览状态跳转到栈顶记录上,此时 URL 为 https://aadonkeyz.com/posts/eb815672
。通过这两个方法,不会触发任何事件,也不会引起浏览器的加载行为。history.pushState()
或者 history.replaceState()
生成的浏览记录时,会触发 window
对象上注册的 onpopstate
事件处理程序,并且在事件处理程序的内部,event.state
中保存着该浏览记录的状态对象的拷贝。因此可以根据这个 event.state
更新页面。render tree 生成时,元素的 computedStyle 是经过层叠计算后得到的。在某些特定的情况下,浏览器会让不同元素之间共享它们的 computedStyle。也就是说,如果多个元素的 computedStyle 不通过计算就可以确认它们相等,那么这个 computedStyle 只会被计算一次,从而提高了性能。
+只要元素之间满足以下条件,它们之间就可以共享 computedStyle。
+id
属性。class
属性必须相同。style
属性,哪怕是这些元素的 style
属性值相同也不可以。first-child
、:last-selector
、+ selector
。采用这种方式时,为了防止浏览器真的去加载对应的 URL,从而返回 404 Not Found
的尴尬局面,需要服务器的支持,如果 URL 匹配不到任何静态资源,则应该返回根页面 HTML。
「前端进阶」彻底弄懂前端路由 中对前端路由进行了简单的实现,感兴趣的可以阅读一下。如果想更细致的了解前端路由的实现方式,可以阅读 react-router 或者 vue-router 的源码。
React 的示例代码如下所示:
-import React, { useState, useRef, useEffect } from 'react'; |
事件冒泡是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的 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 特性,可以通过这个特性来注册事件处理程序。下面以 onclick
事件为例:
// test.js |
|
在 HTML 中注册事件处理程序,会创建一个封装着元素属性值的函数。这个函数中有一个局部变量 event
,也就是事件对象(后面将会讨论这个概念),通过 event
变量可以直接访问事件对象。并且在这个函数内部,this
值等于事件的目标元素。所以你可以将 event
和 this
当做参数,传递给要调用的函数。关于这一点,你可以查看上面例子中 showMessage()
函数打印的内容来验证。
HTML 事件处理程序的缺点:
showMessage()
和 showAnother()
之前就点击了对应的按钮,会抛出错误。可以使用 onclick="try {showMessage()} catch(ex) {}"
的形式来解决这个问题。this
指向元素自身。this
的丢失。在添加的事件处理程序函数内部,可以直接通过 event
变量访问事件对象。也可以通过给程序处理函数定义参数或者使用 arguments
来访问事件对象,在下面有例子。
以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。你可以通过将事件处理程序属性的值设置为 null
来删除添加的事件处理程序。
来看一个例子:
-
|
这个例子中的按钮,只有第一次点击时会打印内容,之后就没有任何反应,因为事件处理程序在第一次触发之后,就被删除了。
-DOM2 级事件定义了两个方法,addEventListener()
和 removeEventListener()
,分别用于注册和删除事件处理程序,所有 DOM 节点都包含这两个方法。
addEventListener()
可以在同一个元素上注册多个事件处理程序,触发的顺序为添加顺序。this
和 event
的使用规则,与 DOM0 级事件处理程序一致。首先介绍 addEventListener()
方法,它的参数如下:
type
:表示监听事件类型的字符串,需要注意的是没有 on
前缀。listener
:作为事件处理程序的函数。options(可选)
:一个对象。其属性如下:capture
:一个布尔值,默认为 false
。当值为 true
时,listener
会在事件捕获阶段时被调用。once
:一个布尔值,默认为 false
。当值为 true
时,listener
会在其被调用之后自动移除。passive
:一个布尔值,默认为 false
。当值为 true
时,listener
内部不允许调用 event.preventDefault()
,否则会抛出错误。useCapture(可选)
:一个布尔值,默认为 false
。当值为 true
时,listener
会在事件捕获阶段时被调用。对于 options
和 useCapture
参数,它们都是该方法的第三个参数,options
是新标准,而 useCapture
是老标准。
接着介绍 removeEventListener()
方法,它的参数如下:
type
:表示监听事件类型的字符串,需要注意的是没有 on
前缀。listener
:作为事件处理程序的函数。options(可选)
:一个对象。其属性如下:capture
:一个布尔值,默认为 false
。当值为 true
时,表示要移除的 listener
是注册在事件捕获阶段的。useCapture(可选)
:一个布尔值,默认为 false
。当值为 true
时,表示要移除的 listener
是注册在事件捕获阶段的。如果一个事件处理程序一共注册了两次,一次在事件捕获阶段,一次在事件冒泡阶段,那么这两次注册需要分别移除,两者不会互相干扰。
-下面的例子用于观察 options.capture
和 useCapture
的效果。
|
上例中 captureListener1
和 captureListener2
都是注册在 outer
的捕获阶段,而 noneCaptureListener
和 innerListener
分别注册在 outer
和 inner
的冒泡阶段。并且 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
上的事件处理程序被调用。下面我只简单的介绍一下我认为比较常用的事件,如果你想比较全面的了解这里,点击下面的链接!
-在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。
-对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document
。也就是说,我们可以为整个页面指定一个 onclick
事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。
每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”,也是造成 Web 应用程序内存与性能问题的主要原因。
-在两种情况下,可能会造成上述问题。第一种情况就是从文档中移除带有事件处理程序的元素时。这可能是通过纯粹的 DOM 操作,例如使用 removeChild()
方法,但更多地是发生在使用 innerHTML
替换页面中某一部分的时候。如果带有事件处理程序的元素被 innerHTML
删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。所以如果你知道某个元素即将被移除,那么最好在此之前手工移除事件处理程序。
另一种情况,就是卸载页面的时候。如果在页面被卸载之前没有清理干净事件处理程序,那它们就会滞留在内存中。每次加载完页面在卸载页面时(可能是在两个页面间来回切换,也可能是单击了“刷新”按钮),内存中滞留的对象数目就会增加,因为事件处理程序占用的内存并没有被释放。因此最好的做法就是在页面卸载之前,先通过 onunload
事件处理程序移除所有事件处理程序。
容器的高度是给定的,且通过设置 overflow: auto
让它的子元素可以滚动。
列表只设置一个 height
属性即可,它的高度由列表项高度和列表项个数决定,它存在的意义是使得滚动条可以正确显示。
列表项分为真实渲染和虚拟渲染两种,真实渲染的列表项需要被包裹起来,然后对包裹层设置 transform: translateY(container's scrollTop)
。如果不这么做,以第二个图为例,会导致第三个真实渲染列表项出现在第一个虚拟渲染列表项的位置。
网络请求跨域的解决方案有:
https://aadonkeyz.com/posts/eb815672/
。location.hash = '#hash'
后,生成一条新的浏览记录,其 URL 为 https://aadonkeyz.com/posts/eb815672/#hash
,并将这条新的记录加入到浏览器历史记录的栈顶。与此同时浏览器的浏览状态会跳转到这个最新的记录上。location.hash
改变时,触发了 window
对象上注册的 onhashchange
事件处理程序。onhashchange
事件处理程序,页面内容会再次进行相应的更新。如果要进行通信的窗口之间主域相同,子域不同,则可以直接将它们的 document.domain
修改为主域,这样就不存在跨域问题了。
在 URL 中,#
以及它之后的内容是用于在页面内定位的锚点,它们的改变不会引起浏览器发送网络请求。
<iframe name="a" src="http://a.domain.com"></iframe> |
<!-- domain 初始值为 a.domain.com --> |
<!-- domain 初始值为 b.domain.com --> |
父窗口 A 修改子窗口 B 的 location.hash
值,会触发对应子窗口 B window
对象的 onhashchange
事件处理程序。这个过程是单向的,但是你可以通过在子窗口 B 中添加一个 iframe
标签,这个 iframe
的 src
域名与父窗口域名相同的,然后在这个 iframe
页面内调用父窗口 A 的函数,此时是不存在跨域的。
https://aadonkeyz.com
。history.pushState()
或者 history.replaceState()
新增/替换浏览器历史记录的栈顶记录,浏览器的浏览状态跳转到栈顶记录上,此时 URL 为 https://aadonkeyz.com/posts/eb815672
。通过这两个方法,不会触发任何事件,也不会引起浏览器的加载行为。history.pushState()
或者 history.replaceState()
生成的浏览记录时,会触发 window
对象上注册的 onpopstate
事件处理程序,并且在事件处理程序的内部,event.state
中保存着该浏览记录的状态对象的拷贝。因此可以根据这个 event.state
更新页面。window
对象的 name
属性有一个独特之处,一个窗口只要一直存在并且没有被主动的更改 name
属性,那么它的 name
属性会一直保持不变。即窗口的 URL(或 iframe
的 src
)随意变化,name
属性也不会变。name
属性内存最大可以支持 2MB,所以可以通过它来传递数据。
采用这种方式时,为了防止浏览器真的去加载对应的 URL,从而返回 404 Not Found
的尴尬局面,需要服务器的支持,如果 URL 匹配不到任何静态资源,则应该返回根页面 HTML。
通过 window.postMessage
方法和 message 事件完成数据的传递。
「前端进阶」彻底弄懂前端路由 中对前端路由进行了简单的实现,感兴趣的可以阅读一下。如果想更细致的了解前端路由的实现方式,可以阅读 react-router 或者 vue-router 的源码。
+React 的示例代码如下所示:
+import React, { useState, useRef, useEffect } from 'react'; |
容器的高度是给定的,且通过设置 overflow: auto
让它的子元素可以滚动。
列表只设置一个 height
属性即可,它的高度由列表项高度和列表项个数决定,它存在的意义是使得滚动条可以正确显示。
列表项分为真实渲染和虚拟渲染两种,真实渲染的列表项需要被包裹起来,然后对包裹层设置 transform: translateY(container's scrollTop)
。如果不这么做,以第二个图为例,会导致第三个真实渲染列表项出现在第一个虚拟渲染列表项的位置。
如果要进行通信的窗口之间主域相同,子域不同,则可以直接将它们的 document.domain
修改为主域,这样就不存在跨域问题了。
<iframe name="a" src="http://a.domain.com"></iframe> |
<!-- domain 初始值为 a.domain.com --> |
<!-- domain 初始值为 b.domain.com --> |
父窗口 A 修改子窗口 B 的 location.hash
值,会触发对应子窗口 B window
对象的 onhashchange
事件处理程序。这个过程是单向的,但是你可以通过在子窗口 B 中添加一个 iframe
标签,这个 iframe
的 src
域名与父窗口域名相同的,然后在这个 iframe
页面内调用父窗口 A 的函数,此时是不存在跨域的。
window
对象的 name
属性有一个独特之处,一个窗口只要一直存在并且没有被主动的更改 name
属性,那么它的 name
属性会一直保持不变。即窗口的 URL(或 iframe
的 src
)随意变化,name
属性也不会变。name
属性内存最大可以支持 2MB,所以可以通过它来传递数据。
通过 window.postMessage
方法和 message 事件完成数据的传递。
在事件被触发时,设定 n 秒后执行对应的回调函数。如果在 n 秒之内事件被重复触发,则重新计时。如果连续触发事件,对应的回调函数可能永远不会被调用。
+function debounce (fn, timeRange) { |
一段时间周期内,最多只执行一次回调函数。
+function throttle (fn, timeRange) { |
在实际工作中给你,往往都是直接引用 lodash,而 lodash 的 debounce 和 throttle 的规则是可以在进行适当配置的。
]]>在事件被触发时,设定 n 秒后执行对应的回调函数。如果在 n 秒之内事件被重复触发,则重新计时。如果连续触发事件,对应的回调函数可能永远不会被调用。
+# create a branch |
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) { |
一段时间周期内,最多只执行一次回调函数。
+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.
$ git checkout -b bugFix |
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.
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.
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) { |
在实际工作中给你,往往都是直接引用 lodash,而 lodash 的 debounce 和 throttle 的规则是可以在进行适当配置的。
-]]> + +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 |
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.
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.
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 |
At this point, you can go back to the master
branch and do a fast-forward merge.
$ git checkout master |
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!
# create a branch |
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.
$ git checkout -b bugFix |
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.
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.
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.
太多了,整理一会之后果断放弃,MDN上的介绍
+
|
<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
:鼠标点击瞬间。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 |
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.
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>
标签用于接受来自用户的数据。它有如下属性:
autocomplete
:一个字符串,描述输入应提供的任何类型的自动完成功能。自动完成的典型实现只是回忆在同一输入字段中输入的先前值,但可以存在更复杂的自动完成形式。例如,浏览器可以与设备的联系人列表集成,以在电子邮件输入字段中自动完成电子邮件地址。不过当type
属性值为button
、file
等时,<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>
的类型。包括:button
、checkbox
、color
、date
、datetime-local
、email
、file
、hidden
、image
、month
、number
、password
、radio
、range
、reset
、search
、submit
、tel
、text
、time
、url
、week
。value
:该<input>
的当前值。In addition to merge
, if you want to integrate changes from experiment
into master
, there is another way.
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 |
At this point, you can go back to the master
branch and do a fast-forward merge.
$ git checkout master |
<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!
width: 100%
,而 left 和 right 的width
属性值分别为 content 的左右内边距。margin-left
,由于 middle 也是浮动元素,所以这样会使 left 和 right 向上层浮动。position: relative
,分别将 left 和 right 向左右两侧拉扯。由于 left 设置了margin-left: -100%
,当 content 内容区的宽度小于 left 的宽度时,此时 left 左移的距离小于 left 自身的宽度,导致 left 并不会向上移动到与 middle 同层。简言之,布局乱了。
width
与height
的情况是一致的,下面以width
为例进行说明。
<div class="content"> |
圣杯布局的升级版,修改 DOM 结构,然后使用margin
代替padding
。
min-width
的优先级高于width
,即使有!important
也是如此。
<div></div> |
min-width
的优先级高于max-width
,即使有!important
也是如此。
<div></div> |
max-width
的优先级高于width
,即使有!important
也是如此。
<div class="content"> |
<div></div> |
all
:所有媒体braile
:盲文触觉设备embossed
:盲文打印机handheld
:手持设备print
:打印预览或打印机projection
:项目演示screen
:彩屏设备speech
:听觉类似的媒体tty
:不适用像素的设备,如电传打字机tv
:电视width: 100%
,而 left 和 right 的width
属性值分别为 content 的左右内边距。margin-left
,由于 middle 也是浮动元素,所以这样会使 left 和 right 向上层浮动。position: relative
,分别将 left 和 right 向左右两侧拉扯。以下特性,除scan
和grid
外,都可以加上min
或max
前缀以指定范围
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
属性中指定媒体查询是CSS2的方式
<link rel="stylesheet" type="text/css" media="screen" href="screenstyles.css" /> |
@media
是在样式表中使用的媒体查询方式
@media screen { ... } |
@import
可以根据媒体查询将其他样式表加载到当前样式表中
@import url("phone.css") screen |
and
:类似于逻辑与,
:类似于逻辑或not
:用于对整个媒体查询取反only
:指定某种特定的媒体类型,用来对那些不支持媒体特性但支持媒体类型的设备隐藏样式表<div class="content"> |
圣杯布局的升级版,修改 DOM 结构,然后使用margin
代替padding
。
/* 当设备是彩屏设备并且视口最小宽度大于等于400px时生效 */ |
<div class="content"> |
太多了,整理一会之后果断放弃,MDN上的介绍
-
|
<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
:鼠标点击瞬间。事件冒泡是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的 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 特性,可以通过这个特性来注册事件处理程序。下面以 onclick
事件为例:
// test.js |
|
在 HTML 中注册事件处理程序,会创建一个封装着元素属性值的函数。这个函数中有一个局部变量 event
,也就是事件对象(后面将会讨论这个概念),通过 event
变量可以直接访问事件对象。并且在这个函数内部,this
值等于事件的目标元素。所以你可以将 event
和 this
当做参数,传递给要调用的函数。关于这一点,你可以查看上面例子中 showMessage()
函数打印的内容来验证。
HTML 事件处理程序的缺点:
showMessage()
和 showAnother()
之前就点击了对应的按钮,会抛出错误。可以使用 onclick="try {showMessage()} catch(ex) {}"
的形式来解决这个问题。下面展示一个使用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>
标签用于接受来自用户的数据。它有如下属性:
autocomplete
:一个字符串,描述输入应提供的任何类型的自动完成功能。自动完成的典型实现只是回忆在同一输入字段中输入的先前值,但可以存在更复杂的自动完成形式。例如,浏览器可以与设备的联系人列表集成,以在电子邮件输入字段中自动完成电子邮件地址。不过当type
属性值为button
、file
等时,<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>
的类型。包括:button
、checkbox
、color
、date
、datetime-local
、email
、file
、hidden
、image
、month
、number
、password
、radio
、range
、reset
、search
、submit
、tel
、text
、time
、url
、week
。value
:该<input>
的当前值。this
指向元素自身。this
的丢失。在添加的事件处理程序函数内部,可以直接通过 event
变量访问事件对象。也可以通过给程序处理函数定义参数或者使用 arguments
来访问事件对象,在下面有例子。
以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。你可以通过将事件处理程序属性的值设置为 null
来删除添加的事件处理程序。
来看一个例子:
+
|
这个例子中的按钮,只有第一次点击时会打印内容,之后就没有任何反应,因为事件处理程序在第一次触发之后,就被删除了。
+DOM2 级事件定义了两个方法,addEventListener()
和 removeEventListener()
,分别用于注册和删除事件处理程序,所有 DOM 节点都包含这两个方法。
addEventListener()
可以在同一个元素上注册多个事件处理程序,触发的顺序为添加顺序。this
和 event
的使用规则,与 DOM0 级事件处理程序一致。<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
会在事件捕获阶段时被调用。对于 options
和 useCapture
参数,它们都是该方法的第三个参数,options
是新标准,而 useCapture
是老标准。
接着介绍 removeEventListener()
方法,它的参数如下:
type
:表示监听事件类型的字符串,需要注意的是没有 on
前缀。listener
:作为事件处理程序的函数。options(可选)
:一个对象。其属性如下:capture
:一个布尔值,默认为 false
。当值为 true
时,表示要移除的 listener
是注册在事件捕获阶段的。useCapture(可选)
:一个布尔值,默认为 false
。当值为 true
时,表示要移除的 listener
是注册在事件捕获阶段的。如果一个事件处理程序一共注册了两次,一次在事件捕获阶段,一次在事件冒泡阶段,那么这两次注册需要分别移除,两者不会互相干扰。
+下面的例子用于观察 options.capture
和 useCapture
的效果。
|
上例中 captureListener1
和 captureListener2
都是注册在 outer
的捕获阶段,而 noneCaptureListener
和 innerListener
分别注册在 outer
和 inner
的冒泡阶段。并且 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
上的事件处理程序被调用。下面我只简单的介绍一下我认为比较常用的事件,如果你想比较全面的了解这里,点击下面的链接!
+在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。
+对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document
。也就是说,我们可以为整个页面指定一个 onclick
事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。
每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”,也是造成 Web 应用程序内存与性能问题的主要原因。
+在两种情况下,可能会造成上述问题。第一种情况就是从文档中移除带有事件处理程序的元素时。这可能是通过纯粹的 DOM 操作,例如使用 removeChild()
方法,但更多地是发生在使用 innerHTML
替换页面中某一部分的时候。如果带有事件处理程序的元素被 innerHTML
删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。所以如果你知道某个元素即将被移除,那么最好在此之前手工移除事件处理程序。
另一种情况,就是卸载页面的时候。如果在页面被卸载之前没有清理干净事件处理程序,那它们就会滞留在内存中。每次加载完页面在卸载页面时(可能是在两个页面间来回切换,也可能是单击了“刷新”按钮),内存中滞留的对象数目就会增加,因为事件处理程序占用的内存并没有被释放。因此最好的做法就是在页面卸载之前,先通过 onunload
事件处理程序移除所有事件处理程序。
width
与height
的情况是一致的,下面以width
为例进行说明。
min-width
的优先级高于width
,即使有!important
也是如此。
all
:所有媒体braile
:盲文触觉设备embossed
:盲文打印机handheld
:手持设备print
:打印预览或打印机projection
:项目演示screen
:彩屏设备speech
:听觉类似的媒体tty
:不适用像素的设备,如电传打字机tv
:电视<div></div> |
min-width
的优先级高于max-width
,即使有!important
也是如此。
以下特性,除scan
和grid
外,都可以加上min
或max
前缀以指定范围
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> |
max-width
的优先级高于width
,即使有!important
也是如此。
在<link>
标签的media
属性中指定媒体查询是CSS2的方式
<link rel="stylesheet" type="text/css" media="screen" href="screenstyles.css" /> |
@media
是在样式表中使用的媒体查询方式
@media screen { ... } |
@import
可以根据媒体查询将其他样式表加载到当前样式表中
@import url("phone.css") screen |
and
:类似于逻辑与,
:类似于逻辑或not
:用于对整个媒体查询取反only
:指定某种特定的媒体类型,用来对那些不支持媒体特性但支持媒体类型的设备隐藏样式表<div></div> |
/* 当设备是彩屏设备并且视口最小宽度大于等于400px时生效 */ |
<div> |
<div> |
<div> |
<div> |
假设元素内设置了data-attr="value1 value2 value3"
,下面介绍对它使用属性选择器的几种方式:
[data-attr]
:根据属性名称;[data-attr="value1 value2 value3"]
:根据属性名和属性值;[data-attr^="val"]
:以……开头;[data-attr*="value2"]
:包含……;[data-attr$="3"]
:以……结尾。<div> |
选择器的组合使用:
div, span
:同时选择<div>
和<span>
元素;div.classValue
:选择class
属性值包含classValue
的<div>
元素;div#idValue
:选择id
属性值为idValue
的<div>
元素;div[data-attr]
:选择具有data-attr
属性的<div>
元素;div.class1.class2
:选择class
属性值同时包含class1
和class2
的<div>
元素。根据结构来使用选择器:
selector1 selector2
:当selector2
是selector1
的后代元素时,选择selector2
;selector1 > selector2
:当selector2
是selector1
的子元素时,选择selector2
;selector1 + selector2
:当selector2
是selector1
的同胞元素,且selector2
紧跟着selector1
时,选择selector2
;selector1 ~ selector2
:当selector2
是selector1
的同胞元素,且selector2
位于selector1
后面时,选择selector2
;selector1 * selector2
:当selector2
是selector1
的后代元素,且selector2
不是selector1
的子元素时,选择selector2
。以下就是浏览器层叠各个来源样式的顺序:
100
:ID选择器。10
:类选择器、伪类选择器和属性选择器。1
:标签选择器和伪元素。!import
的优先级最高(开挂一样的存在)。!import
。!import
和行内样式,其他情况下,特指度大的优先级高。<div> |
<div> |
<div> |
<div> |
假设元素内设置了data-attr="value1 value2 value3"
,下面介绍对它使用属性选择器的几种方式:
[data-attr]
:根据属性名称;[data-attr="value1 value2 value3"]
:根据属性名和属性值;[data-attr^="val"]
:以……开头;[data-attr*="value2"]
:包含……;[data-attr$="3"]
:以……结尾。<div> |
选择器的组合使用:
div, span
:同时选择<div>
和<span>
元素;div.classValue
:选择class
属性值包含classValue
的<div>
元素;div#idValue
:选择id
属性值为idValue
的<div>
元素;div[data-attr]
:选择具有data-attr
属性的<div>
元素;div.class1.class2
:选择class
属性值同时包含class1
和class2
的<div>
元素。根据结构来使用选择器:
selector1 selector2
:当selector2
是selector1
的后代元素时,选择selector2
;selector1 > selector2
:当selector2
是selector1
的子元素时,选择selector2
;selector1 + selector2
:当selector2
是selector1
的同胞元素,且selector2
紧跟着selector1
时,选择selector2
;selector1 ~ selector2
:当selector2
是selector1
的同胞元素,且selector2
位于selector1
后面时,选择selector2
;selector1 * selector2
:当selector2
是selector1
的后代元素,且selector2
不是selector1
的子元素时,选择selector2
。以下就是浏览器层叠各个来源样式的顺序:
100
:ID选择器。10
:类选择器、伪类选择器和属性选择器。1
:标签选择器和伪元素。!import
的优先级最高(开挂一样的存在)。!import
。!import
和行内样式,其他情况下,特指度大的优先级高。let target = {} |
类不能直接被修改为将代理用作自身的原型,因为它们的 prototype
属性是不可写入的。然而你可以使用一点变通手段,利用继承来创建一个把代理作为自身原型的类。
function Super () {} |
Set
是不包含重复值的列表。你一般不会像对待数组那样来访问 Set
中的某个项。相反更常见的是,只在 Set
中检查某个值是否存在。
+Map
则是键与相对应的值的集合。因此,Map
中的每个项都存储了两块数据,通过指定所需读取的键即可检索对应的值。Map
常被用作缓存,存储数据以便此后快速检索。
在 ES5 中,开发者使用对象属性来模拟 Set
与 Map
,就像这样:
let set = Object.create(null) |
本例中的 set
变量是一个原型为 null
的对象,确保在此对象上没有继承属性。使用对象的属性作为需要检查的唯一值在 ES5 中是很常用的方法。当一个属性被添加到 set
对象时,它的值也被设为 true
,因此条件判断语句就可以简单判断出该值是否存在。
使用对象模拟 Set
与模拟 Map
之间唯一真正的区别是所存储的值。例如:以下例子将对象作为 Map
使用:
let map = Object.create(null) |
此代码将字符串值 "bar"
存储在 foo
键上。与 Set
不同,Map
多数被用来提取数据,而不是仅检查键的存在性。
尽管在简单情况下将对象作为 Set
与 Map
来使用都是可行的,但一旦接触到对象属性的局限性,此方式就会遇到更多麻烦。研究如下代码:
let map = Object.create(null) |
造成上面例子中问题的原因是对象属性只可以是字符串或符号类型,所以当你为对象定义属性时,如果属性名不是字符串或符号类型,那么它会调用属性名的 toString()
方法将属性名转换为字符串,然后再进行后续操作。所以 map[key1] === map[key2]
、map[key3] === map[key4]
。
ES6 新增了 Set
类型,这是一种无重复值的有序列表。Set
允许对它包含的数据进行快速访问,从而增加了一个追踪离散值的更有效方式。
Set
使用 new Set()
来创建,而调用 add()
方法就能向 Set
中添加项目,检查 size
属性还能查看其中包含有多少项。
let set = new Set() |
Set
不会使用强制类型转换来判断值是否重复。这意味着 Set
可以同时包含数值 5
与字符串 "5"
,将它们都作为相对独立的项(在 Set
内部的比较使用了 Object.is()
方法来判断两个值是否相等,唯一的例外是+0与-0被判断为是相等的)。你还可以向 Set
添加多个对象,它们不会被合并为同一项:
let set = new Set() |
由于 key1
与 key2
并不会被转换为字符串,所以它们在这个 Set
内部被认为是两个不同的项(记住:如果它们被转换为字符串,那么都会等于 "[object Object]"
)。
如果 add()
方法用相同值进行了多次调用,那么在第一次之后的调用实际上会被忽略:
let set = new Set() |
你可以使用数组来初始化一个 Set
,并且 Set
构造器会确保不重复地使用这些值。
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]) |
Set
构造器实际上可以接收任意可迭代对象作为参数。能使用数组是因为它们默认就是可迭代的,Map
也是一样的。
你可以使用 has()
方法来检测某个值是否存在于 Set
中,就像这样:
let set = new Set() |
使用 delete()
方法可以移除 Set
中的某个值,使用 clear()
方法可以移除 Set
中的所有值。
let set = new Set() |
Set上 的 forEach()
方法类似于数组中的 forEach()
方法,它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this
值。传入的函数会接收三个参数:Set 元素的值,Set 元素的值和目标 Set 自身。你没看错,第一个和第二个参数是完全相同的,这么设计的目的是为了保持所有对象上 forEach()
方法的统一。
使用扩展运算符可以很轻松的将 Set
转换为数组,用一个数组去重的例子来展示它是如何转换的:
function eliminateDuplicates (array) { |
由于 Set
类型存储对象引用的方式,它也可以被称为 Strong Set。对象存储在 Set
的一个实例中时,实际上相当于把对象存储在变量中。只要对 Set
实例的引用仍然存在,所存储的对象就无法被垃圾回收机制回收,从而无法释放内存。例如:
let set = new Set() |
在本例中,将 key
设置为 null
清楚了对 key
对象的一个引用,但是另一个引用还存在于 set
内部。你仍然可以使用扩展运算符将 Set
转换为数组,然后访问数组的第一项,key
变量就取回了原先的对象。
为了缓解这个问题,ES6 也包含了 Weak Set,该类型只允许存储对象弱引用,而不能存储基本类型的值。对象的弱引用在它自己成为该对象的唯一引用时,不会阻止垃圾回收。
+Weak Set 使用 WeakSet
构造器来创建,并包含 add()
方法、has()
方法以及 delete()
方法。以下例子使用了这三个方法:
let set = new WeakSet() |
使用 Weak Set 很像在使用正规的 Set
。你可以在 Weak Set 上添加、移除或检查引用,也可以给构造器传入一个可迭代对象来初始化 Weak Set的值:
let key1 = {} |
在本例中,一个数组被传给了 WeakSet
构造器。由于该数组包含了两个对象,这些对象就被添加到了 Weak Set 中。要记住若数组中包含了非对象的值,就会抛出错误,因为 WeakSet
构造器不接受基本类型的值。
Weak Set 与正规 Set 之间最大的区别是对象的弱引用。此处有个例子说明了这种差异:
+let set = new WeakSet() |
当代码被执行后,Weak Set 中的 key
引用就不能再访问了。核实这一点是不可能的,因为需要把对于该对象的一个引用传递给 has()
方法(而只要存在其他引用,Weak Set 内部的弱引用就不会消失)。这会使得难以对 Weak Set 的引用特征进行测试,但 JS 引擎已经正确地将引用移除了,这一点你可以信任。
这些例子演示了 Weak Set 与正规 Set 的一些共有特征,但是它们还有一些关键的差异,即:
+WeakSet
的实例,若调用 add()
方法时传入非对象的参数,就会抛出错误(has()
或 delete()
则会在传入了非对象的参数时返回 false
)。for-of
循环中。forEach()
方法。clear()
方法。size
属性。ES6 的 Map
类型是键值对的有序列表,而键和值都可以是任意类型。
Map
对键的处理方式与 Set
对值的处理一样,采用的是 Object.is()
方法,而不会进行强制类型转换,唯一的例外是 +0 与 -0 被判断为是相等的。
你可以调用 set()
方法并给它传递一个键与一个关联的值,来给 Map 添加项。此后使用键名来调用 get()
方法便能提取对应的值。如果任意一个键不存在于Map中,则 get()
方法就会返回特殊值 undefined
。例如:
let map = new Map() |
此代码使用了对象 key1
与 key2
作为 Map 的键,并存储了两个不同的值。由于这些键不会被强制转换成其他形式,每个对象就都被认为是唯一的。这允许你给对象关联额外数据,而无须修改对象自身。
与 Set 类似,你能将数组传递给 Map
构造器,以便使用数据来初始化一个 Map
。该数组中的每一项也必须是数组,内部数组的首个项会作为键,第二项则为对应值。因此整个 Map 就被这些双项数组所填充。例如:
let map = new Map([['name', 'Nicholas'], ['age', 25]]) |
虽然有数组构成的数组看起来有点奇怪,但这对于准确表示键来说却是必要的。因为键允许是任意数据类型,将键存储在数组中,是确保它们在被添加到 Map 之前不会被强制转换为其他类型的唯一方法。
+has(key)
:判断指定的键是否存在于 Map 中。delete(key)
:移除 Map 中的键以及对应的值。clear()
:移除 Map 中所有的键与值。size
:用于指示包含了多少个键值对。Map 的 forEach()
方法类似于 Set 与数组上的 forEach()
方法,不过它是按照键值对被添加到 Map 中的顺序来迭代的。它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this
值。传入的函数会接收三个参数:Map 项的值、该值所对应的的键和目标Map自身。
ES6 的 WeakMap
类型是键值对的无序列表,其中键必须是非空的对象,值则允许是任意类型。WeakMap
的接口与 Map
的非常相似,都使用 set()
与 get()
方法来分别添加与提取数据。
类似于 Weak Set,它没有 size
属性,没有任何办法可以确定 Weak Map 是否为空。在其他引用被移除后,由于对键的引用不再有残留,也就无法调用 get()
方法来去取对应的值。Weak Map 已经切断了对于该值的访问,其所占的内存在垃圾回收器运行时便会被释放。
为了初始化 Weak Map,需要把一个由数组构成的数组传递给 WeakMap
构造器。就像正规 Map 那样,每个内部数组都应当有两个项,第一项是作为键的对象,第二项则是对应的值(任意类型)。例如:
let key1 = {} |
对象 key1
与 key2
被用作 Weak Map 的键,get()
与 has()
方法则能访问他们。在传递给 WeakMap
构造器的参数中,若任意键值对使用了非对象的键,构造器就会抛出错误。
Weak Map 只有两个附加方法能用来与键值对交互。has()
方法用于判断指定的键是否存在于 Map 中,而 delete()
方法则用于移除一个特定的键值对。clear()
方法不存在,这是因为没有必要对键进行枚举,并且枚举 Weak Map 也是不可能的,这与 Weak Set 相同。
ES5 中的创建私有数据的方式:
+var Person = (function () { |
此例用 IIFE 包裹了 Person
的定义,其中含有两个私有属性:privateData
和 privateId
。privateData
对象存储了每个实例的私有信息,而 privateId
则被用于为每个实例产生一个唯一ID。当 Person
构造器被调用时,一个不可枚举、不可配置、不可写入的 _id
属性就被添加了。
接下来在 privateData
对象中建立了与实例ID对应的一个入口,其中存储着 name
的值。随后在 getName()
函数中,就能使用 this._id
作为 privateData
的键来提取该值。由于 privateData
无法从 IIFE 外部进行访问,实际的数据就是安全的,尽管 this._id
在 privateData
对象上依然是公开暴露的。
此方式的最大问题在于 privateData
中的数据永远不会消失,因为在对象实例被销毁时没有任何方法可以获知该数据,privateData
对象就将永远包含多余的数据。这个问题现在可以换用 Weak Map 来解决了,如下:
let Person = (function () { |
此版本的 Person
范例使用了 Weak Map 而不是对象来保存私有数据。由于 Person
对象的实例本身能被作为键来使用,于是也就无须再记录单独的 ID。当 Person
构造器被调用时,将 this
作为键在 Weak Map 上建立了一个入口,而包含私有信息的对象成为了对应的值,其中只存放了 name
属性。通过将 this
传递给 privateData.get()
方法,以获取值对象并访问其 name
属性,getName()
函数便能提取私有信息。这种技术让私有信息能够保持私有状态,并且当与之关联的对象实例被销毁时,私有信息也会被同时销毁。
Set
是不包含重复值的列表。你一般不会像对待数组那样来访问 Set
中的某个项。相反更常见的是,只在 Set
中检查某个值是否存在。
-Map
则是键与相对应的值的集合。因此,Map
中的每个项都存储了两块数据,通过指定所需读取的键即可检索对应的值。Map
常被用作缓存,存储数据以便此后快速检索。
在 ES5 中,开发者使用对象属性来模拟 Set
与 Map
,就像这样:
let set = Object.create(null) |
本例中的 set
变量是一个原型为 null
的对象,确保在此对象上没有继承属性。使用对象的属性作为需要检查的唯一值在 ES5 中是很常用的方法。当一个属性被添加到 set
对象时,它的值也被设为 true
,因此条件判断语句就可以简单判断出该值是否存在。
使用对象模拟 Set
与模拟 Map
之间唯一真正的区别是所存储的值。例如:以下例子将对象作为 Map
使用:
let map = Object.create(null) |
此代码将字符串值 "bar"
存储在 foo
键上。与 Set
不同,Map
多数被用来提取数据,而不是仅检查键的存在性。
尽管在简单情况下将对象作为 Set
与 Map
来使用都是可行的,但一旦接触到对象属性的局限性,此方式就会遇到更多麻烦。研究如下代码:
let map = Object.create(null) |
造成上面例子中问题的原因是对象属性只可以是字符串或符号类型,所以当你为对象定义属性时,如果属性名不是字符串或符号类型,那么它会调用属性名的 toString()
方法将属性名转换为字符串,然后再进行后续操作。所以 map[key1] === map[key2]
、map[key3] === map[key4]
。
ES6 新增了 Set
类型,这是一种无重复值的有序列表。Set
允许对它包含的数据进行快速访问,从而增加了一个追踪离散值的更有效方式。
Set
使用 new Set()
来创建,而调用 add()
方法就能向 Set
中添加项目,检查 size
属性还能查看其中包含有多少项。
let set = new Set() |
Set
不会使用强制类型转换来判断值是否重复。这意味着 Set
可以同时包含数值 5
与字符串 "5"
,将它们都作为相对独立的项(在 Set
内部的比较使用了 Object.is()
方法来判断两个值是否相等,唯一的例外是+0与-0被判断为是相等的)。你还可以向 Set
添加多个对象,它们不会被合并为同一项:
let set = new Set() |
由于 key1
与 key2
并不会被转换为字符串,所以它们在这个 Set
内部被认为是两个不同的项(记住:如果它们被转换为字符串,那么都会等于 "[object Object]"
)。
如果 add()
方法用相同值进行了多次调用,那么在第一次之后的调用实际上会被忽略:
let set = new Set() |
你可以使用数组来初始化一个 Set
,并且 Set
构造器会确保不重复地使用这些值。
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]) |
Set
构造器实际上可以接收任意可迭代对象作为参数。能使用数组是因为它们默认就是可迭代的,Map
也是一样的。
你可以使用 has()
方法来检测某个值是否存在于 Set
中,就像这样:
let set = new Set() |
使用 delete()
方法可以移除 Set
中的某个值,使用 clear()
方法可以移除 Set
中的所有值。
let set = new Set() |
Set上 的 forEach()
方法类似于数组中的 forEach()
方法,它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this
值。传入的函数会接收三个参数:Set 元素的值,Set 元素的值和目标 Set 自身。你没看错,第一个和第二个参数是完全相同的,这么设计的目的是为了保持所有对象上 forEach()
方法的统一。
使用扩展运算符可以很轻松的将 Set
转换为数组,用一个数组去重的例子来展示它是如何转换的:
function eliminateDuplicates (array) { |
由于 Set
类型存储对象引用的方式,它也可以被称为 Strong Set。对象存储在 Set
的一个实例中时,实际上相当于把对象存储在变量中。只要对 Set
实例的引用仍然存在,所存储的对象就无法被垃圾回收机制回收,从而无法释放内存。例如:
let set = new Set() |
在本例中,将 key
设置为 null
清楚了对 key
对象的一个引用,但是另一个引用还存在于 set
内部。你仍然可以使用扩展运算符将 Set
转换为数组,然后访问数组的第一项,key
变量就取回了原先的对象。
为了缓解这个问题,ES6 也包含了 Weak Set,该类型只允许存储对象弱引用,而不能存储基本类型的值。对象的弱引用在它自己成为该对象的唯一引用时,不会阻止垃圾回收。
-Weak Set 使用 WeakSet
构造器来创建,并包含 add()
方法、has()
方法以及 delete()
方法。以下例子使用了这三个方法:
let set = new WeakSet() |
使用 Weak Set 很像在使用正规的 Set
。你可以在 Weak Set 上添加、移除或检查引用,也可以给构造器传入一个可迭代对象来初始化 Weak Set的值:
let key1 = {} |
在本例中,一个数组被传给了 WeakSet
构造器。由于该数组包含了两个对象,这些对象就被添加到了 Weak Set 中。要记住若数组中包含了非对象的值,就会抛出错误,因为 WeakSet
构造器不接受基本类型的值。
Weak Set 与正规 Set 之间最大的区别是对象的弱引用。此处有个例子说明了这种差异:
-let set = new WeakSet() |
当代码被执行后,Weak Set 中的 key
引用就不能再访问了。核实这一点是不可能的,因为需要把对于该对象的一个引用传递给 has()
方法(而只要存在其他引用,Weak Set 内部的弱引用就不会消失)。这会使得难以对 Weak Set 的引用特征进行测试,但 JS 引擎已经正确地将引用移除了,这一点你可以信任。
这些例子演示了 Weak Set 与正规 Set 的一些共有特征,但是它们还有一些关键的差异,即:
-WeakSet
的实例,若调用 add()
方法时传入非对象的参数,就会抛出错误(has()
或 delete()
则会在传入了非对象的参数时返回 false
)。for-of
循环中。forEach()
方法。clear()
方法。size
属性。ES6 的 Map
类型是键值对的有序列表,而键和值都可以是任意类型。
Map
对键的处理方式与 Set
对值的处理一样,采用的是 Object.is()
方法,而不会进行强制类型转换,唯一的例外是 +0 与 -0 被判断为是相等的。
你可以调用 set()
方法并给它传递一个键与一个关联的值,来给 Map 添加项。此后使用键名来调用 get()
方法便能提取对应的值。如果任意一个键不存在于Map中,则 get()
方法就会返回特殊值 undefined
。例如:
let map = new Map() |
此代码使用了对象 key1
与 key2
作为 Map 的键,并存储了两个不同的值。由于这些键不会被强制转换成其他形式,每个对象就都被认为是唯一的。这允许你给对象关联额外数据,而无须修改对象自身。
与 Set 类似,你能将数组传递给 Map
构造器,以便使用数据来初始化一个 Map
。该数组中的每一项也必须是数组,内部数组的首个项会作为键,第二项则为对应值。因此整个 Map 就被这些双项数组所填充。例如:
let map = new Map([['name', 'Nicholas'], ['age', 25]]) |
虽然有数组构成的数组看起来有点奇怪,但这对于准确表示键来说却是必要的。因为键允许是任意数据类型,将键存储在数组中,是确保它们在被添加到 Map 之前不会被强制转换为其他类型的唯一方法。
-has(key)
:判断指定的键是否存在于 Map 中。delete(key)
:移除 Map 中的键以及对应的值。clear()
:移除 Map 中所有的键与值。size
:用于指示包含了多少个键值对。Map 的 forEach()
方法类似于 Set 与数组上的 forEach()
方法,不过它是按照键值对被添加到 Map 中的顺序来迭代的。它接收两个参数:要在每一项上运行的函数和(可选的)运行该函数时的 this
值。传入的函数会接收三个参数:Map 项的值、该值所对应的的键和目标Map自身。
ES6 的 WeakMap
类型是键值对的无序列表,其中键必须是非空的对象,值则允许是任意类型。WeakMap
的接口与 Map
的非常相似,都使用 set()
与 get()
方法来分别添加与提取数据。
类似于 Weak Set,它没有 size
属性,没有任何办法可以确定 Weak Map 是否为空。在其他引用被移除后,由于对键的引用不再有残留,也就无法调用 get()
方法来去取对应的值。Weak Map 已经切断了对于该值的访问,其所占的内存在垃圾回收器运行时便会被释放。
为了初始化 Weak Map,需要把一个由数组构成的数组传递给 WeakMap
构造器。就像正规 Map 那样,每个内部数组都应当有两个项,第一项是作为键的对象,第二项则是对应的值(任意类型)。例如:
let key1 = {} |
对象 key1
与 key2
被用作 Weak Map 的键,get()
与 has()
方法则能访问他们。在传递给 WeakMap
构造器的参数中,若任意键值对使用了非对象的键,构造器就会抛出错误。
Weak Map 只有两个附加方法能用来与键值对交互。has()
方法用于判断指定的键是否存在于 Map 中,而 delete()
方法则用于移除一个特定的键值对。clear()
方法不存在,这是因为没有必要对键进行枚举,并且枚举 Weak Map 也是不可能的,这与 Weak Set 相同。
ES5 中的创建私有数据的方式:
-var Person = (function () { |
此例用 IIFE 包裹了 Person
的定义,其中含有两个私有属性:privateData
和 privateId
。privateData
对象存储了每个实例的私有信息,而 privateId
则被用于为每个实例产生一个唯一ID。当 Person
构造器被调用时,一个不可枚举、不可配置、不可写入的 _id
属性就被添加了。
接下来在 privateData
对象中建立了与实例ID对应的一个入口,其中存储着 name
的值。随后在 getName()
函数中,就能使用 this._id
作为 privateData
的键来提取该值。由于 privateData
无法从 IIFE 外部进行访问,实际的数据就是安全的,尽管 this._id
在 privateData
对象上依然是公开暴露的。
此方式的最大问题在于 privateData
中的数据永远不会消失,因为在对象实例被销毁时没有任何方法可以获知该数据,privateData
对象就将永远包含多余的数据。这个问题现在可以换用 Weak Map 来解决了,如下:
let Person = (function () { |
此版本的 Person
范例使用了 Weak Map 而不是对象来保存私有数据。由于 Person
对象的实例本身能被作为键来使用,于是也就无须再记录单独的 ID。当 Person
构造器被调用时,将 this
作为键在 Weak Map 上建立了一个入口,而包含私有信息的对象成为了对应的值,其中只存放了 name
属性。通过将 this
传递给 privateData.get()
方法,以获取值对象并访问其 name
属性,getName()
函数便能提取私有信息。这种技术让私有信息能够保持私有状态,并且当与之关联的对象实例被销毁时,私有信息也会被同时销毁。
模块是使用不同方式加载的 JS 文件(与 JS 原先的脚本加载方式相对)。这种不同模式很有必要,因为它与脚本(script)有大大不同的语义:
this
值为 undefined
。模块是使用不同方式加载的 JS 文件(与 JS 原先的脚本加载方式相对)。这种不同模式很有必要,因为它与脚本(script)有大大不同的语义:
this
值为 undefined
。CommonJS 是在 ES6 之前的模块规范,至今仍然被广泛使用。点击这里了解 CommonJS!
ES6 的导出有两种形式:命名导出和默认导出。
@@ -2759,79 +2759,6 @@foo()
内部创建的箭头函数会捕获调用时 foo()
的 this
。由于 foo()
的 this
绑定到 obj1
,bar
(引用箭头函数)的 this
也会绑定到 obj1
,箭头函数的绑定无法被修改
箭头函数可以像 bind()
一样确保函数的 this
被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。
ES6 能更容易地为参数提供默认值,它使用了初始化形式,以便在参数未被正式传递进来时使用。在函数声明中能指定任意一个参数的默认值,即使该参数排在未指定默认值的参数之前也是可以的。
-只有在某个参数未传递,或明确传递 undefined
时,才会应用参数的默认值。null
值被认为是有效的。
function makeRequest(url, timeout = 2000, callback) { |
参数默认值最有意思的特性或许就是默认值并不要求一定是基本类型的值。例如,你可以执行一个函数来产生参数的默认值:
-function getValue () { |
需要注意的是,仅在调用 add()
函数而未提供第二个参数时,getValue()
函数才会被调用,而在 getValue()
函数声明初次被解析时并不会进行调用。另外在书写代码时要小心,将函数调用作为参数的默认值时一定不要遗漏了括号,否则含义就变了。
参数默认值与 let
和 const
声明类似,都存在着暂时性死区。函数每个参数都会创建一个新的标识符绑定,它在初始化之前不允许被访问,否则会抛出错误。参数初始化会在函数被调用时进行,无论是给参数传递了一个值、还是使用了参数的默认值。
所以在为函数参数指定默认值时,后面的参数可以使用前面的参数,反过来则会抛出错误。
-// 后面参数使用前面参数 |
剩余参数由三个点(…)与一个紧跟着的具名参数指定,它会是包含传递给函数的其余参数的一个数组,名称中的“剩余”也由此而来。需要注意的是,函数的 length
属性用于指示具名参数的数量,而剩余参数对其毫无影响。
function pick (string, ...keys) { |
剩余参数的两个限制条件:
-set
函数中,不能使用剩余参数。原因是对象的 set
被限定只能使用单个参数,而剩余参数按照定义是不限制参数数量的。与剩余参数关联最密切的就是扩展运算符。剩余参数允许你把多个独立的参数合并到一个数组中。而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。
-用扩展运算符传递参数,使得更容易将数组作为函数参数来使用,你会发现在大部分场景中扩展运算符都是 apply()
方法的合适替代品。并且扩展运算符可以与其他参数混用。
扩展运算符的使用没有位置和数量限制。
-let values = [25, 50, 75, 100] |
定义函数有各种各样的方式,在 JavaScript 中识别函数就变得很有挑战性。此外,匿名函数表达的流行使得调试有点困难,经常导致堆栈跟踪难以被阅读与解释。正因为此,ES6 给所有函数添加了 name
属性。
需要注意的是,函数的 name
属性值未必会关联到同名变量。name
属性是为了在调试时获得有用的相关信息,所以不能用 name
属性值去获取对函数的引用。
ES6 中所有函数都有适当的 name
属性值。为了理解其实际运作,请看下例————它展示了一个函数与一个函数表达式,并将二者的 name
属性都打印出来:
function doSomething () { |
在此代码中,由于是一个函数声明,doSomething()
就拥有一个值为 "doSomething"
的 name
属性。而匿名函数表达式 doAnotherThing()
的 name
属性值是 "doAnotherThing"
,因为这是该函数所赋值的变量的名称。
虽然函数声明与函数表达式的名称易于查找,但 ES6 更进一步确保了所有函数都拥有合适的名称。为了表明这点,请参考如下程序:
-var doSomething = function doSomethingElse () { |
本例中的 doSomething.name
的值是 "doSomethingElse"
,因为该函数表达式自己拥有一个名称,并且此名称的优先级要高于赋值目标的变量名。person.sayName()
的 name
属性值是 "sayName"
,正如对象字面量指定的那样。类似的, person.firstName
实际上是个访问器属性的 get
函数,因此它的名称是 "get firstName"
,以标明它的特征。同样,访问器属性的 set
函数也会带有 set
前缀( get
与 set
函数都必须用 Object.getOwnPropertyDescriptor()
来检索)。
函数名称还有另外两个特殊情况。使用 bind()
创建的函数会在名称属性值之前带有 "bound"
前缀。而使用 Function
构造器创建的函数,其名称属性则会有 "anonymous"
前缀,正如此例:
var doSomething = function () { |
JavaScript 为函数提供了两个不同的内部方法:[[Call]]
和 [[Construct]]
。当函数未使用 new
进行调用时,[[Call]]
方法会被执行,运行的是代码中显示的函数体。而当函数使用 new
进行调用时,[[Construct]]
方法则会被执行,负责创建一个新的对象,并且使用该对象作为 this
去执行函数体。
记住并不是所有函数都拥有 [[Construct]]
方法(比如箭头函数),因此不是所有函数都可以用 new
来调用。
在 ES5 中判断函数是不是使用了 new
来调用,最流行的方式是使用 instanceof
,但是这个方式存在漏洞。
function Person (name) { |
为了解决上述问题,ES6 引入了 new.target
元属性。当函数的 [[Construct]]
方法被调用时,new.target
的值为一个指向该构造函数的引用。而当 [[Call]]
方法被调用时,new.target
的值为 undefined
。
function Person (name) { |
ES6 最有意思的一个新部分就是箭头函数。箭头函数正如名称所示那样使用一个“箭头”(=>
)来定义,但它的行为在很多重要方面与传统的 JavaScript 函数不同:
this
、super
、arguments
和 new.target
:箭头函数本身没有 this
、super
、arguments
和 new.target
,如果在箭头函数中引用了这些变量,那这些变量也是外层作用域的,跟它没关系。new
调用:箭头函数没有 [[Construct]]
方法,因此不能被用为构造函数,使用 new
调用箭头函数会抛出错误。new
,那么它也不需要原型,也就是没有 prototype
属性。没有花括号,就不允许有 return
,否则会抛出错误。
var reflect = value => value |
使用传统函数创建立即调用函数表达式时,(function(){/*函数体*/})()
与 (function(){/*函数体*/}())
两种方式都是可行的。
但若使用箭头函数,只有 (()=>{/*函数体*/})()
这一种方式可行,也就是说括号必须仅包裹箭头函数的定义。
JavaScript 最常见的错误领域之一就是在函数内的 this
绑定。请看下面的例子:
var PageHandler = { |
调用 this.doSomething()
会抛出错误的原因是 this
是对事件目标对象(在此案例中就是 document
)的一个引用,而不是被绑定到 PageHandler
上。下面的代码将使用 bind()
方法修复这个问题。
var PageHandler = { |
现在此代码能像预期那样运行,但看起来有点奇怪。接着让我们看看使用箭头函数如何解决这个问题的。
-var PageHandler = { |
因为箭头函数没有 this
绑定,意味着在箭头函数内部使用 this
值时,引擎是通过作用域链来确定的,而 JavaScript 中的作用域机制是词法作用域,所以这个箭头函数内部使用的 this
是 init()
方法的 this
。
因为箭头函数没有 this
绑定,所以对箭头函数使用 call()
、apply()
或 bind()
方法时,函数内的 this
并不会受影响。
永远要记住,this
是在函数调用时进行绑定的!
let obj1 = { |
上面例子中箭头函数内使用的 this
是函数 showThisA
的 this
,但是函数 showThisA
的 this
具体绑定到哪里是由它的调用方式决定的。
在 ES6 中对函数最有趣的改动或许就是一项引擎优化,它改变了尾部调用的系统。尾调用指的是调用函数的语句是另一个函数的最后语句,就像这样:
-function doSomething () { |
在 ES5 引擎中实现的尾调用,其处理就像其他函数调用一样:一个新的栈帧被创建并推到调用栈之上,用于表示该次函数调用。这意味着之前每个栈帧都被保留在内存中,当调用栈太大时会出问题。
-ES6 在严格模式下力图为特定尾调用减少调用栈的大小(非严格模式的尾调用则保持不变)。当满足以下条件时,尾调用优化会清除当前栈帧并在此利用它,而不是为尾调用创建新的栈帧:
-ES6 能更容易地为参数提供默认值,它使用了初始化形式,以便在参数未被正式传递进来时使用。在函数声明中能指定任意一个参数的默认值,即使该参数排在未指定默认值的参数之前也是可以的。
+只有在某个参数未传递,或明确传递 undefined
时,才会应用参数的默认值。null
值被认为是有效的。
function makeRequest(url, timeout = 2000, callback) { |
参数默认值最有意思的特性或许就是默认值并不要求一定是基本类型的值。例如,你可以执行一个函数来产生参数的默认值:
+function getValue () { |
需要注意的是,仅在调用 add()
函数而未提供第二个参数时,getValue()
函数才会被调用,而在 getValue()
函数声明初次被解析时并不会进行调用。另外在书写代码时要小心,将函数调用作为参数的默认值时一定不要遗漏了括号,否则含义就变了。
参数默认值与 let
和 const
声明类似,都存在着暂时性死区。函数每个参数都会创建一个新的标识符绑定,它在初始化之前不允许被访问,否则会抛出错误。参数初始化会在函数被调用时进行,无论是给参数传递了一个值、还是使用了参数的默认值。
所以在为函数参数指定默认值时,后面的参数可以使用前面的参数,反过来则会抛出错误。
+// 后面参数使用前面参数 |
剩余参数由三个点(…)与一个紧跟着的具名参数指定,它会是包含传递给函数的其余参数的一个数组,名称中的“剩余”也由此而来。需要注意的是,函数的 length
属性用于指示具名参数的数量,而剩余参数对其毫无影响。
function pick (string, ...keys) { |
剩余参数的两个限制条件:
+set
函数中,不能使用剩余参数。原因是对象的 set
被限定只能使用单个参数,而剩余参数按照定义是不限制参数数量的。与剩余参数关联最密切的就是扩展运算符。剩余参数允许你把多个独立的参数合并到一个数组中。而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。
+用扩展运算符传递参数,使得更容易将数组作为函数参数来使用,你会发现在大部分场景中扩展运算符都是 apply()
方法的合适替代品。并且扩展运算符可以与其他参数混用。
扩展运算符的使用没有位置和数量限制。
+let values = [25, 50, 75, 100] |
定义函数有各种各样的方式,在 JavaScript 中识别函数就变得很有挑战性。此外,匿名函数表达的流行使得调试有点困难,经常导致堆栈跟踪难以被阅读与解释。正因为此,ES6 给所有函数添加了 name
属性。
需要注意的是,函数的 name
属性值未必会关联到同名变量。name
属性是为了在调试时获得有用的相关信息,所以不能用 name
属性值去获取对函数的引用。
ES6 中所有函数都有适当的 name
属性值。为了理解其实际运作,请看下例————它展示了一个函数与一个函数表达式,并将二者的 name
属性都打印出来:
function doSomething () { |
在此代码中,由于是一个函数声明,doSomething()
就拥有一个值为 "doSomething"
的 name
属性。而匿名函数表达式 doAnotherThing()
的 name
属性值是 "doAnotherThing"
,因为这是该函数所赋值的变量的名称。
虽然函数声明与函数表达式的名称易于查找,但 ES6 更进一步确保了所有函数都拥有合适的名称。为了表明这点,请参考如下程序:
+var doSomething = function doSomethingElse () { |
本例中的 doSomething.name
的值是 "doSomethingElse"
,因为该函数表达式自己拥有一个名称,并且此名称的优先级要高于赋值目标的变量名。person.sayName()
的 name
属性值是 "sayName"
,正如对象字面量指定的那样。类似的, person.firstName
实际上是个访问器属性的 get
函数,因此它的名称是 "get firstName"
,以标明它的特征。同样,访问器属性的 set
函数也会带有 set
前缀( get
与 set
函数都必须用 Object.getOwnPropertyDescriptor()
来检索)。
函数名称还有另外两个特殊情况。使用 bind()
创建的函数会在名称属性值之前带有 "bound"
前缀。而使用 Function
构造器创建的函数,其名称属性则会有 "anonymous"
前缀,正如此例:
var doSomething = function () { |
JavaScript 为函数提供了两个不同的内部方法:[[Call]]
和 [[Construct]]
。当函数未使用 new
进行调用时,[[Call]]
方法会被执行,运行的是代码中显示的函数体。而当函数使用 new
进行调用时,[[Construct]]
方法则会被执行,负责创建一个新的对象,并且使用该对象作为 this
去执行函数体。
记住并不是所有函数都拥有 [[Construct]]
方法(比如箭头函数),因此不是所有函数都可以用 new
来调用。
在 ES5 中判断函数是不是使用了 new
来调用,最流行的方式是使用 instanceof
,但是这个方式存在漏洞。
function Person (name) { |
为了解决上述问题,ES6 引入了 new.target
元属性。当函数的 [[Construct]]
方法被调用时,new.target
的值为一个指向该构造函数的引用。而当 [[Call]]
方法被调用时,new.target
的值为 undefined
。
function Person (name) { |
ES6 最有意思的一个新部分就是箭头函数。箭头函数正如名称所示那样使用一个“箭头”(=>
)来定义,但它的行为在很多重要方面与传统的 JavaScript 函数不同:
this
、super
、arguments
和 new.target
:箭头函数本身没有 this
、super
、arguments
和 new.target
,如果在箭头函数中引用了这些变量,那这些变量也是外层作用域的,跟它没关系。new
调用:箭头函数没有 [[Construct]]
方法,因此不能被用为构造函数,使用 new
调用箭头函数会抛出错误。new
,那么它也不需要原型,也就是没有 prototype
属性。没有花括号,就不允许有 return
,否则会抛出错误。
var reflect = value => value |
使用传统函数创建立即调用函数表达式时,(function(){/*函数体*/})()
与 (function(){/*函数体*/}())
两种方式都是可行的。
但若使用箭头函数,只有 (()=>{/*函数体*/})()
这一种方式可行,也就是说括号必须仅包裹箭头函数的定义。
JavaScript 最常见的错误领域之一就是在函数内的 this
绑定。请看下面的例子:
var PageHandler = { |
调用 this.doSomething()
会抛出错误的原因是 this
是对事件目标对象(在此案例中就是 document
)的一个引用,而不是被绑定到 PageHandler
上。下面的代码将使用 bind()
方法修复这个问题。
var PageHandler = { |
现在此代码能像预期那样运行,但看起来有点奇怪。接着让我们看看使用箭头函数如何解决这个问题的。
+var PageHandler = { |
因为箭头函数没有 this
绑定,意味着在箭头函数内部使用 this
值时,引擎是通过作用域链来确定的,而 JavaScript 中的作用域机制是词法作用域,所以这个箭头函数内部使用的 this
是 init()
方法的 this
。
因为箭头函数没有 this
绑定,所以对箭头函数使用 call()
、apply()
或 bind()
方法时,函数内的 this
并不会受影响。
永远要记住,this
是在函数调用时进行绑定的!
let obj1 = { |
上面例子中箭头函数内使用的 this
是函数 showThisA
的 this
,但是函数 showThisA
的 this
具体绑定到哪里是由它的调用方式决定的。
在 ES6 中对函数最有趣的改动或许就是一项引擎优化,它改变了尾部调用的系统。尾调用指的是调用函数的语句是另一个函数的最后语句,就像这样:
+function doSomething () { |
在 ES5 引擎中实现的尾调用,其处理就像其他函数调用一样:一个新的栈帧被创建并推到调用栈之上,用于表示该次函数调用。这意味着之前每个栈帧都被保留在内存中,当调用栈太大时会出问题。
+ES6 在严格模式下力图为特定尾调用减少调用栈的大小(非严格模式的尾调用则保持不变)。当满足以下条件时,尾调用优化会清除当前栈帧并在此利用它,而不是为尾调用创建新的栈帧:
+ECMAScript 的变量是松散类型的,可以用来保存任何类型的数据,换句话说,变量只是一个用于保存值得占位符而已,变量没有类型,数据才有类型!
+数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。
var
声明变量,变量为声明该变量的作用域中的局部变量。即在全局作用域中声明的变量为全局变量,在局部作用域中声明的变量为局部变量。var
声明的变量如果未初始化,则变量保存的值默认为 undefined
。configurable
:表示能否通过 delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,这个特性默认值为 true
。enumerable
:表示能否通过 for-in
循环返回属性。直接在对象上定义的属性,这个特性默认值为 true
。writable
:表示能否修改属性的值。直接在对象上定义的属性,这个特性默认值为 true
。value
:包含这个属性的数据值。读取属性值得时候,从这个位置读。写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined
。可以使用 Object.defineProperty(obj, prop, descriptor)
修改数据属性的特性:
obj
:数据属性所在的对象。prop
:数据属性的名称,可以是字符串或符号。descriptor
:描述符对象,可包含的属性有 configurable
、enumerable
、writable
和 value
。如果通过 Object.defineProperty(obj, prop, descriptor)
定义一个新的数据属性,descriptor
中缺失的特性会被赋予 false
或 undefined
。
JavaScript 的数据类型分为“基本数据类型”和“引用数据类型”两种。
+var obj = { test: 1 } |
关于writable:当 writable
为 false
时,在非严格模式下通过赋值语句修改属性值,赋值操作将被忽略。在严格模式下则会抛出错误。但是如果通过 Object.defineProperty() 方法修改 value 特性则不会有任何问题。
var obj = { test: 1} |
关于configurable:当 configurable
为 false
时,不允许删除属性,不允许修改属性的 enumerable
、configurable
,不可以将 writable
由 false
修改为 true
,但是可以将 writable
由 true
修改为 false
,也可以修改属性的 value
特性。
当 writable 和 configurable 均为 false 时,不允许通过任何方式修改属性值,直接赋值或者通过 Object.defineProperty() 都不可以!
+/** |
访问器属性不包含数据值。它们包含一对 get
和 set
函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 get
函数,这个函数负责返回有效的值。在写入访问器属性时,会调用 set
函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性。
Undefined
、Null
、Boolean
、Number
、String
和 Symbol
(ES6 提出)。Object
。configurable
:表示能否通过 delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。直接在对象上定义的属性,它们的这个特性默认值为 true
。enumerable
:表示能否通过 for-in
循环返回属性。直接在对象上定义的属性,它们的这个特性默认值为 true
。get
:在读取属性时调用的函数。默认值为 undefined
。set
:在写入属性时调用的函数。默认值为 undefined
。对一个值使用 typeof
操作符,可能返回下列某个字符串(均为英文单词小写形式):
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
+var book = { |
不一定非要同时指定 get
和 set
。只指定 get
意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了 get
函数的属性会抛出错误。而读取只指定 set
的属性会返回 undefined
可以通过 Object.defineProperty() 实现数据属性与访问器属性的转换,但是切记不能同时指定数据属性和访问器属性,这样会抛出错误!
+ES5 定义了一个 Object.defineProperties()
方法用来为对象定义多个属性。
var book = {} |
使用 ES5 的 Object.getOwnPropertyDescriptor()
方法可以取得给定属性的描述符。该方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象。这个方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用。
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions()
。如果想检测一个对象是否可以添加新属性,可以使用 Object.isExtensible()
。
不可以添加新属性,但是删除旧属性还是可以的。
+var myObject = { a: 2 } |
在非严格模式下,创建属性 b
会静默失败。在严格模式下,将会抛出 TypeError
错误。
Object.seal()
会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions()
并把所有现有属性标记为 configurable: false
。
Object.freeze()
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal()
并把所有“数据访问”属性标记为 writable: false
。
工厂模式就是调用函数返回一个包含特定属性和方法的对象,工厂模式的问题在于它没有解决对象识别的问题(即怎样知道一个对象的类型)。
+function createPerson (name, age) { |
ES 中可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法,下面使用构造函数模式重写工厂模式中的例子。
+function Person (name, age) { |
构造函数模式与工厂模式的区别:
"undefined"
: 如果这个值未定义。"boolean"
: 如果这个值是布尔值。"string"
: 如果这个值是字符串。"number"
: 如果这个值是数值。"symbol"
: 如果这个值是符号。"function"
: 如果这个值是函数(其实函数属于对象的一种)。"object"
: 如果这个值是对象或 null
。this
对象.return
语句。用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
object instanceof constructor
=> true | false
《JavaScript 高级程序设计》中说:“根据规定,所有引用数据类型的值都是Object的实例。因此,在检测一个引用数据类型值和Object构造函数时,instanceof操作符始终会返回true。”
随着版本的迭代,这个说法变得不准确了,有例外了!!!
var a = Object.create(null) |
返回一个表示类型的字符串,详情请看 MDN官网。
-Undefined
类型只有一个值,即特殊的 undefined
。在使用 var
声明变量但未对其加以初始化时,这个变量的值就是 undefined
。对未初始化的变量执行 typeof
操作会返回 undefined
,而对未声明的变量执行 typeof
操作同样会返回 undefined
。
除了可以对未声明的变量执行 typeof
操作之外,在非严格模式下还可以对其进行 delete
操作。除了这两种情况之外,对未声明的变量进行任何其他操作,都会抛出错误。
Null
类型也只有一个值 - null
。
typeof null === 'object' // true |
Boolean
类型的字面值 true
和 false
是区分大小写的。也就是说,True
和 False
都不是 Boolean
值,只是标识符。
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 个步骤:
new
)。this
绑定到这个新对象。length
属性取得,包含双子节字符的可能不准确。toString()
方法,并且数值的 toString()
方法可以接收一个参数(数值的基数),但是 null
和 undefined
没有。String()
方法。使用 new
创建新对象的时候,如果存在类的继承,那么在 ES5 和 ES6 中这个过程是有差别的。查看详情
ECMAScript 中的对象其实就是一组数据和功能的集合,它的属性或方法是没有顺序之分的!!!
-Object 类型是所有它的实例的基础,它所具有的任何属性和方法也同样存在于更具体的对象中。Object 的每个实例都具有下列属性和方法:
+构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new
操作符来调用,那么它就可以作为构造函数。
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍,但是有的方法是所有实例都应该共享的,没有创建多次的必要。
+我们创建的每一个函数都有一个 prototype
(原型)属性,这个属性值是一个对象的引用,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor
(构造函数)属性,这个属性包含一个指向 prototype
属性所在函数的引用。
function Person () {} |
上图展示了 Person
构造函数、Person
的原型属性以及 Person
现有的两个实例之间的关系。注意 Person
的每个实例,person1
和 person2
都包含一个内部属性 [[Prototype]]
,该属性仅仅指向了 Person.prototype
。换句话说,对象实例与构造函数没有直接的关系。
constructor
hasOwnProperty(propertyName)
isPrototypeOf(object)
propertyIsEnumerable(propertyName)
toLocaleString()
toString()
valueOf()
isPrototypeOf()
:用于测试一个对象是否存在于另一个对象的原型链上。hasOwnProperty()
:用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在,该方法是从 Object
继承而来的。Object.getPrototypeOf()
:返回指定对象的原型(对象内部 [[Prototype]]
属性的值)。在将一个值赋给变量时,如果这个值是基本数据类型,则变量中保存的是这个值本身。如果这个值是引用数据类型,则变量中保存的只是该值的一个引用(通过引用,可以在内存中找到这个引用数据类型的值)!
-// =================================================== |
ECMAScript 中所有函数的参数都是按值传递的。简单来说就是数据进行复制,然后传递给给函数作为参数。
-ECMAScript 对象的属性没有顺序。因此,通过 for-in
循环输出的属性名的顺序是不可预测的。
for-in
不能对 null
和 undefined
进行迭代。
可以在 switch 语句中使用任何数据类型(在很多其他语言中只能使用数值),并且每个 case 的值不一定是常量,可以是变量,甚至是表达式。
-switch对 case 进行匹配时,遵循的是 ===
严格相等。
首先说明我的观点,不推荐使用 arguments
!!!
ECMAScript 中的函数参数是用一个数组来表示的,可以通过 arguments
对象来访问这个数组,arguments
是一个类数组对象。
关于 arguments
和 命名参数
之间是如何相互影响的,《JavaScript高级程序设计》与《深入理解ES6》的说法有些矛盾,我在谷歌浏览器中简单的试了一下,他们之间的同步关系好像是双向的。
ECMAScript 中的函数没有重载,如果声明了多个同名函数,后面的会覆盖前面的。
-JavaScript 中的变量分为全局变量和局部变量,其中全局变量一直存在,不会被清除。
-而局部变量只在函数执行的过程中存在,当函数执行结束后,局部变量会被自动清除。
-当然如果存在闭包的话,局部变量被清除的时机需要取决于闭包。
-]]> -valueOf()
方法,如果返回基本类型的值,则转换成功。valueOf()
方法返回的还是引用类型值,则改为调用自身的 toString()
方法。如果 toString()
方法返回基本类型的值,则转换成功。toString()
方法返回的是引用类型值,抛出错误。需要注意的是,数组的默认 toString()
方法经过了重新定义,会将所有单元字符串化以后再用 ","
连接起来。
null
:"null"
。undefined
:"undefined"
。"true"
或 "false"
。toString()
方法的返回值。// "null" |
null
:0
。undefined
:NaN
。0
或 1
。NaN
。// 0 |
在将值转换为布尔值时,除了下述 5 种情况,其他所有情况都会转换为 true
:
null
undefined
false
+0
、-0
或 NaN
""
+
、-
、++
和 --
都会调用 Number()
将其他类型转换为数值,或将日期对象转换为对应的毫秒数。注意不要混淆 +
、++
、+ +
,-
、--
、- -
。~
会先将值强制类型转换为 32 位数值,然后执行按位非操作,可以将 ~x
等价于 -(x+1)
。但是 ~-1
的结果是 0
而不是 -0
,因为 ~
是字位操作而非数学运算。!
运算符。var a = '1' |
+
和 -
作为一元运算符和作为多元运算符时具有不同的含义,别混淆了!
+
运算符既能用于数值加法,也能用于字符串拼接。JavaScript 怎样来判断我们要执行的是哪个操作?
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例开始,如果在实例中找到了具有给定名字的属性,则返回该属性。如果没有找到,则沿着对象的原型链向上逐层查找具有给定名字的属性,如果找到了则返回这个属性的值。
+当用赋值语句给实例对象设置已经在原型链上层存在的同名属性时,会有以下三种情况:
与 +
运算符不同,-
运算符会只会执行减法运算。所以它会先将非数值类型的数据转换为数值,然后进行减法运算。
var a = '12' |
==
允许在相等比较中进行强制类型转换,而 ===
不允许。NaN
不等于 NaN
。+0
严格等于 -0
。!=
与 !==
分别类似于 ==
与 ===
。下面主要介绍 ==
是如何进行强制类型转换的。
首先将字符串转换为数值类型,然后进行比较。
-var a = 42 |
首先将布尔类型的值转换为数值类型,然后进行比较。
-var a = '42' |
在 ==
中 null
和 undefined
相等(它们也与其自身相等),除此之外其他情况都不相等。
writable: true
,那么会直接在实例中添加一个同名的新属性,它是屏蔽属性。writable: false
,那么无法修改已有属性,也无法在实例对象上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。如果在非严格模式下,赋值语句会被忽略。set
描述符,那么一定会调用这个 set
。实例对象上并不会添加新的属性,也不会重新定义这个 set
。var a = null |
首先将引用类型转换为基本类型,然后进行比较。
+但是 JS 这门语言很灵活,如果上述所说的存在于原型链上层的同名属性中保存的是某一个引用类型值的引用,那么你还是可以修改这个引用类型的值的(并没有违反规则,因为保存的引用并没有改变)!比如,这个属性保存的是某一个数组的引用,那么我就可以通过 push
方法去改变这个数组。
如果你无论如何也想要屏蔽原型链上层的属性,那么你可以使用 Object.defineProperty()
方法!
有些情况下会隐式产生屏蔽,一定要当心。思考下面的代码:
+var anotherObject = { a: 2 } |
有两种方式使用 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 () {} |
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。即使是先创建了实例后修改原型也是如此。
+function Person () {} |
但是如果先创建了实例然后重写整个原型对象,那么情况就不一样了。具体的变化看图吧!
+ +此时 instanceof
操作符已经不好使了!
构造函数找不到最初的原型对象了!
现有实例也找不到新的原型对象了!
function Person () {} |
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型模式的最大问题。原型模式的最大问题是由其共享的本性所导致的(主要针对引用类型值的属性来说)。
+创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存,另外这种混成模式还支持向构造函数传递参数。
+function Person (name, age, job) { |
动态原型模式把所有信息封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效来决定是否需要初始化原型。
+function Person (name, age, job) { |
在使用动态原型模式时,禁止使用对象字面量重写原型!
+在前面几种模式都不适用的情况下,可以使用寄生构造函数模式,这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
+function Person (name, age, job) { |
在这个例子中,Person
函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用 new
操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return
语句,可以重写调用构造函数时返回的值。
关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof
操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。
道格拉斯·克罗克福德发明了 JavaScript 中的稳妥对象这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this
的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this
和 new
),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与计生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this
。二是不使用 new
操作符调用构造函数。
function Person (name, age, job) { |
function SuperType () { |
我觉得用文字解释这个原型链有点绕嘴,没有上图方便,就直接看下面的图片吧!
+ +instanceof
操作符用于测试构造函数的 prototype
属性是否出现在对象的原型链中。isPrototypeOf()
方法用于测试一个对象是否存在于另一个对象的原型链上。var a = 42 |
这种情况就是判断两个变量的引用是否相同
+原型链的第一个问题类似于上面介绍的原型模式的问题,这里就不详细介绍了。
它的第二个问题是在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数(通过 call()
或 apply()
方法)。
function SuperType (name) { |
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题-方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
+组合继承有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
+function SuperType (name) { |
两个实例上的 colors
属性屏蔽了原型链上的同名属性。
function object (o) { |
在 object()
函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质讲,object()
对传入其中的对象执行了一次浅复制。ES5 新增的 Object.create()
方法规范化了原型式继承。
寄生式继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
+function object (o) { |
前面说过组合继承是 JavaScript 最常用的继承函数,不过它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
+function SuperType (name) { |
为了解决上述问题,我们使用寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
+function object (o) { |
这个例子的高效率体现在它只调用了一次 SuperType
构造函数,并因此避免了在 SubType.prototype
上面创建不必要的、多余的属性。与此同时,原型链还能保持不变。因此,还能够正常使用 instanceof
和 isPrototypeOf()
。
符号没有字面量形式,这在JS的基本类型中是独一无二的。你可以使用全局 Symbol
函数来创建一个符号值,正如下面这个例子:
let firstName = Symbol() |
此代码创建了一个符号类型的 firstName
变量,并将它作为 person
对象的一个属性,而每次访问该属性都要使用这个符号值。
由于符号值是基本类型的值,因此调用 new Symbol()
将会抛出错误。你可以通过 new Object(yourSymbol)
来创建一个符号实例,但尚不清楚这能有什么作用。
Symbol
函数还可以接受一个额外的参数用于描述符号值,该描述并不能用来访问对应属性,但它能用于调试,例如:
let firstName = Symbol('first name') |
符号的描述信息被存储在内部属性 [[Description]]
中,当符号的 toString()
方法被显式或隐式调用时,该属性都会被读取。
由于符号是基本类型的值,你可以使用 typeof
运算符来判断一个变量是否为符号。ES6 扩充了 typeof
的功能以便让它在作用于符号值的时候能够返回 symbol
。
你可以在任意能使用“需计算属性名”的场合使用符号。此外还可以在 Object.defineProperty()
或 Object.defineProperties()
调用中使用它。
由于符号不存在字面量形式,所以如果以符号作为对象的属性名,就算该属性的 enumerable
被设置为 true
,该属性也无法用 for-in
循环,并且不会显示在 Object.keys()
的结果中。但是你可以使用 in
操作符来判断该属性是否存在!
let firstName = Symbol('first name') |
你或许想在不同的代码段中使用相同的符号值,例如:假设在应用中需要在两个不同的对象类型中使用同一个符号属性,用来表示一个唯一标识符。跨越文件或代码来追踪符号值是很困难并且易错的,为此,ES6 提供了“全局符号注册表”供你在任意时间点进行访问。
-若你想创建共享符号值,应使用 Symbol.for()
方法而不是 Symbol()
方法。Symbol.for()
方法仅接受单个字符串类型的参数,作为目标符号值的标识符,同时此参数也会成为该符号的描述信息。例如:
let uid = Symbol.for('uid') |
Symbol.for()
方法首先会搜索全局符号注册表,看是否存在一个键值为 "uid"
的符号值。若是,该方法会返回这个已存在的符号值。否则,会创建一个新的符号值,并使用该键值将其记录到全局符号注册表中,然后返回这个新的符号值。这就意味着此后使用同一个键值去调用 Symbol.for()
方法都将返回同一个符号值,就像下面这个例子:
let uid = Symbol.for('uid') |
共享符号值还有另一个独特用法,你可以使用 Symbol.keyFor()
方法在全局符号注册表中根据符号值检索出对应的键值,例如:
let uid = Symbol.for('uid') |
注意:使用符号值 uid
与 uid2
都返回了键值 "uid"
,而符号值 uid3
在全局符号注册表中并不存在,因此没有关联的键值,Symbol.keyFot()
只会返回 undefined
。
类型转换是 JS 语言重要的一部分,能够非常灵活地将一种数据类型转换为另一种。然而符号类型在进行转换时非常不灵活,因为其他类型缺乏与符号值的合理等价,尤其是符号值无法被转换为字符串值或数值。因此将符号作为属性所达成的效果,是其他类型所无法替代的。
-在之前的例子中使用了 console.log()
来展示符号值的输出,能这么做是由于自动调用了符号值的 String()
方法来产生输出。你也可以直接调用 String()
方法来获取相同的结果,例如:
let uid = Symbol.for('uid') |
String()
方法调用了 uid.toString()
来获取符号的字符串描述信息。但若你想直接将符号转换为字符串,则会引发错误:
let uid = Symbol.for('uid') |
将uid
与空字符串相连接,会首先要求把uid
转换为一个字符串,而这会引发错误,从而阻止了转换行为。
相似地,你也不能将符号转换为数值,对符号使用所有数学运算符都会引发错误,例如:
-let uid = Symbol.for('uid') |
此例试图把符号值除以 1,同样引发了错误。无论对符号使用哪种数学运算符都会导致错误,但使用逻辑运算符则不会,因为符号值在运算符中会被认为等价于 true
。
只能使用 ES6 新增的 Object.getOwnPropertySymbols()
方法用来检索对象的符号属性。Object.keys()
和 Object.getOwnPropertyNames()
方法都不行。
ES6 定义了“知名符号”来代表JS中一些公共行为,而这些行为此前被认为只能是内部操作。每一个知名符号都对应全局 Symbol
对象的一个属性,这些知名符号是:
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
。下面将介绍其中的一些知名符号。
-每个函数都具有一个 Symbol.hasInstance
方法,用于判断指定对象是否为本函数的一个实例。这个方法定义在 Function.prototype
上,因此所有函数都继承了面对 instanceof
运算符时的默认行为。Symbol.hasInstance
属性自身是不可写入、不可配置、不可枚举的,从而保证它不会被错误地重写。
Symbol.hasInstance
方法只接受单个参数,即需要检测的值。如果该值是本函数的一个实例,则方法会返回 true
。为了理解该方法是如何工作的,可研究下述代码:
obj instanceof Array |
ES6 从本质上将 instanceof
运算符重定义为上述方法调用的简写语法,这样使用 instanceof
便会出发一次方法调用,实际上允许你改变该运算符的工作。
假设你想定义一个函数,使得任意对象都不会被判断为该函数的一个实例,你可以采用硬编码的方式来让该函数的 Symbol.hasInstance
方法始终返回 false
,就像这样:
function MyObject () { |
上例中通过 Object.defineProperty()
方法在 MyObject
对象上设置了 Symbol.hasInstance
属性,从而屏蔽了原型上不可写入的 Symbol.hasInstance
属性。
首先请看下面数组 concat()
方法的例子:
let colors1 = [ 'red', 'green' ] |
concat()
方法会区别对待自己接收到的参数,如果参数为数组类型,那么它会自动的将数组扁平化(即分离数组中的元素)。而其他非数组类型的参数无需如此处理。在 ES6 之前,没有任何手段可以改变这种行为。
Symbol.isConcatSpreadable
属性是一个布尔类型的属性,它默认情况下并不会作为任意常规对象的属性。它只出现在特定类型的对象上,用来标示该对象作为 concat()
参数时应如何工作。
成功使用这个属性的前提条件是拥有该属性的对象,要在两个方面与数组类似:拥有数值类型的键和拥有 length
属性。
当该属性为 true
时,将该属性所属对象传递给 concat()
方法时,将所属对象扁平化。当该属性为 false
时,所属对象不会被扁平化。请看下面的例子:
let obj1 = { |
在 JS 中,字符串与正则表达式有着密切的联系,尤其是字符串具有几个可以接受正则表达式作为参数的方法:match、replace、search和split方法。
-在 ES6 之前这些方法的实现细节对开发者是隐藏的,使得开发者无法将自定义对象模拟成正则表达式(并将它们传递给字符串的这些方法)。而 ES6 定义了 4 个符号以及对应的方法,将原生行为外包到内置的 RegExp
对象上。
JavaScript 的数据类型分为“基本数据类型”和“引用数据类型”两种。
Symbol.match
:此函数接受一个字符串参数,并返回一个包含匹配结果的数组。若匹配失败,则返回 null
。Symbol.replace
:此函数接受一个字符串参数与一个替换用的字符串,并返回替换后的结果字符串。Symbol.search
:此函数接受一个字符串参数,并返回匹配结果的数值索引。若匹配失败,则返回 -1。Symbol.split
:此函数接受一个字符串参数,并返回一个用匹配值分割而成的字符串数组。Undefined
、Null
、Boolean
、Number
、String
和 Symbol
(ES6 提出)。Object
。在对象上定义这些属性,允许你创建能过进行模式匹配的对象,而无需使用这则表达式,并且允许在任何需要正则表达式的方法中使用该对象。这里有一个例子,展示了这些符号的使用:
-// 有效等价于/^.{10}$/ |
Symbol.toPrimitive
方法被定义在所有常规类型的原型上,规定了在对象被转换为基本类型值的时候会发生什么。当需要转换时,Symbol.toPrimitive
会被调用,并按照规范传入一个提示性的字符串参数。该参数有 3 种可能:当参数值为 number
的时候,应当返回一个数值。当参数值为 string
的时候,应当返回一个字符串。而当参数为 default
的时候,对返回值类型没有特别要求。
对于大部分常规对象,“数值模式”依次会有下述行为:
+对一个值使用 typeof
操作符,可能返回下列某个字符串(均为英文单词小写形式):
valueOf()
方法,如果方法返回值是一个基本类型值,那么返回它。toString()
方法,如果方法返回值是一个基本类型值,那么返回它。"undefined"
: 如果这个值未定义。"boolean"
: 如果这个值是布尔值。"string"
: 如果这个值是字符串。"number"
: 如果这个值是数值。"symbol"
: 如果这个值是符号。"function"
: 如果这个值是函数(其实函数属于对象的一种)。"object"
: 如果这个值是对象或 null
。类似的,对于大部分常规对象,“字符串模式”依次会有下述行为:
+用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
object instanceof constructor
=> true | false
《JavaScript 高级程序设计》中说:“根据规定,所有引用数据类型的值都是Object的实例。因此,在检测一个引用数据类型值和Object构造函数时,instanceof操作符始终会返回true。”
随着版本的迭代,这个说法变得不准确了,有例外了!!!
var a = Object.create(null) |
返回一个表示类型的字符串,详情请看 MDN官网。
+Undefined
类型只有一个值,即特殊的 undefined
。在使用 var
声明变量但未对其加以初始化时,这个变量的值就是 undefined
。对未初始化的变量执行 typeof
操作会返回 undefined
,而对未声明的变量执行 typeof
操作同样会返回 undefined
。
除了可以对未声明的变量执行 typeof
操作之外,在非严格模式下还可以对其进行 delete
操作。除了这两种情况之外,对未声明的变量进行任何其他操作,都会抛出错误。
Null
类型也只有一个值 - null
。
typeof null === 'object' // true |
Boolean
类型的字面值 true
和 false
是区分大小写的。也就是说,True
和 False
都不是 Boolean
值,只是标识符。
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)。length
属性取得,包含双子节字符的可能不准确。toString()
方法,并且数值的 toString()
方法可以接收一个参数(数值的基数),但是 null
和 undefined
没有。String()
方法。ECMAScript 中的对象其实就是一组数据和功能的集合,它的属性或方法是没有顺序之分的!!!
+Object 类型是所有它的实例的基础,它所具有的任何属性和方法也同样存在于更具体的对象中。Object 的每个实例都具有下列属性和方法:
toString()
方法,如果方法返回值是一个基本类型值,那么返回它。valueOf()
方法,如果方法返回值是一个基本类型值,那么返回它。constructor
hasOwnProperty(propertyName)
isPrototypeOf(object)
propertyIsEnumerable(propertyName)
toLocaleString()
toString()
valueOf()
在多数情况下,常规对象的默认模式都等价于数值模式(只有 Date
类型例外,它默认使用字符串模式)。通过定义 Symbol.toPrimitive
方法,你可以重写这些默认的转换行为。
使用 Symbol.toPrimitive
属性并将一个函数赋值给它,便可以重写默认的转换行为,例如:
function Temperature (degrees) { |
JS 最有趣的课题之一是在多个不同的全局执行环境中使用,这种情况会在浏览器页面包含内联帧(iframe)的时候出现,此时页面与内联帧均拥有各自的全局执行环境。大多数情况下这并不是一个问题,使用一些轻量级的转换操作就能够在不同的运行环境之间传递数据。问题出现在想要识别目标对象到底是什么类型的时候,而此时该对象已经在环境之间经历了传递。
-该问题的典型例子就是从内联帧向容器页面传递数组,或者反过来。在 ES6 术语中,内联帧与包含它的容器页面分别拥有一个不同的“域”,以作为 JS 的运行环境,每个“域”都拥有各自的全局作用域以及各自的全局对象拷贝。无论哪个“域”创建的数组都是正规的数组,但当它跨域进行传递时,使用 instanceof Array
进行检测却会得到 false
的结果,因为该数组是由另外一个“域”的数组构造器创建的,有别于当前“域”的数组构造器。
变通的解决方案为 Object.prototype.toString.call()
。
ES6 通过 Symbol.toStringTag
重定义了相关行为,该符号是对象的一个属性,定义了 Object.prototype.toString.call()
被调用时应当返回什么值。
function Person (name) { |
尽管将来的代码无疑会停用 with
语句,但 ES6 仍然在非严格模式中提供了对于 with
语句的支持,以便向下兼容。为此需要寻找方法让使用 with
语句的代码能够适当地继续工作。为了理解这个任务的复杂性,可研究如下代码:
let values = [1, 2, 3] |
在此例中,...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 中 |
在将一个值赋给变量时,如果这个值是基本数据类型,则变量中保存的是这个值本身。如果这个值是引用数据类型,则变量中保存的只是该值的一个引用(通过引用,可以在内存中找到这个引用数据类型的值)!
+// =================================================== |
ECMAScript 中所有函数的参数都是按值传递的。简单来说就是数据进行复制,然后传递给给函数作为参数。
+ECMAScript 对象的属性没有顺序。因此,通过 for-in
循环输出的属性名的顺序是不可预测的。
for-in
不能对 null
和 undefined
进行迭代。
可以在 switch 语句中使用任何数据类型(在很多其他语言中只能使用数值),并且每个 case 的值不一定是常量,可以是变量,甚至是表达式。
+switch对 case 进行匹配时,遵循的是 ===
严格相等。
首先说明我的观点,不推荐使用 arguments
!!!
ECMAScript 中的函数参数是用一个数组来表示的,可以通过 arguments
对象来访问这个数组,arguments
是一个类数组对象。
关于 arguments
和 命名参数
之间是如何相互影响的,《JavaScript高级程序设计》与《深入理解ES6》的说法有些矛盾,我在谷歌浏览器中简单的试了一下,他们之间的同步关系好像是双向的。
ECMAScript 中的函数没有重载,如果声明了多个同名函数,后面的会覆盖前面的。
+JavaScript 中的变量分为全局变量和局部变量,其中全局变量一直存在,不会被清除。
+而局部变量只在函数执行的过程中存在,当函数执行结束后,局部变量会被自动清除。
+当然如果存在闭包的话,局部变量被清除的时机需要取决于闭包。
+]]>数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。
-configurable
:表示能否通过 delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,这个特性默认值为 true
。enumerable
:表示能否通过 for-in
循环返回属性。直接在对象上定义的属性,这个特性默认值为 true
。writable
:表示能否修改属性的值。直接在对象上定义的属性,这个特性默认值为 true
。value
:包含这个属性的数据值。读取属性值得时候,从这个位置读。写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined
。可以使用 Object.defineProperty(obj, prop, descriptor)
修改数据属性的特性:
obj
:数据属性所在的对象。prop
:数据属性的名称,可以是字符串或符号。descriptor
:描述符对象,可包含的属性有 configurable
、enumerable
、writable
和 value
。如果通过 Object.defineProperty(obj, prop, descriptor)
定义一个新的数据属性,descriptor
中缺失的特性会被赋予 false
或 undefined
。
valueOf()
方法,如果返回基本类型的值,则转换成功。valueOf()
方法返回的还是引用类型值,则改为调用自身的 toString()
方法。如果 toString()
方法返回基本类型的值,则转换成功。toString()
方法返回的是引用类型值,抛出错误。需要注意的是,数组的默认 toString()
方法经过了重新定义,会将所有单元字符串化以后再用 ","
连接起来。
var obj = { test: 1 } |
关于writable:当 writable
为 false
时,在非严格模式下通过赋值语句修改属性值,赋值操作将被忽略。在严格模式下则会抛出错误。但是如果通过 Object.defineProperty() 方法修改 value 特性则不会有任何问题。
var obj = { test: 1} |
关于configurable:当 configurable
为 false
时,不允许删除属性,不允许修改属性的 enumerable
、configurable
,不可以将 writable
由 false
修改为 true
,但是可以将 writable
由 true
修改为 false
,也可以修改属性的 value
特性。
当 writable 和 configurable 均为 false 时,不允许通过任何方式修改属性值,直接赋值或者通过 Object.defineProperty() 都不可以!
-/** |
访问器属性不包含数据值。它们包含一对 get
和 set
函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 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 = { |
不一定非要同时指定 get
和 set
。只指定 get
意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了 get
函数的属性会抛出错误。而读取只指定 set
的属性会返回 undefined
可以通过 Object.defineProperty() 实现数据属性与访问器属性的转换,但是切记不能同时指定数据属性和访问器属性,这样会抛出错误!
-ES5 定义了一个 Object.defineProperties()
方法用来为对象定义多个属性。
var book = {} |
使用 ES5 的 Object.getOwnPropertyDescriptor()
方法可以取得给定属性的描述符。该方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象。这个方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用。
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions()
。如果想检测一个对象是否可以添加新属性,可以使用 Object.isExtensible()
。
不可以添加新属性,但是删除旧属性还是可以的。
+// "null" |
null
:0
。undefined
:NaN
。0
或 1
。NaN
。var myObject = { a: 2 } |
在非严格模式下,创建属性 b
会静默失败。在严格模式下,将会抛出 TypeError
错误。
Object.seal()
会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions()
并把所有现有属性标记为 configurable: false
。
Object.freeze()
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal()
并把所有“数据访问”属性标记为 writable: false
。
工厂模式就是调用函数返回一个包含特定属性和方法的对象,工厂模式的问题在于它没有解决对象识别的问题(即怎样知道一个对象的类型)。
-function createPerson (name, age) { |
ES 中可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法,下面使用构造函数模式重写工厂模式中的例子。
-function Person (name, age) { |
构造函数模式与工厂模式的区别:
-this
对象.return
语句。// 0 |
在将值转换为布尔值时,除了下述 5 种情况,其他所有情况都会转换为 true
:
null
undefined
false
+0
、-0
或 NaN
""
要创建 Person
的新实例,必须使用 new
操作符。以这种方式调用构造函数实际上会经历一下 4 个步骤:
+
、-
、++
和 --
都会调用 Number()
将其他类型转换为数值,或将日期对象转换为对应的毫秒数。注意不要混淆 +
、++
、+ +
,-
、--
、- -
。~
会先将值强制类型转换为 32 位数值,然后执行按位非操作,可以将 ~x
等价于 -(x+1)
。但是 ~-1
的结果是 0
而不是 -0
,因为 ~
是字位操作而非数学运算。!
运算符。var a = '1' |
+
和 -
作为一元运算符和作为多元运算符时具有不同的含义,别混淆了!
+
运算符既能用于数值加法,也能用于字符串拼接。JavaScript 怎样来判断我们要执行的是哪个操作?
new
)。this
绑定到这个新对象。使用 new
创建新对象的时候,如果存在类的继承,那么在 ES5 和 ES6 中这个过程是有差别的。查看详情
与 +
运算符不同,-
运算符会只会执行减法运算。所以它会先将非数值类型的数据转换为数值,然后进行减法运算。
var a = '12' |
==
允许在相等比较中进行强制类型转换,而 ===
不允许。NaN
不等于 NaN
。+0
严格等于 -0
。!=
与 !==
分别类似于 ==
与 ===
。构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new
操作符来调用,那么它就可以作为构造函数。
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍,但是有的方法是所有实例都应该共享的,没有创建多次的必要。
-我们创建的每一个函数都有一个 prototype
(原型)属性,这个属性值是一个对象的引用,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
下面主要介绍 ==
是如何进行强制类型转换的。
首先将字符串转换为数值类型,然后进行比较。
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor
(构造函数)属性,这个属性包含一个指向 prototype
属性所在函数的引用。
function Person () {} |
上图展示了 Person
构造函数、Person
的原型属性以及 Person
现有的两个实例之间的关系。注意 Person
的每个实例,person1
和 person2
都包含一个内部属性 [[Prototype]]
,该属性仅仅指向了 Person.prototype
。换句话说,对象实例与构造函数没有直接的关系。
isPrototypeOf()
:用于测试一个对象是否存在于另一个对象的原型链上。hasOwnProperty()
:用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在,该方法是从 Object
继承而来的。Object.getPrototypeOf()
:返回指定对象的原型(对象内部 [[Prototype]]
属性的值)。var a = 42 |
首先将布尔类型的值转换为数值类型,然后进行比较。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例开始,如果在实例中找到了具有给定名字的属性,则返回该属性。如果没有找到,则沿着对象的原型链向上逐层查找具有给定名字的属性,如果找到了则返回这个属性的值。
-当用赋值语句给实例对象设置已经在原型链上层存在的同名属性时,会有以下三种情况:
+var a = '42' |
在 ==
中 null
和 undefined
相等(它们也与其自身相等),除此之外其他情况都不相等。
var a = null |
首先将引用类型转换为基本类型,然后进行比较。
+var a = 42 |
这种情况就是判断两个变量的引用是否相同
+]]> +符号没有字面量形式,这在JS的基本类型中是独一无二的。你可以使用全局 Symbol
函数来创建一个符号值,正如下面这个例子:
let firstName = Symbol() |
此代码创建了一个符号类型的 firstName
变量,并将它作为 person
对象的一个属性,而每次访问该属性都要使用这个符号值。
由于符号值是基本类型的值,因此调用 new Symbol()
将会抛出错误。你可以通过 new Object(yourSymbol)
来创建一个符号实例,但尚不清楚这能有什么作用。
Symbol
函数还可以接受一个额外的参数用于描述符号值,该描述并不能用来访问对应属性,但它能用于调试,例如:
let firstName = Symbol('first name') |
符号的描述信息被存储在内部属性 [[Description]]
中,当符号的 toString()
方法被显式或隐式调用时,该属性都会被读取。
由于符号是基本类型的值,你可以使用 typeof
运算符来判断一个变量是否为符号。ES6 扩充了 typeof
的功能以便让它在作用于符号值的时候能够返回 symbol
。
你可以在任意能使用“需计算属性名”的场合使用符号。此外还可以在 Object.defineProperty()
或 Object.defineProperties()
调用中使用它。
由于符号不存在字面量形式,所以如果以符号作为对象的属性名,就算该属性的 enumerable
被设置为 true
,该属性也无法用 for-in
循环,并且不会显示在 Object.keys()
的结果中。但是你可以使用 in
操作符来判断该属性是否存在!
let firstName = Symbol('first name') |
你或许想在不同的代码段中使用相同的符号值,例如:假设在应用中需要在两个不同的对象类型中使用同一个符号属性,用来表示一个唯一标识符。跨越文件或代码来追踪符号值是很困难并且易错的,为此,ES6 提供了“全局符号注册表”供你在任意时间点进行访问。
+若你想创建共享符号值,应使用 Symbol.for()
方法而不是 Symbol()
方法。Symbol.for()
方法仅接受单个字符串类型的参数,作为目标符号值的标识符,同时此参数也会成为该符号的描述信息。例如:
let uid = Symbol.for('uid') |
Symbol.for()
方法首先会搜索全局符号注册表,看是否存在一个键值为 "uid"
的符号值。若是,该方法会返回这个已存在的符号值。否则,会创建一个新的符号值,并使用该键值将其记录到全局符号注册表中,然后返回这个新的符号值。这就意味着此后使用同一个键值去调用 Symbol.for()
方法都将返回同一个符号值,就像下面这个例子:
let uid = Symbol.for('uid') |
共享符号值还有另一个独特用法,你可以使用 Symbol.keyFor()
方法在全局符号注册表中根据符号值检索出对应的键值,例如:
let uid = Symbol.for('uid') |
注意:使用符号值 uid
与 uid2
都返回了键值 "uid"
,而符号值 uid3
在全局符号注册表中并不存在,因此没有关联的键值,Symbol.keyFot()
只会返回 undefined
。
类型转换是 JS 语言重要的一部分,能够非常灵活地将一种数据类型转换为另一种。然而符号类型在进行转换时非常不灵活,因为其他类型缺乏与符号值的合理等价,尤其是符号值无法被转换为字符串值或数值。因此将符号作为属性所达成的效果,是其他类型所无法替代的。
+在之前的例子中使用了 console.log()
来展示符号值的输出,能这么做是由于自动调用了符号值的 String()
方法来产生输出。你也可以直接调用 String()
方法来获取相同的结果,例如:
let uid = Symbol.for('uid') |
String()
方法调用了 uid.toString()
来获取符号的字符串描述信息。但若你想直接将符号转换为字符串,则会引发错误:
let uid = Symbol.for('uid') |
将uid
与空字符串相连接,会首先要求把uid
转换为一个字符串,而这会引发错误,从而阻止了转换行为。
相似地,你也不能将符号转换为数值,对符号使用所有数学运算符都会引发错误,例如:
+let uid = Symbol.for('uid') |
此例试图把符号值除以 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 } |
有两种方式使用 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 () {} |
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。即使是先创建了实例后修改原型也是如此。
-function Person () {} |
但是如果先创建了实例然后重写整个原型对象,那么情况就不一样了。具体的变化看图吧!
- -此时 instanceof
操作符已经不好使了!
构造函数找不到最初的原型对象了!
现有实例也找不到新的原型对象了!
function Person () {} |
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型模式的最大问题。原型模式的最大问题是由其共享的本性所导致的(主要针对引用类型值的属性来说)。
-创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存,另外这种混成模式还支持向构造函数传递参数。
-function Person (name, age, job) { |
动态原型模式把所有信息封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效来决定是否需要初始化原型。
-function Person (name, age, job) { |
在使用动态原型模式时,禁止使用对象字面量重写原型!
-在前面几种模式都不适用的情况下,可以使用寄生构造函数模式,这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
-function Person (name, age, job) { |
在这个例子中,Person
函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用 new
操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return
语句,可以重写调用构造函数时返回的值。
关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof
操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。
道格拉斯·克罗克福德发明了 JavaScript 中的稳妥对象这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this
的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this
和 new
),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与计生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this
。二是不使用 new
操作符调用构造函数。
function Person (name, age, job) { |
function SuperType () { |
我觉得用文字解释这个原型链有点绕嘴,没有上图方便,就直接看下面的图片吧!
- +下面将介绍其中的一些知名符号。
+每个函数都具有一个 Symbol.hasInstance
方法,用于判断指定对象是否为本函数的一个实例。这个方法定义在 Function.prototype
上,因此所有函数都继承了面对 instanceof
运算符时的默认行为。Symbol.hasInstance
属性自身是不可写入、不可配置、不可枚举的,从而保证它不会被错误地重写。
Symbol.hasInstance
方法只接受单个参数,即需要检测的值。如果该值是本函数的一个实例,则方法会返回 true
。为了理解该方法是如何工作的,可研究下述代码:
obj instanceof Array |
ES6 从本质上将 instanceof
运算符重定义为上述方法调用的简写语法,这样使用 instanceof
便会出发一次方法调用,实际上允许你改变该运算符的工作。
假设你想定义一个函数,使得任意对象都不会被判断为该函数的一个实例,你可以采用硬编码的方式来让该函数的 Symbol.hasInstance
方法始终返回 false
,就像这样:
function MyObject () { |
上例中通过 Object.defineProperty()
方法在 MyObject
对象上设置了 Symbol.hasInstance
属性,从而屏蔽了原型上不可写入的 Symbol.hasInstance
属性。
首先请看下面数组 concat()
方法的例子:
let colors1 = [ 'red', 'green' ] |
concat()
方法会区别对待自己接收到的参数,如果参数为数组类型,那么它会自动的将数组扁平化(即分离数组中的元素)。而其他非数组类型的参数无需如此处理。在 ES6 之前,没有任何手段可以改变这种行为。
Symbol.isConcatSpreadable
属性是一个布尔类型的属性,它默认情况下并不会作为任意常规对象的属性。它只出现在特定类型的对象上,用来标示该对象作为 concat()
参数时应如何工作。
成功使用这个属性的前提条件是拥有该属性的对象,要在两个方面与数组类似:拥有数值类型的键和拥有 length
属性。
当该属性为 true
时,将该属性所属对象传递给 concat()
方法时,将所属对象扁平化。当该属性为 false
时,所属对象不会被扁平化。请看下面的例子:
let obj1 = { |
在 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) { |
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题-方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
-组合继承有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
-function SuperType (name) { |
两个实例上的 colors
属性屏蔽了原型链上的同名属性。
function object (o) { |
在 object()
函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质讲,object()
对传入其中的对象执行了一次浅复制。ES5 新增的 Object.create()
方法规范化了原型式继承。
寄生式继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
-function object (o) { |
前面说过组合继承是 JavaScript 最常用的继承函数,不过它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
-function SuperType (name) { |
为了解决上述问题,我们使用寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
-function object (o) { |
这个例子的高效率体现在它只调用了一次 SuperType
构造函数,并因此避免了在 SubType.prototype
上面创建不必要的、多余的属性。与此同时,原型链还能保持不变。因此,还能够正常使用 instanceof
和 isPrototypeOf()
。
在对象上定义这些属性,允许你创建能过进行模式匹配的对象,而无需使用这则表达式,并且允许在任何需要正则表达式的方法中使用该对象。这里有一个例子,展示了这些符号的使用:
+// 有效等价于/^.{10}$/ |
Symbol.toPrimitive
方法被定义在所有常规类型的原型上,规定了在对象被转换为基本类型值的时候会发生什么。当需要转换时,Symbol.toPrimitive
会被调用,并按照规范传入一个提示性的字符串参数。该参数有 3 种可能:当参数值为 number
的时候,应当返回一个数值。当参数值为 string
的时候,应当返回一个字符串。而当参数为 default
的时候,对返回值类型没有特别要求。
对于大部分常规对象,“数值模式”依次会有下述行为:
+valueOf()
方法,如果方法返回值是一个基本类型值,那么返回它。toString()
方法,如果方法返回值是一个基本类型值,那么返回它。类似的,对于大部分常规对象,“字符串模式”依次会有下述行为:
+toString()
方法,如果方法返回值是一个基本类型值,那么返回它。valueOf()
方法,如果方法返回值是一个基本类型值,那么返回它。在多数情况下,常规对象的默认模式都等价于数值模式(只有 Date
类型例外,它默认使用字符串模式)。通过定义 Symbol.toPrimitive
方法,你可以重写这些默认的转换行为。
使用 Symbol.toPrimitive
属性并将一个函数赋值给它,便可以重写默认的转换行为,例如:
function Temperature (degrees) { |
JS 最有趣的课题之一是在多个不同的全局执行环境中使用,这种情况会在浏览器页面包含内联帧(iframe)的时候出现,此时页面与内联帧均拥有各自的全局执行环境。大多数情况下这并不是一个问题,使用一些轻量级的转换操作就能够在不同的运行环境之间传递数据。问题出现在想要识别目标对象到底是什么类型的时候,而此时该对象已经在环境之间经历了传递。
+该问题的典型例子就是从内联帧向容器页面传递数组,或者反过来。在 ES6 术语中,内联帧与包含它的容器页面分别拥有一个不同的“域”,以作为 JS 的运行环境,每个“域”都拥有各自的全局作用域以及各自的全局对象拷贝。无论哪个“域”创建的数组都是正规的数组,但当它跨域进行传递时,使用 instanceof Array
进行检测却会得到 false
的结果,因为该数组是由另外一个“域”的数组构造器创建的,有别于当前“域”的数组构造器。
变通的解决方案为 Object.prototype.toString.call()
。
ES6 通过 Symbol.toStringTag
重定义了相关行为,该符号是对象的一个属性,定义了 Object.prototype.toString.call()
被调用时应当返回什么值。
function Person (name) { |
尽管将来的代码无疑会停用 with
语句,但 ES6 仍然在非严格模式中提供了对于 with
语句的支持,以便向下兼容。为此需要寻找方法让使用 with
语句的代码能够适当地继续工作。为了理解这个任务的复杂性,可研究如下代码:
let values = [1, 2, 3] |
在此例中,...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 中 |
对于一个网站来说,如果想要做到仅允许特定用户进行访问,或者为不同用户提供不同的内容,前提条件都是它能够识别当前用户。但由于 HTTP 是无状态协议,所以需要额外使用 Cookie 来辅助完成用户的识别。
-Cookie 技术有四个组成部分:
Cookie 工作流程:
Set-Cookie
字段cookie 的 Name
由 US-ASCII 字符组成,不允许包含:控制字符(CTLs)、空格、制表符(Tab)、()@,;:\"/[]?={}
。
虽然 RFC 并没有规定必须使用 URL encoding 对 Name
进行编码,但是编码可以保证 Name
中不会出现不符合规定的字符。
如果 Name
是以 __Secure-
为前缀,那么必须同时设置 Secure
属性。
如果 Name
是以 __Host-
为前缀,那么必须同时设置 Secure
属性、禁止设置 Domain
属性、Path
属性的值必须为 /
。
cookie 的 Value
由 US-ASCII 字符组成,不允许包含:控制字符(CTLs)、空格、双引号、逗号、分号、反斜线。
虽然 RFC 并没有规定必须使用 URL encoding 对 Value
进行编码,但是编码可以保证 Value
中不会出现不符合规定的字符。
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
属性制定了一个 URL 路径片段,该路径片段必须出现在要请求的资源路径中才可以携带这条 cookie。假设 Path=/docs
,那么 /docs/Web
会携带 cookie,/test
则不会携带 cookie。
cookie 的最长有效时间,形式为符合 HTTP-date 规范的时间戳。
-如果没有设置这个属性,那么表示这是一个 会话期 cookie,当客户端关闭后,会话结束,会话期 cookie 会被浏览器移除。
-很多浏览器支持会话恢复功能,这个功能可以使浏览器保留所有的tab标签,然后在重新打开浏览器的时候将其还原。与此同时,会话期 cookie 也会恢复,就跟从来没有关闭浏览器一样。
-在 cookie 失效前需要经过的秒数。秒数为 0
或负数会使 cookie 直接过期。
如果同时设置了 Expires
和 Max-Age
,Max-Age
的优先级更高。
设置该属性后,除 localhost 外,仅使用 HTTPS
的请求才能使用 cookie。
HTTP 站点无法为 cookie 设置 Secure
属性(在 Chrome 52+ 和 Firefox 52+ 中新引入的限制)。
设置该属性后,无法通过 document.cookie
访问到这条 cookie。通过该属性可以防止 XSS 攻击获取 cookie 信息。
该属性用于控制在跨站请求时,是否允许携带这条 cookie。
Lax
(默认值):跨站请求时不能携带这条 cookie,但如果是由外部站点导航的请求(如一个链接),可以携带这条 cookie。Strict
:跨站请求不能携带这条 cookie。None
:只有同时设置了 Secure
时,SameSite = None
才会生效,此时允许跨站请求携带这条 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。
对于一个网站来说,如果想要做到仅允许特定用户进行访问,或者为不同用户提供不同的内容,前提条件都是它能够识别当前用户。但由于 HTTP 是无状态协议,所以需要额外使用 Cookie 来辅助完成用户的识别。
+Cookie 技术有四个组成部分:
Cookie 工作流程:
Set-Cookie
字段cookie 的 Name
由 US-ASCII 字符组成,不允许包含:控制字符(CTLs)、空格、制表符(Tab)、()@,;:\"/[]?={}
。
虽然 RFC 并没有规定必须使用 URL encoding 对 Name
进行编码,但是编码可以保证 Name
中不会出现不符合规定的字符。
如果 Name
是以 __Secure-
为前缀,那么必须同时设置 Secure
属性。
如果 Name
是以 __Host-
为前缀,那么必须同时设置 Secure
属性、禁止设置 Domain
属性、Path
属性的值必须为 /
。
cookie 的 Value
由 US-ASCII 字符组成,不允许包含:控制字符(CTLs)、空格、双引号、逗号、分号、反斜线。
虽然 RFC 并没有规定必须使用 URL encoding 对 Value
进行编码,但是编码可以保证 Value
中不会出现不符合规定的字符。
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
属性制定了一个 URL 路径片段,该路径片段必须出现在要请求的资源路径中才可以携带这条 cookie。假设 Path=/docs
,那么 /docs/Web
会携带 cookie,/test
则不会携带 cookie。
cookie 的最长有效时间,形式为符合 HTTP-date 规范的时间戳。
+如果没有设置这个属性,那么表示这是一个 会话期 cookie,当客户端关闭后,会话结束,会话期 cookie 会被浏览器移除。
+很多浏览器支持会话恢复功能,这个功能可以使浏览器保留所有的tab标签,然后在重新打开浏览器的时候将其还原。与此同时,会话期 cookie 也会恢复,就跟从来没有关闭浏览器一样。
+在 cookie 失效前需要经过的秒数。秒数为 0
或负数会使 cookie 直接过期。
如果同时设置了 Expires
和 Max-Age
,Max-Age
的优先级更高。
设置该属性后,除 localhost 外,仅使用 HTTPS
的请求才能使用 cookie。
HTTP 站点无法为 cookie 设置 Secure
属性(在 Chrome 52+ 和 Firefox 52+ 中新引入的限制)。
设置该属性后,无法通过 document.cookie
访问到这条 cookie。通过该属性可以防止 XSS 攻击获取 cookie 信息。
该属性用于控制在跨站请求时,是否允许携带这条 cookie。
Lax
(默认值):跨站请求时不能携带这条 cookie,但如果是由外部站点导航的请求(如一个链接),可以携带这条 cookie。Strict
:跨站请求不能携带这条 cookie。None
:只有同时设置了 Secure
时,SameSite = None
才会生效,此时允许跨站请求携带这条 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。
两个链接,读完就懂了:How to parse command line arguments 和 yargs-parser。
-两个链接,读完就懂了:How to parse command line arguments 和 yargs-parser。
+command = { |
const { options } = await getConfigs(command); |
const { options, warnings } = await getConfigs(command); |
因为 rollup 是使用 ts 编写的,所以需要下载 ts-node 来支持本地运行。过程中主要遇到两类问题:
+第一个问题通过修改 tsconfig.json 来避免,第二个问题通过 @ts-ignore 来忽略。
+修改 rollup cli 文件,然后本地运行 cli 文件的,是改动最小的方式。这里的主要改动是让 rollup 去读我们指定的 rollup.config.ts 文件,而不是去项目根目录下寻找。
+在项目根目录下创建 learn 文件夹,里面保存着我们的 rollup.config.ts 和 build.ts。然后在 package.json 中添加对应的命令,就可以在源码中进行 debug 了!
+具体修改内容,可参照 commit
+如果觉得修改麻烦,可以直接在 learn 分支上进行 debug。
]]>Graph.generateModuleGraph
方法会从入口文件开始,通过对源代码进行分析,从而梳理文件之间的依赖关系,逐一的为每个文件生成一个 Module
实例,然后登记在 graph.moduleLoader.modulesById
上。
在对源代码进行分析时,就是通过 graph.acornParse
生成 ast 语法树,然后再基于 rollup 自己定义在 src/nodes/shared 下的节点生成对应的实例,保存在 module.ast
上。在这一系列动作之后,已经知道了当前源文件依赖哪些其他文件,然后就可以顺着依赖关系再对其他文件进行同样的解析。
当然,在这一过程中,会在关键的节点调用 graph.pluginDriver
上的合适方法来抛出对应的钩子,触发用户声明要在编译过程中执行的 plugins。
因为 rollup 是使用 ts 编写的,所以需要下载 ts-node 来支持本地运行。过程中主要遇到两类问题:
-第一个问题通过修改 tsconfig.json 来避免,第二个问题通过 @ts-ignore 来忽略。
-修改 rollup cli 文件,然后本地运行 cli 文件的,是改动最小的方式。这里的主要改动是让 rollup 去读我们指定的 rollup.config.ts 文件,而不是去项目根目录下寻找。
-在项目根目录下创建 learn 文件夹,里面保存着我们的 rollup.config.ts 和 build.ts。然后在 package.json 中添加对应的命令,就可以在源码中进行 debug 了!
-具体修改内容,可参照 commit
-如果觉得修改麻烦,可以直接在 learn 分支上进行 debug。
]]>