Relearn Front End Technology

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;

是一个正确的做法。

类型转换

js类型转换.jpg

装箱转换

虽然Symbol无法使用new来调用,但是我们能够使用装箱得到一个Symbol对象。

1
2
3
4
5
var symbolObject = (function(){return this;}).call(Symbol('a'));

console.log(typeof symbolObject);//object
console.log(symbolObject instanceof Symbol);//true
console.log(symbolObject.constructor == Symbol);//true

面向对象

  1. 对象具有唯一标识性:即便两个完全相同的对象,也并非同一对象。({}!=={})
  2. 对象具有状态:对象具有状态,同一对象可能处于不同状态之下。
  3. 对象具有行为:对象的状态,可能因为它的行为发生变迁。

js对象的两类属性

javascript用一组特征(attribute)来描述属性(property)。第一类属性是数据属性。数据属性具有四个特征:

  1. value:属性的值
  2. writable:是否是只读数据
  3. enumerable:是否能够使用for in能否枚举该属性
  4. configurable:决定该属性能否被删除或者改变特征值。

第二类是访问器属性,他有四个特征:

  1. getter:函数或undefined,在取属性值的时候使用。
  2. setter:函数或undefined,在设置属性的时候使用。
  3. enumerable:是否能够使用for in能否枚举该属性
  4. configurable:决定该属性能否被删除或者改变特征值。

通过Object的内置函数getOwnPropertyDescriptor可以查看属性

1
2
3
var o = {a:1};
Object.getOwnPropertyDessciptor(o,'a');
//{value:1, writable:true, enumerable: true, configurable: true}

如果想要改变属性的特征,可以使用Object.defineProperty
1
2
3
4
5
6
7
var 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
2
var o = { get a() { return 1 } };
console.log(o.a); // 1

原型

c++,java使用了“类”的方式来模拟对象。而javascript使用了原型的方法来模拟对象。基于原型的对象系统通过复制来创建新对象。原型系统的复制操作有两种实现思路:

  1. 并不是真的复制对象,而是新对象持有原型的引用
  2. 真正的复制对象,从此两个对象再无关联

javascript使用了前一种方式。原型系统的大致概念为

  1. 如果所有对象都有私有字段prototype, 那么就是对象的原型
  2. 读一个属性,如果对象本身没有,那么就会继续访问对象的原型,直到原型为空或者找到为止。

ES6开始提供了一些方法让我们能够更加方便地操作原型。

  1. Object.create根据指定的原型创建新对象,原型可以为null
  2. Object.getPrototypeOf获得一个对象的原型
  3. Object.setPrototypeOf设置一个对象的原型

ES6中的类

使用es6中的类就能够避免function和new的搭配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class 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
22
class 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发起的是微观任务。

宏观任务和微观任务

marco-and-mirco-task.jpg
通过宏观任务和微观任务机制,我们就可以实现javascript引擎级和宿主级的任务了。比如promise永远在队列尾部添加微观任务,setTimeout等宿主api,则会添加宏观任务。执行的顺序是同步任务=>微观任务=>宏观任务。在这里有非常详尽的例子。

闭包

Javascript闭包的组成部分为两个,环境部分和表达式部分。
环境部分包括:

  1. 环境:函数的词法环境(执行上下文的一部分)
  2. 标识符列表:函数中用到的未声明的变量

表达式部分就是函数体

执行上下文

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
2
3
var b = {};
let c = 1;
this.a = 2;

想要正确执行她,我们需要知道:

  1. var把b声明到哪里
  2. b表示哪个变量
  3. b的原型是哪个对象
  4. let把c声明到哪里
  5. 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
4
void 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
10
var 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
2
3
function foo(){
//code
}

箭头函数

1
2
3
const foo = () => {
// code
}

方法

1
2
3
4
5
class C {
foo(){
//code
}
}

生成器函数

1
2
3
function* foo(){
// code
}

类(也是函数)

1
2
3
4
5
class Foo {
constructor(){
//code
}
}

异步函数(async)

1
2
3
4
5
6
7
8
9
async function foo(){
// code
}
const foo = async () => {
// code
}
async function foo*(){
// code
}

this

不同的函数,一个明显的区别是this。同一个函数调用方式不同,得到的this也不同。

1
2
3
4
5
6
7
8
9
10
function 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
10
const showThis = () => {
console.log(this);
}

var o = {
showThis: showThis
}

showThis(); // global
o.showThis(); // global

箭头函数不论是用什么引用来调用它,都不影响this。

另外方法的行为也不一样:
1
2
3
4
5
6
7
8
9
10
class 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
10
var a = 1;
foo();

在别处定义了foo:

var b = 2;
function foo(){
console.log(b); // 2
console.log(a); // error
}

