Note of relearn Front End. Course URL
Javascript
Type
undefined,null
有的编程规范要求使用void 0代替undefined。因为undefined是一个变量,为了避免无意的撰改,通常使用void 0来代替。虽然es5中undefined的值是read-only的,但是在局部作用域仍可能被修改。void后跟任何表达式的值都是undefined,而void 0又是最短和表意最清楚的。
undefined和null的表意略有不同, undefined表示未定义,而null表示定义了但是为空。null是javascript的关键字,所以可以放心使用。
String
string是有最大长度2^53-1的。但是考虑到我们通常使用utf-8或者utf-16,所以字符串的最大长度,实际上是受字符串编码长度影响的。
Number
JavaScript 中的 Number 类型有 18437736874454810627(即 2^64-2^53+3) 个值。其中9007199254740990代表NaN,Infinity表示无穷大,-Infinity表示负无穷大。
另外,javascript中有+0和-0.区别是除零时的结果一个是正无穷大,另一个是负无穷大。也可以用1/x是正无穷大还是负无穷大检测一个数是+0还是-0。
javascript存在浮点数不精确的问题(IEEE754)。一个经典的问题就是0.1+0.2不等于0.3.为了解决这个问题1
Math.abs(0.1+0.2-0.3)<=Number.EPSILON;
是一个正确的做法。
类型转换
装箱转换
虽然Symbol无法使用new来调用,但是我们能够使用装箱得到一个Symbol对象。1
2
3
4
5var symbolObject = (function(){return this;}).call(Symbol('a'));
console.log(typeof symbolObject);//object
console.log(symbolObject instanceof Symbol);//true
console.log(symbolObject.constructor == Symbol);//true
面向对象
- 对象具有唯一标识性:即便两个完全相同的对象,也并非同一对象。({}!=={})
- 对象具有状态:对象具有状态,同一对象可能处于不同状态之下。
- 对象具有行为:对象的状态,可能因为它的行为发生变迁。
js对象的两类属性
javascript用一组特征(attribute)来描述属性(property)。第一类属性是数据属性。数据属性具有四个特征:
- value:属性的值
- writable:是否是只读数据
- enumerable:是否能够使用for in能否枚举该属性
- configurable:决定该属性能否被删除或者改变特征值。
第二类是访问器属性,他有四个特征:
- getter:函数或undefined,在取属性值的时候使用。
- setter:函数或undefined,在设置属性的时候使用。
- enumerable:是否能够使用for in能否枚举该属性
- configurable:决定该属性能否被删除或者改变特征值。
通过Object的内置函数getOwnPropertyDescriptor可以查看属性1
2
3var o = {a:1};
Object.getOwnPropertyDessciptor(o,'a');
//{value:1, writable:true, enumerable: true, configurable: true}
如果想要改变属性的特征,可以使用Object.defineProperty1
2
3
4
5
6
7var o = { a: 1 };
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
//a和b都是数据属性,但特征值变化了
Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
o.b = 3;
console.log(o.b); // 2
也可以通过get和set创建访问器属性1
2var o = { get a() { return 1 } };
console.log(o.a); // 1
原型
c++,java使用了“类”的方式来模拟对象。而javascript使用了原型的方法来模拟对象。基于原型的对象系统通过复制来创建新对象。原型系统的复制操作有两种实现思路:
- 并不是真的复制对象,而是新对象持有原型的引用
- 真正的复制对象,从此两个对象再无关联
javascript使用了前一种方式。原型系统的大致概念为
- 如果所有对象都有私有字段prototype, 那么就是对象的原型
- 读一个属性,如果对象本身没有,那么就会继续访问对象的原型,直到原型为空或者找到为止。
ES6开始提供了一些方法让我们能够更加方便地操作原型。
- Object.create根据指定的原型创建新对象,原型可以为null
- Object.getPrototypeOf获得一个对象的原型
- Object.setPrototypeOf设置一个对象的原型
ES6中的类
使用es6中的类就能够避免function和new的搭配。1
2
3
4
5
6
7
8
9
10
11
12
13
14class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea();
}
// Method
calcArea() {
return this.height * this.width;
}
}
类同时赋予了继承能力1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
class Dog extends Animal {
constructor(name) {
super(name); // call the super class constructor and pass in the name parameter
}
speak() {
console.log(this.name + ' barks.');
}
}
let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.
Javascript执行
我们应该形成一个感性的认识:一个javascript引擎会常驻内存中,等待着我们把javascript代码或者函数传递给它执行。在ES3或者更早的版本中,javascript本身还没有异步执行代码的能力,所以传递javascript代码后,引擎就直接把代码顺序执行了。这个任务是宿主发起的任务。在ES5后加入了promise,这样不需要浏览器的安排,javascript本身也可以发起任务了。我们称宿主发起的是宏观任务,javascript发起的是微观任务。
宏观任务和微观任务
通过宏观任务和微观任务机制,我们就可以实现javascript引擎级和宿主级的任务了。比如promise永远在队列尾部添加微观任务,setTimeout等宿主api,则会添加宏观任务。执行的顺序是同步任务=>微观任务=>宏观任务。在这里有非常详尽的例子。
闭包
Javascript闭包的组成部分为两个,环境部分和表达式部分。
环境部分包括:
- 环境:函数的词法环境(执行上下文的一部分)
- 标识符列表:函数中用到的未声明的变量
表达式部分就是函数体
执行上下文
javascript把一段代码,执行所需的所有信息定义为:“执行上下文“。
在ES3中:
- scope: 作用域,常常叫做作用域链。
- variable object: 变量对象,用于储存变量的对象。
- this value: this值
在ES5,执行上下文变成了:
- lexical environment: 词法环境,获取变量时使用。
- variable environment: 变量环境,生命变量时使用。
- this value: this值
在ES2018中,执行上下文变成了:
- lexical environment: 词法环境,当获取变量或者this值时使用
- variable environment: 变量环境,当声明变量时使用。
- code evaluation state: 用于恢复代码执行位置。
- Function: 执行的任务是函数时使用,表示正在被执行的函数。
- ScriptOrModule: 执行的任务是脚本或者模块时使用,表示正在被执行的代码。
- Realm: 使用的基础库和内置对象实例
- Generator: 仅生成器上下文有这个属性,表示当前生成器。
1 | var b = {}; |
想要正确执行她,我们需要知道:
- var把b声明到哪里
- b表示哪个变量
- b的原型是哪个对象
- let把c声明到哪里
- this指向哪个对象
这些信息就需要执行上下文。
var声明与赋值
1 | var b = 1; |
通常我们认为它声明了b,请求为它赋值为1,var声明作用于函数执行的作用域,就是说它会穿透for和if语句。在没有let时,我们通过创建一个立即执行函数来控制var的范围。1
2
3
4
5
6
7
8(function(){
var a;
//code
}());
(function(){
var a;
//code
})();
不过如果function的上一行代码不写分号,括号会被解释为上一行代码最末的函数调用。一种推荐的写法是使用void关键字1
2
3
4void function(){
var a;
//code
}();
let
let使用块级作用域,包括:
- for
- if
- switch
- try/catch/finally
Realm
在9.0标准中,js引入了realm.在js中,很少提到{}的原型问题。但是实际的前端开发中,通过iframe创建多window环境也比较常见。所以促成了新的Realm的诞生。Realm中包含一族完整的内置对象,而且是复制关系。因为,instanceOf基本是失效的。1
2
3
4
5
6
7
8
9
10var iframe = document.createElement('iframe')
document.documentElement.appendChild(iframe)
iframe.src="javascript:var b = {};"
var b1 = iframe.contentWindow.b;
var b2 = {};
console.log(typeof b1, typeof b2); //object object
console.log(b1 instanceof Object, b2 instanceof Object); //false true
可以看到,{}在不同的realm中表现了不同的行为。
函数
普通函数
1 | function foo(){ |
箭头函数
1 | const foo = () => { |
方法
1 | class C { |
生成器函数
1 | function* foo(){ |
类(也是函数)
1 | class Foo { |
异步函数(async)
1 | async function foo(){ |
this
不同的函数,一个明显的区别是this。同一个函数调用方式不同,得到的this也不同。1
2
3
4
5
6
7
8
9
10function showThis(){
console.log(this);
}
var o = {
showThis: showThis
}
showThis(); // global
o.showThis(); // o
普通函数的this由“调用它所使用的引用“决定。我们获取函数的表达式,实际上返回的不是函数本身,而是Reference类型。Reference由两部分组成:一个对象和一个属性值。o.showThis产生的Reference是对象o和属性“showThis”组成。当做一些算数运算,Reference会被解引用,即获取真正的值。而函数调用,delete要用到Reference中的对象。在这个例子中Reference中的对象被当作this传入了执行函数的上下文。所以this即使调用函数时的引用,决定了函数执行时刻的this。
值得一提的是箭头函数的this是不同的:1
2
3
4
5
6
7
8
9
10const showThis = () => {
console.log(this);
}
var o = {
showThis: showThis
}
showThis(); // global
o.showThis(); // global
箭头函数不论是用什么引用来调用它,都不影响this。
另外方法的行为也不一样:1
2
3
4
5
6
7
8
9
10class C {
showThis() {
console.log(this);
}
}
var o = new C();
var showThis = o.showThis;
showThis(); // undefined
o.showThis(); // o
按照我们上面的方法,不难验证出:生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的,异步箭头函数与箭头函数行为是一致的。
this的机制
在javascript标准中,为函数规定了用来保存定义时上下文的私有属性[[Environment]]。当一个函数执行时,会创建一条新的执行环境记录,记录的外层词法环境会被设置成函数的[[Environment]]。这个动作就是切换上下文。1
2
3
4
5
6
7
8
9
10var a = 1;
foo();
在别处定义了foo:
var b = 2;
function foo(){
console.log(b); // 2
console.log(a); // error
}
这里foo能够访问b(定义时词法环境),却不能访问a(执行时词法环境)就是上下文的切换机制。javascript用一个栈管理执行上下文,这个栈中的每一项又包含一个链表:
函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈。javascript定义了[[thisMode]]私有属性。[[thisMode]]有三个取值
- lexical: 表示从上下文中取this,对应了箭头函数。
- global: 表示this为undefined时,取全局对象,对应了普通函数
- strict: 严格模式下严格按照调用时传入的值,可能为null或者undefined.
方法的行为和普通函数有差异,因为class设计成了默认按strict模式执行。严格模式下,之前的代码会是这样的结果:1
2
3
4
5
6
7
8
9
10
11
function showThis(){
console.log(this);
}
var o = {
showThis: showThis
}
showThis(); // undefined
o.showThis(); // o
函数创建新的执行上下文中的词法环境记录时,会根据[[thisMode]]来标记新记录的[[ThisBindingStatus]]私有属性。代码执行遇到this时,会逐层检查当前词法环境记录中的[[ThisBindingStatus]],当找到有this的环境记录时获取this的值。这样规则的实际效果是嵌套的箭头函数中的代码都指向外层this1
2
3
4
5
6
7
8
9
10var o = {}
o.foo = function foo(){
console.log(this);
return () => {
console.log(this);
return () => console.log(this);
}
}
o.foo()()(); // o, o, o
这个例子中,我们定义了三层嵌套的函数,最外层为普通函数,两层是箭头函数。三个函数的this都是对象o.
操作this的内置函数
Function.prototype.call和Function.prototype.apply可以指定函数调用传入的this,示例如下:1
2
3
4
5
6function foo(a, b, c){
console.log(this);
console.log(a, b, c);
}
foo.call({}, 1, 2, 3);
foo.apply({}, [1, 2, 3]);
new与function
Completion类型
1 | function foo(){ |
这段代码中,finally执行了,return也生效了。实际上,finally放return则finally中的return会覆盖try的return。Completion Record用于描述一个语句执行完的结果,它有三个字段:
- [[type]]表示完成的类型,有break, continue, return, throw和normal.
- [[value]]表示语句的返回值,没有就是empty
- [[target]]表示语句的目标,通常是javascript标签。
js依靠completion record类型,在复杂的嵌套结构中实现各种控制。
普通语句
- 声名类语句(var,const,let,函数声明,类声明)
- 表达式语句
- 空语句
- debugger语句
这些语句执行时从前到后顺此执行(忽略var和函数声明的预处理机制)没有任何分支或重复执行逻辑。普通语句执行后会得到[[type]]为normal的completion record,javascript引擎遇到这样的completion record会继续执行下一条语句。只有表达式语句会产生[[value]]。
语句块
语句块completion record的[[type]]如果不为normal会打断语句块后续的语句执行。1
2
3
4
5
6
7
8
9
10
11
12
13{
var i = 1; // normal, empty, empty
i ++; // normal, 1, empty
console.log(i) //normal, undefined, empty
} // normal, undefined, empty
{
var i = 1; // normal, empty, empty
return i; // return, 1, empty
i ++;
console.log(i)
} // return, 1, empty
控制类语句
带if,switch关键字,对不同类型的completion record产生反应。控制类语句分两部分,一类对内部产生影响,如if,switch,while/for,try.另一类对外部造成影响如break,continue,return,throw。
这里的穿透指跳出控制语句向外穿透,消费指控制语句接受语句。
带标签的语句
js语句是可以加标签的,在语句前加冒号:1
firstStatement: var i = 1;
这种语句的作用是跳出多重循环,类似c语言中的go1
2
3
4
5
6outer: while(true) {
inner: while(true) {
break outer;
}
}
console.log("finished")
javascript词法
javascript中有这些词法:
- whitespace 空白字符
- lineTerminator 换行符
- comment 注释
- token 词
- identifierName 标识符名称,如变量名和关键字
- punctuator 符号
- numericLiteral 数字直接量,就是我们写的数字
- stringLiteral 字符串直接量,就是我们用单引号或双引号引起来的直接量。
- template 字符串模板,用反括号`括起来的直接量。
js中不但支持除法运算符’/‘和’/=’,也支持正则表达式’/abc/‘。这种情况对词法分析是没法处理的,所以js定义了两套词法,靠与法分析传一个标识给词法分析器,让它决定使用哪一套词法。另外”${}”内部可以放任何js代码,所以这部分不允许出现’}’。是否允许”}“两种情况和除法及正则表达式两种相乘就是四种词法定义,所以js中有四种定义:
- InputElementDiv
- InputElementRegExp
- InputElementRegExpOrTemplateTail
- InputElementTemplateTail
但为了方便起见,我们还是将其视为普通token.
数字直接量
1 | 12.toString() |
会报错,因为12被认为省略了小数点后面的数字,看作一个整体。所以想让’.’单独成为toke就要加入空格1
12 .toString()
自动插入分号
1 | var a = 1, b = 1, c = 1; |
import和export
1 | import x from './a.js'//引入模块中export的值 |
直接export再import的值还是受原模块控制。仅有export default使导出的值无关。
预处理
var提升
1 | var a = 1; |
函数提升
原本函数与var相似,但函数不仅提升还会赋值。在if等语句中function仍然会产生变量,但不会提前赋值。
class提升
class也会预处理,但是是为了避免class名被其他变量提前声明。所以是抛出错误。
HTML和CSS
带@规则
@charset
1 | @charset "utf-8"; |
提示css的字符编码方式。
@import
1 | @import "mystyle.css"; |
引入css文件
@media
根据设备进行判断1
2
3
4
5@media screen and (max-width: 300px) {
body {
background-color:lightblue;
}
}
选择器
属性选择器
- [att],只要元素有这个属性,不论属性是什么值都能被选中
- [att=val]精确匹配,检查元素的值是否为val
- [att~=val]检查一个元素的值是否是若干值之一,这里的val不是一个单一的值,可以是空格分割的序列。
- [att|=val]检查一个元素的值是否以val开头,只要val开头即可,后面内容不管。
伪类选择器
- :root表示树的根元素,一般用html标签就可以选中根元素,但是随着scoped css和shadow root的出现,选择器可以针对某一子树选择,那么就要root伪类了。
- :nth-child(even) 选中偶数节点
- :nth-child(4n-1)选中4n-1的节点
- :nth-child(3n+1 of li.important)选中第1,4,7个li.important。注意只有li.important会被计数。
- :empty 伪类表示没有子节点的元素
- :any-link 任意的链接包括a,area和link
选择器组合
- 空格:所有后代
- ‘>’:子代(仅往下一层)
- ‘~’:后继,父元素相同,一个节点的后续节点
- ‘+’:直接后继,连着的第一个
- ‘||’:列选择器,表示选中对应列中符合条件的单元格
选择器优先级
令id选择器的数目为a,伪类和class的数目为b,伪元素和标签选择器数目为c则优先级为1
specificity = base * base * a + base * b + c
同一优先级遵循后面覆盖前面的。
伪元素
- ::first-line元素的第一行
- ::first-letter第一个字母
- ::before在元素内容之前插入虚拟的元素
- ::after在元素之后插入虚拟的元素
链接标签
超链接类link标签
canonical型link
1 | <link rel="canonical" href="..."> |
多个url指向同一个页面的情况,搜索引擎访问会去掉重复的页面,link会提示搜索引擎保留哪一个url.
alternate型link
1 | <link rel="alternate" type="application/rss+xml" title="RSS" href="..."> |
提示页面的变形形式比如rss
prev和next型link
提示当前页面的前一项和后一项,next型link告诉浏览器这是很可能访问的下一个页面。
外部资源类link标签
icon型link
图标会被浏览器下载和使用,未指定会使用avicon.ico。
预处理类link
制定浏览器针对一些资源提前操作,提高性能。
- dns-prefetch型link提前对一个域名做dns查询
- preconnect型link提前对服务器建立tcp链接
- prefetch型link提前对href的url内容进行获取
- preload型link提前加载href指定的url
- prerender型link提前渲染href指定的url
modulepreload型的link
提前加载javascript模块。1
2
3
4
5<link rel="modulepreload" href="app.js">
<link rel="modulepreload" href="helpers.js">
<link rel="modulepreload" href="irc.js">
<link rel="modulepreload" href="fog-machine.js">
<script type="module" src="app.js">
a标签
有href时,与一些link一样会产生超链接,也就是用户不操作的时候,它们不会被主动下载的被动型链接。a标签也有类似link的rel,除了之前提到的还有tag表示网页所属的标签,bookmark到上级张杰的链接,nofollow的链接不会被搜索引擎索引,noopener无法用opener获得当前页面的窗口,noreferrer无法用referrer获得当前页面的url.
area标签
area是区域型的链接。支持的rel与a一样。多了一个shape属性:
- 圆形:circle或者circ。coords,支持三个之,表示中心点的x,y坐标和半径r.
- 矩形:rect或rectangle。coords支持两个之表示对角顶点x1,y1和x2,y2.
- 多边形: poly或者polygon.coords至少包括六个值,表示多边形的各个顶点。
1 | <p> |
CSS trick
等分布局
用百分比布局可能会有空档,因为换行被当作空格。如果不想代码格式混乱可以设置字号为0。注意不推荐float,因为float只能顶对齐,不够灵活。1
2
3
4
5
6
7
8
9
10.inner {
width:33.33%;
height:300px;
display:inline-block;
outline:solid 1px blue;
font-size:30px;
}
.outer {
font-size:0;
}
自适应宽
HTML1
2
3
4<div class="outer">
<div class="fixed"></div>
<div class="auto"></div>
</div>
CSS1
2
3
4
5
6
7
8
9
10
11
12.fixed {
display:inline-block;
vertical-align:top;
}
.auto {
margin-left:-200px;
padding-left:200px;
box-sizing:border-box;
width:100%;
display:inline-block;
vertical-align:top;
}
逻辑是把右边的放到左边div下面,然后padding把内容挤出来。
浏览器原理
网页展示流程
- 使用http或https协议,向服务端请求页面
- 请求的html代码经过解析,构建dom树
- 计算dom上的css属性
- 根据css对元素逐个渲染,得到内存中的位图
- 可选:位图进行合成,增加后续绘制速度
- 合成后绘制到界面上
渲染
位图信息是DOM树中占据浏览器内存最多的信息,内存优化也主要是优化这一部分。
渲染可以分为两个大类:图形和文字。盒的背景、边框、SVG 元素、阴影等特性,都需要绘制,通常使用底层库实现。文字则使用字体库。渲染时子元素是不会绘制到渲染的位图上的,这样父子元素相对位置变化时,能够保证渲染的内容能够最大程度被缓存。
合成
把子元素合成到父元素的位图上。浏览器使用position和transform决定合成策略。新的css中规定了will-change属性。
绘制
使用“脏矩形”策略,将屏幕分为几个区域。
工具链
前端工具链大概要:
- 初始化项目
- 运行和调试
- 测试
- 发布
那么,常见的社区工具会是:
- Yeoman
- webpack
- ava/nyc
- aws-cli
但是这样不够,我们需要保证版本一致。一种方法是使用npm script并在依赖中规定版本好。重量级的做法是开发一个包装工具。重量级的好处是我们能够监控:
- 调试/构建次数
- 构建平均时长
- 使用工具版本
- 发布次数