这里foo能够访问b(定义时词法环境),却不能访问a(执行时词法环境)就是上下文的切换机制。javascript用一个栈管理执行上下文,这个栈中的每一项又包含一个链表:
context-stack.jpg
函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈。javascript定义了[[thisMode]]私有属性。[[thisMode]]有三个取值

  • lexical: 表示从上下文中取this,对应了箭头函数。
  • global: 表示this为undefined时,取全局对象,对应了普通函数
  • strict: 严格模式下严格按照调用时传入的值,可能为null或者undefined.

方法的行为和普通函数有差异,因为class设计成了默认按strict模式执行。严格模式下,之前的代码会是这样的结果:

1
2
3
4
5
6
7
8
9
10
11
"use strict"
function showThis(){
console.log(this);
}

var o = {
showThis: showThis
}

showThis(); // undefined
o.showThis(); // o

函数创建新的执行上下文中的词法环境记录时,会根据[[thisMode]]来标记新记录的[[ThisBindingStatus]]私有属性。代码执行遇到this时,会逐层检查当前词法环境记录中的[[ThisBindingStatus]],当找到有this的环境记录时获取this的值。这样规则的实际效果是嵌套的箭头函数中的代码都指向外层this
1
2
3
4
5
6
7
8
9
10
var 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
6
function foo(a, b, c){
console.log(this);
console.log(a, b, c);
}
foo.call({}, 1, 2, 3);
foo.apply({}, [1, 2, 3]);

new与function

new-and-function.png

Completion类型

1
2
3
4
5
6
7
8
9
10
11
function foo(){
try{
return 0;
} catch(err) {

} finally {
console.log("a")
}
}

console.log(foo());

这段代码中,finally执行了,return也生效了。实际上,finally放return则finally中的return会覆盖try的return。Completion Record用于描述一个语句执行完的结果,它有三个字段:

  • [[type]]表示完成的类型,有break, continue, return, throw和normal.
  • [[value]]表示语句的返回值,没有就是empty
  • [[target]]表示语句的目标,通常是javascript标签。

js依靠completion record类型,在复杂的嵌套结构中实现各种控制。
js-yuju.jpg

普通语句

  • 声名类语句(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。
control-sentence-combination.png
这里的穿透指跳出控制语句向外穿透,消费指控制语句接受语句。

带标签的语句

js语句是可以加标签的,在语句前加冒号:

1
firstStatement: var i = 1;

这种语句的作用是跳出多重循环,类似c语言中的go
1
2
3
4
5
6
outer: while(true) {
inner: while(true) {
break outer;
}
}
console.log("finished")

javascript词法

javascript中有这些词法:

  • whitespace 空白字符
  • lineTerminator 换行符
  • comment 注释
  • token 词
    1. identifierName 标识符名称,如变量名和关键字
    2. punctuator 符号
    3. numericLiteral 数字直接量,就是我们写的数字
    4. stringLiteral 字符串直接量,就是我们用单引号或双引号引起来的直接量。
    5. template 字符串模板,用反括号`括起来的直接量。

js中不但支持除法运算符’/‘和’/=’,也支持正则表达式’/abc/‘。这种情况对词法分析是没法处理的,所以js定义了两套词法,靠与法分析传一个标识给词法分析器,让它决定使用哪一套词法。另外”${}”内部可以放任何js代码,所以这部分不允许出现’}’。是否允许”}“两种情况和除法及正则表达式两种相乘就是四种词法定义,所以js中有四种定义:

  • InputElementDiv
  • InputElementRegExp
  • InputElementRegExpOrTemplateTail
  • InputElementTemplateTail

但为了方便起见,我们还是将其视为普通token.

数字直接量

1
12.toString()

会报错,因为12被认为省略了小数点后面的数字,看作一个整体。所以想让’.’单独成为toke就要加入空格

1
12 .toString()

自动插入分号

1
2
3
4
5
6
7
var a = 1, b = 1, c = 1;
a
++
b
++
c
//a=1, b=2, c=2

import和export

1
2
3
import x from './a.js'//引入模块中export的值
import {a as x, modify} from './a.js';//引入模块中的变量
import * as x from './a.js';//把模块中的所有变量以类似对象属性的方式引入

直接export再import的值还是受原模块控制。仅有export default使导出的值无关。
function-and-script.jpg

预处理

var提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = 1;

function foo() {
console.log(a);
var a = 2;
}

foo();//undefined
//===============
var a = 1;

function foo() {
console.log(a);
if(false) {
var a = 2;
}
}

foo();//false,提升只看函数体/脚本/模块

函数提升

原本函数与var相似,但函数不仅提升还会赋值。在if等语句中function仍然会产生变量,但不会提前赋值。

class提升

class也会预处理,但是是为了避免class名被其他变量提前声明。所以是抛出错误。

HTML和CSS

带@规则

@charset

1
@charset "utf-8";

提示css的字符编码方式。

@import

1
2
@import "mystyle.css";
@import url("mystyle.css");

引入css文件

@media

根据设备进行判断

1
2
3
4
5
@media screen and (max-width: 300px) {
body {
background-color:lightblue;
}
}

选择器

属性选择器

  1. [att],只要元素有这个属性,不论属性是什么值都能被选中
  2. [att=val]精确匹配,检查元素的值是否为val
  3. [att~=val]检查一个元素的值是否是若干值之一,这里的val不是一个单一的值,可以是空格分割的序列。
  4. [att|=val]检查一个元素的值是否以val开头,只要val开头即可,后面内容不管。

伪类选择器

  1. :root表示树的根元素,一般用html标签就可以选中根元素,但是随着scoped css和shadow root的出现,选择器可以针对某一子树选择,那么就要root伪类了。
  2. :nth-child(even) 选中偶数节点
  3. :nth-child(4n-1)选中4n-1的节点
  4. :nth-child(3n+1 of li.important)选中第1,4,7个li.important。注意只有li.important会被计数。
  5. :empty 伪类表示没有子节点的元素
  6. :any-link 任意的链接包括a,area和link

选择器组合

  • 空格:所有后代
  • ‘>’:子代(仅往下一层)
  • ‘~’:后继,父元素相同,一个节点的后续节点
  • ‘+’:直接后继,连着的第一个
  • ‘||’:列选择器,表示选中对应列中符合条件的单元格

选择器优先级

令id选择器的数目为a,伪类和class的数目为b,伪元素和标签选择器数目为c则优先级为

1
specificity = base * base * a + base * b + c

同一优先级遵循后面覆盖前面的。

伪元素

  1. ::first-line元素的第一行
  2. ::first-letter第一个字母
  3. ::before在元素内容之前插入虚拟的元素
  4. ::after在元素之后插入虚拟的元素

链接标签

超链接类link标签

1
<link rel="canonical" href="...">

多个url指向同一个页面的情况,搜索引擎访问会去掉重复的页面,link会提示搜索引擎保留哪一个url.

1
<link rel="alternate" type="application/rss+xml" title="RSS" href="...">

提示页面的变形形式比如rss

提示当前页面的前一项和后一项,next型link告诉浏览器这是很可能访问的下一个页面。

外部资源类link标签

图标会被浏览器下载和使用,未指定会使用avicon.ico。

制定浏览器针对一些资源提前操作,提高性能。

  • dns-prefetch型link提前对一个域名做dns查询
  • preconnect型link提前对服务器建立tcp链接
  • prefetch型link提前对href的url内容进行获取
  • preload型link提前加载href指定的url
  • prerender型link提前渲染href指定的url

提前加载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
2
3
4
5
6
7
8
9
10
11
12
13
14
<p>
Please select a shape:
<img src="shapes.png" usemap="#shapes"
alt="Four shapes are available: a red hollow box, a green circle, a blue triangle, and a yellow four-pointed star.">
<map name="shapes">
<area shape=rect coords="50,50,100,100"> <!-- the hole in the red box -->
<area shape=rect coords="25,25,125,125" href="red.html" alt="Red box.">
<area shape=circle coords="200,75,50" href="green.html" alt="Green circle.">
<area shape=poly coords="325,25,262,125,388,125" href="blue.html" alt="Blue triangle.">
<area shape=poly coords="450,25,435,60,400,75,435,90,450,125,465,90,500,75,465,60"
href="yellow.html" alt="Yellow star.">
</map>
</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;
}

自适应宽

HTML

1
2
3
4
<div class="outer">
<div class="fixed"></div>
<div class="auto"></div>
</div>

CSS
1
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把内容挤出来。

浏览器原理

网页展示流程

  1. 使用http或https协议,向服务端请求页面
  2. 请求的html代码经过解析,构建dom树
  3. 计算dom上的css属性
  4. 根据css对元素逐个渲染,得到内存中的位图
  5. 可选:位图进行合成,增加后续绘制速度
  6. 合成后绘制到界面上

how-browser-work.jpg

渲染

位图信息是DOM树中占据浏览器内存最多的信息,内存优化也主要是优化这一部分。

渲染可以分为两个大类:图形和文字。盒的背景、边框、SVG 元素、阴影等特性,都需要绘制,通常使用底层库实现。文字则使用字体库。渲染时子元素是不会绘制到渲染的位图上的,这样父子元素相对位置变化时,能够保证渲染的内容能够最大程度被缓存。

合成

把子元素合成到父元素的位图上。浏览器使用position和transform决定合成策略。新的css中规定了will-change属性。

绘制

使用“脏矩形”策略,将屏幕分为几个区域。

工具链

前端工具链大概要:

  1. 初始化项目
  2. 运行和调试
  3. 测试
  4. 发布

那么,常见的社区工具会是:

  1. Yeoman
  2. webpack
  3. ava/nyc
  4. aws-cli

但是这样不够,我们需要保证版本一致。一种方法是使用npm script并在依赖中规定版本好。重量级的做法是开发一个包装工具。重量级的好处是我们能够监控:

  1. 调试/构建次数
  2. 构建平均时长
  3. 使用工具版本
  4. 发布次数